Repository navigation
Description
Symptom (observed live, captured in a session recording)
While a subagent (Task/Agent) runs, its own internal work — Read/Bash tool calls, narration text, even its final report — paints as rows in the parent's main feed. Only the parent's output belongs there; the subagent's activity should be sealed inside its TaskSubagentRow card. Confirmed by watching a live fan-out and by the session recorder (#467).
Recording evidence (this is the fixture)
A session recording of a real subagent fan-out (~/.config/agent-code/session-recordings/…, ~1065 events) shows the leak in data. One parent session:semantic-event stream contained 13 distinct turns, all src=proxy, interleaving the parent and its subagents:
turn … src=proxy tools=['Agent','Agent'] :: "Perfect — you're recording this pane…" ← parent (real)
turn … src=proxy tools=['Read','Read','Read'] :: "I'll investigate the subagent transcript…" ← SUBAGENT (leak)
turn … src=proxy tools=['Read','Read','Read','Read'] :: "Let me read the remaining key files." ← SUBAGENT (leak)
turn … src=proxy tools=['Bash'] :: "Let me confirm the Codex parent tails only…" ← SUBAGENT (leak)
turn … src=proxy tools=[] :: "I have a complete picture. Here is the data-flow map…" ← SUBAGENT final report (leak)
Parent-stream tool totals: Read ×18, Bash ×8, Agent ×4 — the Read/Bash counts are almost entirely subagent-internal. flow_selected: 10 / flow_ignored: 23 shows the semantic adapter trying to attribute flows but leaking subagent ones through.
Root cause — TWO carriers, neither filtered
The subagent-detection code-read initially concluded "child activity is structurally isolated" — true for one plane, wrong for the feed. There are two:
1. Committed plane (JSONL) — the persistent leak. Claude tags a subagent's turns isSidechain: true and interleaves them into the parent transcript. The field is real (src/providers/claude/types/claudeTranscript.ts:57), parsed (src/providers/claude/renderer/transcript/history.ts:71), and preserved through the codec (packages/agent-transcript-parser/src/codecs/claude.ts:95,114). But the only consumer is the session picker (src/providers/claude/runtime/sessionList.ts:160,339-340), which skips sidechain lines — proof they're in the parent transcript. The feed pipeline never skips them:
RawCommittedEntry(src/renderer/src/rendering/observations/committed.ts:24-37) has noisSidechainfield — structurally blind.collectCommittedCandidates(committed.ts:186-267) only rejectsnot-conversation/meta-entry/ task-notification / synthetic-user. A sidechain assistant turn istype:'assistant', not meta, doesn't start with<→ selected, andminedToolIdseven stamps its inner Read/Bash as owned tools.forwarder.ts:39-42sends the same sidechainjsonl-entryto BOTH the feed and the watcher;jsonlCoalescer/historyLoaderapply no sidechain filter.
2. Live semantic plane (proxy) — the visible streaming leak. The live feed's semantic turns are built from the mitmproxy stream, which intercepts every api.anthropic.com call — parent AND subagent — all tagged src=proxy with no isSidechain marker (it's a JSONL flag, not on the wire). So the adapter's flow attribution can't cleanly separate them and subagent turns render as parent turns while streaming (the recording proof above).
The activity already renders correctly inside the card via the separate watcher channel (SubAgentWatcher → SubAgentState → session:sub-agents → TaskSubagentRow). So the leaked rows are pure duplication — filtering them loses nothing.
The new ledger pipeline reproduces the leak — committed.ts and semantic.ts have zero sidechain/parent-linkage awareness.
Related
- Session Recording Session Recording: continuous debug-gated capture replayable as rendering fixtures #467 (produced the fixture)
- Subagent lifecycle Subagent rows can remain running for days after semantic text says subagents are done #341, umbrella Handle sub-agent rendering for both Claude and Codex #178
- The proxy flow-fingerprinting machinery relevant to carrier Fix post-refactor permission and feed rendering regressions #2 (isSidecarFlow / request_shape, Implement prompt suggestions for Claude/Codex — currently leak into the feed as raw items #174/Handle sub-agent rendering for both Claude and Codex #178)
Resolution plan
See the first comment.
Activity
Juliusolsson05 commentedon Jul 7, 2026
Resolution plan — fixture-first (TDD), two tracks
The whole point of the new pipeline: encode the bug as a failing test (a candidate that should be rejected but is selected), state the expected decision, then fix until green. The decision-reason system makes this a clean assertion.
Track A — Committed plane (clean, test-lockable, ship first)
This kills the persistent leak (what stays after a turn commits) and is a tight, well-diagnosed change.
A1. Fixture / failing tests first.
observations/committed.test.ts): a sidechain assistant entry ({type:'assistant', isSidechain:true, message:{…Read/Bash…}}) must produce a rejectedOwnershipDecisionwith a new reasonsidechain-filtered, NOTselected. AssertminedToolIdsdoes NOT stamp its inner tools as committed-owned. This test fails today (currently selected) — that failure IS the bug captured.__tests__/fixtures.sidechainLeak.test.ts): a parent turn + an interleaved sidechain turn → the ledger paints only the parent rows; the sidechain candidate carries the rejection with evidence. Seed it from the real recording's shape.A2. Fix.
isSidechain?: booleantoRawCommittedEntry(committed.ts:24-37); populate it in the adapter's committed partition (collectLedgerInput.ts) and whereverRawCommittedEntryis built fromEntry(the field already flows through the codec +history.ts).collectCommittedCandidates, add a rejection branch besidemeta-entry/synthetic-user-filtered:if (e.isSidechain === true) → reason 'sidechain-filtered'with evidence['subagent child turn — rendered inside the Task card, not the main feed']. SkipminedToolIdsfor it.'sidechain-filtered'to theRenderReasonenum (model/types.ts) — the fixture-gated enum churns once.TaskSubagentRowvia the watcher channel, so rejecting these entries loses no information. Verify with the existing subagent tests.A3. Corpus. Extract the real recording (via
extract-rendering-recordings.mjs, structure-only redaction) intotesting/fixtures/rendering-recordings/so the leak is regression-locked against real captured data, not only hand-built fixtures. Bless against the fixed output; the sidechain turns must be absent from the painted rows.Track B — Live semantic plane (harder, follow-up, needs proxy attribution)
This is the streaming leak (what you see while a subagent runs). The semantic turns are proxy-derived with no
isSidechainmarker, so the fix is flow attribution, not a field filter:B1. Identify subagent proxy flows by request fingerprint — the subagent request carries a distinct system prompt / tool set. Reuse the
isSidecarFlow/request_shapemachinery (#174/#178). The proxy already captures outbound request bodies (request_shape+body_b64), so the flow can be classified atturn_started.B2. Route classified subagent flows to
SubAgentState(feed the card's live view) instead of emitting them as parentsemantic-currentturns. Theflow_ignoredpath already exists (23 ignored in the recording) — extend it to attribute rather than merely ignore.B3. Fixture: the session recording replays the semantic stream deterministically — assert the parent's rendered turns exclude the subagent-fingerprinted flows.
Sequencing
integration/rendering-pipeline.Why this is the pipeline working as designed
The recorder captured the live symptom, the ownership-decision system gives us the exact assertion surface (
reason: 'sidechain-filtered'), and the recording becomes the regression fixture. Bug → recording → failing test → fix → green → locked.Merge pull request #479 from Juliusolsson05/fix/sidechain-committed-leak
Merge pull request #480 from Juliusolsson05/fix/subagent-flow-bump
Juliusolsson05 commentedon Jul 7, 2026
Fixed — both leak carriers the issue identified are now filtered:
observations/committed.tsrejectsisSidechain === trueturns (reasonsidechain-filtered) before mining their tools — Track A, fix(rendering): filter subagent sidechain turns from the main feed (#477, Track A) #479.ClaudeProxyAdapterdetectscc_is_subagent=trueand routes the subagent flow out of the visible stream (flow_ignored, reasonsubagent) — Track B, fix(subagents): subagent proxy-flow attribution — Track B (#477) #480, hardened in fix: harden recording, rendering & subagents — verified deep-review findings #488.Subagent activity now renders only inside its Task card. Closing.