Skip to content

agentHost: Don't split the final response at the reconnect boundary - #331045

Merged
roblourens merged 2 commits into
mainfrom
roblou/fix-agenthost-reconnect-markdown-split
Aug 15, 2026
Merged

agentHost: Don't split the final response at the reconnect boundary#331045
roblourens merged 2 commits into
mainfrom
roblou/fix-agenthost-reconnect-markdown-split

Conversation

@roblourens

Copy link
Copy Markdown
Member

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:

what it is when it runs
activeTurnToProgress one-shot imperative snapshot reconnect only
_observeTurn always-on reactive observable graph live, reconnect, server-initiated

They de-duplicate through adoptInvocations, which is keyed on live ChatToolInvocation instances. A tool call that had already settled before reconnect is rendered by the snapshot as a toolInvocationSerialized part, 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. ChatResponseModel only merges a markdown update into an immediately preceding markdown part, so the two fragments stayed separate:

markdown prefix
tool invocation   <-- spurious
markdown continuation

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

  • New regression test in the reconnection to active turn suite, verified failing before the change:
    + expected - actual
      [
        "toolInvocationSerialized"
        "markdown:that fallback fails to"
    -   "toolInvocation"
        "markdown:ward destroying history."
      ]
    
  • Full AgentHost suite: 2206 passing.
  • npm run typecheck-client clean.

(Written by Copilot)

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>
Copilot AI balanced review requested due to automatic review settings August 15, 2026 21:38
@roblourens
roblourens enabled auto-merge (squash) August 15, 2026 21:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@roblourens
roblourens marked this pull request as draft August 15, 2026 21:44
stack merge was automatically disabled August 15, 2026 21:44

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>
@roblourens

Copy link
Copy Markdown
Member Author

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:

toolInvocationSerialized
markdown:"that fallback fails to"
toolInvocation              <-- spurious, splits the answer
markdown:"ward destroying history."

The guard declined to skip subagent tools so their child session would still be observed, but that fell through to _setupServerToolCall, which sinks a second live parent invocation. Observation and emission were coupled.

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 replays a settled subagent tool call once, keeping the streamed markdown in one part to cover the case; verified failing before the change.

The three comment-verbosity notes are addressed in the same commit.

Full AgentHost suite: 2207 passing. typecheck-client clean.

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)

@roblourens
roblourens marked this pull request as ready for review August 15, 2026 21:57
roblourens added a commit that referenced this pull request Aug 15, 2026
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>
@roblourens
roblourens merged commit dcfc426 into main Aug 15, 2026
27 checks passed
@roblourens
roblourens deleted the roblou/fix-agenthost-reconnect-markdown-split branch August 15, 2026 23:11
@vs-code-engineering vs-code-engineering Bot added this to the 1.134.0 milestone Aug 15, 2026
roblourens added a commit that referenced this pull request Aug 15, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants