Agent Host: failed transcript-less subagent blocks subsequent user turns
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.4k
- PR merge metrics
- PR metrics pending
Description
## Type of issue
Bug
## Summary
After restoring an Agent Host session that contains a failed subagent with no transcript, sending a new user message can leave the UI indefinitely waiting with no agent response.
The user message reaches AHP as `chat/turnStarted`, but Agent Host rejects the action during asynchronous peer-chat resolution before state reduction or provider/model invocation.
## Reproduction shape
1. Have a Copilot CLI Agent Host session containing a subagent tool call that emits `subagent.started` and then `subagent.failed` immediately, without producing a child transcript.
2. Close/restart and restore the parent session. The failed child is retained as a lazy read-only subagent chat summary.
3. Send a new message in the parent/default chat.
## Expected
The new parent-chat turn is dispatched to the provider and receives either an assistant response or a surfaced error.
## Actual
The client optimistically shows the user turn as in progress, but there is no assistant response or completion. No provider/model call is made.
## Sanitized diagnostics
Correlating `events.jsonl`, AHP traffic, Agent Host logs, and renderer logs from an exported Agent Host debug bundle showed:
- Historical child lifecycle: `subagent.started` -> `subagent.failed` -> unsuccessful `tool.execution_complete`; there are no child transcript turns.
- On restore, the parent session registers the child as a lazy subagent chat summary.
- The new message is sent over AHP as `chat/turnStarted` and optimistically echoed to the client.
- Agent Host then logs:
```text
[AgentService] async dispatchAction failed: Subagent transcript is not available yet: ahp-chat://subagent//
```
- No subsequent `user.message`, `assistant.turn_start`, model usage, or model request exists for the attempted turn.
Other contemporaneous warnings (remote-control disabled, stale Codex catalog entry, embeddings cache 404, and unavailable OTel endpoint) did not correlate with the failed turn and appear non-causal.
## Root-cause analysis
`AgentService.dispatchAction()` treats `ChatTurnStarted` as requiring turn-owner resolution whenever the restored session has unresolved peer chats. It calls `_resolvePeerChatsForTurnValidation()`, which resolves **all** unresolved peers before accepting the new turn.
The failed historical subagent is one of those unresolved peers. Its lazy resolver reaches `_resolveRestoredSubagentTurns()`, receives zero child turns, and throws `Subagent transcript is not available yet`. That rejection propagates through the per-client dispatch queue and causes `rejectClientAction()` to reject the entire new `chat/turnStarted` action before `_dispatchActionNow()` / side effects invoke the provider.
Relevant code:
- `src/vs/platform/agentHost/node/agentService.ts`
- `dispatchAction()` asynchronous prelude and catch
- `_resolvePeerChatsForTurnValidation()`
- `_resolveRestoredSubagentTurns()`
## Possible fix direction
A new parent-chat send should not depend on successfully loading every historical subagent transcript. Options include:
- During turn-ID validation, tolerate an unavailable transcript for a failed/empty lazy child and continue validating loaded/resolvable peers.
- Avoid registering failed transcript-less subagents as resolvable peer-chat summaries.
- Store enough turn-ID metadata in the chat catalog to validate uniqueness without hydrating full peer transcripts.
The client should also receive a terminal surfaced error if an asynchronous `chat/turnStarted` prelude is rejected, rather than retaining an indefinitely in-progress turn.
## Suggested regression test
Restore a session whose parent history includes a subagent tool call that started and failed without child messages. Keep the child as an unresolved lazy chat summary, dispatch a subsequent `ChatTurnStarted` to the default chat, and assert that:
1. the action is accepted and reaches Agent Host side effects/provider send;
2. the stale child remains retryable/read-only as appropriate; and
3. the client turn does not remain indefinitely active.
## Related recent work / ownership context
- #329633 — Agent Host owns chat lifecycle and turn-validation orchestration.
- #323960 — surfaces subagents as persisted read-only peer tabs.
- #329071 — lazily restores peer chats and makes resolver failures retryable.
Contributor guide
Assessment
This issue has not been assessed yet.