{"record":{"id":"9a4252a70e2233ea","repo":"can1357/oh-my-pi","slug":"tool-permission-response-used-unknown-option-id","errorCode":null,"errorMessage":"Tool permission response used unknown option ID: ${outcome.optionId}","messagePattern":"Tool permission response used unknown option ID: (.+?)","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/session/session-tools.ts","lineNumber":830,"sourceCode":"\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tPERMISSION_OPTIONS,\n\t\t\t\t\t\t\tsignal,\n\t\t\t\t\t\t).then(outcome => ({ kind: \"permission\" as const, outcome }));\n\t\t\t\t\t\traced = await Promise.race([permissionPromise, abortPromise]);\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t}\n\t\t\t\t\tif (raced.kind === \"aborted\" || 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\tconst outcome = raced.outcome;\n\t\t\t\t\tif (outcome.outcome === \"cancelled\") {\n\t\t\t\t\t\tthrow new ToolAbortError(\"Permission request cancelled\");\n\t\t\t\t\t}\n\t\t\t\t\tconst selectedOption = PERMISSION_OPTIONS_BY_ID.get(outcome.optionId);\n\t\t\t\t\tif (!selectedOption) {\n\t\t\t\t\t\tthrow new ToolError(`Tool permission response used unknown option ID: ${outcome.optionId}`);\n\t\t\t\t\t}\n\t\t\t\t\tif (selectedOption.kind === \"allow_always\") {\n\t\t\t\t\t\tthis.#acpPermissionDecisions.set(permissionIntent.cacheKey, \"allow_always\");\n\t\t\t\t\t} else if (selectedOption.kind === \"reject_always\") {\n\t\t\t\t\t\tthis.#acpPermissionDecisions.set(permissionIntent.cacheKey, \"reject_always\");\n\t\t\t\t\t}\n\t\t\t\t\tif (selectedOption.kind === \"reject_once\" || selectedOption.kind === \"reject_always\") {\n\t\t\t\t\t\tthrow new ToolError(`Tool call rejected by user (${target.name})`);\n\t\t\t\t\t}\n\t\t\t\t\treturn await target.execute(toolCallId, args as never, signal, onUpdate, ctx);\n\t\t\t\t};\n\t\t\t},\n\t\t}) as T;\n\t}\n\n\t#isExplicitAutoApproveMode(): boolean {\n\t\treturn (\n\t\t\tthis.#autoApprove ||","sourceCodeStart":812,"sourceCodeEnd":848,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/session/session-tools.ts#L812-L848","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Return only option IDs exactly as delivered in the permission request's options list — echo back a chosen option verbatim.","Update the permission client/UI so its option IDs match the current protocol version.","Fall back to a default reject outcome if the client cannot match any offered option, instead of inventing an ID."],"exampleFix":"// before: inventing an id\nreturn { outcome: { outcome: 'selected', optionId: 'yes' } };\n// after: echo a real offered option\nconst allow = options.find(o => o.kind === 'allow_once');\nreturn { outcome: { outcome: 'selected', optionId: allow.id } };","handlingStrategy":"validation","validationCode":"const VALID = new Set(options.map(o => o.id));\nif (!VALID.has(chosenOptionId)) {\n  throw new Error(`optionId ${chosenOptionId} not offered by this permission prompt`);\n}","typeGuard":"function isKnownOptionId(id: string, offered: { id: string }[]): boolean {\n  return offered.some(o => o.id === id);\n}","tryCatchPattern":"try {\n  await runTool();\n} catch (err) {\n  if (err instanceof ToolError && /unknown option ID/.test(err.message)) {\n    logger.error('permission client out of sync', { err });\n    // fall back to a safe default: reject the tool call\n    return;\n  }\n  throw err;\n}","preventionTips":["Always echo option IDs verbatim from the permission request — never construct them by hand.","Keep RPC/ACP client and host versions aligned.","Test custom permission UIs against every option kind the prompt can offer.","Do not persist raw option IDs across sessions; re-resolve against the current prompt."],"tags":["permissions","protocol","invalid-input","rpc"],"backgroundTag":"invalid-permission-option","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}