Skip to content

Resolve tool result rows by call id instead of tool name - #348

Merged
TheGreatAxios merged 1 commit into
release/tui-bugfixesfrom
cl-5562-sub-agent-rows-fail-and-duplicate-in-the-transcript
Aug 7, 2026
Merged

Resolve tool result rows by call id instead of tool name#348
TheGreatAxios merged 1 commit into
release/tui-bugfixesfrom
cl-5562-sub-agent-rows-fail-and-duplicate-in-the-transcript

Conversation

@TheGreatAxios

@TheGreatAxios TheGreatAxios commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • pendingCallIndex matches a tool result to its call row by callId first; an id that matches nothing on the log now returns -1 instead of falling back to the newest same-name pending row (that fallback was the exact LIFO misattribution this PR exists to remove)
  • The name-based fallback survives only when no callId is supplied at all, which is exclusively saved history recorded before ids were threaded through HistoryBlockobserve-map.ts and runner-host.ts always carry a real id from their sources and never reach it
  • Threaded callId through StreamRow, ToolCallRowInput, ToolResultRowInput, HistoryBlock, and the history/observe/subagent-transcript row builders that were dropping it

Verification

  • bun run typecheck, bun run build, bun test all pass, rebased onto current origin/release/tui-bugfixes tip (includes Guard the @-mention lookup against a disposed shell #335, Cap the transcript's retained row history at 600 #338, and the mouse-reporting/resume-seed merges landed since). The only failures seen locally (2) came from this worktree's checkout path containing the substring "agent" in a layout-width assertion; a fresh worktree of the same commit passes clean.
  • tool-rows.test.ts adds: an unmatched-callId case proving pendingCallIndex returns -1 rather than the newest pending row; a case proving a failed call's error text lands in detail behind the expand arrow (isCollapsibleRow true) — the "surfaces its error inline" criterion.
  • history-hydrate.test.ts adds a resumed-transcript case: three parallel task tool_call/tool_result blocks sharing the tool name, resolved correctly by callId.
  • runtime-bridge.test.ts adds a parallel-dispatch case against the live main-session bridge path, which tracks calls by callId in its own map independent of pendingCallIndex.

Does the parent transcript itself reproduce CL-5562? runtime-bridge.ts is unmodified by this PR, so the new test there exercises code this PR did not change — it cannot by itself prove the parent transcript was ever affected. I looked for a live-path mechanism directly:

  • Reproduced task-tool.ts's concurrency-ordering bug for real: deps.sessions.start() marks a session "running" and notifies before withSubAgentSlot acquires a slot. With maxConcurrentSubAgents set to 1 and 3 parallel dispatches, all three report "running" immediately even though only one is actually executing — confirmed via a live SubAgentSessionStore subscription. This is real and affects the chrome strip / observe overlay. It is a distinct defect from CL-5562 (status truthfulness, not row keying) — flagging for a separate issue rather than fixing here.
  • In that same reproduction, all three calls still resolved with the correct callId and no errors once each acquired its slot and ran — concurrency queuing delays when a call's tool.done fires, it does not corrupt which result answers which call. The parent transcript's bag.toolRows map in runtime-bridge.ts keys by callId independent of session-store status, and task-tool.ts forces callId: call.id on every return path (success, cancel, and error), so a queued or requeued dispatch cannot hand the wrong id to the wrong row.
  • Checked the stall-watchdog auto-abort path: it explicitly excludes long in-flight tool calls (streamingType !== "tool") from triggering an interrupt, so a slow multi-agent dispatch cannot self-abort the parent turn.
  • Checked for any code tying sub-agent status to the parent's own reactor.done: none exists in src/subagent; reactor.done only appears in src/tui/runner.ts for the parent session's own run-state persistence, unrelated to sub-agent tracking.
  • Sub-agent transcript entries are explicitly documented and confirmed to never reach the parent transcript (task-tool.ts: "Events are written here only — they are not forwarded into the parent chat transcript").

I could not find a mechanism that reproduces the reported duplicate/failed rows on the plain parent transcript, across every path the symptom's format is consistent with (task tool-call rows use identical verb/summary formatting on all three surfaces). The observe overlay and resumed/hydrated history reproduce the exact reported shape (same Intern Fix CL-5559 heading shake row format, same LIFO misattribution, same duplicate-then-orphan pattern) and are what this PR fixes. If the operator's original report was taken from the plain live transcript with no observe/resume involved, the mechanism is still open — this PR does not claim to have fixed that surface, only to have exhausted the live-path candidates raised so far without finding one there.

Considered and rejected: eliminate the name-based fallback entirely by having history-hydrate.ts synthesize a per-call-position id at load time. This doesn't remove the need for a second matching strategy — it just moves the same sequential-pairing assumption into id synthesis instead of into pendingCallIndex, and a legacy block missing an id on one side (call or result) would still need positional pairing to synthesize the other side's matching id, which is exactly what today's name+pending fallback already does correctly (existing test: sequential same-name calls resolve correctly). turns-to-blocks.ts confirms every current block builder always emits callId, so the optional field exists purely as defensive typing for malformed input, not a real historical corpus — the fallback is a small, single, documented branch guarding against that, not a competing algorithm.

Closes CL-5562

@linear-code

linear-code Bot commented Aug 7, 2026

Copy link
Copy Markdown

CL-5562

CL-5559

@TheGreatAxios
TheGreatAxios changed the base branch from main to release/tui-bugfixes August 7, 2026 04:20
@TheGreatAxios
TheGreatAxios force-pushed the cl-5562-sub-agent-rows-fail-and-duplicate-in-the-transcript branch from 310e181 to b5489c6 Compare August 7, 2026 04:48
Matching a result to its call row by tool name alone breaks the
moment two calls to the same tool are in flight together, which
parallel sub-agent dispatch does on every turn that fires more than
one task call. The newest pending row of that name absorbed whichever
result landed first, stranding the others pending and turning later
results into orphan rows.

An id that matches nothing on the log now answers nothing rather than
falling back to the newest same-name row, since every current caller
carries a real id. The name-based fallback survives only for callId
undefined, which saved history from before ids were threaded through
this path still produces.
@TheGreatAxios
TheGreatAxios force-pushed the cl-5562-sub-agent-rows-fail-and-duplicate-in-the-transcript branch from b5489c6 to b1869f5 Compare August 7, 2026 05:13
@TheGreatAxios
TheGreatAxios merged commit cd21f54 into release/tui-bugfixes Aug 7, 2026
2 checks passed
@TheGreatAxios
TheGreatAxios deleted the cl-5562-sub-agent-rows-fail-and-duplicate-in-the-transcript branch August 7, 2026 06:36
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.

1 participant