fix: guard Auto routing stream pushes against cancelled turns (fixes #341067) by vscodebot-pr · Pull Request #341077 · microsoft/vscode · GitHub
Skip to content

fix: guard Auto routing stream pushes against cancelled turns (fixes #341067) - #341077

Merged
Logan Ramos (lramos15) merged 1 commit into
microsoft:mainfrom
vscodebot-pr:fix/automode-routing-closed-stream-341067-aw-38267707951
Oct 13, 2026
Merged

Logan Ramos (lramos15) merged 1 commit into
microsoft:mainfrom
vscodebot-pr:fix/automode-routing-closed-stream-341067-aw-38267707951

Conversation

@vscodebot-pr

Copy link
Copy Markdown
Contributor

Summary

reportAutoModeRouting installs an onDidRoute listener that pushes Auto explainability parts (ChatResponseAutoModeResolutionPart) into the turn's ChatResponseStream when routing starts and resolves. Routing resolves asynchronously (POST /auto), so when the user cancels the turn mid-route, VS Code core closes the response stream before the listener fires. The subsequent stream.push(...) throws Error: Response stream has been closed, which escapes resolveAutoModeEndpoint and reaches the unhandled-error telemetry pipeline. It affected 108 users on GitHub.copilot-chat 0.69.0.

Fixes #341067
Recommended reviewer: @spurwaha

Culprit Commit

Field Value
Commit d7132ac6c9b5
Author @spurwaha
PR #336757
Message Attribute chat edits to resolved Auto tiers (#336757)
Why This commit rewrote reportAutoModeRouting to push ChatResponseAutoModeResolutionPart onto the turn's stream from inside the async onDidRoute listener (lines 110 and 114). git blame attributes both stream.push(...) lines to this commit, and the bucket first appears in 0.69.0 (the first release shipping it) with a clean 0.68.0 baseline. Before it, routing state was not written back to the stream, so a cancelled turn could not push to a closed stream.

Code Flow

sequenceDiagram
    participant User as User
    participant Handler as chatParticipants handler
    participant Service as AutomodeService
    participant Listener as onDidRoute listener
    participant Stream as ChatResponseStream

    User->>Handler: start Auto turn
    Handler->>Service: resolveAutoModeEndpoint(request)
    Note over Service: await POST /auto<br/>(async routing)
    User->>Stream: cancel turn
    Note over Stream: ⚠️ Root cause:<br/>core closes the stream on cancel
    Service->>Listener: fire onDidRoute (started / resolved)
    Listener->>Stream: stream.push(ChatResponseAutoModeResolutionPart)
    Note over Stream: 💥 Error thrown:<br/>Response stream has been closed
Loading

Affected Files

File Role Evidence
extensions/copilot/src/platform/endpoint/node/automodeService.ts crash site + root cause L110, L114: stream.push(new ChatResponseAutoModeResolutionPart(...)) inside the async onDidRoute listener — pushes after the stream was closed by cancellation
extensions/copilot/src/platform/endpoint/node/automodeService.ts producer (stream lifecycle) L252-L258: resolveAutoModeEndpoint fires _onDidRoute after an awaited _resolveAutoModeEndpoint, so the listener runs post-cancellation
extensions/copilot/src/extension/conversation/vscode-node/chatParticipants.ts caller L211: installs reportAutoModeRouting; owns the request token that signals cancellation

Repro Steps

  1. Enable Auto model in Copilot Chat.
  2. Start a chat turn on a cold conversation so routing must call POST /auto (no cached endpoint).
  3. Cancel the turn (Escape / stop button) while the routing request is still in flight.
  4. Core closes the response stream; when the awaited routing call resolves, the onDidRoute listener calls stream.push(...) on the closed stream and Error: Response stream has been closed is thrown. Because routing latency is network-bound, the race is easiest to hit on slow connections or by cancelling immediately after submitting.

How the Fix Works

Chosen approach — automodeService.ts → reportAutoModeRouting(): thread the turn's CancellationToken into the function and guard both stream.push(...) calls with !token.isCancellationRequested. The chat participant handler (chatParticipants.ts:211) already receives the request token and now forwards it. Once the turn is cancelled, core has closed the stream, so the explainability part is both unsafe to push and moot (there is no live turn to annotate) — skipping the push is the correct behavior, not merely error avoidance.

This is a use-after-close lifecycle race where the producer that closes the stream is VS Code core's chat request plumbing, outside the extension's reach — the extension cannot reorder or block core's stream teardown. The guard therefore lives at the consumer (the listener), which is the justified place for an external/untrusted-boundary lifecycle fix. The fix sits at the real bypass (the unconditional push onto a possibly-closed stream), not at a generic catch-all.

Why this fix: after this change, the listener at automodeService.ts:110 and :114 cannot call stream.push(...) once the turn's token is cancelled, which is exactly when core has closed the stream — so the Response stream has been closed throw on that path is no longer reachable.

Alternatives considered:

  • Wrap the stream.push(...) calls in try/catch — rejected: swallows the error, hides any future unrelated stream failures, and does not express the real invariant (don't push to a cancelled turn).
  • Have ChatResponseStreamImpl silently drop pushes after close — rejected: changes shared stream semantics for every caller to paper over one producer's post-cancel push.

Lifecycle pattern: use-after-dispose (stream closed by cancellation, listener fires afterward).
Producer site: VS Code core chat request plumbing closes the ChatResponseStream on cancellation — not reachable from extensions/copilot. Consumer-side guard justification: the only in-repo signal the extension has for this teardown is the request CancellationToken, so gating the push on that token is the closest-to-producer fix available to the extension.

Recommended Owner

@spurwaha — author of the culprit commit d7132ac6c9b5 (PR #336757), which introduced the stream pushes in reportAutoModeRouting. Active in microsoft/vscode within the last 90 days and lands changes in core src/vs (write access).

…icrosoft#341067)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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.

🟢 Approval recommended

The cancellation guard addresses the reported lifecycle race and all callers and tests are updated consistently.

0 open findings

What changed in this PR

Guards Auto-routing response updates after chat-turn cancellation, preventing writes to a closed response stream.

Changes:

  • Passes the request cancellation token into Auto-routing reporting.
  • Skips routing status pushes after cancellation.
  • Adds regression coverage for cancelled turns.
File Description
extensions/​copilot/​src/​platform/​endpoint/​node/​automodeService.ts Guards routing stream pushes after cancellation.
extensions/​copilot/​src/​extension/​conversation/​vscode-node/​chatParticipants.ts Forwards the request token.
extensions/​copilot/​src/​platform/​endpoint/​node/​test/​automodeService.spec.ts Tests cancellation with a closed stream.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@lramos15
Logan Ramos (lramos15) merged commit e54f253 into microsoft:main Oct 13, 2026
35 checks passed
@vs-code-engineering vs-code-engineering Bot added this to the 1.143.0 milestone Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Error] [GitHub.copilot-chat] unhandlederror-Response stream has been closed Error: Response stream has been closed at Dte.value...

6 participants