From a6ebcfb00c67a2f11ca94f926d88f8ec2861eb9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Fri, 17 Jul 2026 18:33:32 +0800 Subject: [PATCH] fix(server): interrupt turns after tool denial (#1051) --- src/server/__tests__/conversations.test.ts | 50 +++++++++++++++++++++- src/server/services/conversationService.ts | 10 ++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/server/__tests__/conversations.test.ts b/src/server/__tests__/conversations.test.ts index 1abd1605..3332d7f7 100644 --- a/src/server/__tests__/conversations.test.ts +++ b/src/server/__tests__/conversations.test.ts @@ -272,7 +272,13 @@ describe('ConversationService', () => { pendingOutbound: [], stderrLines: [], sdkMessages: [], - pendingPermissionRequests: new Map(), + pendingPermissionRequests: new Map([ + ['req-1', { + toolName: 'ExitPlanMode', + input: {}, + permissionSuggestions: [], + }], + ]), }) const result = svc.respondToPermission( @@ -294,6 +300,48 @@ describe('ConversationService', () => { }, }, }) + expect((sent[0] as any).response.response.interrupt).toBeUndefined() + }) + + it('should interrupt the active turn when desktop denies a tool permission', () => { + const svc = new ConversationService() + const sent: unknown[] = [] + + ;(svc as any).sessions.set('session-1', { + proc: null, + outputCallbacks: [], + workDir: process.cwd(), + sdkToken: 'token', + sdkSocket: { + send(data: string) { + sent.push(JSON.parse(data)) + }, + }, + pendingOutbound: [], + stderrLines: [], + sdkMessages: [], + pendingPermissionRequests: new Map([ + ['req-1', { + toolName: 'Bash', + input: { command: 'rm temp.txt' }, + permissionSuggestions: [], + }], + ]), + }) + + const result = svc.respondToPermission('session-1', 'req-1', false) + + expect(result).toBe(true) + expect(sent[0]).toMatchObject({ + type: 'control_response', + response: { + response: { + behavior: 'deny', + message: 'User denied via UI', + interrupt: true, + }, + }, + }) }) it('should resolve a permission mode request only after the CLI confirms the change', async () => { diff --git a/src/server/services/conversationService.ts b/src/server/services/conversationService.ts index f0e286f8..2dd134fb 100644 --- a/src/server/services/conversationService.ts +++ b/src/server/services/conversationService.ts @@ -622,7 +622,15 @@ export class ConversationService { } : {}), } - : { behavior: 'deny', message: denyMessage || 'User denied via UI' }, + : { + behavior: 'deny', + message: denyMessage || 'User denied via UI', + // Rejecting ExitPlanMode means "keep planning"; other desktop + // denials stop the current agent turn and wait for user input. + ...(pendingRequest?.toolName !== 'ExitPlanMode' + ? { interrupt: true } + : {}), + }, }, }) }