fix: re-check cancellation after postToolUse hook to avoid closed-stream write (fixes #321951)#321959
Merged
vs-code-engineering[bot] merged 1 commit intoJun 19, 2026
Conversation
…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>
pwang347
approved these changes
Jun 19, 2026
pwang347
approved these changes
Jun 19, 2026
connor4312
approved these changes
Jun 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The Copilot Chat extension throws an unhandled
Response stream has been closederror from the agent-mode PostToolUse hook path. When a chat request is cancelled while aPostToolUsehook is running, the response stream is closed by the time the hook resolves;processHookResultsthen writes hook progress/warnings to the now-closed stream, which throws in core'sextHostChatAgents2.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 extension0.53.0(win32) match this signature:cb6c8b17(58 users / 296 hits) and468eb501(21 users / 64 hits) — 79 combined users.Fixes #321951
Recommended reviewer:
@pwang347Culprit Commit
cdf17eb2abdf5fe7055a501a6de38b429a9c7da7(PR #319011, merged 2026-06-09)@pwang347executePostToolUseHook. The hook runs external commands asynchronously, so the request can be cancelled (closing the stream) while the hook is in flight; on return,processHookResultswrites to the closed stream and throws. The underlying race has existed since hook execution was moved into the extension, butcdf17eb2is 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"Affected Files
extensions/copilot/src/extension/chat/vscode-node/chatHookService.tsexecutePostToolUseHooknow re-checks cancellation after the hook await, before any stream writeextensions/copilot/src/extension/intents/node/hookResultProcessor.tsprocessHookResultswrites warnings viaoutputStream.hookProgress(line 129)extensions/copilot/src/extension/prompts/node/panel/toolCalling.tsxappendHookContextholds the pre-hook cancellation guard the prior fix addedsrc/vs/workbench/api/common/extHostChatAgents2.tsthrowIfDone()throws once the stream is closed (correct by design; not modified)Repro Steps
PostToolUsehook that runs a non-trivial external command (so it takes a moment to complete).processHookResultswrites a warning to the now-closed stream, andResponse stream has been closedis thrown to telemetry.How the Fix Works
Chosen approach (
extensions/copilot/src/extension/chat/vscode-node/chatHookService.ts): re-checktoken?.isCancellationRequestedinsideexecutePostToolUseHookimmediately afterawait this.executeHook(...)returns and beforeprocessHookResultsruns; returnundefinedearly when cancelled. The cancellation token is the extension's only signal that the stream has closed — the same proxy the prior fix and the existingtoolCalling.tsxguard already use. BecauseprocessHookResultsruns synchronously (noawaitbetween the check and thehookProgresswrites), 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 anylogService.error/hookProgresscall. Returningundefinedon cancellation is safe because the PostToolUse result only feeds`<PostToolUse-context>`into the next LLM round, which a cancelled turn never reaches.Alternatives considered:
toolCalling.tsxafter theawait— rejected: the throwing writes happen insideexecutePostToolUseHook/processHookResults, so a guard in the caller runs too late.try/catcharound thehookProgresswrites inhookResultProcessor.ts— rejected: it masks the error at the crash site and hides it from error telemetry instead of preventing the invalid write upstream.processHookResultsto 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.