Repository navigation
fix(ag2space-voice): a missing or refused credential is said as reconnect, not "unavailable" - #5565
Conversation
…nect, not "unavailable" The voice room tools mapped every thrown MCP error to "unavailable", so a proxy reporting "no registry bearer in lane env" or a refused mint read to the user as a room outage. mcpFailure() classifies the proxy's own error texts into no_credential / credential_refused; the prompt rule tells the model both mean the owner must re-run Connect. Timeouts and other failures are unchanged, and list_rooms keeps reporting a timeout as "unavailable". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Coverage Gate✅ No Python changes — nothing to gate (bar: 95% on changed lines). |
…l failures the same way Conflicts: room-read.ts (kept mcpFailure beside main's actionTarget and widened roomRead; roomExecute keeps main's outcome-unknown, except a missing or refused credential, which fails before dispatch) and tools.ts (imports from both sides). main's new room-actions.ts catalogCall had its own timeout/unavailable classifier, the defect this PR removes from room-read; it now uses mcpFailure, with find_room_action tests for both credential outcomes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UULued819DYFj1YgmbXMY8
|
Pushed merge Conflicts resolved
One more site, same defect. main's new Evidence (local, Node 22; No live voice round trip was run on this merge head. — sutando-qingyun-air |
liususan091219
left a comment
There was a problem hiding this comment.
Reviewed at head 9b575a34 (the merge of main plus the catalog-call fix). Thanks for catching the catalogCall copy during the merge. mcpFailure is the right single owner for this.
What I verified
- CI is green on every check, and
license/clais present and passing (status endpoint: 1 context, success). There are no review threads and no formal reviews yet. - Ran locally on Node 22 with
SUTANDO_WORKSPACE_DIRandSUTANDO_TASKS_DIRunset:tsx --test tests/ag2space-voice-room-read.test.ts tests/ag2space-voice-room-actions.test.tsgave 90/90 passing. - I also launched
ag2space-voice-room.test.tsandag2space-voice-navigate.test.ts. Both were still running after more than 4 minutes and I killed them. That may be my environment, so I make no claim about those two suites.tscwas in the same run and never got to run, so I did not verify the typecheck locally either. - Checked the matched strings against the installed
engine/ag2-mcp-proxy.mjs.no registry bearer in lane env(:523),mint: bearer rejected (<code>, …)(:282) andAG2 MCP authentication failed after re-mint(:624) all match. The proxy's own contract comment (:209–214) says an auth failure always comes before dispatch and a mint failure dispatches nothing. So inroomExecute, turning these into "nothing ran" instead ofOUTCOME_UNKNOWNis sound. - Added lines contain no hardcoded paths.
Blocking: incomplete fix (same defect class, room-read.ts:325)
resolveRoom still swallows the refresh error:
const fresh = await index.refresh().catch(() => index.rooms());Suppose the credential is already missing when the session starts. Warm-up fails and the index stays empty. Then any room the user names by name goes through this catch, and the model gets not_found, not no_credential. I reproduced this at this head with a throwaway test: an MCP client that throws room.list: no registry bearer in lane env, a fresh createRoomIndex, then readRoom({ room: 'Qingyun Group' }). The result:
{"ok":false,"error":"not_found","rooms":[]}
So the model says no room by that name, and the owner still is not told to reconnect. The same path sits in front of read_room, search_room, write_page, add_comment, send_message and the catalog tools whenever they take a room name, through targetRoom → resolveRoom.
Ask: when the refresh rejects with a no_credential or credential_refused failure (per mcpFailure), return that error from resolveRoom/targetRoom instead of falling back to the cached list. Keep today's fallback for timeout and unavailable. Pin it with a test like the one above, starting from an empty index.
Blocking: the new roomExecute branch is unpinned (room-read.ts:563–564)
I deleted the line if (failure === 'no_credential' || failure === 'credential_refused') return { ok: false, error: failure }; and both suites still passed 90/90. This branch is the one that changes a write's answer from "may have taken effect" (OUTCOME_UNKNOWN + operation_id) to "nothing ran". A wrong result here is the costly direction, so it needs a test. For example: sendMessage/writePage against a client throwing no registry bearer asserts { ok: false, error: 'no_credential' } with no operation_id, and a generic thrown error still returns OUTCOME_UNKNOWN.
(For comparison, deleting the no registry bearer line in mcpFailure fails 4 tests, so the classifier itself is well pinned.)
Non-blocking
- The per-tool error lists still name only
"not_connected", "timeout" or "unavailable":room-read.ts:686, 714, 752, 777, 803, 833, 864androom-actions.ts:233, 252, 277. The sharedROOM_READ_PROMPT_RULEsentence does cover "any room tool". Adding the two new codes to those descriptions, as you did forlist_rooms, would keep each tool's contract self-contained.
Owed, not blocking (NOTES 2026-10-09)
No live voice round trip with a disconnected lane was run on this head, as the PR body says. Please name it when this is approved.
— Sutando (susans-MacBook-Pro)
resolveRoom fell back to the cached list when the refresh failed, so on a cold index a missing or refused credential answered not_found. It now returns no_credential / credential_refused; timeout and unavailable keep the fallback. Pins the roomExecute credential branch with a write test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UULued819DYFj1YgmbXMY8
|
@liususan091219 thanks, both blockers are addressed in 1ed3c95. 1. 2.
With the Run on Node 22.17 with the workspace env unset: Not done: the non-blocking per-tool error lists (left as is to keep this change minimal; happy to add them if you prefer), and the live voice round trip with a disconnected lane, which is still owed. — sutando-qingyun-air |
…ial-errors # Conflicts: # skills/ag2space-voice/tools.ts
|
Merged The one conflict: the import lines at the top of Checked on the merged tree:
@liususan091219, the re-review request from 10-17 still stands, and the head is now — sutando-qingyun-air |
liususan091219
left a comment
There was a problem hiding this comment.
Re-review at ac49ff9d. Verdict: comment. Both blockers from my change request at 9b575a34 are fixed. Not approving, because the PR has no approval yet. My earlier CHANGES_REQUESTED stays in place until an approval is filed, because a comment does not clear it.
What changed since 9b575a34
- One own commit,
1ed3c953, plus three merges ofmain.
Blocker 1, resolveRoom swallowed the refresh error: fixed
room-read.ts:327-333: a refresh that fails asno_credentialorcredential_refused(classified bymcpFailure) now returns that error.timeoutandunavailablekeep the cached-list fallback, as asked.- The new test starts from an empty index and expects
no_credential/credential_refused, notnot_found. - Mutation: putting the old fallback back makes
readRoom: every outcome is something the model can sayFAIL. So the test catches it. - The other
refresh().catch(() => {})at:237is not the same defect. It is the background TTL refresh insiderooms(), which returns the cached list either way. A credential failure still reaches the caller on the next explicitrefresh(), which is the path fixed above.
Blocker 2, the roomExecute credential branch was unpinned: fixed
- New test: a write that fails with
no registry bearerreturnsno_credential, and one that fails withbearer rejected (AGENT_CREDENTIAL_REVOKED…)returnscredential_refused. Neither has anoperation_id. Any other error still returnsoutcome_unknownwith anoperation_id. - Mutation: deleting the branch at
:572makes that test FAIL. Before, both suites stayed green.
Ran here (Node 22.17, SUTANDO_WORKSPACE_DIR/SUTANDO_TASKS_DIR unset)
ag2space-voice-room-readandag2space-voice-room-actions: 96/96 at the head.- Also 97/97 on a local merge with current
main(49847ba2). The branch is 170 commits behind, but it merges with no conflict.
Gates
- CI at the head: 31 success, 1 skipped. The last run was 2026-10-19, so it is older than current
main. license/cla: success, 1 context.- No unresolved review threads.
- No hardcoded
~/.sutandopath or liveSUTANDO_WORKSPACEdefault in the added lines.
Still open, non-blocking: the per-tool error lists (room-read.ts:772, 800, 838, 863; room-actions.ts:233, 252, 277) still name only not_connected/timeout/unavailable. The shared prompt rule covers the two new codes, so this is optional.
Missing before approval: a substantive APPROVED review at this head from a maintainer with write access. Owed, not blocking: the live voice round trip with a disconnected lane that the PR body names.
— Sutando (susans-MacBook-Pro)
…acts Preserve current immediate-write and navigation behavior while retaining credential-specific failures and both review regression tests. Sutando-Task: task-04f2cc0917c41843aa
|
Updated to Point-by-point response to the outstanding change request:
Merge conflicts resolved individually: kept main's current immediate-write/confirmation-after behavior and owner-navigation prompt, then appended the reconnect sentence; combined Validation before push, Node 22.22.3 with fresh Touched TypeScript ESLint and Still owed: a live disconnected-lane voice round trip; this update did not deploy or restart a service. The existing formal CHANGES_REQUESTED remains until its reviewer clears it. Worst-case risk is an incorrect claim that a write did not dispatch; the credential-specific branch and generic unknown-outcome control explicitly pin that distinction. |
qingyun-wu
left a comment
There was a problem hiding this comment.
Exact-head re-review at a0684b1: scoped LGTM; both previously blocking findings are resolved, and I found no new blocker in the current topic diff.
- resolveRoom now propagates missing/refused credentials from an empty-index refresh, while timeout/unavailable retain the cached-list fallback. Independently removed only that credential return: the focused empty-index test ran and failed by assertion (not_found instead of no_credential). Restored source.
- roomExecute preserves the distinction between pre-dispatch credential failures (no operation_id) and ambiguous write failures (outcome_unknown plus operation_id). Independently removed only that branch: the focused write test ran and failed by assertion (outcome_unknown instead of no_credential). Restored source.
- The optional per-tool feedback is addressed through shared credential wording in the read/search/Commons/write/navigation descriptions and the catalog's shared error contract. The merged immediate-write and navigation behavior remains in place; no old confirmation gate was reintroduced.
Ran room-read and room-actions suites: 111 tests pass, none skipped. The two mutation runs each collected one test and each failed as intended. review-checks is PARTIAL (hardcoded-path/root checks clean; no Python prose-cap scope). Worktree clean after restoring the probes and removing the temporary dependency link.
Latest exact-head CI snapshot: 41 successful, 2 queued, 1 running, 1 skipped. No failures observed. I did not run the disconnected-lane live voice round trip or deploy/restart anything. This is COMMENT, not full readiness approval. Susan's formal CHANGES_REQUESTED remains a separate GitHub state and should be replaced/dismissed by its reviewer now that the cited blockers are verified fixed; this comment does not clear it.
sonichi
left a comment
There was a problem hiding this comment.
Approving at a0684b1.
Both blockers from @liususan091219's request for changes at 9b575a3 are fixed. Her non-blocking point is done too.
room-read.ts:328-334: when the room-list refresh fails because the credential is missing or refused,resolveRoomnow returns that error. A timeout or "unavailable" still falls back to the cached list. The cold-index test starts from an empty index and expects the credential error, notnot_found.room-read.ts:573-574: a write that fails because the credential is missing or refused returns that error with nooperation_id. Any other thrown error still returnsoutcome_unknownwith an id. I removed each branch in turn, and each removal fails a test.- Each room tool and room-actions tool description now names the two new codes through
ROOM_CREDENTIAL_ERRORS.
The three matched texts appear verbatim in the installed ag2-mcp-proxy.mjs: no registry bearer, bearer rejected (AGENT_CREDENTIAL_*) and authentication failed after re-mint. The re-mint one is the proxy's persisting-401 branch, which runs before dispatch, so "nothing ran" is accurate. A 401 with no code and a crashed MCP process both stay unavailable. Only the error code reaches the model. The raw proxy text goes to the console log only, which already happened on warm-up.
I ran ag2space-voice-room-read and ag2space-voice-room-actions locally: 111/111 pass.
Nit (room-read.ts:359): the AGENT_CREDENTIAL_ pattern matches anywhere in the error text. Anchoring it to bearer rejected ( would stop an unexpected upstream message from being read as "nothing ran" on a write. One room-actions test fixture would need its text updated.
Still owed, not blocking: the live voice round trip with a disconnected lane that the PR body names. CI on this head is still running and should be green before merge.
(agent-assisted, Blue for Chi's seat)
|
@cla-assistant check |
|
Updated head Conflict resolution: the sole conflict was Review follow-up:
Validation: all four voice suites ( Normal push, no force; author and committer Qingyun Wu qingyun0327@gmail.com. Requesting fresh review from sonichi and marklysze, and a re-review from liususan091219. PR remains unmerged. |
sonichi
left a comment
There was a problem hiding this comment.
Re-approving at de3dda2.
Since my approval at a0684b1 the head is a main merge plus one change to this PR's own code. The credential match in room-read.ts is now anchored to the proxy's bearer rejected (AGENT_CREDENTIAL_… wording, so an upstream error that merely mentions a credential code is no longer read as "reconnect". New tests pin both sides: a bare mention reads as unavailable from mcpFailure, and a send that hits it after dispatch stays outcome_unknown with its operation_id. Otherwise the PR's diff against main is unchanged. Approving once CI is green at this head.
(agent-assisted, Blue for Chi's seat)
liususan091219
left a comment
There was a problem hiding this comment.
Approving at de3dda2b. This replaces my CHANGES_REQUESTED at 9b575a34. My comment at ac49ff9d confirmed both blockers fixed.
Checked at this head
room-read.tsresolveRoom: a refresh thatmcpFailureclasses asno_credentialorcredential_refusedreturns that error.timeoutandunavailablestill fall back to the cached list.roomExecute: a credential failure returns that error with nooperation_id. Any other thrown error staysoutcome_unknownwith one.- New since my last look: the
credential_refusedmatch is anchored tobearer rejected (AGENT_CREDENTIAL_…. An upstream message that only mentions a credential code now reads asunavailable, and on a write it staysoutcome_unknown. Tests pin both sides. This closes the write-side risk @sonichi raised. - My optional point is done too: each room tool description now carries
ROOM_CREDENTIAL_ERRORS.
Gates
- An expert approval is at this head: @sonichi re-approved
de3dda2b. - CI passes on every check (PowerShell is skipped).
license/clais success, 1 context. There are 0 unresolved threads. - No added line hardcodes a path. The tests call the real
room-read/room-actionsexports. No source-grep.
Owed, named, not blocking: the live voice round trip with a disconnected lane that the PR body names.
— Sutando (susans-MacBook-Pro)
Problem
On 2026-10-15 (desktop v0.7.16, engine de81346) the voice agent's AG2 Space MCP proxy kept a stale descriptor after Connect rewrote the credential. Every voice room read then failed with:
The voice model told the owner "I can't read Qingyun Group right now". Nothing told her the fix was to reconnect. That's because every thrown MCP error became
"unavailable".The proxy-side race is fixed in ag2-space/ag2space-cinny-desktop#834. This PR is the voice-side half agreed with that desktop session: name the credential state, so the user hears "reconnect" instead of "unavailable".
Change
mcpFailure(why)inskills/ag2space-voice/room-read.tsclassifies the proxy's own error texts:no registry bearer→no_credentialAGENT_CREDENTIAL_*codes, orauthentication failed after re-mint→credential_refusedtimed out→timeoutunavailable, as beforeroomRead,roomExecuteandlist_roomsuse it.list_roomsstill reports a timeout asunavailable, unchanged.ROOM_READ_PROMPT_RULEgains one sentence: either credential error means the owner must re-run Connect, so say that, not that the room is unavailable.warm-up failed (no_credential): ….Warm-up readiness itself was already correct. "MCP ready" is logged only after
room.listsucceeds through the authenticated proxy. In the incident it had passed at 04:10:24, before Connect's 04:10:28 rewrite. So this PR doesn't change readiness.Evidence
The same three voice suites were run at each commit.
With the fix (HEAD 3dc2876):
New tests against main's
room-read.tsandtools.ts, source stashed:Other checks:
tsc --noEmitandtsc -p tsconfig.skills.json --noEmit: 0 errors.eslinton the touched files: clean.Notes
engine/ag2-mcp-proxy.mjson desktop main and on feat(screen-companion): add pair-debug + pair-review-code configs (closes #799) #834.— sutando-qingyun-air
🤖 Generated with Claude Code