fix: queue concurrent permission requests to prevent silent cancellations#2
fix: queue concurrent permission requests to prevent silent cancellations#2robertpyke wants to merge 3 commits intoformulahendry:mainfrom
Conversation
…ions When multiple permission requests arrived simultaneously, each showQuickPick call would dismiss the previous one, silently returning 'cancelled'. This caused allow_always selections to appear broken — the agent would keep re-asking for permissions that the user had already approved. Added a serial promise queue to PermissionHandler so concurrent requests are shown one at a time.
There was a problem hiding this comment.
Pull request overview
This PR addresses a VS Code UI race where concurrent permission prompts can dismiss each other, causing silent cancellations and repeated permission loops. It does so by serializing permission requests so only one QuickPick is shown at a time.
Changes:
- Queue concurrent
requestPermissioncalls inPermissionHandlerand move the prompt logic into a private helper. - Bump extension version to
0.1.3. - Add a
0.1.3changelog entry documenting the fix.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/handlers/PermissionHandler.ts | Serializes permission prompts to prevent overlapping QuickPick dialogs. |
| package.json | Bumps extension version to 0.1.3. |
| CHANGELOG.md | Documents the 0.1.3 fix for concurrent permission prompt clobbering. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "displayName": "ACP Client", | ||
| "description": "Agent Client Protocol client for VS Code — connect to GitHub Copilot, Claude Code, Gemini CLI, Qwen Code, Codex CLI, OpenCode, and any ACP-compatible AI coding agent", | ||
| "version": "0.1.2", | ||
| "version": "0.1.3", |
There was a problem hiding this comment.
The extension version was bumped here, but package-lock.json still has the root version set to 0.1.2. If this repo commits the lockfile, it should be updated to keep the metadata consistent (otherwise tooling that inspects the lockfile can report the old version).
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
@copilot open a new pull request to apply changes based on the comments in this thread |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const result = this.queue.then(() => this.handlePermission(params)); | ||
| this.queue = result.then( | ||
| () => undefined, | ||
| () => undefined, | ||
| ); | ||
| return result.catch(() => ({ outcome: { outcome: 'cancelled' as const } })); |
There was a problem hiding this comment.
requestPermission converts any thrown error from handlePermission into a { outcome: 'cancelled' } response without logging or emitting a corresponding permission/responded telemetry event. This can mask real failures (e.g., showQuickPick/telemetry errors) as user cancellations and makes troubleshooting/metrics inaccurate. Consider logging the error (e.g., via logError) and reporting it via sendError/sendException, and decide whether to rethrow or return a distinct failure outcome rather than treating it as a user cancel.
| const result = this.queue.then(() => this.handlePermission(params)); | |
| this.queue = result.then( | |
| () => undefined, | |
| () => undefined, | |
| ); | |
| return result.catch(() => ({ outcome: { outcome: 'cancelled' as const } })); | |
| const title = params.toolCall?.title || 'Permission Request'; | |
| const result = this.queue.then(() => this.handlePermission(params)); | |
| this.queue = result.then( | |
| () => undefined, | |
| () => undefined, | |
| ); | |
| return result.catch(error => { | |
| const message = | |
| error instanceof Error | |
| ? `${error.name}: ${error.message}` | |
| : `Unknown error: ${String(error)}`; | |
| log(`requestPermission failed for "${title}": ${message}`); | |
| sendEvent('permission/responded', { | |
| permissionType: title, | |
| outcome: 'error', | |
| }); | |
| throw error; | |
| }); |
I threw together a quick PR after hitting issues with multiple concurrent tool calls breaking the permission prompting. No issue if you throw this out, and do your own thing, but this seems to have fixed the bug for me locally, so I turned it into a PR. More detail below.
Problem
When multiple permission requests arrive simultaneously (e.g. parallel file reads + terminal command), each
showQuickPickcall dismisses the previous one, causing it to silently returnundefined(treated ascancelled). The agent sees the tool call as rejected and re-asks on the next prompt, creating a loop where the user keeps approving but nothing executes.Fix
Added a serial promise queue to
PermissionHandlerso concurrentrequestPermissioncalls are processed one at a time. Each QuickPick waits for the previous one to resolve before showing.Changes
src/handlers/PermissionHandler.ts— Addedqueuefield, extracted logic into privatehandlePermissionCHANGELOG.md— Added 0.1.3 entrypackage.json— Version bump to 0.1.3