Skip to content

fix(orchestration): byte-cap MCP outputs, tombstones, and transcript tail rings - #510

Merged
Juliusolsson05 merged 2 commits into
mainfrom
fix/orchestration-byte-caps
Jul 9, 2026
Merged

Juliusolsson05 merged 2 commits into
mainfrom
fix/orchestration-byte-caps

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Problem

Orchestration MCP outputs were bounded by message COUNT only — nothing capped size (#373):

  • 5x/7x duplication: one child answer of N chars appeared five times in a single orchestration_read_agent response — output.messages[last].text, output.agent.latestAssistantText, output.agent.finalAssistantText, output.latestAssistantText, output.finalAssistantText. finalAssistantText was never independently computed; it was always a verbatim alias of latestAssistantText. Closed agents in orchestration_wait_agents hit seven times (two more copies in agents[i]).
  • No char caps anywhere: visibleMessageSummary capped count (maxMessages, default 20) but messages[].text and the live-turn text were full entryTextContent — one pasted diff or giant tool dump in a child message blew the parent's context.
  • Tombstones stored verbatim: OrchestrationBridge.noteClosed kept the entire pre-close output in main's memory for up to 24h x 500 agents; limitOutputMessages count-sliced only.
  • Tail-ring blowup: AgentTranscriptReader pushed RAW untruncated items into the tail ring (tail schema max 10,000) while streaming; per-item truncation only happened afterwards in boundItems.

Fix

Renderer (orchestrationMcp.ts) — maxCharsPerMessage (default 4000) on every extracted text including the live turn; maxCharsPerAgent running budget (default 24000) across the message tail, oldest dropped first (newest-first scan, mirroring the transcript reader's budget semantics); per-message and per-output truncated flags + totalChars original-size estimates. Truncation marker matches truncateItemText (…\n[truncated]). The record-level latestAssistantText/finalAssistantText mirrors become 400-char excerpts; exactly ONE full (per-message-capped) copy remains at output.latestAssistantText. Lifecycle hazard preserved: hasDurableOutput still sees non-empty text — truncation shortens, never drops, so completed children cannot regress to waiting.

MCP tools (createBuiltInMcpServer.ts) — orchestration_read_agent, orchestration_read_run_outputs, orchestration_wait_agents accept maxCharsPerMessage/maxCharsPerAgent (ranges match the agent_transcript_* tools); the two multi-agent tools add maxTotalChars (default 120,000) enforced in the handler: over budget, outputs degrade summary-first — every agent keeps status/lifecycle/counts plus a short excerpt, then leftover budget restores full outputs in order — with top-level truncated: true. No agent is ever dropped entirely.

Bridge (OrchestrationBridge.ts) — noteClosed byte-clamps stored tombstones (MAX_CLOSED_AGENT_CHARS = 262,144 message budget + 16,384 per mirror field, defense-in-depth against uncapped senders); limitOutputMessages honors caller char caps for tombstone serving; the new cap params joined the cachedReadRunOutputs cache key so differently-capped reads can't collide in the 250ms polling cache.

Transcript reader (AgentTranscriptReader.ts) — the tail ring now stores per-item truncated clones (idempotent; boundItems stays the final pass), and the final-projection re-add check now guards against the ring holding clones instead of raw references (pre-existing duplicate-final edge case).

Plumbing — request types in orchestrationTypes.ts, renderer handler pass-through in workspace/hook/index.ts.

Verification

  • npx tsc -b tsconfig.node.json — clean
  • npx tsc --noEmit -p tsconfig.web.json — 0 errors
  • NODE_ENV=test npx vitest run — 467 passed; only the pre-existing agent-voice-dictation hotkeyBinding suite failure (present on main)
  • Scratch harness exercised the pure cap logic: per-message truncation + flags, oldest-first budget drops, latest-assistant survival under starvation, summary-first total-budget degradation (no agent dropped, result fits budget), under-budget passthrough

Closes #373

🤖 Generated with Claude Code

Juliusolsson05 and others added 2 commits July 9, 2026 11:17
…tail rings

Orchestration MCP reads only capped message COUNT, never size. One child
answer was mirrored 5x per orchestration_read_agent response (7x for
closed agents in wait_agents), a single huge child message could blow the
parent's context, closed-agent tombstones stored pre-close outputs
verbatim in main for 24h, and the transcript reader's tail path buffered
raw untruncated items in its ring before bounding.

- orchestrationMcp: maxCharsPerMessage (default 4000) on every extracted
  text incl. the live turn; maxCharsPerAgent running budget (default
  24000) over the message tail, oldest dropped first; per-message and
  per-output truncated/totalChars metadata; record-level latest/final
  mirrors become 400-char excerpts, keeping exactly one full capped copy
  at output.latestAssistantText (lifecycle hazard preserved: text is
  shortened, never dropped).
- MCP tools: read_agent/read_run_outputs/wait_agents accept
  maxCharsPerMessage/maxCharsPerAgent; read_run_outputs and wait_agents
  add maxTotalChars (default 120000) with summary-first degradation —
  every agent keeps status/lifecycle/counts plus a short excerpt, leftover
  budget restores full outputs in order.
- OrchestrationBridge: tombstones byte-clamped at storage time
  (MAX_CLOSED_AGENT_CHARS 256KiB + per-field clamp), limitOutputMessages
  honors caller char caps, and the new cap params joined the
  cachedReadRunOutputs cache key so differently-capped reads cannot
  collide in the 250ms polling cache.
- AgentTranscriptReader: tail ring now stores per-item truncated clones
  (boundItems stays the final pass), and the final-projection re-add
  guards against the ring holding clones instead of raw references.

Closes #373

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…or, total-budget enforcement, recovery hints

Finding 1 (blocker): budget starvation dropped the NEWEST message. When
maxCharsPerAgent was smaller than one per-message-capped text, both the
renderer's visibleMessageSummary and the bridge's capMessagesByChars
returned `messages: []` for an agent that plainly produced output. The
newest message is now exempt from dropping: it is truncated down to the
agent budget instead (minimal-excerpt floor), in both paths; older
messages drop exactly as before.

Finding 2 (blocker): boundOutputsToTotalChars never enforced
maxTotalChars when the summary-only set already exceeded the budget —
`used` started over budget, the upgrade loop never fired, and the whole
summary set was returned anyway. Summaries now shrink their free-form
text fields in progressive steps (200/100/48 chars) until the set fits;
the floor keeps latestAssistantText present and non-empty (lifecycle
presence invariant). Residual overflow is per-agent JSON skeleton only,
which cannot be cut without dropping agents entirely. wait_agents'
sibling `agents` array previously rode outside the budget; its JSON
size is now charged against maxTotalChars as reservedChars so one
budget governs the whole tool response.

Finding 3 (nit): the three orchestration read/wait tool descriptions
now tell callers how to recover truncated content — re-read with
explicit larger caps, or agent_transcript_read_file for the
disk-backed transcript.

Verified with throwaway runtime smokes (not committed, per repo test
policy): all three fail on the pre-fix code and pass on this commit;
tsc node+web and the vitest suite pass (hotkeyBinding pre-existing
failure ignored; SessionRecorderManager flake passes in isolation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

Add byte caps to orchestration MCP outputs and closed-agent tombstones

1 participant