fix: prevent CancellationError from killing ask_user tool loop#74
Conversation
When ask_user was internally superseded (e.g., a new request arrived while a previous one was pending), the extension threw vscode.CancellationError. VS Code's ToolCallingLoop treats CancellationError as a hard stop at three levels: buildPrompt2 isCancelled check, _runLoop catch/break, and shouldAutoRetry exclusion. After two cancellations the LLM stopped calling ask_user entirely. Changes: - tools.ts: return a normal AskUserToolResult for superseded requests instead of throwing CancellationError. Real token cancellation (Stop button) still throws correctly via createCancellationPromise. - toolCallHandler.ts: update superseded message to instruct the LLM to re-ask the exact same question on retry. - tools.test.ts: update existing tests and add new test for real token cancellation still throwing CancellationError.
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
@4regab is 3 PRs good enough for a merge haha? |
There was a problem hiding this comment.
Pull request overview
Prevents internally superseded ask_user calls from throwing vscode.CancellationError, which can terminate VS Code Copilot Chat’s tool-calling loop. This keeps the agent/tool loop resilient to overlapping ask_user requests while preserving real user-triggered cancellation behavior via the CancellationToken.
Changes:
- Return a normal
AskUserToolResult(instead of throwingCancellationError) when a pendingask_userrequest is superseded. - Update the superseded/cancelled message returned from the webview handler to instruct the LLM to re-ask the exact same question.
- Update/add Vitest coverage around superseded vs real token cancellation behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| tasksync-chat/src/tools.ts | Changes superseded handling to return a normal tool result instead of throwing CancellationError. |
| tasksync-chat/src/webview/toolCallHandler.ts | Updates the superseded/cancelled response text returned to the tool call. |
| tasksync-chat/src/tools.test.ts | Updates existing cancellation tests and adds a new token-cancellation test case. |
Definitely! will review the PRs today |
… cancellation test Address PR review comments: - Extract superseded message to shared constant in remoteConstants.ts (DRY) - Import constant in toolCallHandler.ts and tools.test.ts - Add test for mid-flight token cancellation (createCancellationPromise race path)
Problem
When
ask_useris called by the LLM and then internally superseded (e.g., a new request arrives while a previous one is pending), the TaskSync extension was throwingvscode.CancellationError. This caused VS Code's Copilot Chat agent mode to permanently stop callingask_userafter just two cancellations.Why this happens
VS Code's
ToolCallingLooptreatsCancellationErrorfrom a tool as a hard stop signal at three levels:buildPrompt2()— checksToolResultMetadata.isCancelledon all tool results. If any is cancelled, throwsCancellationError_runLoop()catch block — catchesCancellationErrorandbreaks out of the loop immediatelyshouldAutoRetry()— excludesChatFetchResponseType.Canceledfrom retry eligibilityThe result: after two superseded
ask_usercalls, the conversation history contained two cancelled tool results, and the LLM learned thatask_useris broken and stopped calling it.This is the same class of issue described in microsoft/vscode#241039 — "Cancelled tool call should not cancel the whole chat session, but rather respond to LLM with an error/special message for this specific tool call, and allow LLM to reason based on the received results."
Solution
1. Return normal results for superseded requests (
tools.ts)Instead of throwing
CancellationErrorwhen a request is internally superseded, return the cancelled message as a normalAskUserToolResult. The LLM sees the text and can reason about it and callask_useragain.Real user cancellation (Stop button) still correctly throws
CancellationErrorvia theCancellationToken/createCancellationPromiserace path.2. Instruct the LLM to re-ask the same question (
toolCallHandler.ts)Updated the superseded message from a generic
"[CANCELLED: New request superseded this one]"to a directive that tells the LLM to re-ask the exact same question, reducing the chance of the question/summary changing on retry.3. Tests updated (
tools.test.ts)CancellationErrorthrowCancellationErrorFiles Changed
src/tools.tsCancellationErrorsrc/webview/toolCallHandler.tssrc/tools.test.tsValidation
tsc --noEmit): ✅ 0 errorsvitest run): ✅ 437 tests passingbiome check): ✅ 0 issuescheck-code): ✅ All checks passReferences
isCancelledcheck and loop break happen