Skip to content

feat(server): MCP errors that say what to send, and idempotent archive (TEC-1819) - #356

Merged
bk-agent-01 merged 7 commits into
stagefrom
t3/tec-1819-actionable-errors-idempotent-archive
Oct 29, 2026
Merged

bk-agent-01 merged 7 commits into
stagefrom
t3/tec-1819-actionable-errors-idempotent-archive

Conversation

@bk-agent-01

@bk-agent-01 bk-agent-01 commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Closes TEC-1819

Summary

Agents that send a wrong command now get an error that says what to send. Archive retries are safe. Agents on the OAuth tool list (Toolyard) can now archive a session and read an action's input schema.

Archive error, before:

Orchestration command invariant failed (thread.archive): The V2 command could not complete.

After: a second t3_session_action action=archive returns success with changed: false and dispatches nothing. When a race still causes a refusal, the agent sees the real reason:

Thread <id> is already archived.

t3_dispatch_command with {"type":"thread.archive","commandId":"mcp:1"}, after:

Invalid thread.archive command.
Problem: <the decode issue for this one command>
Required fields: type, commandId, threadId.
Example: {"type":"thread.archive","commandId":"mcp:<unique-id>","threadId":"<threadId>"}
commandId is any new unique string you choose for this command.
Use t3_session_action action=archive instead; it builds this command for you and is safe to retry.

An unknown type gets the closest valid type and the list of valid types one time.

Other changes:

  1. New agent tool t3_session_lifecycle (archive, unarchive, stop, snooze, unsnooze, delete). It uses the same code as t3_session_action. It is marked destructive because of delete. It is in the OAuth agent tool list automatically.
  2. t3_ui_list_tools entries now have a one-line description and requiredFields. tools: [name] (up to 5) or tool: name returns the full inputSchema, so an agent does not need t3_ui_get_tool. query also searches descriptions.
  3. The t3_ui_call, t3_ui_batch and t3_ui_get_tool input schemas no longer contain the enum of about 250 names (about 17k characters). The server still checks the name and returns the closest names.
  4. t3_start_session: the result starts with the full session id. The timeoutSeconds text says to keep it at 50 or less and to retry with the same clientRequestId.
  5. The t3_dispatch_command description points to t3_session_action and t3_ui_get_tool.
  6. Cross-review round 1 fixes: t3_session_lifecycle uses the same session policy as t3_session_action (an in-session credential acts only on its own session). A no-op archive with a clientRequestId writes a command receipt, so a retry replays the no-op. Two archive calls that race both succeed.
  7. Cross-review round 2 fixes: the receipt store is now required, and the control and agent tool registrations provide it, so the no-op receipt persists in production. When an archive loses a race, its rejected receipt is replaced by an accepted no-op, so a retry after an unarchive does not fail.

Decisions

Decision Options Why Undo Confidence
Fix in the fork-owned MCP layer, not in Orchestrator.ts / OrchestrationEngine.ts Change the engine error text; read the cause chain in the MCP layer No upstream-owned file changes, so no merge cost Revert the PR High
Idempotent archive by one read before dispatch Read getThreadShellById first; catch the refusal and turn it into success One indexed row read; no string matching on error text Revert High
Show the deepest string cause as the control error message Keep the generic message; append the cause; replace it The cause is a full sentence for the agent; the generic text has no value Revert rejectionReason Medium: it applies to every control tool error that carries a string cause
Agent tool name t3_session_lifecycle, shared runSessionLifecycle Add t3_session_action to the agent list; new agent-shaped tool Agent tools use session refs and Markdown results; the name avoids words that adapters classify Remove the tool High
t3_ui_list_tools compact entries: name, description, requiredFields, readOnly, authorized (streamMode for streams) Keep all fields The coordinator asked for a compact list; method, category and scope are in t3_ui_get_tool Restore the spread Medium
Descriptions: schema annotation, then a hand-written map of about 45 common actions, then the generic text Annotate every RPC in contracts Contracts have no annotations now; editing contracts is a wider change Remove the map High
Authorize t3_session_lifecycle targets with resolveMcpSessionTarget Own check in the agent tool; shared policy The new tool must never allow more than t3_session_action Revert High
Record a no-op as an accepted command receipt with sequence 0 A new table; no record It reuses the receipt store that the orchestrator replays, so a later dispatch with that id does nothing Revert; the receipts stay but are harmless Medium: sequence 0 marks a no-op
Provide the receipt store to the two MCP registrations (McpHttpServer.ts, marked) Export it from the runtime layer; keep it optional The runtime keeps its store private; the store is a stateless wrapper over the receipt repository that the server already exposes (same as ManagerService.expbkt3.ts) Revert High
Replace this command's rejected receipt with an accepted no-op after a state race Teach the retry path to read a rejected receipt The rejected receipt has only a generic error, so the reason cannot be read back; only this command id is touched Revert Medium
Treat the refusal "already archived" / "not archived" after the read as success Retry; fail That is the state the caller asked for; other refusals still fail Revert High
Drop the name enum from the three bridge schemas Keep the enum The enum made each schema about 17k characters; the server still validates Restore the enum High

Acceptance check

Item Proof Status
Dispatch error lists required fields for thread.archive without threadId dispatchCommandHelp.test.ts pass (CI run 40829597934)
Unknown type suggests the closest type dispatchCommandHelp.test.ts pass (CI run 40829597934)
Archive of an archived session is success with no dispatch handlers.sessionLifecycle.expbkt3.test.ts, sessionLifecycle.expbkt3.test.ts pass (CI run 40829597934)
The cause string "Thread is already archived." reaches the agent handlers.sessionLifecycle.expbkt3.test.ts pass (CI run 40829597934)
t3_ui_list_tools entries have requiredFields and descriptions registration.listTools.expbkt3.test.ts pass (CI run 40829597934)
t3_ui_list_tools tools:[name] returns inputSchema registration.listTools.expbkt3.test.ts pass (CI run 40829597934)
Bridge schemas have no name enum; unknown name gives closest names registration.listTools.expbkt3.test.ts pass (CI run 40829597934)
t3_session_lifecycle is in the agent list, destructive, idempotent archive, same clientRequestId gives same command id sessionLifecycle.expbkt3.test.ts pass (CI run 40829597934)
A provider credential cannot act on another session with t3_session_lifecycle sessionLifecycle.expbkt3.test.ts pass (CI run 40829597934)
A no-op retry with the same id replays after the state changed handlers.sessionLifecycle.expbkt3.test.ts pass (CI run 40829597934)
No-op replay works through the production agent registration sessionLifecycle.registration.expbkt3.test.ts pass (CI run 40829597934)
Race refusal, then unarchive, then retry with the same id: success, no change handlers.sessionLifecycle.expbkt3.test.ts pass (CI run 40829597934)
Two racing archives (and unarchives) both succeed handlers.sessionLifecycle.expbkt3.test.ts pass (CI run 40829597934)
t3_start_session text: full id first, timeout advice Code change in agent/handlers.ts and agent/tools.ts pass (CI run 40829597934)

How I tested

No local tests on the shared host; CI runs the suite.

  • Cross-review round 1 (gpt-6.1-sol): 1 P1 and 2 P2, fixed in b34d819c8 (test type fix in 6c6785abf); threads answered and resolved.
  • Cross-review round 2: r1-2 reopened and 2 new P2, fixed in 9ee59f7b5; threads answered and resolved.
  • Head 9ce0f56b5 (with origin/stage merged, which has the storageCleanup fix from fix(server): storage cleanup handles newer git's broken-gitfile error #357): every named check passes, including typecheck, lint, format, fork markers and all 8 server test shards. The new and changed test files pass (27 tests).

🤖 Generated with Claude Code

…ycle tool for agents

- t3_dispatch_command: a wrong command names its type, required fields and a
  minimal example; an unknown type gets the closest valid one; lifecycle
  commands point to t3_session_action.
- Control errors bring the orchestrator's own reason through (for example
  "Thread x is already archived.") instead of the generic invariant text.
- t3_session_action archive/unarchive are idempotent: already in the target
  state returns success with changed: false and dispatches nothing.
- New agent tool t3_session_lifecycle (archive, unarchive, stop, snooze,
  unsnooze, delete; destructive) on the shared runSessionLifecycle, so OAuth
  agent-list clients can archive sessions.
- t3_ui_list_tools: one-line descriptions, requiredFields, description search,
  and tools/tool to return input schemas without t3_ui_get_tool. The call,
  batch and get schemas no longer embed the ~250-name enum; unknown names get
  the closest matches.
- t3_start_session: advise timeoutSeconds <= 50 and retry with the same
  clientRequestId; the result starts with the full session id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL labels Oct 28, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@bk-agent-01 bk-agent-01 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-review round 1: three findings on the requested head. No tests, builds, or linters ran on this host.

bk-agent-01 and others added 2 commits October 28, 2026 18:22
… race

- t3_session_lifecycle authorizes its target through resolveMcpSessionTarget
  (t3.control), the same policy as t3_session_action: a provider credential
  without user-wide scope acts only on its own session.
- A no-op archive/unarchive with a caller's command id writes an accepted
  command receipt with sequence 0; a retry of that id replays the no-op, and
  a later dispatch of that id replays instead of acting.
- An archive/unarchive that loses a race and is refused as already in the
  target state returns success with changed: false; other refusals still fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@bk-agent-01 bk-agent-01 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[cross-review r2 · gpt-6.1-sol] P2 Two retry defects remain. The authorization fix is accepted.

bk-agent-01 and others added 3 commits October 29, 2026 06:22
- The lifecycle paths now require CommandReceiptStoreV2. The control and
  agent toolkit registrations provide CommandReceiptStore
  .layerFromApplicationReceipts over the receipt repository the server
  already exposes, so no-op receipts persist in production.
- When this command loses a race to the state it asked for, the rejected
  receipt the orchestrator wrote for its command id is replaced by an
  accepted no-op (upsert), so a retry after an inverse action replays the
  no-op instead of failing as previously rejected.
- Tests: rejected-receipt race path, and a replay through the production
  agent registration layer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bk-agent-01

Copy link
Copy Markdown
Collaborator Author

Cross-review record — gpt-6.1-sol on 9ce0f56

Rounds: 3. Verdict: clean.

Round Finding Severity Outcome (commit or reason)
1 r1-1: lifecycle session authorization P1 Fixed in b34d819; accepted in round 2.
1 r1-2: durable no-op replay P2 Fixed in b34d819 and 9ee59f7; accepted in round 3.
1 r1-3: concurrent archive refusal P2 Fixed in b34d819; accepted in round 2.
2 r2-1: production receipt service P2 Fixed in 9ee59f7; accepted in round 3.
2 r2-2: rejected receipt after a race P2 Fixed in 9ee59f7; accepted in round 3.

Deferred: none. Fallback: none.

The lifecycle tools require the receipt service. Both registrations provide the adapter over the same application receipt repository used by the orchestrator. The adapter keeps no separate state.

The command id includes the authenticated caller namespace, target session, lifecycle action, and request id. The replacement uses that same command id. Only the specific archive or unarchive state refusal permits replacement; unrelated errors remain failures.

I read every non-merge commit after 6c6785a and the relevant production paths. I also read the regression tests and CI results. I ran no tests, builds, or linters on this host.

@bk-agent-01
bk-agent-01 marked this pull request as ready for review October 29, 2026 06:39
@bk-agent-01

Copy link
Copy Markdown
Collaborator Author

Merge decision: tushar chose "AI fixes it" (test fix, then merge B1 and B2 to stage) in the T3 session on 2026-10-29. Head 9ce0f56, all checks green, gpt-6.1-sol cross-review clean after 3 rounds, no open threads. bkmain promotion stays tushar's call.

@bk-agent-01
bk-agent-01 merged commit fb99711 into stage Oct 29, 2026
29 checks passed
bk-agent-01 added a commit that referenced this pull request Oct 29, 2026
Promote stage to bkt3 (2026-10-29): agent toolkit compact output, session lifecycle tool, actionable MCP errors (#355, #356, #357)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant