Skip to content

fix: send the Agent SDK's initialize request to every relaunched stream-json child - #330

Merged
anaclumos merged 2 commits into
mainfrom
fix/322-relay-initialize-on-resume
Oct 6, 2026
Merged

anaclumos merged 2 commits into
mainfrom
fix/322-relay-initialize-on-resume

Conversation

@anaclumos

Copy link
Copy Markdown
Owner

Closes #322.

Problem

The Agent SDK sends its initialize control request once per query. The request carries appendSystemPrompt, the hook callback ids, the SDK MCP servers, agents, and the other init settings. The supervisor's stdin relay forwarded each harness line to the live child once, so after a move that resumed with --resume, the relaunched child never received initialize. A T3 Code thread lost its appended runtime instructions after its first move, and an SDK consumer lost its hooks and in-process MCP servers.

Change

  • StdinRelay keeps the latest line of each control request in REPLAYED_REQUESTS (initialize, set_model, set_permission_mode, set_max_thinking_tokens) for the life of the session, and attach sends those lines to every child ahead of the resume prompt and the queued input. A first launch has none, so it is unchanged.
  • The --session-id replay from fix: replay a stream-json session's lost lines on a move before its first transcript row #324 keeps the other lines the killed child received (user lines), and still answers every other control request with an error control_response. Setters now go before the replayed user lines instead of in arrival order, so a replayed first turn runs under the latest model and permission mode.
  • AGENTS.md, DESIGN.md, and docs/content/docs/architecture.mdx describe the relaunch.

Decisions

  • Replaying initialize is safe for the SDK. In @anthropic-ai/claude-agent-sdk 0.3.276, Query.readMessages files a control_response with no pending request in a bounded unmatchedControlResponses map, and hookCallbacks lives on the Query object for the life of the query, so the hook callback ids in the replayed request still resolve. This answers the issue's open question about hook callback ids.
  • Keeping the latest line per subtype bounds the state to four lines, instead of the full control history.

Verification

Hermetic run under a throwaway HOME and TOKENMAXXING_HOME, with a stub claude that records its stdin and writes a respawn marker after the first user line:

  • --resume move, on main: the relaunched child receives only the resume prompt and later input. On this branch: initialize, set_model, the resume prompt, then later input.
  • --session-id move (no transcript) with an mcp_call request before it: the relaunched child receives initialize, set_model, the replayed user line, then later input, and stdout carries the error control_response for the mcp_call.

Not run: a live T3 Code session through a real move, because that spends pooled quota.

…am-json child

The Agent SDK sends initialize once per query. It carries the appended
system prompt, the hook callback ids, the SDK MCP servers, and the other
init settings. The stdin relay forwarded it to the first child only, so a
move that resumed the session with --resume relaunched Claude Code without
those settings.

The relay now keeps the latest line of each control request in
REPLAYED_REQUESTS for the life of the session and sends those lines to
every relaunched child ahead of the resume prompt and the queued input.
The --session-id replay keeps the other lines the killed child received.

Closes #322
@vercel

vercel Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
tokenmaxxing Ready Ready Preview Oct 6, 2026 2:32pm UTC

Request Review

…ume prompt

A REPLAYED_REQUESTS line that arrives while no child is attached now updates the saved state instead of the queue, so the relaunched child receives it before the resume prompt and the turn runs under the latest model, permission mode, and thinking budget.
@anaclumos

Copy link
Copy Markdown
Owner Author

Adversarial review (OpenCode on MiMo v2.6 Pro, because the Codex review plugin is not installed on the host) returned three findings:

  1. Map order with delete then set lets setters precede a new initialize. Not a defect. The Agent SDK sends one initialize per Query, and each query spawns its own process, so one supervisor's stdin carries one initialize. initialize is the first line, so it stays first. The follow-up commit drops the delete, and the saved lines keep first-arrival order.
  2. A setup request that arrives while no child is attached (compaction, depleted wait) went to the queue behind the resume prompt, so that turn ran on the old model or permission mode. Fixed in the follow-up commit: such a line updates the saved state, which attach sends before the resume prompt. Verified hermetically: a set_model sent during a 3-second wait reaches the relaunched child before the resume prompt, and the stale value is no longer sent.
  3. A new initialize does not clear the previous query's setters. Not a defect, for the same reason as finding 1.

@anaclumos
anaclumos merged commit 31528ab into main Oct 6, 2026
5 checks passed
anaclumos added a commit that referenced this pull request Oct 6, 2026
Carry #330 into the restructured architecture page: the harness-session paragraph names the setup requests the supervisor sends each relaunched process before the first prompt.
anaclumos added a commit that referenced this pull request Oct 6, 2026
Closes #311.

The docs site now carries its flows and structures as diagrams, tables, step lists, and file trees, and its long paragraphs are split into short sections.

## What changed

- **Mermaid diagrams.** A fenced `mermaid` block in MDX renders as a diagram, client side, with the `mermaid` package (12.1.0), in the `neutral` theme in light mode and the `dark` theme in dark mode. A diagram that fails to parse falls back to its source in a code block.
- **Fumadocs components.** `Steps`, `Files`, and `Tabs` are registered for MDX.
- **Pages.** `index`, `architecture`, `switching`, `credentials`, `state-directory`, `hub`, `codex`, `pi`, `quickstart`, `configuration`, and `distribution` get 44 diagrams in total, plus tables for enumerations (fields, kinds, cases, conditions to outcomes), `###` subsections that split the long paragraphs, a `Files` tree of the state directory, and `Steps` for the setup procedures.

## Decisions

- **Every claim stays.** The docs rule is to preserve every claim when rewriting, so a sentence was deleted only when a diagram, a table, or another sentence on the same page states all of its content. Each page was checked sentence by sentence against its version on main. The pages are therefore not much shorter in characters; the gain is that no paragraph runs past about four sentences and each flow has a picture.
- **A local remark plugin instead of `remarkMdxMermaid`.** `fumadocs-core` ships `remarkMdxMermaid`, but in the processed Markdown that `llms-full.txt`, each page's `content.md`, and the **Copy Markdown** button serve, its `<Mermaid>` element carries the diagram in a `chart` attribute with every double quote escaped as `&#x22;`, which no Markdown viewer renders as a diagram. `docs/lib/remark-mermaid.ts` emits the same element and sets `_stringify.text` to the original fenced `mermaid` block, so the exported Markdown carries each diagram's source unchanged. `_stringify.node` would not work: the Fumadocs stringifier keeps the JSX handler after it swaps the node, so each diagram exports as `<></>`.
- **Pages left as they are.** `commands`, `platforms`, `statusline`, `troubleshooting`, `limitations`, `terms`, `switching-profile`, and `status-only-pools` are already tables, lists, or short prose.
- **Changes from main carried over.** The sentence changes of #321 (`configuration`, `switching`), #324 and #330 (`architecture`), #328 (`pi`, `quickstart`), #331 (`switching`), and #334 (`configuration`, `hub`) are restated in the restructured text. For #328, the `init` diagram on `quickstart` also names the PATH line on a second `init`. For #331, the new **Banked resets** section keeps every sentence, is split into subsections, and gets a diagram of the spend decision and one of the claim. For #334, the buffered usage quota text is a **Buffered usage quota** section on `configuration` and a subsection under the `api-call` requests on `hub`, with its sentences split into a list.
- **Stale claims found during the review** were filed separately as #327 and fixed in #328, not here, because this change restates the pages without changing what they claim.

## Verification

- Every diagram of every changed page renders with mermaid 12.1.0 in a headless browser in both themes, with no parse failure and a natural width of at most about 950px against the 825px column, and each rendering was looked at for legibility and overlap.
- The processed Markdown keeps all 44 diagrams as fenced `mermaid` blocks with no empty fragment: `llms-full.txt` holds 44, and the `content.md` routes of `architecture`, `quickstart`, `credentials`, and `switching` hold 10, 1, 7, and 7, read from a local dev server.
- A scripted check confirms that every `##` heading and every link of the old pages is still present and that every cross-page and in-page anchor resolves.
- No page has an em dash, an en dash, or a curly quote.
- The Vercel Preview deployment of the head commit is the build and content check. The project names no end-to-end target, and the diff changes only the docs app.
anaclumos added a commit that referenced this pull request Oct 16, 2026
…launched child (#449)

Closes #447.

Author model: claudeAgent/claude-fable-5-1

## Problem

T3 Code 0.1.0 registers its `t3-code` MCP server through the Agent SDK's runtime control request `mcp_set_servers` over stdin (pingdotgg/t3code#17898, merged 2026-10-10), not through `--mcp-config`: every launch of a T3 session, T3's own and the supervisor's moves alike, carries the same argv with no MCP flag, and the child's environment holds no MCP variable. The stdin relay kept only `initialize`, `set_model`, `set_permission_mode`, and `set_max_thinking_tokens` for the life of a session and replayed those to each relaunched child, so a move dropped the MCP request. The moved child came up with no `t3-code` server, `ToolSearch` listed only the servers from settings, and every T3 tool was gone until T3 Code stopped and relaunched the session itself. On 2026-10-16 the T3 Code Captain relaunched seven threads by hand for this, several for the second or third time that day.

## Change

- `src/entries/supervisor.ts`: `mcp_set_servers` joins `REPLAYED_REQUESTS`, so `StdinRelay` keeps the latest one and `attach` sends it to every relaunched child with the other setup requests, ahead of the resume prompt.
- `docs/content/docs/architecture.mdx`: the two sentences that list the replayed setup requests name the MCP servers the harness registers at runtime.

## Decisions

- The replayed set is `initialize` plus the SDK's session-state setters, and every other control request gets no replay because a repeat could run it twice (#330). `mcp_set_servers` is a setter: Claude Code replaces a named server only when its config differs from the one it holds, so a replay to a child that already has the server is a no-op, and a fresh child registers the same servers the harness last set. That makes it idempotent in the same sense as `set_model`, so it belongs in the set rather than in the error-reply path.
- No code comment marks the addition; the docs carry the explanation, as the surrounding list does.
- Nothing changes for the codex and muse supervisors: both spawn with an inherited stdin and have no relay, so this request shape never reaches them.
- Found alongside and filed separately as #448, not changed here: on a `--resume` move, a non-setup control request the killed child never answered gets neither a replay nor the moved-request error reply, where the `--session-id` path answers it.

## Verification

A development host runs no check command, so CI carries the typecheck. Hermetic run under a throwaway `HOME` and `TOKENMAXXING_HOME` with a stub `claude` that records its argv and stdin per launch, creates the session transcript, and writes a respawn marker after its first `user` line. The harness fed `initialize`, an `mcp_set_servers` request for one http server, `set_model`, a `user` line, then another `user` line after the move.

- On `main` (`0e3fc40`): the relaunched `--resume` child received `initialize`, `set_model`, the resume prompt, and the later `user` line. No `mcp_set_servers`.
- On this branch: the relaunched child received `initialize`, `mcp_set_servers`, `set_model`, the resume prompt, and the later `user` line.

Not run: a live T3 Code session through a real move, because that spends pooled quota; the first live moves after the release are the check, read from the moved child's tool list.

## After the merge

CI publishes the next version to npm; hosts pick it up through the self-update tick within a day, or by hand with the pinned `bun add -g` the release rules name, followed by a restart of the hub service.


## Review

- Round 1 on base `37574c82` and head `8f996d54`: Muse Wonder Full (`muse/muse-wonder-full-1.0`, high) answered a quota 429 (usage window reset 10:20Z) with no report; its replacement in the same round, GPT Astra (`codex/gpt-6-astra`, medium), reviewed the prospective merge and approved with no findings, noting that its review was static and that Claude Code's handling of a replayed request id was not independently verified (the hermetic stub run above covers the relay side only).

This branch was successfully deployed

1 active deployment
Preview — 9afb030e Deployed Oct 6, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A move that resumes a stream-json session never sends the Agent SDK's initialize request to the relaunched child

1 participant