can1357/oh-my-pi · info · ToolAbortError
Permission request cancelled
Error message
Permission request cancelled
What it means
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.
Source
Thrown at packages/coding-agent/src/session/session-tools.ts:789
return await target.execute(toolCallId, args as never, signal, onUpdate, ctx);
}
const command =
target.name === "bash" && args && typeof args === "object" && !Array.isArray(args)
? stringProperty(args, "command")
: undefined;
const commandContent = command
? [{ type: "content" as const, content: { type: "text" as const, text: `$ ${command}` } }]
: undefined;
// Short-circuit on persisted decisions.
const persisted = this.#acpPermissionDecisions.get(permissionIntent.cacheKey);
if (persisted === "allow_always") {
return await target.execute(toolCallId, args as never, signal, onUpdate, ctx);
}
if (persisted === "reject_always") {
throw new ToolError(`Tool call rejected by user (preference)`);
}
if (signal?.aborted) {
throw new ToolAbortError("Permission request cancelled");
}
type PermissionRaceResult =
| { kind: "permission"; outcome: ClientBridgePermissionOutcome }
| { kind: "aborted" };
const { promise: abortPromise, resolve: resolveAbort } = Promise.withResolvers<PermissionRaceResult>();
const onAbort = () => resolveAbort({ kind: "aborted" });
signal?.addEventListener("abort", onAbort, { once: true });
let raced: PermissionRaceResult;
try {
const permissionPromise = bridge.requestPermission!(
{
toolCallId,
toolName: target.name,
title: permissionIntent.title,
...(target.name === "bash" ? { kind: "execute" } : {}),
status: "pending",
rawInput: args,
...(commandContent ? { content: commandContent } : {}),View on GitHub (pinned to 9690622007)
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.
Example fix
// before: treating abort as an unexpected crash
try { await tool.run(); } catch (e) { console.error('tool failed', e); }
// after: handle abort explicitly
try { await tool.run(); }
catch (e) {
if (e instanceof ToolAbortError) return; // user cancelled permission
throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
if (signal?.aborted) {
// skip the call entirely; no permission prompt will be answered
return;
} Try / catch
try {
await toolCall();
} catch (err) {
if (err instanceof ToolAbortError || err?.name === 'ToolAbortError') {
return; // treat as benign user cancellation
}
throw err;
} Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- Request was aborted
- Auth broker request aborted
- OAuth refresh ownership aborted by caller
- Request was aborted.
- AbortError
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/74002dac85afd9b8.
Report an issue: GitHub.