Resolve tool result rows by call id instead of tool name - #348
Merged
TheGreatAxios merged 1 commit intoAug 7, 2026
Conversation
TheGreatAxios
force-pushed
the
cl-5562-sub-agent-rows-fail-and-duplicate-in-the-transcript
branch
from
August 7, 2026 04:48
310e181 to
b5489c6
Compare
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
force-pushed
the
cl-5562-sub-agent-rows-fail-and-duplicate-in-the-transcript
branch
from
August 7, 2026 05:13
b5489c6 to
b1869f5
Compare
TheGreatAxios
deleted the
cl-5562-sub-agent-rows-fail-and-duplicate-in-the-transcript
branch
August 7, 2026 06:36
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
pendingCallIndexmatches 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)HistoryBlock—observe-map.tsandrunner-host.tsalways carry a real id from their sources and never reach itStreamRow,ToolCallRowInput,ToolResultRowInput,HistoryBlock, and the history/observe/subagent-transcript row builders that were dropping itVerification
bun run typecheck,bun run build,bun testall pass, rebased onto currentorigin/release/tui-bugfixestip (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.tsadds: an unmatched-callId case provingpendingCallIndexreturns -1 rather than the newest pending row; a case proving a failed call's error text lands indetailbehind the expand arrow (isCollapsibleRowtrue) — the "surfaces its error inline" criterion.history-hydrate.test.tsadds a resumed-transcript case: three paralleltasktool_call/tool_result blocks sharing the tool name, resolved correctly by callId.runtime-bridge.test.tsadds a parallel-dispatch case against the live main-session bridge path, which tracks calls by callId in its own map independent ofpendingCallIndex.Does the parent transcript itself reproduce CL-5562?
runtime-bridge.tsis 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:task-tool.ts's concurrency-ordering bug for real:deps.sessions.start()marks a session"running"and notifies beforewithSubAgentSlotacquires a slot. WithmaxConcurrentSubAgentsset to 1 and 3 parallel dispatches, all three report"running"immediately even though only one is actually executing — confirmed via a liveSubAgentSessionStoresubscription. 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.tool.donefires, it does not corrupt which result answers which call. The parent transcript'sbag.toolRowsmap inruntime-bridge.tskeys by callId independent of session-store status, andtask-tool.tsforcescallId: call.idon every return path (success, cancel, and error), so a queued or requeued dispatch cannot hand the wrong id to the wrong row.streamingType !== "tool") from triggering an interrupt, so a slow multi-agent dispatch cannot self-abort the parent turn.reactor.done: none exists insrc/subagent;reactor.doneonly appears insrc/tui/runner.tsfor the parent session's own run-state persistence, unrelated to sub-agent tracking.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/summaryformatting on all three surfaces). The observe overlay and resumed/hydrated history reproduce the exact reported shape (sameIntern Fix CL-5559 heading shakerow 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.tssynthesize 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 intopendingCallIndex, 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.tsconfirms every current block builder always emitscallId, 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