agentHost: Don't split the final response at the reconnect boundary - #331045
Conversation
Reconnecting to an active turn ran two independent converters over the same response parts: the one-shot `activeTurnToProgress` snapshot and the always-on `_observeTurn` graph. They de-duplicate via `adoptInvocations`, which is keyed on live `ChatToolInvocation` instances — so a tool call that had already settled, and which the snapshot renders as a `toolInvocationSerialized` part, could not be adopted and was emitted a second time as a live invocation. That duplicate lands between the restored markdown prefix and the markdown still streaming into the same response part. The response model only merges a markdown update into an immediately preceding markdown part, so the final answer was split in two — in the observed case mid-word, with the prefix folded into the collapsed activity section and the remainder rendered as a separate response. Record what the snapshot emitted per tool call instead of only the adoptable subset, so per-tool setup can skip a settled tool call that is already fully rendered. Subagent tools are excluded because their setup is what streams the child session's inner tool calls into the response. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Prevents settled tool calls from being re-emitted during agent-host reconnection.
Changes:
- Tracks snapshot tool calls, including serialized invocations.
- Skips duplicate settled tools and adds regression coverage.
Show a summary per file
| File | Description |
|---|---|
agentHostSessionHandler.ts |
Adds snapshot-aware tool-call handling. |
agentHostChatContribution.test.ts |
Tests settled-tool reconnection behavior. |
Review details
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Suppressed comments (1)
src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts:3507
- This exception leaves the reconnect split in place for completed subagent calls. The snapshot has already emitted the serialized parent invocation, but this path continues into
_setupServerToolCall, which creates and sinks a second live parent invocation before observing the child session; that second part can still land between the restored markdown and its continuation. Please separate child-session observation from emitting the parent invocation, and cover the completed-subagent reconnect case.
&& !shouldObserveSubagentChat(initial)) {
- Files reviewed: 2/2 changed files
- Comments generated: 3
- Review effort level: Balanced
Pull request was converted to draft
Code review caught that the subagent exception left the reconnect split in place for completed subagent calls: the guard declined to skip them so their child session would still be observed, but that fell through to `_setupServerToolCall`, which sinks a second live parent invocation. That part lands between the restored markdown prefix and its continuation — the very split the guard exists to prevent. Separate child-session observation from emitting the parent invocation. The invocation is still built so subagent observation has something to drive, but a tool call the snapshot already rendered as a serialized part is no longer emitted a second time. Also trims the inline commentary flagged in review. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Thanks — the suppressed comment about the subagent exception was a real bug, and a good catch. Fixed in 274bd59. I confirmed it before fixing. With a completed subagent tool in the active turn, reconnect produced exactly the split this PR is meant to prevent: The guard declined to skip subagent tools so their child session would still be observed, but that fell through to Fix is exactly the suggested separation: still build the invocation so subagent observation has something to drive, but don't emit it when the snapshot already rendered the tool as a serialized part. Added The three comment-verbosity notes are addressed in the same commit. Full One related gap I am not fixing here, to keep this PR small and safe for today: a completed subagent's inner tool calls also arrive from observation and are appended after the markdown prefix, so they can split the response the same way. That is pre-existing rather than introduced here, and it goes away entirely in the follow-up (#331047), which removes the snapshot so everything replays in response-part order through one producer. (Written by Copilot) |
Takes the single-producer implementation for the handler: the subagent emission guard added in review is part of the snapshot machinery this branch removes, so there is nothing left to suppress. Keeps both reconnect regression tests, with the settled-subagent case asserting this branch's behavior — one live invocation and contiguous markdown. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolves the conflicts from #331045 in favor of the single-producer implementation: the snapshot machinery that PR carefully de-duplicated against is removed here, so there is nothing left to skip or adopt. Also fixes a gap review caught in the reconnect `onFileEdits` wiring. That callback runs during the observer's synchronous replay, which happens inside `provideChatSessionContent` — before the ChatModel is created — so `_ensureSnapshotController` found no model and `controller?.addToolCallEdits(...)` silently no-oped. The active turn is not in `_pendingHistoryTurns` either, so edits from tools that completed before the reconnect never reached a checkpoint and Restore Checkpoint had no boundary for that turn. Queue those edits and flush them when the matching model appears; the pills already rendered either way. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reconnecting to an agent host session while a turn is still streaming could split the final assistant response in two — in the observed case mid-word, with the prefix folded into the collapsed tool/thinking section and the remainder rendered as a separate markdown block outside it, making it look like a tool call had interrupted the answer.
Root cause
Reconnect runs two independent converters over the same response parts:
activeTurnToProgress_observeTurnThey de-duplicate through
adoptInvocations, which is keyed on liveChatToolInvocationinstances. A tool call that had already settled before reconnect is rendered by the snapshot as atoolInvocationSerializedpart, which cannot be adopted — so the live graph emitted a second, live invocation for it.That duplicate lands between the restored markdown prefix and the markdown still streaming into the same response part.
ChatResponseModelonly merges a markdown update into an immediately preceding markdown part, so the two fragments stayed separate:On completion, disclosure grouped the prefix into the preceding activity section and left the continuation outside it, exposing the reconnect boundary as if it were a semantic boundary in the model's answer.
The underlying data was never wrong: the model produced one continuous response, the agent host persisted one continuous markdown part, and AHP delivered the snapshot and continuation correctly. The split was introduced entirely in the workbench.
Fix
Record what the snapshot emitted per tool call rather than only the adoptable subset, so per-tool setup can skip a settled tool call that the snapshot already rendered in full. Subagent tools are excluded from the skip because their setup is what streams the child session's inner tool calls into the parent response.
This is deliberately the minimal, low-risk fix. The deeper problem is that two converters exist at all and must be kept in sync by hand; a follow-up PR removes the snapshot path entirely so reconnect and live turns share one code path. That one is riskier and wants more bake time in Insiders.
Testing
reconnection to active turnsuite, verified failing before the change:AgentHostsuite: 2206 passing.npm run typecheck-clientclean.(Written by Copilot)