Skip to content

fix: re-check cancellation after postToolUse hook to avoid closed-stream write (fixes #321951)#321959

Merged
vs-code-engineering[bot] merged 1 commit into
mainfrom
fix/posttooluse-hook-stream-closed-321951-6933dd116f2e2123
Jun 19, 2026
Merged

fix: re-check cancellation after postToolUse hook to avoid closed-stream write (fixes #321951)#321959
vs-code-engineering[bot] merged 1 commit into
mainfrom
fix/posttooluse-hook-stream-closed-321951-6933dd116f2e2123

Conversation

@vs-code-engineering

Copy link
Copy Markdown
Contributor

Summary

The Copilot Chat extension throws an unhandled Response stream has been closed error from the agent-mode PostToolUse hook path. When a chat request is cancelled while a PostToolUse hook is running, the response stream is closed by the time the hook resolves; processHookResults then writes hook progress/warnings to the now-closed stream, which throws in core's extHostChatAgents2.ts.

This is a regression of the previously-fixed #319003. The prior fix (cdf17eb2, #319011) added a cancellation guard before invoking the hook, but the hook spawns external user-configured commands and can run for an arbitrary time — cancellation can land during that window. Two PostToolUse buckets on extension 0.53.0 (win32) match this signature: cb6c8b17 (58 users / 296 hits) and 468eb501 (21 users / 64 hits) — 79 combined users.

Fixes #321951
Recommended reviewer: @pwang347

Culprit Commit

Field Value
Commit cdf17eb2abdf5fe7055a501a6de38b429a9c7da7 (PR #319011, merged 2026-06-09)
Author vs-code-engineering[bot] (automated fix); reviewed/owned by @pwang347
Why The prior fix guarded cancellation only before executePostToolUseHook. The hook runs external commands asynchronously, so the request can be cancelled (closing the stream) while the hook is in flight; on return, processHookResults writes to the closed stream and throws. The underlying race has existed since hook execution was moved into the extension, but cdf17eb2 is the change that claimed to fix it while leaving the post-await window open.

Code Flow

sequenceDiagram
    participant TC as toolCalling.tsx
    participant CHS as chatHookService.ts
    participant HRP as hookResultProcessor.ts
    participant Core as extHostChatAgents2.ts

    TC->>TC: appendHookContext() guard: isCancellationRequested? (false)
    TC->>CHS: executePostToolUseHook(stream, token)
    CHS->>CHS: await executeHook('PostToolUse') spawns external commands (slow)
    Note over Core: user cancels request, stream.close() sets _isClosed = true
    CHS->>HRP: processHookResults({ outputStream })
    HRP->>Core: outputStream.hookProgress(warning)
    Core-->>HRP: throwIfDone() throws "Response stream has been closed"
Loading

Affected Files

File Role
extensions/copilot/src/extension/chat/vscode-node/chatHookService.ts FixedexecutePostToolUseHook now re-checks cancellation after the hook await, before any stream write
extensions/copilot/src/extension/intents/node/hookResultProcessor.ts Crash site — processHookResults writes warnings via outputStream.hookProgress (line 129)
extensions/copilot/src/extension/prompts/node/panel/toolCalling.tsx Caller — appendHookContext holds the pre-hook cancellation guard the prior fix added
src/vs/workbench/api/common/extHostChatAgents2.ts Core producer of the error — throwIfDone() throws once the stream is closed (correct by design; not modified)

Repro Steps

  1. Configure a PostToolUse hook that runs a non-trivial external command (so it takes a moment to complete).
  2. Start an agent-mode chat turn that invokes a tool, triggering the PostToolUse hook.
  3. Cancel the request while the hook is executing (after the pre-hook guard has already passed).
  4. The hook resolves, processHookResults writes a warning to the now-closed stream, and Response stream has been closed is thrown to telemetry.

How the Fix Works

Chosen approach (extensions/copilot/src/extension/chat/vscode-node/chatHookService.ts): re-check token?.isCancellationRequested inside executePostToolUseHook immediately after await this.executeHook(...) returns and before processHookResults runs; return undefined early when cancelled. The cancellation token is the extension's only signal that the stream has closed — the same proxy the prior fix and the existing toolCalling.tsx guard already use. Because processHookResults runs synchronously (no await between the check and the hookProgress writes), once the token check passes the stream cannot close before the writes complete, so this fully closes the cancel-during-hook window.

This fixes the bug at the producer of the invalid operation — the code that decides to write hook output to the stream — rather than at the crash site. It deliberately does not wrap the write in try/catch (that would hide the error from the telemetry pipeline) and does not remove or alter any logService.error/hookProgress call. Returning undefined on cancellation is safe because the PostToolUse result only feeds `<PostToolUse-context>` into the next LLM round, which a cancelled turn never reaches.

Alternatives considered:

  • Re-checking the token in toolCalling.tsx after the await — rejected: the throwing writes happen inside executePostToolUseHook/processHookResults, so a guard in the caller runs too late.
  • try/catch around the hookProgress writes in hookResultProcessor.ts — rejected: it masks the error at the crash site and hides it from error telemetry instead of preventing the invalid write upstream.
  • Threading the token into processHookResults to guard each write — rejected: more invasive, changes a shared helper also used by the PreToolUse path, and adds extra check/write windows; the single post-await guard is sufficient because the helper is synchronous.

Recommended Owner

@pwang347 — owns the Chat Hooks area, authored/owned the prior fix #319011 for this exact path, and is an active contributor (committed 2026-06-17, the day before this issue was filed). Team-membership and liveness checks both pass.

Generated by errors-fix · 2.1K AIC · ⌖ 220.1 AIC · ⊞ 69.3K ·

…eam write (fixes #321951)

The postToolUse hook can run for a long time because it spawns external,
user-configured commands. If the chat request is cancelled while the hook
runs, the response stream is closed by the time the hook resolves, and
writing hook progress to it throws an unhandled "Response stream has been
closed" error.

The prior fix (cdf17eb, #319011) only checked cancellation before invoking
the hook, leaving the cancellation-during-hook window open. Re-check the
cancellation token in executePostToolUseHook after the await and skip result
processing when cancelled, since a cancelled turn never consumes the result.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 18, 2026 16:25

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@vs-code-engineering vs-code-engineering Bot marked this pull request as ready for review June 18, 2026 16:28
@vs-code-engineering vs-code-engineering Bot enabled auto-merge (squash) June 18, 2026 16:28
@vs-code-engineering vs-code-engineering Bot merged commit 3d575ee into main Jun 19, 2026
25 checks passed
@vs-code-engineering vs-code-engineering Bot deleted the fix/posttooluse-hook-stream-closed-321951-6933dd116f2e2123 branch June 19, 2026 21:32
@vs-code-engineering vs-code-engineering Bot added this to the 1.126.0 milestone Jun 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants