Agent host: a replayed client tool from a subagent is handled on the parent chat after a restart
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.4k
- PR merge metrics
- PR metrics pending
Description
### Summary
A client tool that first ran inside a subagent, and is later replayed by the SDK after VS Code restarts, is handled on the parent chat rather than the subagent chat that owns it. It executes and the turn completes, so this is a grouping defect rather than a hang, but the call appears in the wrong conversation and the parent gains a synthesized duplicate.
The in-process half of this is fixed in #330746. This issue is only the after-restart half, which needs coordinated changes and seemed worth raising as one design question rather than patching.
### Why the parent cannot be resolved
The SDK gives the host nothing to resolve it with. The MCP invocation carries only the tool_use id: `extractToolUseId` reads `extra._meta['claudecode/toolUseId']`, and the CLI has the agent id in scope at that dispatch site and deliberately does not include it. The two channels that do carry subagent identity, `canUseTool`'s `agentID` and `parent_tool_use_id` on streamed envelopes, are both unavailable here: a replayed call streams nothing, and inner subagent tools run under `bypassPermissions` so `canUseTool` is skipped.
The host's own inner-to-parent edge (`SubagentRegistry._innerToParent`) is written only by the stream mapper, so it does not exist for a replayed call. `primeFromTranscript` cannot supply it either: it only records `(toolCallId, agentId)` spawn pairs, and it is fed the main transcript, which excludes sidechain entries. Inner calls live in the per-subagent transcripts.
### Why the obvious fix is harmful
Resolving the parent at invoke time, by keeping the registry edge alive past `drainForegroundSpawns` or priming it from elsewhere, is the smaller change and is worse. It sets `parentToolCallId` in cases where the host has no subagent chat, and the signal is then buffered in `_pendingSubagentSignals` awaiting a `subagent_started` that cannot arrive, since that is emitted only from a live inner stream. The parked MCP handler never settles.
The present coupling is safe by accident: the edge exists only when the same code path already emitted `subagent_started`. Any parent resolution from a non-stream source breaks that invariant, which is why #330746 fixes the in-process case with a host-side routing fallback instead.
### What closing the restart case would take
Three coordinated changes, plus one structural wrinkle:
1. **Prime the edge from the SDK's store.** The mapping is persisted as `subagents/agent-.meta.json` -> `toolUseId`, reachable through `listSubagents` and `getSubagentMessages`, whose envelopes carry `parent_tool_use_id`.
2. **Restore a subagent chat for a dangling `Task`.** `_registerRestoredSubagentSummaries` discovers subagents from `ToolResultSubagentContent` gated on `Completed || Running`. The replay case is exactly a `Task` that never completed, which replays as cancelled with no content, so it is skipped.
3. **Populate the routing map.** `_subagentChats` is written only from a live `subagent_started`; no restore path writes it, so routing has nowhere to go even once the chat exists.
The wrinkle: (1) is an async SDK read on a path that is synchronous today.
Shipping only part of this is worse than shipping none. (1) without (2) and (3) is precisely the harmful shape above.
### Offer
I am happy to implement this if it is wanted, and equally happy for it to be closed or reshaped. I have no attachment to the approach above; it is the shape the code suggested to me, not a proposal I am invested in. The reason I am offering rather than just filing is that I want the in-process fix locally today regardless, so the analysis already exists and offering it publicly costs nothing.
If the answer is that client tool execution should be restructured around the SDK invocation instead, as discussed in #330899, this becomes moot and I would rather not build it.
### Environment
Reproduced against a dev container (remote agent host) on `1.133.x`, using the Claude provider.
Related: #330746 (in-process fix), #330899 (root cause of the wider client-tool execution seam).
*AI disclosure: this issue was written with the assistance of AI.*
### Public patches and patcher scripts
[Public patch catalog and patcher scripts](https://github.com/RyanEwen/vscode-patches/blob/main/CATALOG.md) · [Source patch index](https://github.com/RyanEwen/vscode-patches/blob/main/SOURCE-PATCHES.md). The [public collection](https://github.com/RyanEwen/vscode-patches) includes the maintained patchers, rollback instructions, regression scripts, and historical snapshots. Build restrictions and exact installer coverage are documented there.
Contributor guide
Assessment
This issue has not been assessed yet.