{"record":{"id":"6b80abf39197d351","repo":"can1357/oh-my-pi","slug":"device-authorization-cancelled","errorCode":null,"errorMessage":"Device authorization cancelled","messagePattern":"Device authorization cancelled","errorType":"exception","errorClass":"AIError.LoginCancelledError","httpStatus":null,"severity":"info","filePath":"packages/ai/src/registry/oauth/openai-codex.ts","lineNumber":294,"sourceCode":"\tconst pollIntervalMs =\n\t\t(typeof initData.interval === \"number\"\n\t\t\t? initData.interval\n\t\t\t: parseInt(String(initData.interval ?? \"5\"), 10) || 5) *\n\t\t\t1000 +\n\t\tDEVICE_POLL_SAFETY_MARGIN_MS;\n\n\tctrl.onAuth?.({\n\t\turl: DEVICE_AUTH_URL,\n\t\tinstructions: `Enter code: ${userCode}`,\n\t});\n\n\tctrl.onProgress?.(`Waiting for browser authorization (code: ${userCode})…`);\n\n\tfor (let poll = 0; poll < DEVICE_MAX_POLLS; poll++) {\n\t\tawait Bun.sleep(poll === 0 ? Math.min(pollIntervalMs, DEVICE_POLL_INTERVAL_MS) : pollIntervalMs);\n\n\t\tif (ctrl.signal?.aborted) {\n\t\t\tthrow new AIError.LoginCancelledError(\"Device authorization cancelled\");\n\t\t}\n\n\t\tconst pollResponse = await fetch(DEVICE_TOKEN_URL, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\tbody: JSON.stringify({\n\t\t\t\tdevice_auth_id: initData.device_auth_id,\n\t\t\t\tuser_code: userCode,\n\t\t\t}),\n\t\t\tsignal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS),\n\t\t});\n\n\t\t// 403/404 = authorization pending, keep polling\n\t\tif (pollResponse.status === 403 || pollResponse.status === 404) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (!pollResponse.ok) {","sourceCodeStart":276,"sourceCodeEnd":312,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/registry/oauth/openai-codex.ts#L276-L312","documentation":"This is an AIError.LoginCancelledError (not OAuthError) thrown inside the device-token polling loop of loginOpenAICodexDevice when the caller-supplied AbortSignal is observed to be aborted between polls. It lets a user or host application stop a pending 'waiting for browser authorization' flow cleanly instead of polling to exhaustion.","triggerScenarios":"The host passes ctrl.signal and aborts it (user pressing Ctrl-C in an interactive prompt, UI cancel button, or an outer timeout aborting the controller) while the loop is sleeping between DEVICE_TOKEN_URL polls.","commonSituations":"User cancels the login dialog in a TUI; an orchestrator enforces its own overall timeout by aborting; test harnesses aborting after a short delay; Ctrl-C during CLI login being translated into an abort.","solutions":["This is intentional cancellation — restart loginOpenAICodexDevice() when you actually want to log in","If unintended, check what is aborting the signal (your own timeout or UI code) and raise/adjust it","Catch AIError.LoginCancelledError specifically so cancellation isn't logged as a failure","Make sure the same AbortController isn't shared with other operations that finish earlier and abort early"],"exampleFix":"// before: cancellation treated as generic failure\ntry { await loginOpenAICodexDevice(); } catch (e) { log.error(e); }\n// after: distinguish user cancellation\ntry {\n  await loginOpenAICodexDevice();\n} catch (e) {\n  if (e instanceof AIError.LoginCancelledError) { log.info('Login cancelled by user'); return; }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"// pass your own signal only if you intend cancellation; pre-check its state\nif (ctrl.signal?.aborted) {\n  // don't even start the device flow — it will be cancelled immediately\n}","typeGuard":null,"tryCatchPattern":"try {\n  await loginOpenAICodexDevice();\n} catch (e) {\n  if (e instanceof AIError.LoginCancelledError) {\n    // expected user cancellation — log at info level and exit cleanly\n    return;\n  }\n  throw e;\n}","preventionTips":["Use a dedicated AbortController for the login so unrelated aborts don't cancel it","Catch AIError.LoginCancelledError separately from OAuthError in UI code","Only wire the signal when you actually provide a cancel affordance to the user","Set external timeouts longer than DEVICE_MAX_POLLS * pollIntervalMs if you rely on them instead of the built-in budget"],"tags":["oauth","openai-codex","device-flow","cancelled","abort-signal"],"backgroundTag":"operation-cancelled","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}