@iislee/opencodex 2.11.0 → 2.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (675) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +114 -19
  3. package/bin/ocx.mjs +164 -36
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-BF38heuV.js +104 -0
  6. package/gui/dist/assets/index-DMiI18Kv.css +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +19 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +376 -52
  44. package/src/adapters/base.ts +54 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +601 -0
  48. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  49. package/src/adapters/cursor/cursor-errors.ts +129 -6
  50. package/src/adapters/cursor/discovery.ts +61 -4
  51. package/src/adapters/cursor/effort-map.ts +27 -3
  52. package/src/adapters/cursor/framing.ts +39 -0
  53. package/src/adapters/cursor/h2-pool.ts +123 -0
  54. package/src/adapters/cursor/http1-bidi.ts +361 -0
  55. package/src/adapters/cursor/images.ts +704 -0
  56. package/src/adapters/cursor/live-models.ts +174 -58
  57. package/src/adapters/cursor/live-transport.ts +609 -170
  58. package/src/adapters/cursor/native-exec-common.ts +23 -2
  59. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  60. package/src/adapters/cursor/native-exec-network.ts +1 -1
  61. package/src/adapters/cursor/native-exec-shell.ts +0 -1
  62. package/src/adapters/cursor/native-exec.ts +101 -14
  63. package/src/adapters/cursor/protobuf-events.ts +829 -11
  64. package/src/adapters/cursor/protobuf-request.ts +383 -65
  65. package/src/adapters/cursor/request-builder.ts +237 -31
  66. package/src/adapters/cursor/tool-definitions.ts +242 -12
  67. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  68. package/src/adapters/cursor/transport.ts +22 -0
  69. package/src/adapters/cursor/types.ts +28 -1
  70. package/src/adapters/cursor.ts +133 -7
  71. package/src/adapters/google-antigravity-replay.ts +558 -34
  72. package/src/adapters/google-antigravity-wire.ts +43 -10
  73. package/src/adapters/google-http.ts +38 -10
  74. package/src/adapters/google-tool-schema.ts +80 -15
  75. package/src/adapters/google-truncation.ts +11 -0
  76. package/src/adapters/google.ts +618 -74
  77. package/src/adapters/identity.ts +39 -6
  78. package/src/adapters/kiro-errors.ts +11 -0
  79. package/src/adapters/kiro-events.ts +19 -1
  80. package/src/adapters/kiro-thinking.ts +18 -2
  81. package/src/adapters/kiro-tools.ts +10 -1
  82. package/src/adapters/kiro.ts +92 -54
  83. package/src/adapters/mimo-free.ts +17 -0
  84. package/src/adapters/openai-chat-url.ts +11 -0
  85. package/src/adapters/openai-chat.ts +1311 -336
  86. package/src/adapters/openai-responses-url.ts +16 -0
  87. package/src/adapters/openai-responses.ts +830 -56
  88. package/src/adapters/registry.ts +175 -0
  89. package/src/adapters/responses-tool-schema.ts +67 -0
  90. package/src/adapters/tool-call-id.ts +119 -0
  91. package/src/adapters/tool-catalog-nudge.ts +104 -21
  92. package/src/adapters/xai-web-search.ts +185 -0
  93. package/src/bridge.ts +428 -61
  94. package/src/chat/inbound.ts +43 -19
  95. package/src/chat/outbound.ts +82 -26
  96. package/src/claude/agents-inject.ts +32 -9
  97. package/src/claude/context-windows.ts +21 -5
  98. package/src/claude/desktop-3p.ts +243 -9
  99. package/src/claude/gateway-cache.ts +41 -4
  100. package/src/claude/inbound.ts +72 -3
  101. package/src/claude/model-info.ts +38 -15
  102. package/src/claude/outbound.ts +70 -16
  103. package/src/cli/account-api.ts +35 -1
  104. package/src/cli/account-auth.ts +33 -6
  105. package/src/cli/account-catalog-refresh.ts +14 -0
  106. package/src/cli/account-extended.ts +389 -2
  107. package/src/cli/account-main.ts +317 -0
  108. package/src/cli/account.ts +30 -6
  109. package/src/cli/agent.ts +45 -1
  110. package/src/cli/claude-agent-startup-sync.ts +73 -0
  111. package/src/cli/claude-desktop.ts +29 -4
  112. package/src/cli/claude.ts +103 -34
  113. package/src/cli/codex-log-guard-doctor.ts +103 -0
  114. package/src/cli/codex-shim-autorestore.ts +2 -0
  115. package/src/cli/codex-shim-readiness.ts +76 -0
  116. package/src/cli/combo.ts +8 -0
  117. package/src/cli/config-command.ts +74 -10
  118. package/src/cli/dispatch.ts +593 -0
  119. package/src/cli/doctor.ts +315 -43
  120. package/src/cli/ensure-desired-integrations.ts +152 -0
  121. package/src/cli/export-command.ts +46 -20
  122. package/src/cli/help.ts +20 -271
  123. package/src/cli/index.ts +378 -519
  124. package/src/cli/init.ts +4 -17
  125. package/src/cli/integrations.ts +120 -2
  126. package/src/cli/lab.ts +607 -0
  127. package/src/cli/launcher-context.ts +77 -0
  128. package/src/cli/minimax.ts +497 -0
  129. package/src/cli/models-runtime.ts +35 -2
  130. package/src/cli/models.ts +100 -14
  131. package/src/cli/observe.ts +92 -3
  132. package/src/cli/opencode.ts +4 -2
  133. package/src/cli/provider-runtime.ts +18 -1
  134. package/src/cli/provider.ts +24 -3
  135. package/src/cli/ready.ts +301 -0
  136. package/src/cli/registry.ts +437 -0
  137. package/src/cli/root.ts +86 -0
  138. package/src/cli/route-policy.ts +92 -0
  139. package/src/cli/runtime-api.ts +6 -3
  140. package/src/cli/star-prompt.ts +71 -15
  141. package/src/cli/status.ts +10 -3
  142. package/src/cli/system-restart-client.ts +146 -0
  143. package/src/cli/tray-proxy.ts +153 -6
  144. package/src/cli/v2.ts +105 -10
  145. package/src/cli.ts +1 -1
  146. package/src/clients/config-export.ts +1358 -21
  147. package/src/codex/account-label.ts +14 -1
  148. package/src/codex/account-lifecycle.ts +130 -13
  149. package/src/codex/account-namespaces.ts +49 -3
  150. package/src/codex/account-priority.ts +83 -0
  151. package/src/codex/account-store.ts +29 -2
  152. package/src/codex/account-usability.ts +25 -2
  153. package/src/codex/admission.ts +256 -0
  154. package/src/codex/affinity-debug.ts +162 -0
  155. package/src/codex/app-server-processes.ts +493 -106
  156. package/src/codex/app-server-restart-service.ts +232 -0
  157. package/src/codex/auth-api.ts +849 -242
  158. package/src/codex/auth-collision.ts +5 -3
  159. package/src/codex/auth-context.ts +345 -32
  160. package/src/codex/autostart-health.ts +8 -1
  161. package/src/codex/catalog/account-models.ts +67 -0
  162. package/src/codex/catalog/aggregation.ts +68 -10
  163. package/src/codex/catalog/bundled.ts +331 -33
  164. package/src/codex/catalog/effort.ts +121 -30
  165. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  166. package/src/codex/catalog/kinds.ts +2 -0
  167. package/src/codex/catalog/metadata.ts +529 -45
  168. package/src/codex/catalog/native-models.ts +72 -0
  169. package/src/codex/catalog/parsing.ts +224 -30
  170. package/src/codex/catalog/provider-fetch.ts +1460 -134
  171. package/src/codex/catalog/sync.ts +1449 -186
  172. package/src/codex/catalog-admission.ts +199 -0
  173. package/src/codex/catalog-refresh-status.ts +105 -0
  174. package/src/codex/catalog-write-serialization.ts +242 -0
  175. package/src/codex/catalog.ts +6 -3
  176. package/src/codex/codex-write-lock.ts +384 -0
  177. package/src/codex/convergence-types.ts +614 -0
  178. package/src/codex/convergence.ts +651 -0
  179. package/src/codex/coordinator-doctor.ts +332 -0
  180. package/src/codex/custom-model-catalog-migration.ts +176 -0
  181. package/src/codex/desired-state.ts +230 -0
  182. package/src/codex/features.ts +636 -39
  183. package/src/codex/generation.ts +202 -0
  184. package/src/codex/history-job.ts +407 -0
  185. package/src/codex/history-lock.ts +242 -0
  186. package/src/codex/history-migration-guardian.ts +26 -20
  187. package/src/codex/history-provider.ts +231 -28
  188. package/src/codex/history-transition.ts +105 -0
  189. package/src/codex/history-worker.ts +220 -0
  190. package/src/codex/inject-coordination.ts +290 -0
  191. package/src/codex/inject.ts +1073 -152
  192. package/src/codex/injected-marker.ts +37 -3
  193. package/src/codex/integration-record.ts +266 -0
  194. package/src/codex/internal/catalog-writer.ts +203 -0
  195. package/src/codex/internal/history-writer.ts +80 -0
  196. package/src/codex/journal.ts +66 -4
  197. package/src/codex/log-guard/inspect.ts +506 -0
  198. package/src/codex/log-guard/lock.ts +150 -0
  199. package/src/codex/log-guard/maintenance.ts +403 -0
  200. package/src/codex/log-guard/path-safety.ts +88 -0
  201. package/src/codex/log-guard/policy.ts +44 -0
  202. package/src/codex/log-guard/processes.ts +205 -0
  203. package/src/codex/log-guard/protection.ts +489 -0
  204. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  205. package/src/codex/main-account-cache.ts +24 -0
  206. package/src/codex/main-account.ts +29 -1
  207. package/src/codex/management-convergence.ts +167 -0
  208. package/src/codex/model-cache.ts +56 -10
  209. package/src/codex/model-entitlements.ts +353 -0
  210. package/src/codex/native-main-admission.ts +47 -0
  211. package/src/codex/native-main-auth-temp.ts +187 -0
  212. package/src/codex/native-main-claim.ts +178 -0
  213. package/src/codex/native-main-lock-file.ts +162 -0
  214. package/src/codex/native-main-owner.ts +329 -0
  215. package/src/codex/native-profile-api.ts +247 -0
  216. package/src/codex/native-profile-manager.ts +1531 -0
  217. package/src/codex/native-profile-processes.ts +121 -0
  218. package/src/codex/native-profile-recovery.ts +99 -0
  219. package/src/codex/native-profile-stage-store.ts +387 -0
  220. package/src/codex/native-profile-startup.ts +492 -0
  221. package/src/codex/native-profile-store.ts +855 -0
  222. package/src/codex/native-profile-types.ts +120 -0
  223. package/src/codex/native-residue.ts +682 -0
  224. package/src/codex/paths.ts +80 -1
  225. package/src/codex/plan-from-token.ts +140 -0
  226. package/src/codex/plan.ts +40 -0
  227. package/src/codex/plugins-doctor.ts +1 -1
  228. package/src/codex/pool-rotation.ts +74 -4
  229. package/src/codex/project-config-warnings.ts +20 -6
  230. package/src/codex/prompt-journal.ts +352 -0
  231. package/src/codex/prompt-layers.ts +967 -0
  232. package/src/codex/prompt-lock.ts +143 -0
  233. package/src/codex/quota-rejection.ts +298 -0
  234. package/src/codex/quota.ts +175 -13
  235. package/src/codex/refresh.ts +11 -2
  236. package/src/codex/reset-credit-recovery.ts +1044 -0
  237. package/src/codex/routing.ts +505 -94
  238. package/src/codex/runtime.ts +159 -38
  239. package/src/codex/shim.ts +1009 -28
  240. package/src/codex/subagent-model-fallback.ts +350 -35
  241. package/src/codex/sync.ts +191 -2
  242. package/src/codex/transition-state.ts +612 -0
  243. package/src/codex/upstream-host-health.ts +368 -0
  244. package/src/codex/user-identity.ts +557 -0
  245. package/src/codex/warmup.ts +187 -81
  246. package/src/codex/write-coordination.ts +114 -0
  247. package/src/combos/failover.ts +20 -0
  248. package/src/combos/index.ts +4 -0
  249. package/src/combos/request.ts +32 -0
  250. package/src/combos/types.ts +81 -9
  251. package/src/config/provider-name.ts +24 -0
  252. package/src/config.ts +1762 -140
  253. package/src/generated/compatibility-version.json +3164 -0
  254. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  255. package/src/grok/inject.ts +16 -5
  256. package/src/grok/inspect.ts +45 -0
  257. package/src/grok/sync.ts +2 -2
  258. package/src/images/loop.ts +152 -29
  259. package/src/images/plan.ts +23 -13
  260. package/src/integrations/config-io.ts +269 -0
  261. package/src/integrations/journal.ts +315 -0
  262. package/src/integrations/merge.ts +135 -0
  263. package/src/integrations/mutation-flight.ts +71 -0
  264. package/src/integrations/native/ownership-preflight.ts +202 -0
  265. package/src/integrations/omp-yaml-source.ts +358 -0
  266. package/src/integrations/owned-refresh.ts +74 -0
  267. package/src/integrations/ownership.ts +111 -0
  268. package/src/integrations/registry.ts +159 -0
  269. package/src/integrations/serialize.ts +314 -0
  270. package/src/integrations/state.ts +361 -0
  271. package/src/integrations/store.ts +103 -0
  272. package/src/integrations/writer-lock.ts +98 -0
  273. package/src/integrations/writer.ts +691 -0
  274. package/src/lab/artifacts/sanitize.ts +586 -0
  275. package/src/lab/artifacts/secure-fs.ts +475 -0
  276. package/src/lab/artifacts/store.ts +310 -0
  277. package/src/lab/automation/budgets.ts +78 -0
  278. package/src/lab/automation/config-persistence.ts +256 -0
  279. package/src/lab/automation/constants.ts +39 -0
  280. package/src/lab/automation/cooldown.ts +103 -0
  281. package/src/lab/automation/dispatch.ts +211 -0
  282. package/src/lab/automation/index.ts +13 -0
  283. package/src/lab/automation/orchestrator.ts +499 -0
  284. package/src/lab/automation/persistence.ts +512 -0
  285. package/src/lab/automation/planner.ts +371 -0
  286. package/src/lab/automation/policy.ts +136 -0
  287. package/src/lab/automation/queue.ts +191 -0
  288. package/src/lab/automation/recovery.ts +24 -0
  289. package/src/lab/automation/route-context.ts +21 -0
  290. package/src/lab/automation/run-key.ts +44 -0
  291. package/src/lab/automation/runs-query.ts +34 -0
  292. package/src/lab/automation/types.ts +160 -0
  293. package/src/lab/conformance/assertion.ts +325 -0
  294. package/src/lab/conformance/digest.ts +22 -0
  295. package/src/lab/conformance/executor.ts +741 -0
  296. package/src/lab/conformance/fixture-provider.ts +27 -0
  297. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  298. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  299. package/src/lab/conformance/harness-budget.ts +47 -0
  300. package/src/lab/conformance/index.ts +5 -0
  301. package/src/lab/conformance/jcs.ts +64 -0
  302. package/src/lab/conformance/json-pointer.ts +39 -0
  303. package/src/lab/conformance/manifest.ts +180 -0
  304. package/src/lab/conformance/mcp-stub.ts +179 -0
  305. package/src/lab/conformance/negative-controls.ts +164 -0
  306. package/src/lab/conformance/observation.ts +355 -0
  307. package/src/lab/conformance/runner.ts +68 -0
  308. package/src/lab/conformance/sse-normalize.ts +59 -0
  309. package/src/lab/conformance/suite-manifest.ts +78 -0
  310. package/src/lab/conformance/types.ts +214 -0
  311. package/src/lab/constants.ts +126 -0
  312. package/src/lab/digest.ts +64 -0
  313. package/src/lab/events/errors.ts +9 -0
  314. package/src/lab/events/limits.ts +117 -0
  315. package/src/lab/events/types.ts +229 -0
  316. package/src/lab/events/validate.ts +781 -0
  317. package/src/lab/fabric/constants.ts +40 -0
  318. package/src/lab/fabric/executor.ts +492 -0
  319. package/src/lab/fabric/index.ts +80 -0
  320. package/src/lab/fabric/manifest.ts +222 -0
  321. package/src/lab/fabric/observe.ts +489 -0
  322. package/src/lab/fabric/patch.ts +79 -0
  323. package/src/lab/fabric/producer-child.ts +139 -0
  324. package/src/lab/fabric/producer-isolate.ts +276 -0
  325. package/src/lab/fabric/producer-protocol.ts +61 -0
  326. package/src/lab/fabric/scratch.ts +439 -0
  327. package/src/lab/fabric/subject.ts +106 -0
  328. package/src/lab/fabric/types.ts +134 -0
  329. package/src/lab/fabric/verifier.ts +98 -0
  330. package/src/lab/index.ts +54 -0
  331. package/src/lab/ledger/artifact-refs.ts +127 -0
  332. package/src/lab/ledger/invalidation.ts +136 -0
  333. package/src/lab/ledger/purge.ts +310 -0
  334. package/src/lab/ledger/store.ts +532 -0
  335. package/src/lab/live/credential-lease.ts +53 -0
  336. package/src/lab/live/destination.ts +155 -0
  337. package/src/lab/live/executor.ts +336 -0
  338. package/src/lab/live/inert-tools.ts +56 -0
  339. package/src/lab/live/manifest.ts +85 -0
  340. package/src/lab/live/mcp-loopback.ts +57 -0
  341. package/src/lab/live/runner.ts +19 -0
  342. package/src/lab/live/sandbox.ts +61 -0
  343. package/src/lab/live/suite-manifest.ts +41 -0
  344. package/src/lab/live/transport.ts +118 -0
  345. package/src/lab/live/types.ts +197 -0
  346. package/src/lab/observe/from-conformance.ts +301 -0
  347. package/src/lab/observe/from-live.ts +117 -0
  348. package/src/lab/paths.ts +153 -0
  349. package/src/lab/projection/rebuild.ts +495 -0
  350. package/src/lab/projection/schema.ts +135 -0
  351. package/src/lab/projection/verdicts.ts +474 -0
  352. package/src/lab/projection/verification.ts +412 -0
  353. package/src/lab/public/bundle.ts +217 -0
  354. package/src/lab/public/community-authority.ts +175 -0
  355. package/src/lab/public/community-files.ts +29 -0
  356. package/src/lab/public/community.ts +479 -0
  357. package/src/lab/public/file-safety.ts +155 -0
  358. package/src/lab/public/ids.ts +26 -0
  359. package/src/lab/public/index.ts +16 -0
  360. package/src/lab/public/mutation-lock.ts +424 -0
  361. package/src/lab/public/operator.ts +353 -0
  362. package/src/lab/public/origin-purge.ts +79 -0
  363. package/src/lab/public/origin.ts +203 -0
  364. package/src/lab/public/privacy.ts +143 -0
  365. package/src/lab/public/private-file.ts +261 -0
  366. package/src/lab/public/project.ts +124 -0
  367. package/src/lab/public/purge-test-fault.ts +21 -0
  368. package/src/lab/public/purge.ts +223 -0
  369. package/src/lab/public/registry.ts +44 -0
  370. package/src/lab/public/revocation.ts +252 -0
  371. package/src/lab/public/signature.ts +243 -0
  372. package/src/lab/public/storage.ts +105 -0
  373. package/src/lab/public/strict-json.ts +206 -0
  374. package/src/lab/public/time.ts +26 -0
  375. package/src/lab/public/types.ts +172 -0
  376. package/src/lab/public/validate.ts +391 -0
  377. package/src/lab/query/catalog.ts +101 -0
  378. package/src/lab/query/connection.ts +107 -0
  379. package/src/lab/query/constants.ts +4 -0
  380. package/src/lab/query/cursor.ts +132 -0
  381. package/src/lab/query/dto-map.ts +277 -0
  382. package/src/lab/query/errors.ts +22 -0
  383. package/src/lab/query/freshness.ts +53 -0
  384. package/src/lab/query/index.ts +45 -0
  385. package/src/lab/query/latest-observation.ts +59 -0
  386. package/src/lab/query/passive-production.ts +159 -0
  387. package/src/lab/query/queries.ts +444 -0
  388. package/src/lab/query/types.ts +266 -0
  389. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  390. package/src/lab/subject/installation-salt.ts +112 -0
  391. package/src/lab/subject/protocol-subject.ts +80 -0
  392. package/src/lab/subject/route-subject.ts +74 -0
  393. package/src/lib/app-owned-memory-stores.ts +22 -0
  394. package/src/lib/bounded-body.ts +153 -9
  395. package/src/lib/bun-runtime.ts +125 -12
  396. package/src/lib/bun-stream-caps.ts +13 -9
  397. package/src/lib/codex-restart-contract.ts +120 -0
  398. package/src/lib/config-ownership.ts +6 -2
  399. package/src/lib/destination-policy.ts +65 -1
  400. package/src/lib/errors.ts +44 -2
  401. package/src/lib/fabric-task-execution-authority.ts +7 -0
  402. package/src/lib/fabric-task-host.ts +29 -0
  403. package/src/lib/lab-activation.ts +223 -0
  404. package/src/lib/lab-live-execution-authority.ts +13 -0
  405. package/src/lib/lab-live-host.ts +30 -0
  406. package/src/lib/lab-live-pinned-sender.ts +56 -0
  407. package/src/lib/lab-live-route-production.ts +130 -0
  408. package/src/lib/lab-passive-linker-registration.ts +26 -0
  409. package/src/lib/local-management-attestation.ts +51 -0
  410. package/src/lib/local-management-capability.ts +100 -0
  411. package/src/lib/local-provider-reload-contract.ts +100 -0
  412. package/src/lib/optional-shutdown-hooks.ts +57 -0
  413. package/src/lib/pinned-http.ts +145 -26
  414. package/src/lib/process-control.ts +4 -1
  415. package/src/lib/provider-outbound.ts +49 -9
  416. package/src/lib/redact.ts +419 -3
  417. package/src/lib/self-launch-argv.ts +15 -0
  418. package/src/lib/server-resource-ownership.ts +71 -0
  419. package/src/lib/shadow-call.ts +35 -4
  420. package/src/lib/sse-decoder.ts +41 -0
  421. package/src/lib/state-store-registrations.ts +10 -2
  422. package/src/lib/system-restart-contract.ts +73 -0
  423. package/src/lib/token-estimate.ts +19 -2
  424. package/src/lib/tool-argument-integers.ts +202 -0
  425. package/src/lib/translator-budget.ts +44 -0
  426. package/src/lib/upstream-http-version.ts +57 -0
  427. package/src/lib/upstream-reachability.ts +95 -0
  428. package/src/lib/upstream-retry.ts +156 -3
  429. package/src/lib/windows-atomic-replace.ts +155 -0
  430. package/src/lib/windows-elevation.ts +70 -2
  431. package/src/lib/windows-secret-acl.ts +409 -69
  432. package/src/lib/windows-service-wrappers.ts +72 -0
  433. package/src/lib/windows-text.ts +106 -0
  434. package/src/lib/windows-user-principal.ts +341 -0
  435. package/src/lib/winsw.ts +33 -5
  436. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  437. package/src/oauth/account-import/index.ts +15 -0
  438. package/src/oauth/account-import/parser.ts +83 -0
  439. package/src/oauth/account-import/registry.ts +18 -0
  440. package/src/oauth/account-import/service.ts +75 -0
  441. package/src/oauth/account-import/types.ts +91 -0
  442. package/src/oauth/anthropic.ts +12 -1
  443. package/src/oauth/callback-server.ts +8 -2
  444. package/src/oauth/chatgpt.ts +12 -1
  445. package/src/oauth/command-code.ts +239 -0
  446. package/src/oauth/cursor.ts +46 -5
  447. package/src/oauth/google-antigravity.ts +35 -3
  448. package/src/oauth/health.ts +20 -12
  449. package/src/oauth/index.ts +398 -66
  450. package/src/oauth/key-providers.ts +16 -0
  451. package/src/oauth/kimi.ts +16 -2
  452. package/src/oauth/kiro.ts +50 -6
  453. package/src/oauth/local-token-detect.ts +11 -2
  454. package/src/oauth/log.ts +3 -1
  455. package/src/oauth/login-cli.ts +88 -28
  456. package/src/oauth/nous.ts +798 -0
  457. package/src/oauth/store.ts +119 -21
  458. package/src/oauth/token-guardian.ts +9 -3
  459. package/src/pi/models.ts +2 -2
  460. package/src/providers/alibaba-region-migration.ts +1 -1
  461. package/src/providers/antigravity-models.ts +521 -31
  462. package/src/providers/base-url-choices.ts +10 -0
  463. package/src/providers/codex-capacity.ts +292 -0
  464. package/src/providers/command-code-efforts.ts +144 -0
  465. package/src/providers/context-cap.ts +22 -5
  466. package/src/providers/cursor-pool.ts +72 -0
  467. package/src/providers/derive.ts +253 -6
  468. package/src/providers/fastwire.ts +501 -0
  469. package/src/providers/free-directory.ts +10 -7
  470. package/src/providers/google-vertex-location.ts +14 -0
  471. package/src/providers/key-failover.ts +71 -3
  472. package/src/providers/label.ts +1 -1
  473. package/src/providers/model-discovery-limits.ts +16 -0
  474. package/src/providers/model-discovery.ts +115 -22
  475. package/src/providers/model-rename-migration.ts +255 -0
  476. package/src/providers/model-rename-startup.ts +28 -0
  477. package/src/providers/openai-sidecar.ts +72 -4
  478. package/src/providers/openai-tier-startup.ts +31 -2
  479. package/src/providers/openai-tiers.ts +119 -4
  480. package/src/providers/openai-virtual-models.ts +1 -0
  481. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  482. package/src/providers/provider-id-rewrite.ts +29 -0
  483. package/src/providers/quota.ts +1319 -38
  484. package/src/providers/registry.ts +1429 -111
  485. package/src/providers/request-pacing.ts +310 -0
  486. package/src/providers/service-tier.ts +277 -0
  487. package/src/providers/slug-codec.ts +42 -6
  488. package/src/providers/static-model-discovery.ts +86 -0
  489. package/src/providers/xai-responses-opt-in.ts +15 -0
  490. package/src/providers/xai-transport.ts +11 -4
  491. package/src/reasoning-effort.ts +49 -1
  492. package/src/responses/compaction.ts +26 -1
  493. package/src/responses/custom-tool-compat.ts +266 -0
  494. package/src/responses/hosted-tool-policy.ts +9 -0
  495. package/src/responses/namespace-tool-compat.ts +355 -0
  496. package/src/responses/parser.ts +220 -38
  497. package/src/responses/provider-continuation.ts +98 -0
  498. package/src/responses/provider-opaque-metadata.ts +73 -0
  499. package/src/responses/reasoning-envelope.ts +9 -1
  500. package/src/responses/reasoning-replay-cache.ts +426 -0
  501. package/src/responses/schema.ts +7 -1
  502. package/src/responses/spill-store.ts +75 -10
  503. package/src/responses/state.ts +565 -27
  504. package/src/responses/thought-signature-replay.ts +347 -0
  505. package/src/responses/tool-search-compat.ts +301 -0
  506. package/src/responses/truncated-stop-reason.ts +60 -0
  507. package/src/router.ts +366 -30
  508. package/src/routing/analytics.ts +378 -0
  509. package/src/routing/capability.ts +244 -0
  510. package/src/routing/compatibility/assemble.ts +73 -0
  511. package/src/routing/compatibility/behavior.ts +278 -0
  512. package/src/routing/compatibility/catalog.ts +99 -0
  513. package/src/routing/compatibility/endpoint.ts +52 -0
  514. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  515. package/src/routing/compatibility/policy.ts +181 -0
  516. package/src/routing/compatibility/provider-slot.ts +56 -0
  517. package/src/routing/compatibility/reader.ts +110 -0
  518. package/src/routing/compatibility/subject.ts +191 -0
  519. package/src/routing/compatibility/types.ts +64 -0
  520. package/src/routing/compatibility/version.ts +104 -0
  521. package/src/routing/cost.ts +77 -0
  522. package/src/routing/evaluator.ts +495 -0
  523. package/src/routing/health.ts +412 -0
  524. package/src/routing/history/cursor.ts +43 -0
  525. package/src/routing/history/indexer.ts +605 -0
  526. package/src/routing/history/schema.ts +72 -0
  527. package/src/routing/profile-namespace.ts +15 -0
  528. package/src/routing/profile.ts +547 -0
  529. package/src/routing/quota.ts +145 -0
  530. package/src/routing/request-evidence.ts +45 -0
  531. package/src/routing/trace.ts +776 -0
  532. package/src/server/adapter-resolve.ts +2 -29
  533. package/src/server/auth-cors.ts +267 -46
  534. package/src/server/background-lifecycle.ts +182 -0
  535. package/src/server/chat-completions.ts +130 -56
  536. package/src/server/chat-native-sse.ts +331 -0
  537. package/src/server/chat-native.ts +426 -0
  538. package/src/server/claude-messages.ts +159 -43
  539. package/src/server/direct-local-http.ts +347 -0
  540. package/src/server/effort-policy.ts +18 -0
  541. package/src/server/github-copilot-responses-repair.ts +338 -0
  542. package/src/server/gui-static.ts +39 -10
  543. package/src/server/images.ts +94 -12
  544. package/src/server/index.ts +865 -181
  545. package/src/server/lifecycle.ts +284 -13
  546. package/src/server/live.ts +136 -17
  547. package/src/server/local-management-read-client.ts +90 -0
  548. package/src/server/local-provider-reload-client.ts +137 -0
  549. package/src/server/management/agent-settings-routes.ts +398 -103
  550. package/src/server/management/api-key-usage.ts +31 -5
  551. package/src/server/management/body.ts +6 -0
  552. package/src/server/management/combo-routes.ts +62 -24
  553. package/src/server/management/config-routes.ts +464 -51
  554. package/src/server/management/context.ts +80 -2
  555. package/src/server/management/integration-routes.ts +498 -0
  556. package/src/server/management/lab-automation-routes.ts +206 -0
  557. package/src/server/management/lab-routes.ts +563 -0
  558. package/src/server/management/logs-usage-routes.ts +101 -32
  559. package/src/server/management/model-routes.ts +189 -131
  560. package/src/server/management/model-rows.ts +163 -0
  561. package/src/server/management/native-integration-routes.ts +769 -0
  562. package/src/server/management/oauth-account-routes.ts +80 -4
  563. package/src/server/management/provider-capability-config.ts +48 -0
  564. package/src/server/management/provider-routes.ts +764 -157
  565. package/src/server/management/request-history-routes.ts +191 -0
  566. package/src/server/management/routing-analytics-routes.ts +74 -0
  567. package/src/server/management/routing-profile-routes.ts +380 -0
  568. package/src/server/management/shared.ts +27 -11
  569. package/src/server/management/sidebar-routes.ts +47 -31
  570. package/src/server/management/storage-log-guard-routes.ts +186 -0
  571. package/src/server/management/sync-response.ts +69 -0
  572. package/src/server/management/system-restart.ts +288 -32
  573. package/src/server/management/system-routes.ts +77 -0
  574. package/src/server/management/usage-summary-cache.ts +9 -1
  575. package/src/server/management/vision-sidecar-options.ts +167 -0
  576. package/src/server/management/web-search-sidecar-options.ts +120 -0
  577. package/src/server/management-api.ts +115 -14
  578. package/src/server/management-auth.ts +220 -5
  579. package/src/server/passive-route-linker.ts +66 -0
  580. package/src/server/ports.ts +41 -1
  581. package/src/server/proxy-liveness.ts +132 -5
  582. package/src/server/readiness.ts +99 -0
  583. package/src/server/relay-eager.ts +82 -42
  584. package/src/server/relay.ts +236 -76
  585. package/src/server/request-decompress.ts +113 -6
  586. package/src/server/request-log.ts +235 -22
  587. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  588. package/src/server/responses/agent-task-recovery.ts +465 -0
  589. package/src/server/responses/collaboration.ts +204 -35
  590. package/src/server/responses/compact.ts +442 -55
  591. package/src/server/responses/core.ts +2872 -331
  592. package/src/server/responses/empty-completion-guard.ts +276 -0
  593. package/src/server/responses/encrypted-payload.ts +62 -39
  594. package/src/server/responses/fetch-helpers.ts +79 -4
  595. package/src/server/responses/input-admission.ts +185 -0
  596. package/src/server/responses/pacing-overload.ts +13 -0
  597. package/src/server/responses/policy-fallback.ts +178 -0
  598. package/src/server/responses/responses-field-backfill.ts +251 -0
  599. package/src/server/responses/terminal-guard.ts +26 -5
  600. package/src/server/responses/upstream-error.ts +5 -0
  601. package/src/server/responses/ws-upstream.ts +308 -0
  602. package/src/server/responses-custom-tool-repair.ts +282 -0
  603. package/src/server/responses-item-id-repair.ts +54 -6
  604. package/src/server/responses-json-events.ts +90 -0
  605. package/src/server/responses-model-rewrite.ts +29 -0
  606. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  607. package/src/server/responses-snapshot-repair.ts +621 -0
  608. package/src/server/responses-terminal-repair.ts +342 -0
  609. package/src/server/responses-tool-search-repair.ts +267 -0
  610. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  611. package/src/server/responses.ts +18 -2
  612. package/src/server/search.ts +78 -13
  613. package/src/server/sse-frame-buffer.ts +292 -0
  614. package/src/server/sse-payload-rewrite.ts +110 -22
  615. package/src/server/startup-action-control.ts +8 -1
  616. package/src/server/startup-health-cache.ts +19 -1
  617. package/src/server/system-env.ts +80 -9
  618. package/src/server/ws-bridge.ts +39 -38
  619. package/src/service-manager-probe.ts +892 -0
  620. package/src/service.ts +1111 -90
  621. package/src/sidecar/auth.ts +92 -0
  622. package/src/sidecar/candidates.ts +83 -0
  623. package/src/storage/cleanup.ts +2 -2
  624. package/src/storage/scanner.ts +1 -1
  625. package/src/storage/worker-lifecycle.ts +14 -14
  626. package/src/tray/windows-tray.ps1 +83 -9
  627. package/src/tray/windows.ts +43 -16
  628. package/src/types/accounts.ts +37 -0
  629. package/src/types/config.ts +845 -0
  630. package/src/types/provider.ts +545 -0
  631. package/src/types/request.ts +384 -0
  632. package/src/types/tools.ts +131 -0
  633. package/src/types/wire.ts +80 -0
  634. package/src/types.ts +104 -1236
  635. package/src/update/index.ts +32 -19
  636. package/src/update/job.ts +442 -67
  637. package/src/update/notify.ts +12 -6
  638. package/src/update/npm-cache-preflight.d.mts +47 -0
  639. package/src/update/npm-cache-preflight.mjs +201 -0
  640. package/src/update/transactional-install.d.mts +22 -0
  641. package/src/update/transactional-install.mjs +259 -0
  642. package/src/usage/cost.ts +0 -0
  643. package/src/usage/expected-prices.ts +268 -16
  644. package/src/usage/log.ts +606 -41
  645. package/src/usage/summary.ts +177 -9
  646. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  647. package/src/usage/user-cost-overlays.ts +314 -0
  648. package/src/vision/anthropic-describe.ts +10 -6
  649. package/src/vision/backends.ts +97 -0
  650. package/src/vision/describe.ts +9 -3
  651. package/src/vision/eligibility.ts +250 -0
  652. package/src/vision/index.ts +238 -24
  653. package/src/vision/reasoning.ts +55 -0
  654. package/src/vision/routed-describe.ts +175 -0
  655. package/src/vision/timeout-bounds.ts +9 -0
  656. package/src/web-search/anthropic-executor.ts +13 -7
  657. package/src/web-search/backends.ts +108 -0
  658. package/src/web-search/exa-executor.ts +88 -0
  659. package/src/web-search/executor.ts +11 -3
  660. package/src/web-search/gemini-executor.ts +141 -0
  661. package/src/web-search/index.ts +150 -15
  662. package/src/web-search/loop.ts +279 -50
  663. package/src/web-search/parse.ts +125 -30
  664. package/src/web-search/sources.ts +60 -0
  665. package/src/web-search/xai-executor.ts +219 -0
  666. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  667. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  668. package/gui/dist/provider-icons/antigravity.svg +0 -1
  669. package/gui/dist/provider-icons/claude.svg +0 -1
  670. package/gui/dist/provider-icons/copilot.svg +0 -1
  671. package/gui/dist/provider-icons/cursor.svg +0 -2
  672. package/gui/dist/provider-icons/gemini.svg +0 -1
  673. package/gui/dist/provider-icons/grok-color.svg +0 -1
  674. package/gui/dist/provider-icons/kiro.svg +0 -14
  675. package/src/cli/internal-dispatch.ts +0 -20
@@ -1,5 +1,11 @@
1
- import { ConfigMutationLockError, loadConfig, mutatePersistedConfig, saveConfigPreservingClaudeCode } from "../config";
2
- import { withCodexAccountLogLabel } from "./account-label";
1
+ import {
2
+ ConfigMutationLockError,
3
+ loadConfig,
4
+ mutatePersistedConfig,
5
+ saveConfigPreservingClaudeCode,
6
+ withConfigMutationLockSync,
7
+ } from "../config";
8
+ import { codexAccountLogLabel, withCodexAccountLogLabel } from "./account-label";
3
9
  import {
4
10
  getCodexAccountCredential,
5
11
  getValidCodexToken,
@@ -14,27 +20,52 @@ import {
14
20
  TokenRefreshError,
15
21
  } from "./account-store";
16
22
  import { deleteCodexAccount, reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
23
+ import {
24
+ appendDefaultCodexAccountNamespace,
25
+ codexAccountPickerEnabled,
26
+ } from "./account-namespaces";
27
+ import {
28
+ catalogRefreshIsPending,
29
+ normalizeCatalogDisposition,
30
+ } from "./catalog-refresh-status";
17
31
  import { isCodexAccountPaused, setCodexAccountPaused } from "./account-pause";
18
32
  import {
33
+ clearCodexAccountPin,
34
+ getCodexAccountPriority,
35
+ isCodexAccountPriorityKey,
36
+ pinnedCodexAccountId,
37
+ setCodexAccountPin,
38
+ setCodexAccountPriority,
39
+ } from "./account-priority";
40
+ import {
41
+ claimDueCodexQuotaRecoveryProbes,
19
42
  clearCodexAccountCooldown,
20
43
  clearThreadAccountMapForAccount,
21
44
  getEffectiveActiveCodexAccountId,
45
+ isEffectiveCodexAccountPinned,
22
46
  reconcileCodexActiveAfterExclusion,
23
47
  resetCodexRoutingForManualSelection,
48
+ settleCodexQuotaRecoveryProbe,
24
49
  } from "./routing";
25
50
  import {
51
+ DEFAULT_ACCOUNT_PRIORITY,
52
+ MAX_ACCOUNT_PRIORITY,
53
+ MIN_ACCOUNT_PRIORITY,
26
54
  normalizeAccountPoolStickyLimit,
27
55
  normalizeAccountPoolStrategy,
28
56
  parseAccountPoolStickyLimit,
29
57
  parseAccountPoolStrategy,
58
+ parseAccountPriority,
30
59
  } from "./pool-rotation";
31
60
  import { checkAccountIdCollision, getMainChatgptAccountId, readCodexTokens, readCodexTokensResult } from "./auth-collision";
61
+ import { codexPlanValue, isThirtyDayOnlyCodexPlan } from "./plan";
32
62
  export { checkAccountIdCollision, getMainChatgptAccountId } from "./auth-collision";
33
63
  export { clearAccountNeedsReauth, isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
34
64
  import { clearAccountNeedsReauth, isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
35
65
  import {
36
66
  clearAccountQuota,
37
67
  getAccountQuota,
68
+ isCompleteCodexQuotaRecoverySnapshot,
38
69
  isCodexQuotaExhausted,
39
70
  listAccountQuotas,
40
71
  parseUsageQuota,
@@ -51,24 +82,30 @@ export {
51
82
  setAccountQuotaFromParsed,
52
83
  updateAccountQuota,
53
84
  } from "./quota";
54
- import { extractAccountId, decodeJwtPayload } from "../oauth/chatgpt";
55
- import { getMainAccountPlan, MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
56
- import { captureConfigGeneration } from "../lib/state-store-sweeper";
85
+ import { extractAccountId } from "../oauth/chatgpt";
86
+ import { getMainAccountPlan, isMainAccountTokenVerifiablyLive, MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
87
+ import { captureConfigGeneration, registerStateSweepAfterTick } from "../lib/state-store-sweeper";
57
88
  import { reconcileLiveStateStores } from "../lib/state-store-registrations";
58
89
  import {
90
+ captureMainAccountIdentityGeneration,
59
91
  clearMainAccountInfoCache,
92
+ getMainAccountCredentialPresence,
60
93
  getMainAccountInfoCache,
94
+ isMainAccountIdentityGenerationLive,
95
+ setMainAccountCredentialPresence,
61
96
  setMainAccountInfoCache,
62
97
  type MainAccountInfo,
63
98
  } from "./main-account-cache";
64
99
  export { clearMainAccountInfoCache } from "./main-account-cache";
65
100
  import { maskEmail } from "../lib/privacy";
66
- import { CodexWarmupError, codexWarmupFailureReason, warmCodexAccount } from "./warmup";
101
+ import { codexWarmupFailureReason, warmCodexAccount } from "./warmup";
67
102
  export { maskEmail } from "../lib/privacy";
68
- import type { CodexAccount, OcxConfig } from "../types";
103
+ import type { CodexAccount, CodexAccountCredentials, OcxConfig } from "../types";
104
+ import type { CatalogDisposition } from "./convergence-types";
69
105
  import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
70
106
  import { providerCodexAccountMode } from "../providers/registry";
71
- import { readBoundedResponseBody } from "../lib/bounded-body";
107
+ import { BOUNDED_BODY_MAX_BYTES, readBoundedResponseBody } from "../lib/bounded-body";
108
+ import { cancelBodyOnAbort, signalWithTimeout } from "../lib/abort";
72
109
  import {
73
110
  oauthAccountHealthFields,
74
111
  projectCodexAccountHealth,
@@ -82,7 +119,20 @@ import {
82
119
  isValidCodexAccountId,
83
120
  } from "./account-id";
84
121
  import { codexAccountIdNamespaceCollisionError } from "./account-namespace-match";
85
- import { ResourceAdmissionError } from "../lib/admission";
122
+ import { ResourceAdmissionError, type AdmissionLease } from "../lib/admission";
123
+ import { tryAcquireNativeMainProfileClaim } from "./native-main-admission";
124
+ import { withNativeMainSharedClaim } from "./native-main-claim";
125
+ import { resolveNativeProfileContext } from "./native-profile-store";
126
+ import { NativeProfileError } from "./native-profile-types";
127
+
128
+ function isNativeMainClaimUnavailable(error: unknown): error is NativeProfileError {
129
+ return error instanceof NativeProfileError
130
+ && (error.code === "NATIVE_MAIN_CLAIM_BUSY" || error.code === "NATIVE_MAIN_CLAIM_UNAVAILABLE");
131
+ }
132
+
133
+ function withNativeMainCredentialClaim<T>(operation: () => Promise<T>): Promise<T> {
134
+ return withNativeMainSharedClaim(resolveNativeProfileContext(), operation);
135
+ }
86
136
 
87
137
  function jsonResponse(data: unknown, status = 200): Response {
88
138
  return new Response(JSON.stringify(data), {
@@ -91,11 +141,28 @@ function jsonResponse(data: unknown, status = 200): Response {
91
141
  });
92
142
  }
93
143
 
94
- const MANUAL_IMPORT_ENV = "OPENCODEX_ENABLE_UNVERIFIED_CODEX_IMPORT";
144
+ function nativeMainProfileBusyResponse(): Response {
145
+ const response = jsonResponse({ error: "server_busy", code: "server_busy" }, 503);
146
+ response.headers.set("Retry-After", "1");
147
+ return response;
148
+ }
149
+
150
+ const CODEX_CREDENTIAL_PERSISTENCE_ERROR = "Account was saved, but credential setup did not complete. Reauthenticate or remove the account.";
151
+ const CODEX_CREDENTIAL_PERSISTENCE_CODE = "codex_credential_persistence_failed";
95
152
 
96
153
  const MAX_CODEX_LOGIN_STATE_ROWS = 32;
97
154
  const CODEX_LOGIN_TERMINAL_TTL_MS = 300_000;
98
- interface CodexLoginStateRow { status: string; startedAt: number; accountId?: string; email?: string; error?: string; doneAt?: number }
155
+ interface CodexLoginStateRow {
156
+ status: string;
157
+ startedAt: number;
158
+ accountId?: string;
159
+ email?: string;
160
+ error?: string;
161
+ code?: string;
162
+ needsReauth?: boolean;
163
+ catalogRefreshPending?: boolean;
164
+ doneAt?: number;
165
+ }
99
166
  const codexAuthLoginState = new Map<string, CodexLoginStateRow>();
100
167
  export class CodexLoginStateBusyError extends ResourceAdmissionError {
101
168
  constructor() { super("codex_login_state_rows", MAX_CODEX_LOGIN_STATE_ROWS); this.name = "CodexLoginStateBusyError"; }
@@ -142,19 +209,19 @@ function codexAccountPersistenceConflict(
142
209
  : undefined;
143
210
  }
144
211
 
145
- function isThirtyDayOnlyPlan(plan: string | null | undefined): boolean {
146
- const normalized = plan?.trim().toLowerCase();
147
- return normalized === "go" || normalized === "free";
148
- }
149
-
150
212
  function quotaForPlan<T extends Omit<StoredAccountQuota, "updatedAt"> | StoredAccountQuota | null>(
151
213
  quota: T,
152
- plan: string | null | undefined,
214
+ plan: unknown,
153
215
  ): T {
154
- if (!quota || !isThirtyDayOnlyPlan(plan)) return quota;
216
+ if (!quota || !isThirtyDayOnlyCodexPlan(plan)) return quota;
155
217
  return {
156
218
  ...(quota.monthlyPercent !== undefined ? { monthlyPercent: quota.monthlyPercent } : {}),
157
219
  ...(quota.monthlyResetAt !== undefined ? { monthlyResetAt: quota.monthlyResetAt } : {}),
220
+ // A 30-day plan can still carry a burst window, and it blocks the account on its own.
221
+ // Dropping it here would show a healthy card for an account upstream is refusing (#1791).
222
+ ...(quota.shortPercent !== undefined ? { shortPercent: quota.shortPercent } : {}),
223
+ ...(quota.shortResetAt !== undefined ? { shortResetAt: quota.shortResetAt } : {}),
224
+ ...(quota.shortWindowSeconds !== undefined ? { shortWindowSeconds: quota.shortWindowSeconds } : {}),
158
225
  ...(quota.resetCredits !== undefined ? { resetCredits: quota.resetCredits } : {}),
159
226
  ...("updatedAt" in quota ? { updatedAt: quota.updatedAt } : {}),
160
227
  } as T;
@@ -165,18 +232,21 @@ function poolAccountDto(
165
232
  quotaResult: PoolQuotaResult,
166
233
  hasCredential: boolean,
167
234
  paused: boolean,
235
+ priority: number,
168
236
  ): CodexAuthAccountDto {
169
- const quota = quotaForPlan(quotaResult.quota, account.plan);
237
+ const plan = codexPlanValue(account.plan);
238
+ const quota = quotaForPlan(quotaResult.quota, plan);
170
239
  const needsReauth = !hasCredential || quotaResult.needsReauth || isAccountNeedsReauth(account.id);
171
240
  const health = projectCodexAccountHealth({ accountId: account.id, needsReauth });
172
241
  return {
173
242
  id: account.id,
174
- email: account.email,
243
+ email: maskEmail(account.email) ?? account.email,
175
244
  ...(account.alias !== undefined ? { alias: account.alias } : {}),
176
- ...(account.plan !== undefined ? { plan: account.plan } : {}),
177
- ...(account.logLabel !== undefined ? { logLabel: account.logLabel } : {}),
245
+ ...(plan !== undefined ? { plan } : {}),
246
+ logLabel: codexAccountLogLabel(account),
178
247
  isMain: false,
179
248
  paused,
249
+ priority,
180
250
  quota: quota ? { ...quota } : null,
181
251
  needsReauth,
182
252
  hasCredential,
@@ -185,20 +255,52 @@ function poolAccountDto(
185
255
  };
186
256
  }
187
257
 
188
- async function resolveResetCreditAuth(
258
+ interface ResetCreditAuth {
259
+ isMain: boolean;
260
+ accessToken: string;
261
+ chatgptAccountId: string;
262
+ nativeMainLease?: AdmissionLease;
263
+ nativeMainSharedClaimHeld?: true;
264
+ }
265
+
266
+ async function withResetCreditAuth<T>(
189
267
  runtimeConfig: OcxConfig,
190
268
  accountId: string,
191
- ): Promise<
192
- | { ok: true; isMain: boolean; accessToken: string; chatgptAccountId: string }
193
- | { ok: false; response: Response }
194
- > {
269
+ operation: (auth: ResetCreditAuth) => Promise<T>,
270
+ ): Promise<{ ok: true; value: T } | { ok: false; response: Response }> {
195
271
  if (accountId === MAIN_CODEX_ACCOUNT_ID) {
196
272
  if (hasLegacyMainCodexPoolAccount(runtimeConfig.codexAccounts)) {
197
273
  return { ok: false, response: jsonResponse({ error: "Remove the legacy __main__ pool row before using the Desktop account" }, 409) };
198
274
  }
199
- const tokens = readCodexTokens();
200
- if (!tokens) return { ok: false, response: jsonResponse({ error: "Main Codex account not logged in" }, 401) };
201
- return { ok: true, isMain: true, accessToken: tokens.access_token, chatgptAccountId: tokens.account_id };
275
+ const nativeMainLease = tryAcquireNativeMainProfileClaim();
276
+ if (!nativeMainLease) return { ok: false, response: nativeMainProfileBusyResponse() };
277
+ try {
278
+ try {
279
+ return await withNativeMainCredentialClaim(async () => {
280
+ const tokens = readCodexTokens();
281
+ if (!tokens) {
282
+ return { ok: false, response: jsonResponse({ error: "Main Codex account not logged in" }, 401) };
283
+ }
284
+ return {
285
+ ok: true,
286
+ value: await operation({
287
+ isMain: true,
288
+ accessToken: tokens.access_token,
289
+ chatgptAccountId: tokens.account_id,
290
+ nativeMainLease,
291
+ nativeMainSharedClaimHeld: true,
292
+ }),
293
+ };
294
+ });
295
+ } catch (error) {
296
+ if (isNativeMainClaimUnavailable(error)) {
297
+ return { ok: false, response: nativeMainProfileBusyResponse() };
298
+ }
299
+ throw error;
300
+ }
301
+ } finally {
302
+ nativeMainLease.release();
303
+ }
202
304
  }
203
305
  if (!isValidCodexAccountId(accountId)) {
204
306
  return { ok: false, response: jsonResponse({ error: "Invalid account id format" }, 400) };
@@ -207,7 +309,14 @@ async function resolveResetCreditAuth(
207
309
  return { ok: false, response: jsonResponse({ error: "Unknown Codex account" }, 404) };
208
310
  }
209
311
  const cred = await getValidCodexToken(accountId);
210
- return { ok: true, isMain: false, accessToken: cred.accessToken, chatgptAccountId: cred.chatgptAccountId };
312
+ return {
313
+ ok: true,
314
+ value: await operation({
315
+ isMain: false,
316
+ accessToken: cred.accessToken,
317
+ chatgptAccountId: cred.chatgptAccountId,
318
+ }),
319
+ };
211
320
  }
212
321
 
213
322
  function safeResetCreditsDto(input: unknown): { credits: { granted_at: string; expires_at: string }[]; available_count?: number } {
@@ -233,8 +342,41 @@ function safeResetCreditConsumeDto(input: unknown): { code: string } {
233
342
  return { code: typeof obj.code === "string" ? obj.code : "unknown" };
234
343
  }
235
344
 
236
- export function isUnverifiedCodexImportEnabled(): boolean {
237
- return process.env[MANUAL_IMPORT_ENV] === "1";
345
+ type ResetCreditJsonRead =
346
+ | { ok: true; value: unknown }
347
+ | { ok: false };
348
+
349
+ function cancelResponseBodyWithoutWaiting(body: ReadableStream<Uint8Array> | null): void {
350
+ if (!body) return;
351
+ try {
352
+ void body.cancel().catch(() => undefined);
353
+ } catch {
354
+ // Some stream implementations throw synchronously from cancel().
355
+ }
356
+ }
357
+
358
+ async function readResetCreditJson(
359
+ response: Response,
360
+ signal: AbortSignal,
361
+ ): Promise<ResetCreditJsonRead> {
362
+ const declaredLength = Number(response.headers.get("content-length"));
363
+ if (Number.isSafeInteger(declaredLength)
364
+ && declaredLength >= 0
365
+ && declaredLength > BOUNDED_BODY_MAX_BYTES) {
366
+ cancelResponseBodyWithoutWaiting(response.body);
367
+ return { ok: false };
368
+ }
369
+ try {
370
+ const body = await readBoundedResponseBody(response, {
371
+ signal,
372
+ maxBytes: BOUNDED_BODY_MAX_BYTES,
373
+ fatalUtf8: true,
374
+ });
375
+ if (!body.displaySafe || body.truncated || !body.text.trim()) return { ok: false };
376
+ return { ok: true, value: JSON.parse(body.text) as unknown };
377
+ } catch {
378
+ return { ok: false };
379
+ }
238
380
  }
239
381
 
240
382
  function manualImportDisabledResponse(): Response {
@@ -254,13 +396,10 @@ async function verifyCodexAccountWarmup(
254
396
  return { ok: true, validatedAt: Date.now() };
255
397
  } catch (err) {
256
398
  const reason = codexWarmupFailureReason(err);
257
- const upstream = err instanceof CodexWarmupError ? err.upstreamDetail : undefined;
258
399
  return {
259
400
  ok: false,
260
401
  response: jsonResponse({
261
- error: upstream
262
- ? `Codex account warmup failed: ${upstream}`
263
- : "Codex account warmup failed. Reauthenticate the account and try again.",
402
+ error: "Codex account warmup failed. Reauthenticate the account and try again.",
264
403
  code: "codex_warmup_failed",
265
404
  reason,
266
405
  accountId,
@@ -291,7 +430,7 @@ const POOL_CACHE_TTL = 5 * 60_000;
291
430
  const POOL_QUOTA_REFRESH_CONCURRENCY = 4;
292
431
 
293
432
  function nonEmptyPlan(value: unknown): string | null {
294
- return typeof value === "string" && value.trim() !== "" ? value : null;
433
+ return codexPlanValue(value) ?? null;
295
434
  }
296
435
 
297
436
  function isRuntimeConfig(config: OcxConfig): boolean {
@@ -311,6 +450,100 @@ function saveRuntimeConfig(sourceConfig: OcxConfig, nextConfig: OcxConfig): void
311
450
  Object.assign(sourceConfig, nextConfig);
312
451
  }
313
452
 
453
+ interface StagedNewCodexAccountState {
454
+ credential: CodexAccountCredentials;
455
+ validatedAt: number;
456
+ }
457
+
458
+ type PersistNewCodexAccountOutcome =
459
+ | { status: "committed"; pickerVisibilityChanged: boolean }
460
+ | { status: "publication-failed"; pickerVisibilityChanged: boolean };
461
+
462
+ function codexCredentialPersistenceFailure(accountId: string, catalogRefreshPending: boolean) {
463
+ return {
464
+ error: CODEX_CREDENTIAL_PERSISTENCE_ERROR,
465
+ code: CODEX_CREDENTIAL_PERSISTENCE_CODE,
466
+ accountId,
467
+ needsReauth: true as const,
468
+ ...(catalogRefreshPending ? { catalogRefreshPending: true as const } : {}),
469
+ };
470
+ }
471
+
472
+ /** Persist config before publishing secret or runtime state under the shared mutation coordinator. */
473
+ function persistNewCodexAccount(
474
+ sourceConfig: OcxConfig,
475
+ runtimeConfig: OcxConfig,
476
+ addedAccount: CodexAccount,
477
+ staged: StagedNewCodexAccountState,
478
+ ): PersistNewCodexAccountOutcome {
479
+ return withConfigMutationLockSync(() => {
480
+ const previousConfig = { ...runtimeConfig };
481
+ let pickerVisibilityChanged: boolean;
482
+ try {
483
+ const accounts = [...(runtimeConfig.codexAccounts ?? [])];
484
+ const retainedPickerBindingRestored = codexAccountPickerEnabled(runtimeConfig)
485
+ && Object.values(runtimeConfig.codexAccountNamespaces ?? {}).includes(addedAccount.id);
486
+ accounts.push(addedAccount);
487
+ runtimeConfig.codexAccounts = accounts;
488
+
489
+ // Presence of the explicit flag distinguishes a dashboard-managed map from
490
+ // a hand-authored legacy map. Preserve manual maps exactly.
491
+ const tracksPickerNamespaces = runtimeConfig.codexAccountPickerEnabled !== undefined;
492
+ if (tracksPickerNamespaces && runtimeConfig.codexAccountNamespaces) {
493
+ runtimeConfig.codexAccountNamespaces = { ...runtimeConfig.codexAccountNamespaces };
494
+ }
495
+ const namespaceAdded = tracksPickerNamespaces
496
+ && appendDefaultCodexAccountNamespace(runtimeConfig, addedAccount);
497
+ pickerVisibilityChanged = namespaceAdded || retainedPickerBindingRestored;
498
+ saveRuntimeConfig(sourceConfig, runtimeConfig);
499
+ } catch (error) {
500
+ for (const key of Object.keys(runtimeConfig) as Array<keyof OcxConfig>) {
501
+ delete runtimeConfig[key];
502
+ }
503
+ Object.assign(runtimeConfig, previousConfig);
504
+ throw error;
505
+ }
506
+
507
+ try {
508
+ saveCodexAccountCredential(addedAccount.id, staged.credential);
509
+ markCodexAccountValidated(addedAccount.id, staged.validatedAt);
510
+ clearAccountNeedsReauth(addedAccount.id);
511
+ } catch {
512
+ // Config is already durable. Return the failure outcome through the coordinator so its
513
+ // generation commit is not rolled back while config.json remains changed.
514
+ return { status: "publication-failed" as const, pickerVisibilityChanged };
515
+ }
516
+ return { status: "committed" as const, pickerVisibilityChanged };
517
+ });
518
+ }
519
+
520
+ /** Bounded catalog-convergence callback supplied by the management dispatcher. */
521
+ export type CodexAuthCatalogConvergence = () => Promise<CatalogDisposition>;
522
+
523
+ interface AccountNamespaceCatalogRefresh {
524
+ catalogRefreshPending: boolean;
525
+ }
526
+
527
+ /** Collapse post-persistence convergence into the one public recovery bit. */
528
+ async function convergeAccountNamespaceCatalog(
529
+ config: OcxConfig,
530
+ changed: boolean,
531
+ convergeCodexCatalog?: CodexAuthCatalogConvergence,
532
+ ): Promise<AccountNamespaceCatalogRefresh> {
533
+ if (!changed || !codexAccountPickerEnabled(config)) {
534
+ return { catalogRefreshPending: false };
535
+ }
536
+ if (!convergeCodexCatalog) return { catalogRefreshPending: true };
537
+
538
+ try {
539
+ const catalogRefresh = normalizeCatalogDisposition(await convergeCodexCatalog());
540
+ if (!catalogRefresh) return { catalogRefreshPending: true };
541
+ return { catalogRefreshPending: catalogRefreshIsPending(catalogRefresh) };
542
+ } catch {
543
+ return { catalogRefreshPending: true };
544
+ }
545
+ }
546
+
314
547
  async function mapWithConcurrency<T, R>(
315
548
  items: T[],
316
549
  concurrency: number,
@@ -333,12 +566,31 @@ const MAIN_TERMINAL_AUTH_CODES = new Set([
333
566
  "invalid_refresh_token",
334
567
  ]);
335
568
 
336
- async function isTerminalMainAuthResponse(resp: Response): Promise<boolean> {
337
- if (resp.status === 401) return true;
569
+ /**
570
+ * A WHAM 401 is not itself proof the local credential died. Upstream edges can
571
+ * transiently reject a still-valid access token (region/anti-abuse/rotation
572
+ * races), and fail-closing on every bare 401 makes a healthy main account flip
573
+ * needs-reauth on the next GUI quota poll. Only treat the response as terminal
574
+ * when the body carries a known terminal code or the local access token is not
575
+ * verifiably live (`accessTokenLive`). Liveness must be strict: a JWT whose
576
+ * `exp` cannot be decoded is NOT live — an undecodable token that vouched for
577
+ * itself would make a real 401 permanently transient.
578
+ */
579
+ async function isTerminalMainAuthResponse(resp: Response, accessTokenLive: boolean): Promise<boolean> {
580
+ if (resp.status === 401) {
581
+ if (!accessTokenLive) return true;
582
+ const code = await readMainAuthErrorCode(resp);
583
+ return typeof code === "string" && MAIN_TERMINAL_AUTH_CODES.has(code);
584
+ }
338
585
  if (resp.status !== 403) return false;
586
+ const code = await readMainAuthErrorCode(resp);
587
+ return typeof code === "string" && MAIN_TERMINAL_AUTH_CODES.has(code);
588
+ }
589
+
590
+ async function readMainAuthErrorCode(resp: Response): Promise<unknown> {
339
591
  try {
340
592
  const body = await readBoundedResponseBody(resp, { totalTimeoutMs: 1_000, inactivityTimeoutMs: 1_000 });
341
- if (!body.displaySafe) return false;
593
+ if (!body.displaySafe) return undefined;
342
594
  const parsed = JSON.parse(body.text) as {
343
595
  detail?: { code?: unknown } | string;
344
596
  error?: { code?: unknown } | string;
@@ -349,23 +601,41 @@ async function isTerminalMainAuthResponse(resp: Response): Promise<boolean> {
349
601
  : typeof parsed.error === "object" && parsed.error !== null
350
602
  ? parsed.error.code
351
603
  : parsed.code;
352
- return typeof code === "string" && MAIN_TERMINAL_AUTH_CODES.has(code);
604
+ return code;
353
605
  } catch {
354
- return false;
606
+ return undefined;
355
607
  }
356
608
  }
357
609
 
358
610
  interface MainAccountInfoFetchResult {
359
611
  info: MainAccountInfo;
612
+ /** Whether this attempt safely inspected the physical native-main credential. */
613
+ credentialChecked: boolean;
614
+ /** Meaningful only when credentialChecked is true. */
615
+ hasCredential: boolean;
616
+ /** Main identity generation captured while the native-main claim was held. */
617
+ identityGeneration?: number;
360
618
  /** Present only when this call freshly parsed a WHAM usage response. */
361
619
  freshQuota?: Omit<StoredAccountQuota, "updatedAt">;
362
620
  /** Present only when this call's WHAM response included `rate_limit_reset_credits.available_count`. */
363
621
  freshResetCredits?: number;
364
622
  }
365
623
 
624
+ export interface MainAccountInfoSnapshot {
625
+ info: MainAccountInfo;
626
+ mainIdentityGeneration: number;
627
+ }
628
+
629
+ export async function fetchMainAccountInfoSnapshot(forceRefresh = false): Promise<MainAccountInfoSnapshot> {
630
+ const result = await fetchMainAccountInfoAttempt(forceRefresh, 1);
631
+ return {
632
+ info: result.info,
633
+ mainIdentityGeneration: result.identityGeneration ?? captureMainAccountIdentityGeneration(),
634
+ };
635
+ }
636
+
366
637
  export async function fetchMainAccountInfo(forceRefresh = false): Promise<MainAccountInfo> {
367
- const { info } = await fetchMainAccountInfoAttempt(forceRefresh, 1);
368
- return info;
638
+ return (await fetchMainAccountInfoSnapshot(forceRefresh)).info;
369
639
  }
370
640
 
371
641
  const EMPTY_MAIN_ACCOUNT_INFO: MainAccountInfo = { email: null, plan: null, quota: null };
@@ -373,19 +643,72 @@ const EMPTY_MAIN_ACCOUNT_INFO: MainAccountInfo = { email: null, plan: null, quot
373
643
  async function retryMainAccountInfoIfIdentityChanged(
374
644
  requestAccountId: string | null,
375
645
  retriesRemaining: number,
646
+ nativeMainLease: AdmissionLease,
647
+ explicitRefresh: boolean,
376
648
  ): Promise<MainAccountInfoFetchResult | null> {
377
649
  const currentAccountId = getMainChatgptAccountId();
378
650
  if (currentAccountId === null || currentAccountId === requestAccountId) return null;
379
651
  reconcileMainCodexAccountRuntimeState();
380
652
  return retriesRemaining > 0
381
- ? fetchMainAccountInfoAttempt(true, retriesRemaining - 1)
382
- : { info: EMPTY_MAIN_ACCOUNT_INFO };
653
+ ? fetchMainAccountInfoWhileOwned(true, retriesRemaining - 1, nativeMainLease, explicitRefresh)
654
+ : { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
383
655
  }
384
656
 
385
- async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaining: number): Promise<MainAccountInfoFetchResult> {
657
+ async function fetchMainAccountInfoAttempt(
658
+ forceRefresh: boolean,
659
+ retriesRemaining: number,
660
+ existingNativeMainLease?: AdmissionLease,
661
+ nativeMainSharedClaimHeld = false,
662
+ ): Promise<MainAccountInfoFetchResult> {
663
+ const nativeMainLease = existingNativeMainLease ?? tryAcquireNativeMainProfileClaim();
664
+ if (!nativeMainLease) {
665
+ return {
666
+ info: EMPTY_MAIN_ACCOUNT_INFO,
667
+ credentialChecked: false,
668
+ hasCredential: false,
669
+ identityGeneration: captureMainAccountIdentityGeneration(),
670
+ };
671
+ }
672
+ try {
673
+ const operation = async () => ({
674
+ ...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease),
675
+ identityGeneration: captureMainAccountIdentityGeneration(),
676
+ });
677
+ if (nativeMainSharedClaimHeld) return await operation();
678
+ try {
679
+ return await withNativeMainCredentialClaim(operation);
680
+ } catch (error) {
681
+ if (isNativeMainClaimUnavailable(error)) {
682
+ return {
683
+ info: EMPTY_MAIN_ACCOUNT_INFO,
684
+ credentialChecked: false,
685
+ hasCredential: false,
686
+ identityGeneration: captureMainAccountIdentityGeneration(),
687
+ };
688
+ }
689
+ throw error;
690
+ }
691
+ } finally {
692
+ if (!existingNativeMainLease) nativeMainLease.release();
693
+ }
694
+ }
695
+
696
+ async function fetchMainAccountInfoWhileOwned(
697
+ forceRefresh: boolean,
698
+ retriesRemaining: number,
699
+ nativeMainLease: AdmissionLease,
700
+ /**
701
+ * Whether the *caller* asked for this refresh. `forceRefresh` also means "bypass the
702
+ * cache", and `retryMainAccountInfoIfIdentityChanged` re-enters with it set purely to
703
+ * re-read after the identity changed. Keeping the two apart stops that retry from
704
+ * promoting a background poll into operator intent below.
705
+ */
706
+ explicitRefresh: boolean = forceRefresh,
707
+ ): Promise<MainAccountInfoFetchResult> {
386
708
  const writerGeneration = captureConfigGeneration();
387
709
  reconcileMainCodexAccountRuntimeState();
388
710
  const tokenRead = readCodexTokensResult();
711
+ setMainAccountCredentialPresence(tokenRead.status === "ok");
389
712
  if (tokenRead.status !== "ok") {
390
713
  // A local read failure is NOT proof of sign-out: a missing file can be a non-atomic rewrite
391
714
  // gap, and malformed JSON can be a half-written file. Clearing the cache and marking the
@@ -394,13 +717,13 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
394
717
  // routing stays fail-closed because getMainAccountToken() re-reads the file itself, and the
395
718
  // account DTO still reports hasCredential=false while the file is unreadable.
396
719
  const preserved = getMainAccountInfoCache();
397
- return { info: preserved ?? EMPTY_MAIN_ACCOUNT_INFO };
720
+ return { info: preserved ?? EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: false };
398
721
  }
399
722
  const tokens = tokenRead.tokens;
400
723
  const requestAccountId = extractAccountId(tokens.id_token, tokens.access_token) ?? (tokens.account_id || null);
401
724
  const cached = getMainAccountInfoCache();
402
725
  if (!forceRefresh && cached && Date.now() - cached.ts < MAIN_CACHE_TTL) {
403
- return { info: cached };
726
+ return { info: cached, credentialChecked: true, hasCredential: true };
404
727
  }
405
728
  try {
406
729
  const resp = await fetch("https://chatgpt.com/backend-api/wham/usage", {
@@ -408,17 +731,17 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
408
731
  signal: AbortSignal.timeout(8000),
409
732
  });
410
733
  if (!resp.ok) {
411
- const terminalAuthFailure = await isTerminalMainAuthResponse(resp);
412
- const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining);
734
+ const terminalAuthFailure = await isTerminalMainAuthResponse(resp, isMainAccountTokenVerifiablyLive());
735
+ const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
413
736
  if (retried) return retried;
414
737
  if (terminalAuthFailure) {
415
738
  clearMainAccountInfoCache();
416
739
  markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
417
740
  }
418
- return { info: EMPTY_MAIN_ACCOUNT_INFO };
741
+ return { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
419
742
  }
420
743
  const data = (await resp.json()) as WhamUsageResponse;
421
- const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining);
744
+ const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
422
745
  if (retried) return retried;
423
746
  const plan = nonEmptyPlan(data.plan_type) ?? nonEmptyPlan(cached?.plan) ?? nonEmptyPlan(getMainAccountPlan());
424
747
  const quota = parseUsageQuota({ ...data, ...(plan ? { plan_type: plan } : {}) });
@@ -430,7 +753,16 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
430
753
  ts: Date.now(),
431
754
  };
432
755
  setMainAccountInfoCache(result);
433
- clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
756
+ // Only an explicit refresh may retract a reauth quarantine. A 200 from
757
+ // /wham/usage proves the token authenticates to the usage endpoint; it does not
758
+ // prove the account can serve Responses traffic, which is a different backend path
759
+ // and still answers 403 for a workspace the token may no longer select (#327).
760
+ // Letting the background poll clear the flag put such an account straight back into
761
+ // rotation: the next request failed the same way and re-marked it, so needsReauth
762
+ // never settled and the dashboard kept showing nothing — the symptom #327 reported.
763
+ // An explicit refresh is an operator asking to re-evaluate, normally right after
764
+ // signing in again, so it stays authoritative.
765
+ if (explicitRefresh) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
434
766
  // Mirror main quota + plan into the shared stores so the rotation engine can
435
767
  // score and auto-switch the main account exactly like a pool account (Option A).
436
768
  setMainAccountPlan(result.plan);
@@ -439,12 +771,14 @@ async function fetchMainAccountInfoAttempt(forceRefresh: boolean, retriesRemaini
439
771
  }
440
772
  return {
441
773
  info: result,
774
+ credentialChecked: true,
775
+ hasCredential: true,
442
776
  ...(quota ? { freshQuota: quota } : {}),
443
777
  ...(freshResetCredits !== undefined ? { freshResetCredits } : {}),
444
778
  };
445
779
  } catch {
446
- const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining);
447
- return retried ?? { info: EMPTY_MAIN_ACCOUNT_INFO };
780
+ const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
781
+ return retried ?? { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
448
782
  }
449
783
  }
450
784
 
@@ -514,6 +848,8 @@ export interface CodexAuthAccountDto {
514
848
  logLabel?: string;
515
849
  isMain: boolean;
516
850
  paused: boolean;
851
+ /** Selection order; higher is used earlier. Always present, 0 when unset. */
852
+ priority: number;
517
853
  quota: (StoredAccountQuota | (Omit<StoredAccountQuota, "updatedAt"> & { updatedAt: number })) | null;
518
854
  needsReauth?: boolean;
519
855
  hasCredential: boolean;
@@ -551,6 +887,15 @@ function reconcileFreshPoolAccountPlans(runtimeConfig: OcxConfig, updates: Fresh
551
887
  accepted.push(update);
552
888
  if (persistedAccount.plan !== update.plan) {
553
889
  persistedAccount.plan = update.plan;
890
+ // WHAM is the authoritative plan source: stamp provenance so a later JWT
891
+ // reconcile cannot overwrite this observation within the same credential
892
+ // generation (src/codex/plan-from-token.ts jwtMayWritePlan). Stamped only
893
+ // alongside a real plan change: a steady-state refresh whose plan is
894
+ // unchanged must stay write-free (no-config-write contract), and an
895
+ // unchanged value needs no fence — a JWT rewrite to the same text is a
896
+ // no-op under the caller's own equality check.
897
+ persistedAccount.planSource = "wham";
898
+ persistedAccount.planCredentialGeneration = update.credentialGeneration;
554
899
  changed = true;
555
900
  }
556
901
  }
@@ -570,6 +915,8 @@ function reconcileFreshPoolAccountPlans(runtimeConfig: OcxConfig, updates: Fresh
570
915
  const liveAccount = configuredPoolAccount(runtimeConfig, update.accountId);
571
916
  if (liveAccount) {
572
917
  liveAccount.plan = update.plan;
918
+ liveAccount.planSource = "wham";
919
+ liveAccount.planCredentialGeneration = update.credentialGeneration;
573
920
  }
574
921
  }
575
922
  }
@@ -689,6 +1036,60 @@ async function fetchPoolAccountQuota(accountId: string, forceRefresh = false, co
689
1036
  }
690
1037
 
691
1038
  let primeInFlight: Promise<void> | null = null;
1039
+ let cooldownRecoveryInFlight: Promise<void> | null = null;
1040
+
1041
+ export async function runCodexCooldownRecoveryProbes(config: OcxConfig, now = Date.now()): Promise<void> {
1042
+ const openai = config.providers[OPENAI_CODEX_PROVIDER_ID];
1043
+ if (!openai
1044
+ || openai.disabled === true
1045
+ || !isCanonicalOpenAiForwardProvider(openai)
1046
+ || providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, openai) !== "pool") return;
1047
+ if (cooldownRecoveryInFlight) return cooldownRecoveryInFlight;
1048
+ cooldownRecoveryInFlight = (async () => {
1049
+ const claims = claimDueCodexQuotaRecoveryProbes(config, POOL_QUOTA_REFRESH_CONCURRENCY, now);
1050
+ await mapWithConcurrency(claims, POOL_QUOTA_REFRESH_CONCURRENCY, async claim => {
1051
+ const account = configuredPoolAccount(config, claim.accountId);
1052
+ if (!account) {
1053
+ settleCodexQuotaRecoveryProbe(claim, false, {}, now);
1054
+ return;
1055
+ }
1056
+ try {
1057
+ const result = await fetchPoolAccountQuota(claim.accountId, true, account.plan);
1058
+ // Defence in depth: `spark` is already excluded at the claim site, since generic WHAM
1059
+ // cannot prove a spark recovery. Keep the settle-side guard so a future claim change
1060
+ // cannot silently start clearing spark on generic evidence.
1061
+ const recovered = claim.scope !== "spark"
1062
+ && isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan);
1063
+ settleCodexQuotaRecoveryProbe(claim, recovered, {
1064
+ credentialGeneration: result.freshCredentialGeneration,
1065
+ }, now);
1066
+ } catch {
1067
+ settleCodexQuotaRecoveryProbe(claim, false, {}, now);
1068
+ }
1069
+ });
1070
+ })().catch(() => {
1071
+ // Background recovery is best-effort; routing keeps the cooldown on failure.
1072
+ }).finally(() => { cooldownRecoveryInFlight = null; });
1073
+ return cooldownRecoveryInFlight;
1074
+ }
1075
+
1076
+ export function registerCodexCooldownRecoveryProbeWorker(config: OcxConfig): void {
1077
+ registerStateSweepAfterTick({
1078
+ name: "codex-cooldown-recovery",
1079
+ afterTick: () => { void runCodexCooldownRecoveryProbes(config); },
1080
+ });
1081
+ }
1082
+
1083
+ export interface PrimeCodexPoolQuotasOptions {
1084
+ /** Test seams for proving fenced/recovery priming performs no native-main work. */
1085
+ reconcileMainAccount?: typeof reconcileMainCodexAccountRuntimeState;
1086
+ readMainTokens?: typeof readCodexTokens;
1087
+ fetchMainInfo?: typeof fetchMainAccountInfo;
1088
+ }
1089
+
1090
+ function tryAcquireNativeMainPrimeLease(): AdmissionLease | null {
1091
+ return tryAcquireNativeMainProfileClaim();
1092
+ }
692
1093
 
693
1094
  /**
694
1095
  * Best-effort prime of pool-account (and main) quota so the rotation engine has
@@ -705,7 +1106,11 @@ let primeInFlight: Promise<void> | null = null;
705
1106
  * cost, so the worst case is one WHAM call per account per TTL window. Failures
706
1107
  * are swallowed: a blocked WSL network must never crash startup or a request.
707
1108
  */
708
- export async function primeCodexPoolQuotas(config: OcxConfig, reason: string): Promise<void> {
1109
+ export async function primeCodexPoolQuotas(
1110
+ config: OcxConfig,
1111
+ reason: string,
1112
+ options: PrimeCodexPoolQuotasOptions = {},
1113
+ ): Promise<void> {
709
1114
  const openai = config.providers[OPENAI_CODEX_PROVIDER_ID];
710
1115
  if (
711
1116
  !openai
@@ -714,10 +1119,6 @@ export async function primeCodexPoolQuotas(config: OcxConfig, reason: string): P
714
1119
  || providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, openai) !== "pool"
715
1120
  ) return;
716
1121
  if (primeInFlight) return primeInFlight;
717
- // Seed the observed physical main identity before startup/lazy priming can populate quota or
718
- // plan state. Otherwise the first post-startup account switch sees no previous identity and
719
- // skips the purge that protects the stable __main__ alias.
720
- reconcileMainCodexAccountRuntimeState();
721
1122
  primeInFlight = (async () => {
722
1123
  const runtimeConfig = getRuntimeConfig(config);
723
1124
  const pool = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
@@ -725,10 +1126,30 @@ export async function primeCodexPoolQuotas(config: OcxConfig, reason: string): P
725
1126
  const q = getAccountQuota(a.id);
726
1127
  return !q || Date.now() - q.updatedAt >= POOL_CACHE_TTL;
727
1128
  });
728
- const primeMain = !!readCodexTokens() && !getAccountQuota(MAIN_CODEX_ACCOUNT_ID);
1129
+ const primeMain = async () => {
1130
+ const mainLease = tryAcquireNativeMainPrimeLease();
1131
+ if (!mainLease) return;
1132
+ try {
1133
+ try {
1134
+ await withNativeMainCredentialClaim(async () => {
1135
+ // Keep one local owner and one cross-process reader from physical
1136
+ // identity reconciliation through WHAM and all quota publication.
1137
+ (options.reconcileMainAccount ?? reconcileMainCodexAccountRuntimeState)();
1138
+ if (getAccountQuota(MAIN_CODEX_ACCOUNT_ID)) return;
1139
+ if (!(options.readMainTokens ?? readCodexTokens)()) return;
1140
+ if (options.fetchMainInfo) await options.fetchMainInfo(false);
1141
+ else await fetchMainAccountInfoAttempt(false, 1, mainLease, true);
1142
+ });
1143
+ } catch (error) {
1144
+ if (!isNativeMainClaimUnavailable(error)) throw error;
1145
+ }
1146
+ } finally {
1147
+ mainLease.release();
1148
+ }
1149
+ };
729
1150
  try {
730
1151
  await Promise.allSettled([
731
- primeMain ? fetchMainAccountInfo(false) : Promise.resolve(),
1152
+ primeMain(),
732
1153
  mapWithConcurrency(stale, POOL_QUOTA_REFRESH_CONCURRENCY, async a => {
733
1154
  if (!getCodexAccountCredential(a.id)) return;
734
1155
  await fetchPoolAccountQuota(a.id, false, a.plan);
@@ -750,10 +1171,27 @@ export function clearCodexQuotaPrimeState(): void {
750
1171
  primeInFlight = null;
751
1172
  }
752
1173
 
753
- export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = false): Promise<CodexAuthAccountDto[]> {
1174
+ /** Test-only reset for the worker-level single-flight. */
1175
+ export function clearCodexCooldownRecoveryProbeState(): void {
1176
+ cooldownRecoveryInFlight = null;
1177
+ }
1178
+
1179
+ export function effectiveCodexAuthAccountId(config: OcxConfig): string {
1180
+ return getEffectiveActiveCodexAccountId(config) ?? MAIN_CODEX_ACCOUNT_ID;
1181
+ }
1182
+
1183
+ export interface CodexAuthAccountsSnapshot {
1184
+ accounts: CodexAuthAccountDto[];
1185
+ mainIdentityGeneration: number;
1186
+ }
1187
+
1188
+ export async function listCodexAuthAccountsSnapshot(
1189
+ config: OcxConfig,
1190
+ forceRefresh = false,
1191
+ ): Promise<CodexAuthAccountsSnapshot> {
754
1192
  const runtimeConfig = getRuntimeConfig(config);
755
1193
  const poolAccounts = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
756
- const mainInfo = await fetchMainAccountInfo(forceRefresh);
1194
+ const mainResult = await fetchMainAccountInfoAttempt(forceRefresh, 1);
757
1195
  const refreshedPool = await mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async account => {
758
1196
  const cred = getCodexAccountCredential(account.id);
759
1197
  let quotaResult: PoolQuotaResult;
@@ -796,6 +1234,7 @@ export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = fa
796
1234
  { quota: null, needsReauth: true },
797
1235
  false,
798
1236
  isCodexAccountPaused(runtimeConfig, accountId),
1237
+ getCodexAccountPriority(runtimeConfig, accountId),
799
1238
  )];
800
1239
  }
801
1240
  const resultGeneration = quotaResult.credentialGeneration ?? quotaResult.freshCredentialGeneration;
@@ -814,10 +1253,17 @@ export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = fa
814
1253
  effectiveQuotaResult,
815
1254
  true,
816
1255
  isCodexAccountPaused(runtimeConfig, accountId),
1256
+ getCodexAccountPriority(runtimeConfig, accountId),
817
1257
  )];
818
1258
  });
819
- const hasMainCredential = readCodexTokens() !== null;
820
- const mainNeedsReauth = !hasMainCredential || isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
1259
+ const fetchedMainGeneration = mainResult.identityGeneration ?? captureMainAccountIdentityGeneration();
1260
+ const mainSnapshotLive = isMainAccountIdentityGenerationLive(fetchedMainGeneration);
1261
+ const mainInfo = mainSnapshotLive ? mainResult.info : EMPTY_MAIN_ACCOUNT_INFO;
1262
+ const hasMainCredential = mainSnapshotLive && mainResult.credentialChecked
1263
+ ? mainResult.hasCredential
1264
+ : getMainAccountCredentialPresence() ?? false;
1265
+ const mainNeedsReauth = (mainSnapshotLive && mainResult.credentialChecked && !hasMainCredential)
1266
+ || isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
821
1267
  const mainHealth = projectCodexAccountHealth({
822
1268
  accountId: MAIN_CODEX_ACCOUNT_ID,
823
1269
  needsReauth: mainNeedsReauth,
@@ -826,14 +1272,30 @@ export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = fa
826
1272
  id: MAIN_CODEX_ACCOUNT_ID,
827
1273
  email: mainInfo.email ?? "Codex App login",
828
1274
  plan: mainInfo.plan,
1275
+ logLabel: "main",
829
1276
  isMain: true,
830
1277
  paused: isCodexAccountPaused(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
1278
+ priority: getCodexAccountPriority(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
831
1279
  hasCredential: hasMainCredential,
832
1280
  needsReauth: mainNeedsReauth,
833
- quota: mainInfo.quota ? { ...quotaForPlan({ ...mainInfo.quota, updatedAt: Date.now() }, mainInfo.plan) } : null,
1281
+ quota: mainInfo.quota ? {
1282
+ ...quotaForPlan({
1283
+ ...mainInfo.quota,
1284
+ updatedAt: getAccountQuota(MAIN_CODEX_ACCOUNT_ID)?.updatedAt ?? Date.now(),
1285
+ }, mainInfo.plan),
1286
+ } : null,
834
1287
  ...oauthAccountHealthFields("codex", MAIN_CODEX_ACCOUNT_ID, mainHealth),
835
1288
  };
836
- return [main, ...withQuota];
1289
+ return {
1290
+ accounts: [main, ...withQuota],
1291
+ mainIdentityGeneration: mainSnapshotLive
1292
+ ? fetchedMainGeneration
1293
+ : captureMainAccountIdentityGeneration(),
1294
+ };
1295
+ }
1296
+
1297
+ export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = false): Promise<CodexAuthAccountDto[]> {
1298
+ return (await listCodexAuthAccountsSnapshot(config, forceRefresh)).accounts;
837
1299
  }
838
1300
 
839
1301
  interface PauseExhaustedResult {
@@ -846,63 +1308,102 @@ function selectFallbackAfterPause(config: OcxConfig, pausedActiveId: string): vo
846
1308
  reconcileCodexActiveAfterExclusion(config, pausedActiveId);
847
1309
  }
848
1310
 
849
- async function pauseExhaustedCodexAccounts(config: OcxConfig): Promise<PauseExhaustedResult> {
1311
+ async function pauseExhaustedCodexAccounts(
1312
+ config: OcxConfig,
1313
+ persistPausedAccounts: () => void,
1314
+ ): Promise<PauseExhaustedResult> {
850
1315
  const poolAccounts = (config.codexAccounts ?? []).filter(account => !account.isMain);
851
- const mainAttempted = readCodexTokens() !== null;
852
- const [mainResult, poolResults] = await Promise.all([
853
- fetchMainAccountInfoAttempt(true, 1),
854
- mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async account => {
855
- if (!getCodexAccountCredential(account.id)) return { account, quotaResult: null };
856
- return {
857
- account,
858
- quotaResult: await fetchPoolAccountQuota(account.id, true, account.plan),
1316
+ const nativeMainLease = tryAcquireNativeMainProfileClaim();
1317
+ try {
1318
+ const performPause = async (mainLease?: AdmissionLease): Promise<PauseExhaustedResult> => {
1319
+ const mainWork = async (): Promise<{
1320
+ shouldPause: boolean;
1321
+ checkedAccountCount: number;
1322
+ failedAccountCount: number;
1323
+ }> => {
1324
+ if (!mainLease) return { shouldPause: false, checkedAccountCount: 0, failedAccountCount: 1 };
1325
+ const mainResult = await fetchMainAccountInfoAttempt(true, 1, mainLease, true);
1326
+ if (!mainResult.credentialChecked || !mainResult.hasCredential) {
1327
+ return { shouldPause: false, checkedAccountCount: 0, failedAccountCount: 0 };
1328
+ }
1329
+ if (!mainResult.freshQuota || !mainResult.info.plan) {
1330
+ return { shouldPause: false, checkedAccountCount: 0, failedAccountCount: 1 };
1331
+ }
1332
+ return {
1333
+ shouldPause: !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
1334
+ && isCodexQuotaExhausted(mainResult.freshQuota, mainResult.info.plan),
1335
+ checkedAccountCount: 1,
1336
+ failedAccountCount: 0,
1337
+ };
859
1338
  };
860
- }),
861
- ]);
862
-
863
- let checkedAccountCount = 0;
864
- let failedAccountCount = 0;
865
- const exhaustedIds: string[] = [];
866
- if (mainAttempted) {
867
- if (mainResult.freshQuota && mainResult.info.plan) {
868
- checkedAccountCount += 1;
869
- if (
870
- !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
871
- && isCodexQuotaExhausted(mainResult.freshQuota, mainResult.info.plan)
872
- ) {
873
- exhaustedIds.push(MAIN_CODEX_ACCOUNT_ID);
1339
+ const [mainResult, poolResults] = await Promise.all([
1340
+ mainWork(),
1341
+ mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async account => {
1342
+ if (!getCodexAccountCredential(account.id)) return { account, quotaResult: null };
1343
+ try {
1344
+ return {
1345
+ account,
1346
+ quotaResult: await fetchPoolAccountQuota(account.id, true, account.plan),
1347
+ };
1348
+ } catch {
1349
+ // Settle each pool probe independently so a busy/failing account cannot
1350
+ // abandon an already-confirmed main decision before atomic publication.
1351
+ return { account, quotaResult: null };
1352
+ }
1353
+ }),
1354
+ ]);
1355
+
1356
+ let checkedAccountCount = mainResult.checkedAccountCount;
1357
+ let failedAccountCount = mainResult.failedAccountCount;
1358
+ const exhaustedIds: string[] = mainResult.shouldPause ? [MAIN_CODEX_ACCOUNT_ID] : [];
1359
+ for (const { account, quotaResult } of poolResults) {
1360
+ const currentAccount = (config.codexAccounts ?? []).find(candidate => candidate.id === account.id && !candidate.isMain);
1361
+ if (!currentAccount) continue;
1362
+ const generation = quotaResult?.freshCredentialGeneration;
1363
+ const plan = quotaResult?.freshPlan ?? currentAccount.plan;
1364
+ if (!quotaResult?.freshQuota || generation === undefined || !isCodexAccountGenerationLive(account.id, generation) || !plan) {
1365
+ failedAccountCount += 1;
1366
+ continue;
1367
+ }
1368
+ checkedAccountCount += 1;
1369
+ if (!isCodexAccountPaused(config, account.id) && isCodexQuotaExhausted(quotaResult.freshQuota, plan)) {
1370
+ exhaustedIds.push(account.id);
1371
+ }
874
1372
  }
875
- } else {
876
- failedAccountCount += 1;
877
- }
878
- }
879
- for (const { account, quotaResult } of poolResults) {
880
- const currentAccount = (config.codexAccounts ?? []).find(candidate => candidate.id === account.id && !candidate.isMain);
881
- if (!currentAccount) continue;
882
- const generation = quotaResult?.freshCredentialGeneration;
883
- const plan = quotaResult?.freshPlan ?? currentAccount.plan;
884
- if (!quotaResult?.freshQuota || generation === undefined || !isCodexAccountGenerationLive(account.id, generation) || !plan) {
885
- failedAccountCount += 1;
886
- continue;
887
- }
888
- checkedAccountCount += 1;
889
- if (!isCodexAccountPaused(config, account.id) && isCodexQuotaExhausted(quotaResult.freshQuota, plan)) {
890
- exhaustedIds.push(account.id);
891
- }
892
- }
893
1373
 
894
- for (const id of exhaustedIds) {
895
- setCodexAccountPaused(config, id, true);
896
- clearThreadAccountMapForAccount(id);
1374
+ for (const id of exhaustedIds) {
1375
+ setCodexAccountPaused(config, id, true);
1376
+ clearThreadAccountMapForAccount(id);
1377
+ }
1378
+ for (const id of exhaustedIds) selectFallbackAfterPause(config, id);
1379
+ const result = {
1380
+ pausedAccountIds: exhaustedIds,
1381
+ checkedAccountCount,
1382
+ failedAccountCount,
1383
+ };
1384
+ // Persist while both the in-process admission and cross-process shared
1385
+ // claim still own the physical-main identity used for the decision.
1386
+ if (result.pausedAccountIds.length > 0) persistPausedAccounts();
1387
+ return result;
1388
+ };
1389
+
1390
+ if (!nativeMainLease) return await performPause();
1391
+ try {
1392
+ return await withNativeMainCredentialClaim(() => performPause(nativeMainLease));
1393
+ } catch (error) {
1394
+ if (isNativeMainClaimUnavailable(error)) return await performPause();
1395
+ throw error;
1396
+ }
1397
+ } finally {
1398
+ nativeMainLease?.release();
897
1399
  }
898
- for (const id of exhaustedIds) selectFallbackAfterPause(config, id);
899
- return { pausedAccountIds: exhaustedIds, checkedAccountCount, failedAccountCount };
900
1400
  }
901
1401
 
902
1402
  export async function handleCodexAuthAPI(
903
1403
  req: Request,
904
1404
  url: URL,
905
1405
  config: OcxConfig,
1406
+ convergeCodexCatalog?: CodexAuthCatalogConvergence,
906
1407
  ): Promise<Response | null> {
907
1408
 
908
1409
  if (url.pathname === "/api/codex-auth/accounts" && req.method === "GET") {
@@ -911,50 +1412,7 @@ export async function handleCodexAuthAPI(
911
1412
  }
912
1413
 
913
1414
  if (url.pathname === "/api/codex-auth/accounts" && req.method === "POST") {
914
- if (!isUnverifiedCodexImportEnabled()) return manualImportDisabledResponse();
915
-
916
- let body: { id: string; email: string; plan?: string; accessToken: string; refreshToken: string; chatgptAccountId: string };
917
- try { body = (await req.json()) as typeof body; } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
918
- if (!body.id || !body.email || !body.accessToken || !body.refreshToken || !body.chatgptAccountId) {
919
- return jsonResponse({ error: "Missing required fields" }, 400);
920
- }
921
- if (!isValidCodexAccountId(body.id)) {
922
- return jsonResponse({ error: "Invalid account id format" }, 400);
923
- }
924
- if (body.accessToken.length > 10_000 || body.refreshToken.length > 10_000) {
925
- return jsonResponse({ error: "Input too large" }, 400);
926
- }
927
- const runtimeConfig = getRuntimeConfig(config);
928
- const preflightConflict = codexAccountPersistenceConflict(runtimeConfig, body.id, "create");
929
- if (preflightConflict) return jsonResponse({ error: preflightConflict }, 400);
930
- // 1.1: Duplicate check is scoped by personal vs workspace plan bucket.
931
- const derivedAccountId = extractAccountId(undefined, body.accessToken) ?? body.chatgptAccountId;
932
- const collision = checkAccountIdCollision(derivedAccountId, body.email, body.plan);
933
- if (collision.collision) {
934
- return jsonResponse({ error: collision.reason }, 400);
935
- }
936
- // 4.2: use JWT exp for expiresAt instead of hardcoded 1 hour
937
- const payload = decodeJwtPayload(body.accessToken);
938
- const exp = typeof payload?.exp === "number" ? payload.exp * 1000 : Date.now() + 3600_000;
939
- const warmup = await verifyCodexAccountWarmup(body.id, body.accessToken, derivedAccountId);
940
- if (!warmup.ok) return warmup.response;
941
- const latestConfig = getRuntimeConfig(config);
942
- const commitConflict = codexAccountPersistenceConflict(latestConfig, body.id, "create");
943
- if (commitConflict) return jsonResponse({ error: commitConflict }, 400);
944
- saveCodexAccountCredential(body.id, {
945
- accessToken: body.accessToken,
946
- refreshToken: body.refreshToken,
947
- expiresAt: exp,
948
- chatgptAccountId: derivedAccountId,
949
- });
950
- markCodexAccountValidated(body.id, warmup.validatedAt);
951
- clearAccountNeedsReauth(body.id);
952
- const accounts = latestConfig.codexAccounts ?? [];
953
- accounts.push(withCodexAccountLogLabel({ id: body.id, email: body.email, plan: body.plan, isMain: false }, accounts));
954
- latestConfig.codexAccounts = accounts;
955
- saveRuntimeConfig(config, latestConfig);
956
- reconcileLiveStateStores();
957
- return jsonResponse({ ok: true });
1415
+ return manualImportDisabledResponse();
958
1416
  }
959
1417
 
960
1418
  if (url.pathname === "/api/codex-auth/accounts" && req.method === "DELETE") {
@@ -966,10 +1424,15 @@ export async function handleCodexAuthAPI(
966
1424
  if (!isValidCodexAccountId(id) && !isLegacyPoolAccount) {
967
1425
  return jsonResponse({ error: "Invalid account id format" }, 400);
968
1426
  }
969
- deleteCodexAccount(runtimeConfig, id);
1427
+ const pickerVisibilityChanged = deleteCodexAccount(runtimeConfig, id);
970
1428
  saveRuntimeConfig(config, runtimeConfig);
971
1429
  reconcileLiveStateStores();
972
- return jsonResponse({ ok: true });
1430
+ const catalogRefresh = await convergeAccountNamespaceCatalog(
1431
+ runtimeConfig,
1432
+ pickerVisibilityChanged,
1433
+ convergeCodexCatalog,
1434
+ );
1435
+ return jsonResponse({ ok: true, ...catalogRefresh });
973
1436
  }
974
1437
 
975
1438
  if (url.pathname === "/api/codex-auth/accounts/alias" && req.method === "PUT") {
@@ -1018,9 +1481,59 @@ export async function handleCodexAuthAPI(
1018
1481
  });
1019
1482
  }
1020
1483
 
1484
+ // Deliberately a route of its own rather than a field on the alias PATCH: aliases
1485
+ // are display-only and reject __main__, while selection order is routing metadata
1486
+ // that the Desktop account must be able to carry. Re-ordering never kicks a live
1487
+ // thread, so there is no affinity clearing and no appliesImmediately here.
1488
+ if (url.pathname === "/api/codex-auth/accounts/priority" && req.method === "PUT") {
1489
+ let parsedBody: unknown;
1490
+ try { parsedBody = await req.json(); } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
1491
+ if (typeof parsedBody !== "object" || parsedBody === null || Array.isArray(parsedBody)) {
1492
+ return jsonResponse({ error: "body must be an object" }, 400);
1493
+ }
1494
+ const body = parsedBody as { id?: unknown; priority?: unknown };
1495
+ const id = typeof body.id === "string" ? body.id.trim() : "";
1496
+ if (!isCodexAccountPriorityKey(id)) {
1497
+ return jsonResponse({ error: "Invalid account id format" }, 400);
1498
+ }
1499
+
1500
+ let priority = DEFAULT_ACCOUNT_PRIORITY;
1501
+ if (body.priority !== null) {
1502
+ const parsed = parseAccountPriority(body.priority);
1503
+ if (parsed === null) {
1504
+ return jsonResponse({
1505
+ error: `priority must be null or an integer ${MIN_ACCOUNT_PRIORITY}-${MAX_ACCOUNT_PRIORITY}`,
1506
+ }, 400);
1507
+ }
1508
+ priority = parsed;
1509
+ }
1510
+
1511
+ const runtimeConfig = getRuntimeConfig(config);
1512
+ const exists = id === MAIN_CODEX_ACCOUNT_ID
1513
+ || (runtimeConfig.codexAccounts ?? []).some(account => isSelectableCodexPoolAccount(account) && account.id === id);
1514
+ if (!exists) return jsonResponse({ error: "Account not found" }, 404);
1515
+
1516
+ setCodexAccountPriority(runtimeConfig, id, priority);
1517
+ // Both a pin and an order are the operator saying which account to use, so the newer
1518
+ // statement wins. Without this a pin made before any order existed — an ordinary
1519
+ // account switch — would outrank the order forever: it blocks preemption and caps
1520
+ // every eligibility list at its own tier until that account drains or is paused.
1521
+ clearCodexAccountPin(runtimeConfig);
1522
+ saveRuntimeConfig(config, runtimeConfig);
1523
+ return jsonResponse({
1524
+ ok: true,
1525
+ id,
1526
+ priority,
1527
+ activeCodexAccountId: getEffectiveActiveCodexAccountId(runtimeConfig) ?? null,
1528
+ });
1529
+ }
1530
+
1021
1531
  if (url.pathname === "/api/codex-auth/accounts/pause-exhausted" && req.method === "PUT") {
1022
1532
  const runtimeConfig = getRuntimeConfig(config);
1023
- const result = await pauseExhaustedCodexAccounts(runtimeConfig);
1533
+ const result = await pauseExhaustedCodexAccounts(
1534
+ runtimeConfig,
1535
+ () => saveRuntimeConfig(config, runtimeConfig),
1536
+ );
1024
1537
  const { pausedAccountIds, checkedAccountCount, failedAccountCount } = result;
1025
1538
  if (checkedAccountCount === 0 && failedAccountCount > 0) {
1026
1539
  return jsonResponse({
@@ -1030,7 +1543,6 @@ export async function handleCodexAuthAPI(
1030
1543
  failedAccountCount,
1031
1544
  }, 502);
1032
1545
  }
1033
- if (pausedAccountIds.length > 0) saveRuntimeConfig(config, runtimeConfig);
1034
1546
  return jsonResponse({
1035
1547
  ok: true,
1036
1548
  pausedAccountIds,
@@ -1078,6 +1590,15 @@ export async function handleCodexAuthAPI(
1078
1590
  if (!exists) return jsonResponse({ error: "Account not found" }, 400);
1079
1591
  }
1080
1592
  runtimeConfig.activeCodexAccountId = body.accountId ?? undefined;
1593
+ // "Use this account now" outranks selection order until the account is spent:
1594
+ // persisted here rather than in resetCodexRoutingForManualSelection, which is
1595
+ // runtime state only. A null id clears the selection instead of making one, so it
1596
+ // must release the pin rather than record one: pinning the `targetAccountId`
1597
+ // fallback would leave a pin that no effective active account matches, which
1598
+ // `isEffectiveCodexAccountPinned` reports as unpinned while the tier filter still
1599
+ // honours it as a ceiling — invisibly capping the pool at the main account's tier.
1600
+ if (body.accountId == null) clearCodexAccountPin(runtimeConfig);
1601
+ else setCodexAccountPin(runtimeConfig, targetAccountId);
1081
1602
  resetCodexRoutingForManualSelection(targetAccountId);
1082
1603
  saveRuntimeConfig(config, runtimeConfig);
1083
1604
  return jsonResponse({ ok: true, activeCodexAccountId: body.accountId, appliesImmediately: true });
@@ -1087,6 +1608,13 @@ export async function handleCodexAuthAPI(
1087
1608
  const runtimeConfig = getRuntimeConfig(config);
1088
1609
  return jsonResponse({
1089
1610
  activeCodexAccountId: getEffectiveActiveCodexAccountId(runtimeConfig) ?? null,
1611
+ pinned: isEffectiveCodexAccountPinned(runtimeConfig),
1612
+ // Which account carries the pin, not just whether the active one does. Under
1613
+ // round-robin or fill-first the pin caps the tier ceiling at its own tier while the
1614
+ // strategy cursor moves freely inside that tier, so `pinned` alone goes false on a
1615
+ // sibling's turn even though the pin is still suppressing every higher tier. The id
1616
+ // lets a surface mark the account the operator actually chose.
1617
+ pinnedAccountId: pinnedCodexAccountId(runtimeConfig) ?? null,
1090
1618
  autoSwitchThreshold: runtimeConfig.autoSwitchThreshold ?? 80,
1091
1619
  upstreamFailoverThreshold: runtimeConfig.upstreamFailoverThreshold ?? 3,
1092
1620
  accountPoolStrategy: normalizeAccountPoolStrategy(runtimeConfig.accountPoolStrategy),
@@ -1169,24 +1697,47 @@ export async function handleCodexAuthAPI(
1169
1697
  if (!accountId) return jsonResponse({ error: "accountId required" }, 400);
1170
1698
 
1171
1699
  try {
1172
- const auth = await resolveResetCreditAuth(getRuntimeConfig(config), accountId);
1173
- if (!auth.ok) return auth.response;
1174
-
1175
- const resp = await fetch(
1176
- "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
1177
- {
1178
- headers: {
1179
- Authorization: `Bearer ${auth.accessToken}`,
1180
- "ChatGPT-Account-Id": auth.chatgptAccountId,
1181
- },
1182
- signal: AbortSignal.timeout(8000),
1183
- },
1184
- );
1185
- if (!resp.ok) {
1186
- await resp.body?.cancel().catch(() => {});
1187
- return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
1188
- }
1189
- return jsonResponse(safeResetCreditsDto(await resp.json()));
1700
+ const result = await withResetCreditAuth(getRuntimeConfig(config), accountId, async auth => {
1701
+ const linkedSignal = signalWithTimeout(8000, req.signal);
1702
+ let detachBodyAbort = () => {};
1703
+ try {
1704
+ let resp: Response;
1705
+ try {
1706
+ resp = await fetch(
1707
+ "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
1708
+ {
1709
+ headers: {
1710
+ Authorization: `Bearer ${auth.accessToken}`,
1711
+ "ChatGPT-Account-Id": auth.chatgptAccountId,
1712
+ },
1713
+ signal: linkedSignal.signal,
1714
+ },
1715
+ );
1716
+ } catch (error) {
1717
+ if (linkedSignal.signal.aborted) {
1718
+ return jsonResponse({ error: "Invalid upstream reset-credit response" }, 502);
1719
+ }
1720
+ throw error;
1721
+ }
1722
+ // Own the response body before the bounded reader attaches. If the client
1723
+ // disconnects in that narrow window, Bun otherwise tears down the native
1724
+ // body off the awaited path and can report an unhandled rejection.
1725
+ detachBodyAbort = cancelBodyOnAbort(resp.body, linkedSignal.signal);
1726
+ if (!resp.ok) {
1727
+ await resp.body?.cancel().catch(() => {});
1728
+ return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
1729
+ }
1730
+ const parsed = await readResetCreditJson(resp, linkedSignal.signal);
1731
+ if (!parsed.ok) {
1732
+ return jsonResponse({ error: "Invalid upstream reset-credit response" }, 502);
1733
+ }
1734
+ return jsonResponse(safeResetCreditsDto(parsed.value));
1735
+ } finally {
1736
+ detachBodyAbort();
1737
+ linkedSignal.cleanup();
1738
+ }
1739
+ });
1740
+ return result.ok ? result.value : result.response;
1190
1741
  } catch (e) {
1191
1742
  return jsonResponse({ error: e instanceof Error ? e.message : "Reset credit lookup failed" }, 500);
1192
1743
  }
@@ -1195,49 +1746,55 @@ export async function handleCodexAuthAPI(
1195
1746
  if (url.pathname === "/api/codex-auth/reset-credits/consume" && req.method === "POST") {
1196
1747
  const body = (await req.json().catch(() => ({}))) as { accountId?: string };
1197
1748
  if (!body.accountId) return jsonResponse({ error: "accountId required" }, 400);
1749
+ const accountId = body.accountId;
1198
1750
 
1199
1751
  try {
1200
- const auth = await resolveResetCreditAuth(getRuntimeConfig(config), body.accountId);
1201
- if (!auth.ok) return auth.response;
1202
-
1203
- const idempotencyKey = crypto.randomUUID();
1204
- const resp = await fetch(
1205
- "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
1206
- {
1207
- method: "POST",
1208
- headers: {
1209
- Authorization: `Bearer ${auth.accessToken}`,
1210
- "ChatGPT-Account-Id": auth.chatgptAccountId,
1211
- "Content-Type": "application/json",
1752
+ const operation = await withResetCreditAuth(getRuntimeConfig(config), accountId, async auth => {
1753
+ const idempotencyKey = crypto.randomUUID();
1754
+ const resp = await fetch(
1755
+ "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
1756
+ {
1757
+ method: "POST",
1758
+ headers: {
1759
+ Authorization: `Bearer ${auth.accessToken}`,
1760
+ "ChatGPT-Account-Id": auth.chatgptAccountId,
1761
+ "Content-Type": "application/json",
1762
+ },
1763
+ body: JSON.stringify({ redeem_request_id: idempotencyKey }),
1764
+ signal: AbortSignal.timeout(10_000),
1212
1765
  },
1213
- body: JSON.stringify({ redeem_request_id: idempotencyKey }),
1214
- signal: AbortSignal.timeout(10_000),
1215
- },
1216
- );
1217
- if (!resp.ok) {
1218
- await resp.body?.cancel().catch(() => {});
1219
- return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
1220
- }
1221
- const result = safeResetCreditConsumeDto(await resp.json());
1222
- // After a successful redeem (or an idempotent already_redeemed), refresh WHAM usage
1223
- // and return remaining only when that refresh freshly parsed available_count.
1224
- // Do not fall back to a preserved cached resetCredits (failed/omitted refresh).
1225
- if (result.code === "reset" || result.code === "already_redeemed") {
1226
- let freshResetCredits: number | undefined;
1227
- if (auth.isMain) {
1228
- ({ freshResetCredits } = await fetchMainAccountInfoAttempt(true, 1));
1229
- } else {
1230
- const account = configuredPoolAccount(getRuntimeConfig(config), body.accountId);
1231
- ({ freshResetCredits } = await fetchPoolAccountQuota(body.accountId, true, account?.plan));
1766
+ );
1767
+ if (!resp.ok) {
1768
+ await resp.body?.cancel().catch(() => {});
1769
+ return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
1232
1770
  }
1233
- return jsonResponse({
1234
- code: result.code,
1235
- ...(typeof freshResetCredits === "number" && Number.isFinite(freshResetCredits)
1236
- ? { remaining: freshResetCredits }
1237
- : {}),
1238
- });
1239
- }
1240
- return jsonResponse(result);
1771
+ const result = safeResetCreditConsumeDto(await resp.json());
1772
+ // After a successful redeem (or an idempotent already_redeemed), refresh WHAM usage
1773
+ // and return remaining only when that refresh freshly parsed available_count.
1774
+ // Do not fall back to a preserved cached resetCredits (failed/omitted refresh).
1775
+ if (result.code === "reset" || result.code === "already_redeemed") {
1776
+ let freshResetCredits: number | undefined;
1777
+ if (auth.isMain) {
1778
+ ({ freshResetCredits } = await fetchMainAccountInfoAttempt(
1779
+ true,
1780
+ 1,
1781
+ auth.nativeMainLease,
1782
+ auth.nativeMainSharedClaimHeld === true,
1783
+ ));
1784
+ } else {
1785
+ const account = configuredPoolAccount(getRuntimeConfig(config), accountId);
1786
+ ({ freshResetCredits } = await fetchPoolAccountQuota(accountId, true, account?.plan));
1787
+ }
1788
+ return jsonResponse({
1789
+ code: result.code,
1790
+ ...(typeof freshResetCredits === "number" && Number.isFinite(freshResetCredits)
1791
+ ? { remaining: freshResetCredits }
1792
+ : {}),
1793
+ });
1794
+ }
1795
+ return jsonResponse(result);
1796
+ });
1797
+ return operation.ok ? operation.value : operation.response;
1241
1798
  } catch (e) {
1242
1799
  if (e instanceof PoolQuotaProbeBusyError) {
1243
1800
  const response = jsonResponse({ error: "server_busy", code: "server_busy" }, 503);
@@ -1278,7 +1835,7 @@ export async function handleCodexAuthAPI(
1278
1835
  const loginOwner: CodexLoginStateRow = { status: "starting", startedAt: Date.now() };
1279
1836
  codexAuthLoginState.set(flowId, loginOwner);
1280
1837
  try {
1281
- const { startLoginFlow, getLoginStatus } = await import("../oauth");
1838
+ const { startLoginFlow, getLoginStatus, publicOAuthAuthenticationErrorMessage } = await import("../oauth");
1282
1839
  const result = await startLoginFlow("chatgpt", { forceLogin: true });
1283
1840
 
1284
1841
  // Open the browser server-side (same pattern as /api/oauth/login in management-api.ts).
@@ -1391,6 +1948,8 @@ export async function handleCodexAuthAPI(
1391
1948
  const latestConfig = getRuntimeConfig(config);
1392
1949
  const accounts = latestConfig.codexAccounts ?? [];
1393
1950
  const existingIdx = accounts.findIndex(account => account.id === accountId);
1951
+ let pickerVisibilityChanged = false;
1952
+ let newAccountPersistence: PersistNewCodexAccountOutcome | null = null;
1394
1953
  const commitConflict = codexAccountPersistenceConflict(
1395
1954
  latestConfig,
1396
1955
  accountId,
@@ -1406,22 +1965,21 @@ export async function handleCodexAuthAPI(
1406
1965
  break;
1407
1966
  }
1408
1967
 
1409
- saveCodexAccountCredential(accountId, {
1968
+ const credential: CodexAccountCredentials = {
1410
1969
  accessToken: cred.access,
1411
1970
  refreshToken: cred.refresh,
1412
1971
  expiresAt: cred.expires,
1413
1972
  chatgptAccountId: oauthAccountId,
1414
- });
1415
- // A successful reauthentication replaces the credential generation. Do not let a
1416
- // failed optional WHAM probe make the replacement inherit quota from the old record.
1417
- if (reauth) clearAccountQuota(accountId);
1418
- markCodexAccountValidated(accountId, warmup.validatedAt);
1419
- clearAccountNeedsReauth(accountId);
1420
- if (quota) {
1421
- setAccountQuotaFromParsed(accountId, quota);
1422
- }
1973
+ };
1423
1974
 
1424
1975
  if (existingIdx >= 0) {
1976
+ saveCodexAccountCredential(accountId, credential);
1977
+ // A successful reauthentication replaces the credential generation. Do not let a
1978
+ // failed optional WHAM probe make the replacement inherit quota from the old record.
1979
+ if (reauth) clearAccountQuota(accountId);
1980
+ markCodexAccountValidated(accountId, warmup.validatedAt);
1981
+ clearAccountNeedsReauth(accountId);
1982
+ if (quota) setAccountQuotaFromParsed(accountId, quota);
1425
1983
  // Keep the pool id stable; refresh display metadata after a successful login/reauth.
1426
1984
  accounts[existingIdx] = withCodexAccountLogLabel({
1427
1985
  ...accounts[existingIdx],
@@ -1432,18 +1990,60 @@ export async function handleCodexAuthAPI(
1432
1990
  latestConfig.codexAccounts = accounts;
1433
1991
  saveRuntimeConfig(config, latestConfig);
1434
1992
  } else {
1435
- accounts.push(withCodexAccountLogLabel({ id: accountId, email, plan, isMain: false }, accounts));
1436
- latestConfig.codexAccounts = accounts;
1437
- saveRuntimeConfig(config, latestConfig);
1993
+ const addedAccount = withCodexAccountLogLabel({ id: accountId, email, plan, isMain: false }, accounts);
1994
+ newAccountPersistence = persistNewCodexAccount(
1995
+ config,
1996
+ latestConfig,
1997
+ addedAccount,
1998
+ {
1999
+ credential,
2000
+ validatedAt: warmup.validatedAt,
2001
+ },
2002
+ );
2003
+ pickerVisibilityChanged = newAccountPersistence.pickerVisibilityChanged;
1438
2004
  }
1439
2005
  reconcileLiveStateStores();
1440
- setCodexLoginState(flowId, { status: "done", accountId, email, doneAt: Date.now() });
1441
- completed = true;
2006
+ if (newAccountPersistence?.status === "publication-failed") {
2007
+ markAccountNeedsReauth(accountId);
2008
+ }
2009
+ // A new quota row is generation-gated by live account ownership. Reconcile the
2010
+ // durable config owner first so a partial prior sweep cannot reject this write.
2011
+ if (newAccountPersistence?.status === "committed" && quota) {
2012
+ setAccountQuotaFromParsed(accountId, quota);
2013
+ }
2014
+ const { catalogRefreshPending } = await convergeAccountNamespaceCatalog(
2015
+ latestConfig,
2016
+ pickerVisibilityChanged,
2017
+ convergeCodexCatalog,
2018
+ );
2019
+ if (newAccountPersistence?.status === "publication-failed") {
2020
+ setCodexLoginState(flowId, {
2021
+ status: "error",
2022
+ ...codexCredentialPersistenceFailure(accountId, catalogRefreshPending),
2023
+ doneAt: Date.now(),
2024
+ });
2025
+ completed = true;
2026
+ } else {
2027
+ setCodexLoginState(flowId, {
2028
+ status: "done",
2029
+ accountId,
2030
+ email,
2031
+ ...(catalogRefreshPending ? { catalogRefreshPending: true } : {}),
2032
+ doneAt: Date.now(),
2033
+ });
2034
+ completed = true;
2035
+ }
1442
2036
  }
1443
2037
  break;
1444
2038
  }
1445
2039
  if (st.done && st.error) {
1446
- setCodexLoginState(flowId, { status: "error", error: st.error, doneAt: Date.now() });
2040
+ setCodexLoginState(flowId, {
2041
+ status: "error",
2042
+ // startLoginFlow projects background failures before storing login status, so
2043
+ // fixed actionable OAuth messages retain their type-derived remediation here.
2044
+ error: st.error,
2045
+ doneAt: Date.now(),
2046
+ });
1447
2047
  completed = true;
1448
2048
  break;
1449
2049
  }
@@ -1461,7 +2061,7 @@ export async function handleCodexAuthAPI(
1461
2061
  ? "Configuration is busy; retry login shortly."
1462
2062
  : error instanceof CodexCredentialRefreshBusyError || error instanceof CodexCredentialRefreshStaleError
1463
2063
  ? "Credential refresh is busy; retry login shortly."
1464
- : error instanceof Error ? error.message : String(error);
2064
+ : publicOAuthAuthenticationErrorMessage(error);
1465
2065
  setCodexLoginState(flowId, {
1466
2066
  status: "error",
1467
2067
  error: message,
@@ -1478,7 +2078,7 @@ export async function handleCodexAuthAPI(
1478
2078
  } catch (e) {
1479
2079
  if (codexAuthLoginState.get(flowId) === loginOwner) codexAuthLoginState.delete(flowId);
1480
2080
  const msg = e instanceof Error ? e.message : String(e);
1481
- if (msg.includes("already in progress")) {
2081
+ if (msg === "A login for chatgpt is already in progress") {
1482
2082
  return jsonResponse({ error: msg, status: "pending" }, 409);
1483
2083
  }
1484
2084
  if (e instanceof CodexCredentialRefreshBusyError || e instanceof CodexCredentialRefreshStaleError) {
@@ -1486,7 +2086,8 @@ export async function handleCodexAuthAPI(
1486
2086
  response.headers.set("Retry-After", "1");
1487
2087
  return response;
1488
2088
  }
1489
- return jsonResponse({ error: msg }, 500);
2089
+ const { publicOAuthAuthenticationErrorMessage } = await import("../oauth");
2090
+ return jsonResponse({ error: publicOAuthAuthenticationErrorMessage(e) }, 500);
1490
2091
  }
1491
2092
  }
1492
2093
 
@@ -1524,7 +2125,13 @@ export async function handleCodexAuthAPI(
1524
2125
  const reauthStatus = url.searchParams.get("reauth") === "1";
1525
2126
  if (flowId) {
1526
2127
  const st = codexAuthLoginState.get(flowId);
1527
- if (!st && accountId && !reauthStatus && getCodexAccountCredential(accountId)) {
2128
+ if (
2129
+ !st
2130
+ && accountId
2131
+ && !reauthStatus
2132
+ && !isAccountNeedsReauth(accountId)
2133
+ && getCodexAccountCredential(accountId)
2134
+ ) {
1528
2135
  return jsonResponse({ status: "done", accountId });
1529
2136
  }
1530
2137
  return jsonResponse(st ? { ...st, email: st.email ?? undefined } : { status: "expired" });