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

  1. Treat ToolAbortError as an expected, benign outcome: catch it and stop the turn rather than retrying.
  2. If it fires unexpectedly, check who owns the AbortSignal — ensure the UI/client is still alive to answer permission requests before dispatching tools.
  3. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/74002dac85afd9b8. Report an issue: GitHub.