microsoft / microsoft/vscode

Agent Host: failed transcript-less subagent blocks subsequent user turns

Open
#332,423 0 comments 0 reactions 1 assignee Claimed by @roblourens View on GitHub
bug
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.