{"record":{"id":"74002dac85afd9b8","repo":"can1357/oh-my-pi","slug":"permission-request-cancelled","errorCode":null,"errorMessage":"Permission request cancelled","messagePattern":"Permission request cancelled","errorType":"exception","errorClass":"ToolAbortError","httpStatus":null,"severity":"info","filePath":"packages/coding-agent/src/session/session-tools.ts","lineNumber":789,"sourceCode":"\t\t\t\t\t\treturn await target.execute(toolCallId, args as never, signal, onUpdate, ctx);\n\t\t\t\t\t}\n\t\t\t\t\tconst command =\n\t\t\t\t\t\ttarget.name === \"bash\" && args && typeof args === \"object\" && !Array.isArray(args)\n\t\t\t\t\t\t\t? stringProperty(args, \"command\")\n\t\t\t\t\t\t\t: undefined;\n\t\t\t\t\tconst commandContent = command\n\t\t\t\t\t\t? [{ type: \"content\" as const, content: { type: \"text\" as const, text: `$ ${command}` } }]\n\t\t\t\t\t\t: undefined;\n\t\t\t\t\t// Short-circuit on persisted decisions.\n\t\t\t\t\tconst persisted = this.#acpPermissionDecisions.get(permissionIntent.cacheKey);\n\t\t\t\t\tif (persisted === \"allow_always\") {\n\t\t\t\t\t\treturn await target.execute(toolCallId, args as never, signal, onUpdate, ctx);\n\t\t\t\t\t}\n\t\t\t\t\tif (persisted === \"reject_always\") {\n\t\t\t\t\t\tthrow new ToolError(`Tool call rejected by user (preference)`);\n\t\t\t\t\t}\n\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\tthrow new ToolAbortError(\"Permission request cancelled\");\n\t\t\t\t\t}\n\t\t\t\t\ttype PermissionRaceResult =\n\t\t\t\t\t\t| { kind: \"permission\"; outcome: ClientBridgePermissionOutcome }\n\t\t\t\t\t\t| { kind: \"aborted\" };\n\t\t\t\t\tconst { promise: abortPromise, resolve: resolveAbort } = Promise.withResolvers<PermissionRaceResult>();\n\t\t\t\t\tconst onAbort = () => resolveAbort({ kind: \"aborted\" });\n\t\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t\tlet raced: PermissionRaceResult;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst permissionPromise = bridge.requestPermission!(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttoolCallId,\n\t\t\t\t\t\t\t\ttoolName: target.name,\n\t\t\t\t\t\t\t\ttitle: permissionIntent.title,\n\t\t\t\t\t\t\t\t...(target.name === \"bash\" ? { kind: \"execute\" } : {}),\n\t\t\t\t\t\t\t\tstatus: \"pending\",\n\t\t\t\t\t\t\t\trawInput: args,\n\t\t\t\t\t\t\t\t...(commandContent ? { content: commandContent } : {}),","sourceCodeStart":771,"sourceCodeEnd":807,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/session/session-tools.ts#L771-L807","documentation":"When a tool call requires a permission decision, SessionTools races the permission request against the AbortSignal. If the signal fires before an outcome arrives, the library throws ToolAbortError('Permission request cancelled') instead of waiting forever or executing the tool. This is a deliberate cooperative-abort path, not a bug: the caller (agent loop or SDK consumer) cancelled the turn while the permission prompt was still pending.","triggerScenarios":"A tool execute() call reached the permission-check path, `persisted` was neither 'allow' nor 'reject_always', and signal.aborted was already true at the moment the permission request was about to be sent (session-tools.ts:787-789).","commonSituations":"User pressed Escape / aborted the agent turn while a permission prompt was queued; SDK consumer called session.abort() during tool dispatch; a timeout raced ahead of a slow UI responding to the prompt.","solutions":["Treat ToolAbortError as an expected, benign outcome: catch it and stop the turn rather than retrying.","If it fires unexpectedly, check who owns the AbortSignal — ensure the UI/client is still alive to answer permission requests before dispatching tools.","If you want fewer prompts, pre-approve the tool (allow-always preference) so the permission request path is skipped."],"exampleFix":"// before: treating abort as an unexpected crash\ntry { await tool.run(); } catch (e) { console.error('tool failed', e); }\n// after: handle abort explicitly\ntry { await tool.run(); }\ncatch (e) {\n  if (e instanceof ToolAbortError) return; // user cancelled permission\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"if (signal?.aborted) {\n  // skip the call entirely; no permission prompt will be answered\n  return;\n}","typeGuard":null,"tryCatchPattern":"try {\n  await toolCall();\n} catch (err) {\n  if (err instanceof ToolAbortError || err?.name === 'ToolAbortError') {\n    return; // treat as benign user cancellation\n  }\n  throw err;\n}","preventionTips":["Check signal.aborted before dispatching tools.","Keep the permission UI/client responsive so prompts are answered before aborts fire.","Pre-approve frequently used tools to reduce prompt windows."],"tags":["abort","permissions","tool-execution","cancellation"],"backgroundTag":"abort-signal-cancelled","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}