Skip to content

Agent host: a replayed client tool from a subagent is handled on the parent chat after a restart #331595

Description

@RyanEwen

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-<id>.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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions