can1357/oh-my-pi · error · ToolError

Tool permission response used unknown option ID: ${outcome.o

Error message

Tool permission response used unknown option ID: ${outcome.optionId}

What it means

The permission client responded with an optionId that is not present in PERMISSION_OPTIONS_BY_ID (the registry of valid allow/reject options offered with the prompt). SessionTools throws ToolError because an unrecognized option ID means the responder is out of sync with the prompt contract — proceeding would silently guess the user's intent.

Source

Thrown at packages/coding-agent/src/session/session-tools.ts:830

								),
							},
							PERMISSION_OPTIONS,
							signal,
						).then(outcome => ({ kind: "permission" as const, outcome }));
						raced = await Promise.race([permissionPromise, abortPromise]);
					} finally {
						signal?.removeEventListener("abort", onAbort);
					}
					if (raced.kind === "aborted" || signal?.aborted) {
						throw new ToolAbortError("Permission request cancelled");
					}
					const outcome = raced.outcome;
					if (outcome.outcome === "cancelled") {
						throw new ToolAbortError("Permission request cancelled");
					}
					const selectedOption = PERMISSION_OPTIONS_BY_ID.get(outcome.optionId);
					if (!selectedOption) {
						throw new ToolError(`Tool permission response used unknown option ID: ${outcome.optionId}`);
					}
					if (selectedOption.kind === "allow_always") {
						this.#acpPermissionDecisions.set(permissionIntent.cacheKey, "allow_always");
					} else if (selectedOption.kind === "reject_always") {
						this.#acpPermissionDecisions.set(permissionIntent.cacheKey, "reject_always");
					}
					if (selectedOption.kind === "reject_once" || selectedOption.kind === "reject_always") {
						throw new ToolError(`Tool call rejected by user (${target.name})`);
					}
					return await target.execute(toolCallId, args as never, signal, onUpdate, ctx);
				};
			},
		}) as T;
	}

	#isExplicitAutoApproveMode(): boolean {
		return (
			this.#autoApprove ||

View on GitHub (pinned to 9690622007)

Solutions

  1. Return only option IDs exactly as delivered in the permission request's options list — echo back a chosen option verbatim.
  2. Update the permission client/UI so its option IDs match the current protocol version.
  3. Fall back to a default reject outcome if the client cannot match any offered option, instead of inventing an ID.

Example fix

// before: inventing an id
return { outcome: { outcome: 'selected', optionId: 'yes' } };
// after: echo a real offered option
const allow = options.find(o => o.kind === 'allow_once');
return { outcome: { outcome: 'selected', optionId: allow.id } };
Defensive patterns

Strategy: validation

Validate before calling

const VALID = new Set(options.map(o => o.id));
if (!VALID.has(chosenOptionId)) {
  throw new Error(`optionId ${chosenOptionId} not offered by this permission prompt`);
}

Type guard

function isKnownOptionId(id: string, offered: { id: string }[]): boolean {
  return offered.some(o => o.id === id);
}

Try / catch

try {
  await runTool();
} catch (err) {
  if (err instanceof ToolError && /unknown option ID/.test(err.message)) {
    logger.error('permission client out of sync', { err });
    // fall back to a safe default: reject the tool call
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: The client bridge's requestPermission handler returned `outcome.optionId` that isn't one of the IDs registered for this prompt (session-tools.ts:828-830) — e.g. stale/cached option IDs, a custom handler inventing IDs, or a protocol mismatch between host and client versions.

Common situations: Custom permission UI sending hand-written option IDs; mismatched versions of the RPC/ACP protocol where option ID formats changed; persisting and replaying an old decision option that no longer exists.

Related errors


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