@bitkyc08/opencodex 2.80.0 → 2.82.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 (376) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +166 -73
  3. package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
  4. package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
  5. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  6. package/gui/dist/assets/index-D3FBCNiP.js +94 -0
  7. package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/gui/dist/provider-icons/claude-green.svg +1 -0
  10. package/package.json +3 -2
  11. package/src/adapters/anthropic-model-contract.ts +8 -2
  12. package/src/adapters/anthropic.ts +8 -3
  13. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  14. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  15. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  16. package/src/adapters/coding-agent/protocol.ts +122 -17
  17. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  18. package/src/adapters/coding-agent/turn.ts +101 -44
  19. package/src/adapters/cursor/catalog.ts +8 -0
  20. package/src/adapters/cursor/effort-map.ts +2 -0
  21. package/src/adapters/devin/live-models.ts +2 -0
  22. package/src/adapters/kiro-retry.ts +9 -4
  23. package/src/adapters/openai-chat/messages.ts +7 -6
  24. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  25. package/src/adapters/openai-responses/passthrough.ts +9 -4
  26. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  27. package/src/adapters/physical-send.ts +8 -5
  28. package/src/adapters/qoder/adapter.ts +41 -6
  29. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  30. package/src/adapters/zed.ts +16 -1
  31. package/src/claude/agents-inject.ts +14 -3
  32. package/src/claude/context-windows.ts +19 -17
  33. package/src/claude/desktop-3p.ts +88 -12
  34. package/src/claude/desktop-profile.ts +2 -1
  35. package/src/claude/gateway-cache.ts +18 -1
  36. package/src/claude/inbound-cache-stabilize.ts +4 -9
  37. package/src/claude/inbound.ts +78 -3
  38. package/src/claude/intercept/cli-catalog.ts +3 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  41. package/src/claude/intercept/picker-listener.ts +71 -4
  42. package/src/claude/intercept/picker-models.ts +14 -6
  43. package/src/claude/intercept/settings.ts +3 -0
  44. package/src/claude/long-context.ts +66 -0
  45. package/src/claude/model-info.ts +19 -16
  46. package/src/claude/outbound.ts +94 -9
  47. package/src/claude/subagent-model.ts +40 -11
  48. package/src/cli/account-anthropic-threshold.ts +14 -5
  49. package/src/cli/account-api.ts +3 -1
  50. package/src/cli/account-extended.ts +13 -9
  51. package/src/cli/account-policy-dto.ts +1 -1
  52. package/src/cli/account-policy.ts +13 -4
  53. package/src/cli/account.ts +3 -3
  54. package/src/cli/agent.ts +9 -5
  55. package/src/cli/aside-profile-recovery.ts +33 -0
  56. package/src/cli/capabilities-accounts.ts +2 -2
  57. package/src/cli/capabilities-agents-routing.ts +24 -0
  58. package/src/cli/capabilities-base.ts +5 -5
  59. package/src/cli/capabilities-integrations.ts +18 -2
  60. package/src/cli/cli-path-diagnostics.ts +132 -0
  61. package/src/cli/codex-shim-autorestore.ts +3 -0
  62. package/src/cli/desktop-runtime-guidance.ts +65 -0
  63. package/src/cli/dispatch.ts +15 -1
  64. package/src/cli/doctor.ts +16 -3
  65. package/src/cli/help.ts +4 -1
  66. package/src/cli/index.ts +25 -14
  67. package/src/cli/integration-aside-sync.ts +2 -1
  68. package/src/cli/integration-plan-dto.ts +1 -0
  69. package/src/cli/integration-preview.ts +8 -1
  70. package/src/cli/integrations.ts +48 -6
  71. package/src/cli/message-args.ts +34 -0
  72. package/src/cli/message-command.ts +43 -0
  73. package/src/cli/message-runtime.ts +6 -0
  74. package/src/cli/observe.ts +3 -1
  75. package/src/cli/provider.ts +29 -6
  76. package/src/cli/registry.ts +24 -2
  77. package/src/cli/resolve.ts +25 -2
  78. package/src/cli/root.ts +4 -2
  79. package/src/cli/status.ts +64 -12
  80. package/src/cli/update-restart-child.ts +4 -2
  81. package/src/cli/update-restart-home.ts +28 -10
  82. package/src/cli/update-restart-service-record.ts +116 -0
  83. package/src/cli/update-restart-supervision.ts +96 -0
  84. package/src/cli/update-restart.ts +59 -13
  85. package/src/cli/v2.ts +27 -20
  86. package/src/cli/version-skew-notice.ts +77 -0
  87. package/src/clients/config-export/commandcode.ts +131 -0
  88. package/src/clients/config-export/contracts.ts +12 -0
  89. package/src/clients/config-export.ts +64 -1
  90. package/src/clients/lazycodex.ts +1 -1
  91. package/src/clients/omo-role-models.ts +32 -7
  92. package/src/codex/auth-api/account-list.ts +11 -3
  93. package/src/codex/auth-api/account-selection.ts +107 -0
  94. package/src/codex/auth-api/main-account-probe.ts +25 -8
  95. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  96. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  97. package/src/codex/auth-api/routes.ts +11 -49
  98. package/src/codex/auth-context.ts +20 -1
  99. package/src/codex/autostart-health.ts +12 -3
  100. package/src/codex/catalog/auto-review.ts +13 -6
  101. package/src/codex/catalog/build-entries.ts +33 -7
  102. package/src/codex/catalog/bundled.ts +155 -26
  103. package/src/codex/catalog/combo-member.ts +3 -2
  104. package/src/codex/catalog/control-plane.ts +45 -0
  105. package/src/codex/catalog/effort.ts +37 -6
  106. package/src/codex/catalog/gather-capture.ts +10 -3
  107. package/src/codex/catalog/model-hints.ts +15 -8
  108. package/src/codex/catalog/model-visibility.ts +3 -1
  109. package/src/codex/catalog/operator-tiers.ts +38 -0
  110. package/src/codex/catalog/parsing.ts +2 -1
  111. package/src/codex/catalog/provider-models.ts +58 -44
  112. package/src/codex/catalog/retained-sync.ts +7 -5
  113. package/src/codex/catalog/routed-gather.ts +15 -1
  114. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  115. package/src/codex/catalog-auto-refresh.ts +19 -0
  116. package/src/codex/catalog.ts +1 -1
  117. package/src/codex/codex-home-owner.ts +3 -0
  118. package/src/codex/config-write-lock.ts +333 -0
  119. package/src/codex/control-plane-models.ts +6 -0
  120. package/src/codex/convergence.ts +2 -1
  121. package/src/codex/features.ts +257 -200
  122. package/src/codex/inject/config-write-section.ts +34 -0
  123. package/src/codex/inject/multi-agent-v2.ts +14 -6
  124. package/src/codex/inject/remove.ts +36 -11
  125. package/src/codex/inject/restore.ts +85 -20
  126. package/src/codex/inject-coordination.ts +15 -6
  127. package/src/codex/inject.ts +62 -20
  128. package/src/codex/journal.ts +62 -17
  129. package/src/codex/main-account-cache.ts +43 -0
  130. package/src/codex/paths.ts +2 -0
  131. package/src/codex/prepared-home.ts +18 -0
  132. package/src/codex/prompt-journal.ts +25 -12
  133. package/src/codex/prompt-layers.ts +49 -26
  134. package/src/codex/prompt-lock-claim.ts +134 -0
  135. package/src/codex/prompt-lock-evidence.ts +26 -0
  136. package/src/codex/prompt-lock-io.ts +16 -0
  137. package/src/codex/prompt-lock-owner.ts +130 -0
  138. package/src/codex/prompt-lock.ts +84 -53
  139. package/src/codex/routing/failure-window.ts +146 -0
  140. package/src/codex/routing/selection.ts +4 -1
  141. package/src/codex/routing.ts +13 -17
  142. package/src/codex/runtime.ts +106 -18
  143. package/src/codex/shim.ts +45 -19
  144. package/src/combos/jev-decision-contract.ts +25 -2
  145. package/src/combos/jev-service-exchange.ts +281 -0
  146. package/src/combos/jev.ts +34 -219
  147. package/src/combos/types.ts +1 -1
  148. package/src/config/atomic-write.ts +1 -1
  149. package/src/config/codex-ws-reuse-setting.ts +10 -0
  150. package/src/config/diagnostics.ts +27 -16
  151. package/src/config/live-reconcile.ts +17 -0
  152. package/src/config/load-degrade.ts +42 -0
  153. package/src/config/rebase-provenance.ts +11 -0
  154. package/src/config/schema/anthropic-account-pool.ts +143 -0
  155. package/src/config/schema/config-schema.ts +11 -1
  156. package/src/config/schema/leaf-validators.ts +4 -0
  157. package/src/config.ts +2 -0
  158. package/src/generated/compatibility-version.json +566 -298
  159. package/src/generated/model-metadata.ts +7 -6
  160. package/src/grok/inject.ts +50 -4
  161. package/src/grok/sync.ts +2 -1
  162. package/src/images/loop.ts +30 -11
  163. package/src/integrations/aside-profiles.ts +10 -3
  164. package/src/integrations/catalog-refresh.ts +19 -3
  165. package/src/integrations/mutation-plan.ts +7 -0
  166. package/src/integrations/registry.ts +8 -0
  167. package/src/integrations/state.ts +9 -2
  168. package/src/integrations/writer.ts +17 -5
  169. package/src/lib/bun-path-runtime.d.mts +32 -0
  170. package/src/lib/bun-path-runtime.mjs +95 -0
  171. package/src/lib/bun-runtime-preflight.ts +140 -0
  172. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  173. package/src/lib/desktop-cli-handoff.mjs +92 -0
  174. package/src/lib/desktop-cli-record.d.mts +29 -0
  175. package/src/lib/desktop-cli-record.mjs +102 -0
  176. package/src/lib/lab-live-route-production.ts +9 -1
  177. package/src/lib/request-execution-budget.ts +244 -58
  178. package/src/lib/spend-pool-alias-validation.ts +79 -0
  179. package/src/lib/spend-pool-continuity.ts +34 -0
  180. package/src/lib/spend-reservation-ledger.ts +412 -50
  181. package/src/lib/state-store-registrations.ts +20 -3
  182. package/src/lib/synced-state-location.ts +46 -6
  183. package/src/lib/test-home-guard.ts +87 -1
  184. package/src/lib/translator-budget.ts +17 -4
  185. package/src/lib/upstream-retry.ts +17 -5
  186. package/src/lib/windows-atomic-replace.ts +38 -16
  187. package/src/lib/windows-elevation.ts +6 -0
  188. package/src/lib/windows-secret-acl.ts +92 -25
  189. package/src/lib/winsw.ts +2 -3
  190. package/src/lib/workflow-budget.ts +53 -11
  191. package/src/messaging/budget.ts +45 -0
  192. package/src/messaging/discovery.ts +57 -0
  193. package/src/messaging/envelope.ts +49 -0
  194. package/src/messaging/input.ts +37 -0
  195. package/src/messaging/rpc.ts +196 -0
  196. package/src/messaging/send.ts +58 -0
  197. package/src/messaging/socket.ts +72 -0
  198. package/src/messaging/types.ts +29 -0
  199. package/src/oauth/anthropic-account-refusal.ts +90 -18
  200. package/src/oauth/anthropic-account-threshold.ts +8 -2
  201. package/src/oauth/anthropic-continuity.ts +15 -1
  202. package/src/oauth/anthropic-model-quota.ts +120 -48
  203. package/src/oauth/anthropic-model-routes.ts +11 -3
  204. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  205. package/src/oauth/anthropic-pool-config.ts +30 -0
  206. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  207. package/src/oauth/anthropic-routing.ts +1202 -866
  208. package/src/oauth/anthropic-send-ownership.ts +41 -0
  209. package/src/oauth/anthropic.ts +4 -1
  210. package/src/oauth/chatgpt.ts +113 -41
  211. package/src/oauth/generic-account-failover.ts +3 -1
  212. package/src/oauth/health.ts +8 -6
  213. package/src/oauth/index.ts +96 -175
  214. package/src/oauth/kiro.ts +22 -9
  215. package/src/oauth/login-cli.ts +2 -0
  216. package/src/oauth/model-discovery-auth.ts +43 -0
  217. package/src/oauth/pool-kernel.ts +11 -5
  218. package/src/oauth/pool-settings-capability.ts +5 -3
  219. package/src/oauth/store-anthropic-instance.ts +87 -0
  220. package/src/oauth/store.ts +68 -10
  221. package/src/oauth/token-guardian.ts +2 -0
  222. package/src/oauth/types.ts +2 -0
  223. package/src/protocols/plan-snapshot.ts +5 -0
  224. package/src/protocols/settings.ts +20 -5
  225. package/src/providers/anthropic-instance-id.ts +47 -0
  226. package/src/providers/anthropic-instance.ts +48 -0
  227. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  228. package/src/providers/azure-model-metadata.ts +115 -0
  229. package/src/providers/derive.ts +23 -0
  230. package/src/providers/kiro-models.ts +3 -0
  231. package/src/providers/label.ts +1 -1
  232. package/src/providers/model-rename-fields.ts +2 -0
  233. package/src/providers/quota/account-cache.ts +37 -11
  234. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  235. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  236. package/src/providers/quota/report-cache.ts +24 -1
  237. package/src/providers/quota/vendor-probes-key.ts +127 -17
  238. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  239. package/src/providers/quota.ts +23 -33
  240. package/src/providers/registry/entries-core.ts +35 -30
  241. package/src/providers/registry/model-ids.ts +1 -0
  242. package/src/providers/registry/model-seeds.ts +7 -4
  243. package/src/providers/registry/types.ts +3 -0
  244. package/src/providers/registry.ts +3 -3
  245. package/src/responses/reasoning-envelope.ts +29 -1
  246. package/src/responses/reasoning-replay-cache.ts +17 -1
  247. package/src/responses/spill-store.ts +20 -3
  248. package/src/responses/state/metrics.ts +95 -0
  249. package/src/responses/state/snapshot-codec.ts +6 -0
  250. package/src/responses/state/spill-failure.ts +12 -0
  251. package/src/responses/state/spill-queue.ts +13 -5
  252. package/src/responses/state/unforced-store-false.ts +55 -0
  253. package/src/responses/state.ts +68 -76
  254. package/src/responses/task-input.ts +17 -0
  255. package/src/router.ts +28 -4
  256. package/src/routing/compatibility/assemble.ts +1 -1
  257. package/src/routing/quota.ts +7 -3
  258. package/src/server/auth-cors.ts +5 -0
  259. package/src/server/background-lifecycle.ts +27 -0
  260. package/src/server/chat-completions.ts +7 -1
  261. package/src/server/chat-native.ts +64 -28
  262. package/src/server/claude-messages.ts +59 -8
  263. package/src/server/gui-static.ts +38 -4
  264. package/src/server/inbound-body-admission.ts +1 -0
  265. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  266. package/src/server/index/serve-options.ts +10 -2
  267. package/src/server/index/spend-ledger-lifecycle.ts +12 -5
  268. package/src/server/index/startup-warnings.ts +19 -0
  269. package/src/server/index.ts +5 -2
  270. package/src/server/inference/context.ts +1 -1
  271. package/src/server/lifecycle.ts +1 -1
  272. package/src/server/management/agent-settings-routes.ts +123 -38
  273. package/src/server/management/anthropic-account-threshold.ts +12 -9
  274. package/src/server/management/anthropic-pool-settings.ts +72 -0
  275. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  276. package/src/server/management/codex-agent-role-routes.ts +4 -2
  277. package/src/server/management/codex-prompt-routes.ts +1 -0
  278. package/src/server/management/codex-role-auto-assign.ts +13 -2
  279. package/src/server/management/config-routes.ts +85 -14
  280. package/src/server/management/context.ts +2 -2
  281. package/src/server/management/decision-routes.ts +2 -1
  282. package/src/server/management/integration-routes.ts +25 -2
  283. package/src/server/management/oauth-account-routes.ts +79 -98
  284. package/src/server/management/provider-instance-ownership.ts +44 -0
  285. package/src/server/management/provider-routes.ts +21 -28
  286. package/src/server/management/route-registry.ts +3 -1
  287. package/src/server/management/routing-profile-routes.ts +4 -4
  288. package/src/server/management/shared.ts +3 -3
  289. package/src/server/management/system-restart.ts +44 -14
  290. package/src/server/management/system-routes.ts +24 -2
  291. package/src/server/management/vision-sidecar-options.ts +9 -6
  292. package/src/server/management/web-search-sidecar-options.ts +51 -2
  293. package/src/server/management-auth.ts +6 -0
  294. package/src/server/messages-native-eligibility.ts +3 -2
  295. package/src/server/messages-native-oauth.ts +91 -45
  296. package/src/server/messages-native-selector.ts +39 -0
  297. package/src/server/messages-native.ts +133 -38
  298. package/src/server/messages-response-headers.ts +14 -0
  299. package/src/server/request-log-conversation.ts +48 -5
  300. package/src/server/request-log.ts +36 -2
  301. package/src/server/responses/adapter-continuation.ts +31 -19
  302. package/src/server/responses/adapter-dispatch.ts +75 -24
  303. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  304. package/src/server/responses/codex-ws-exchange.ts +17 -2
  305. package/src/server/responses/codex-ws-pool.ts +100 -13
  306. package/src/server/responses/codex-ws-wire.ts +9 -0
  307. package/src/server/responses/combo-requested-effort.ts +3 -1
  308. package/src/server/responses/compact.ts +25 -8
  309. package/src/server/responses/compaction-recovery.ts +1 -1
  310. package/src/server/responses/core-codex-account.ts +22 -1
  311. package/src/server/responses/core-combo-native.ts +24 -4
  312. package/src/server/responses/core-combo.ts +38 -25
  313. package/src/server/responses/core-lifetime.ts +3 -36
  314. package/src/server/responses/core-normalize.ts +1 -0
  315. package/src/server/responses/core-options.ts +8 -1
  316. package/src/server/responses/core-replay.ts +78 -0
  317. package/src/server/responses/core.ts +3 -3
  318. package/src/server/responses/fetch-helpers.ts +26 -3
  319. package/src/server/responses/passthrough-delivery.ts +30 -4
  320. package/src/server/responses/passthrough-dispatch.ts +39 -9
  321. package/src/server/responses/request-prepare.ts +41 -5
  322. package/src/server/responses/request-send-budget.ts +150 -50
  323. package/src/server/responses/request-spend.ts +325 -33
  324. package/src/server/responses/request-transport.ts +132 -52
  325. package/src/server/responses/response-effects.ts +1 -0
  326. package/src/server/responses/run-turn-execution.ts +13 -3
  327. package/src/server/responses/sidecar-execution.ts +113 -22
  328. package/src/server/responses/sidecar-send-budget.ts +58 -0
  329. package/src/server/responses/ws-upstream.ts +19 -6
  330. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  331. package/src/server/startup-health-cache.ts +2 -1
  332. package/src/server/workflow-refusal.ts +33 -0
  333. package/src/service/cli.ts +20 -4
  334. package/src/service/desktop-command-guard.ts +32 -0
  335. package/src/service/desktop-startup.ts +66 -40
  336. package/src/service/desktop-supervision.d.mts +26 -0
  337. package/src/service/desktop-supervision.mjs +140 -0
  338. package/src/service/managing-cli.ts +1 -0
  339. package/src/service/orchestration.ts +46 -20
  340. package/src/service/ownership-mutation-lease.d.mts +1 -1
  341. package/src/service/ownership-mutation-lease.mjs +5 -7
  342. package/src/service/repair.ts +25 -9
  343. package/src/service/state.ts +4 -3
  344. package/src/service/systemd.ts +17 -8
  345. package/src/service/windows-ops.ts +61 -22
  346. package/src/sidecar/anthropic-binding.ts +79 -0
  347. package/src/sidecar/auth.ts +76 -2
  348. package/src/storage/policy-job.ts +5 -1
  349. package/src/storage/policy-worker.ts +3 -1
  350. package/src/storage/policy.ts +9 -0
  351. package/src/types/anthropic-account-pool.ts +33 -0
  352. package/src/types/config.ts +33 -28
  353. package/src/types/provider.ts +5 -0
  354. package/src/types/request.ts +5 -0
  355. package/src/types/tools.ts +7 -0
  356. package/src/types/wire.ts +1 -0
  357. package/src/types.ts +4 -0
  358. package/src/update/index.ts +57 -9
  359. package/src/update/restart-ownership.ts +10 -3
  360. package/src/update/runtime-ownership.d.mts +6 -0
  361. package/src/update/runtime-ownership.mjs +23 -7
  362. package/src/usage/cost.ts +3 -2
  363. package/src/usage/expected-prices.ts +66 -14
  364. package/src/vision/anthropic-describe.ts +16 -3
  365. package/src/vision/index.ts +3 -0
  366. package/src/vision/plan.ts +19 -6
  367. package/src/web-search/alpha-search.ts +7 -1
  368. package/src/web-search/anthropic-executor.ts +16 -4
  369. package/src/web-search/index.ts +7 -3
  370. package/src/web-search/loop.ts +32 -7
  371. package/src/web-search/passthrough-bridge.ts +4 -1
  372. package/src/web-search/sidecar-providers.ts +4 -3
  373. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  374. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  375. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  376. package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
@@ -0,0 +1,131 @@
1
+ // Command Code config export.
2
+ import type { ExportContext, ManagedContribution } from "./contracts";
3
+ import { normalizeExportModels, authoritativeContextWindow, singleFragment } from "./model-metadata";
4
+ import { OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants";
5
+ import { sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
6
+
7
+ export interface CommandCodeModelEntry {
8
+ reasoningEfforts?: string[];
9
+ contextWindow?: number;
10
+ }
11
+
12
+ export interface CommandCodeProviderBlock {
13
+ name: "OpenCodex";
14
+ api: "openai-completions";
15
+ baseURL: string;
16
+ apiKey: string | boolean;
17
+ models: Record<string, CommandCodeModelEntry>;
18
+ }
19
+
20
+ export interface CommandCodeGeneratedConfig {
21
+ provider: Record<string, CommandCodeProviderBlock>;
22
+ /** The root a pre-existing target already uses; see {@link commandCodeProviderRoot}. */
23
+ providers?: Record<string, CommandCodeProviderBlock>;
24
+ }
25
+
26
+ /**
27
+ * LOSSY spelling-collision key for a routed selector — not model identity.
28
+ *
29
+ * One provider model reaches this exporter under interchangeable spellings: the raw
30
+ * selector keeps inner slashes (`command-code/deepseek/deepseek-v4.1-flash`, what
31
+ * `/v1/models` publishes) while the Codex-facing form encodes them as dashes
32
+ * (`command-code/deepseek-deepseek-v4.1-flash`, what `~/.codex/config.toml` stores and
33
+ * what an operator typing `--model` copies). Command Code addresses models by exact key,
34
+ * so a catalog that carries both spellings can resolve the active model against one and
35
+ * miss it in the other.
36
+ *
37
+ * The slash is meaningful: it separates the provider from the model id, and a provider id
38
+ * never contains one. But the encoding is NOT bijective — `p/a/b` and `p/a-b` are
39
+ * different model ids that encode to the same string. That is the same lossy relation
40
+ * `slugEquivalenceKey` documents in src/providers/slug-codec.ts and the same ambiguity
41
+ * `resolveSlugAliasCollisions` guards in the catalog. Callers must therefore never drop
42
+ * a row on this key alone; buildCommandCodeClientConfig additionally compares provider
43
+ * and id before folding two rows into one.
44
+ */
45
+ function canonicalSpellingOf(namespaced: string): string {
46
+ const slash = namespaced.indexOf("/");
47
+ if (slash <= 0) return namespaced;
48
+ return namespaced.slice(0, slash) + "/" + namespaced.slice(slash + 1).replaceAll("/", "-");
49
+ }
50
+
51
+ export function buildCommandCodeClientConfig(ctx: ExportContext): CommandCodeGeneratedConfig {
52
+ const models: Record<string, CommandCodeModelEntry> = {};
53
+ // Fold interchangeable spellings of one model onto a single key. The first
54
+ // occurrence wins per provider+id identity; distinct model ids that happen to
55
+ // share a lossy spelling collision must both remain addressable.
56
+ const emittedIdentitiesByCollision = new Map<string, Set<string>>();
57
+ for (const model of normalizeExportModels(ctx.models)) {
58
+ const emittedKey = model.namespaced;
59
+ const collisionKey = canonicalSpellingOf(model.namespaced);
60
+ const identity = `${model.provider}\u0000${model.id}`;
61
+ const identities = emittedIdentitiesByCollision.get(collisionKey) ?? new Set<string>();
62
+ if (identities.has(identity)) continue;
63
+ identities.add(identity);
64
+ emittedIdentitiesByCollision.set(collisionKey, identities);
65
+ const entry: CommandCodeModelEntry = {};
66
+ const context = authoritativeContextWindow(model.contextWindow);
67
+ if (context !== undefined) {
68
+ entry.contextWindow = context;
69
+ }
70
+ const efforts = sanitizeCodexReasoningEfforts(model.reasoningEfforts)
71
+ ?.filter(effort => ["low", "medium", "high", "xhigh", "max"].includes(effort));
72
+ if (efforts && efforts.length > 0) {
73
+ entry.reasoningEfforts = efforts;
74
+ }
75
+ models[emittedKey] = entry;
76
+ }
77
+ return {
78
+ provider: {
79
+ [OPENCODE_PROVIDER_ID]: {
80
+ name: "OpenCodex",
81
+ api: "openai-completions",
82
+ baseURL: ctx.baseUrl.replace(/\/v1\/?$/, "") + "/v1",
83
+ apiKey: false,
84
+ models,
85
+ },
86
+ },
87
+ };
88
+ }
89
+
90
+ /**
91
+ * The root key Command Code actually reads: `provider`, else `providers`.
92
+ *
93
+ * Verified against the published client (`command-code@1.66.0`, `dist/cli.mjs`):
94
+ * `const o = e.provider ?? e.providers;` — singular wins, and the plural branch is
95
+ * only reached when the singular root is nullish (`null` or `undefined`). Adding a
96
+ * singular root to a document that already carries a plural one therefore does not
97
+ * merge the two: the old entries stay on disk, byte for byte, and become invisible
98
+ * to the consumer.
99
+ *
100
+ * Evaluates nullish precedence before container validation so that non-nullish
101
+ * invalid singular values (such as strings or arrays) remain selected and are refused
102
+ * by the container guard rather than falling through to write an unreachable plural block.
103
+ */
104
+ export function commandCodeProviderRoot(document: unknown): "provider" | "providers" {
105
+ const doc = document as { provider?: unknown; providers?: unknown } | null | undefined;
106
+ if (doc?.provider !== undefined && doc?.provider !== null) return "provider";
107
+ if (doc?.providers !== undefined && doc?.providers !== null) return "providers";
108
+ // Nothing established yet: the singular root is the one this exporter writes.
109
+ return "provider";
110
+ }
111
+
112
+ export function summarizeCommandCode(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
113
+ const doc = document as CommandCodeGeneratedConfig | undefined;
114
+ const root = commandCodeProviderRoot(document);
115
+ const models = Object.values(doc?.[root]?.[OPENCODE_PROVIDER_ID]?.models ?? {}) as CommandCodeModelEntry[];
116
+ return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
117
+ }
118
+
119
+ /**
120
+ * The provider block, written under whichever root the target document already uses.
121
+ *
122
+ * `ExportContext` carries the parsed target document so the root can be chosen from
123
+ * what is on disk rather than assumed. A fresh file gets the singular root this
124
+ * exporter has always written; a document that already carries a plural root keeps
125
+ * it, so enabling OpenCodex never makes the user's other providers unreadable.
126
+ */
127
+ export function buildCommandCodeContribution(ctx: ExportContext): ManagedContribution {
128
+ const doc = buildCommandCodeClientConfig(ctx);
129
+ const root = commandCodeProviderRoot(ctx.document);
130
+ return singleFragment("commandcode", [root, OPENCODE_PROVIDER_ID], doc.provider[OPENCODE_PROVIDER_ID]);
131
+ }
@@ -185,6 +185,17 @@ export interface ExportContext {
185
185
  */
186
186
  config?: OcxConfig;
187
187
  droidReasoningDefaults?: DroidReasoningDefaults;
188
+ /**
189
+ * The parsed target document, when the caller has one in hand.
190
+ *
191
+ * A client whose reader picks between two roots needs the bytes on disk to
192
+ * choose: Command Code resolves `document.provider ?? document.providers`, so
193
+ * writing the singular root into a document that already carries the plural one
194
+ * leaves the user's providers unreadable rather than merging with them. Absent
195
+ * for `ocx export`, which has no target file — the builder then writes its
196
+ * default root, exactly as before.
197
+ */
198
+ document?: unknown;
188
199
  }
189
200
 
190
201
  /** Namespaced model selector to one of that model's declared reasoning efforts. */
@@ -206,6 +217,7 @@ export type ExportClientId =
206
217
  | "raycast"
207
218
  | "omo"
208
219
  | "cline"
220
+ | "commandcode"
209
221
  | "kilo"
210
222
  | "droid";
211
223
 
@@ -21,7 +21,7 @@
21
21
  */
22
22
  import { homedir } from "node:os";
23
23
  import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
24
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
24
+ import { basename, dirname, isAbsolute, join, resolve, win32 } from "node:path";
25
25
  import { shouldInjectApiAuthHeader, standaloneCodexRoutingTarget } from "../codex/inject";
26
26
  import { FORMAT_MEDIA_TYPE, serializeDocument, type ConfigFormat } from "../integrations/serialize";
27
27
  import { canonicalizeReasoningEfforts } from "../reasoning-effort";
@@ -69,6 +69,18 @@ import { buildKiloClientConfig, summarizeKilo, buildKiloContribution, kiloConfig
69
69
  export { kiloConfigPath, kiloHomeDir, kiloCandidatePath, KILO_CONFIG_CANDIDATES } from "./config-export/kilo";
70
70
  export type { KiloGeneratedConfig, KiloProviderBlock, KiloModelEntry } from "./config-export/kilo";
71
71
  import { droidConfigPath, buildDroidClientConfig, summarizeDroid, buildDroidContribution } from "./config-export/droid";
72
+ import {
73
+ buildCommandCodeClientConfig,
74
+ summarizeCommandCode,
75
+ buildCommandCodeContribution,
76
+ commandCodeProviderRoot,
77
+ type CommandCodeGeneratedConfig,
78
+ type CommandCodeModelEntry,
79
+ type CommandCodeProviderBlock,
80
+ } from "./config-export/commandcode";
81
+
82
+ export type { CommandCodeGeneratedConfig, CommandCodeModelEntry, CommandCodeProviderBlock };
83
+ export { buildCommandCodeClientConfig, summarizeCommandCode, buildCommandCodeContribution, commandCodeProviderRoot };
72
84
 
73
85
 
74
86
 
@@ -474,6 +486,43 @@ export function zcodeConfigPath(env: OpencodeLaunchEnv = process.env, home: stri
474
486
  return join(zcodeHomeDir(env, home), "v2", "config.json");
475
487
  }
476
488
 
489
+ /**
490
+ * The home directory Command Code itself resolves: `env.HOME ?? env.USERPROFILE`
491
+ * (`homeDir15` in the published `command-code@1.66.0` bundle).
492
+ *
493
+ * On POSIX `os.homedir()` already reads `$HOME`, so the two agree and `home` is
494
+ * returned as is. On Windows `os.homedir()` reads `%USERPROFILE%` and ignores
495
+ * `HOME`, which Git for Windows, MSYS2 and Cygwin set, so a user whose `HOME` differs
496
+ * would get a provider block in a store Command Code never opens. Only the OS-default
497
+ * home is replaced: a caller that injects a different root (a test, the writer's
498
+ * `input.home`) keeps it, and a blank or relative value falls back to `home`.
499
+ */
500
+ export function commandCodeUserHome(env: OpencodeLaunchEnv = process.env, home: string = homedir(), platform: NodeJS.Platform = process.platform): string {
501
+ if (platform !== "win32" || home !== homedir()) return home;
502
+ const value = env.HOME ?? env.USERPROFILE;
503
+ return typeof value === "string" && value.trim() && win32.isAbsolute(value) ? value : home;
504
+ }
505
+
506
+ /**
507
+ * Command Code's provider store directory — `.commandcode` under the home the client
508
+ * resolves (see {@link commandCodeUserHome}), with no override.
509
+ *
510
+ * There is deliberately no `COMMANDCODE_HOME` here, unlike the other clients in this
511
+ * file: `grep -c COMMANDCODE_HOME` over the shipped `dist/cli.mjs` and
512
+ * `dist/index.mjs` returns 0. An override we honour but the client ignores is worse
513
+ * than none: `enable` would report success and write a provider block at a path no
514
+ * Command Code process ever opens, and the user would see an empty model list with no
515
+ * error anywhere. An apply against a relocated home is instead refused by `detectDir`
516
+ * not existing.
517
+ */
518
+ export function commandCodeHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir(), platform: NodeJS.Platform = process.platform): string {
519
+ return join(commandCodeUserHome(env, home, platform), ".commandcode");
520
+ }
521
+
522
+ export function commandCodeConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir(), platform: NodeJS.Platform = process.platform): string {
523
+ return join(commandCodeHomeDir(env, home, platform), "providers.json");
524
+ }
525
+
477
526
  /**
478
527
  * The provider store a current ZCode reads, which is NOT the file above.
479
528
  *
@@ -1487,6 +1536,20 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
1487
1536
  // remote binds refuse — same reasoning as MCode.
1488
1537
  loopbackOnly: true,
1489
1538
  },
1539
+ commandcode: {
1540
+ id: "commandcode",
1541
+ filename: "providers.json",
1542
+ destination: env => commandCodeConfigPath(env),
1543
+ // No env var exists behind this integration: buildCommandCodeClientConfig
1544
+ // writes `apiKey: false`, the documented keyless form for a loopback endpoint.
1545
+ apiKeyEnv: "",
1546
+ exportHint: "The provider is written with `apiKey: false` — Command Code treats a keyless loopback endpoint as already authenticated. No key is stored or referenced.",
1547
+ build: buildCommandCodeClientConfig,
1548
+ format: "json",
1549
+ summarize: summarizeCommandCode,
1550
+ buildContribution: buildCommandCodeContribution,
1551
+ loopbackOnly: true,
1552
+ },
1490
1553
  prime: {
1491
1554
  id: "prime",
1492
1555
  filename: "prime-models.json",
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Detection of Codex-based omo (LazyCodex), the variant that reads Codex role files and
3
- * `codex.agents.<role>.model` in `~/.omo/omo.jsonc`.
3
+ * `"[codex]".agents.<role>.model` in `~/.omo/omo.jsonc`.
4
4
  *
5
5
  * "omo" names three products. Pi-based omo (senpi) owns `~/.omo/agent` and is the omo file
6
6
  * integration in `src/integrations/registry.ts`; OpenCode-based omo (oh-my-opencode) keeps its
@@ -1,5 +1,7 @@
1
1
  /**
2
- * omo (Codex / LazyCodex)'s per-role model setting, `codex.agents.<role>.model` in `~/.omo/omo.jsonc`.
2
+ * omo (Codex / LazyCodex)'s per-role model setting, `"[codex]".agents.<role>.model` in `~/.omo/omo.jsonc`.
3
+ * The section name carries its brackets: LazyCodex reads `[codex]`, and its strict loader drops a
4
+ * bare `codex` key as unknown, so a pick written there never reaches a role.
3
5
  *
4
6
  * LazyCodex 5.1.1 and later reads it, and callers reach this only after `detectLazyCodex`
5
7
  * says LazyCodex is installed. The file is omo's, so this writes only on an explicit
@@ -30,6 +32,17 @@ export function omoJsoncPath(env: NodeJS.ProcessEnv = process.env, home: string
30
32
 
31
33
  type JsonObject = Record<string, unknown>;
32
34
 
35
+ const CODEX_SECTION = "[codex]";
36
+
37
+ /** The reasoning levels LazyCodex accepts for a role; it names Codex's `none` `off`. */
38
+ const LAZYCODEX_REASONING = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
39
+
40
+ /** The `reasoning` value LazyCodex reads for a Codex effort, or null when it has no such level. */
41
+ export function omoReasoningFor(effort: string): string | null {
42
+ const level = effort === "none" ? "off" : effort;
43
+ return LAZYCODEX_REASONING.has(level) ? level : null;
44
+ }
45
+
33
46
  function isObject(value: unknown): value is JsonObject {
34
47
  return typeof value === "object" && value !== null && !Array.isArray(value);
35
48
  }
@@ -71,7 +84,7 @@ function load(path: string): Loaded {
71
84
  }
72
85
 
73
86
  function agentsOf(doc: JsonObject): JsonObject | null | undefined {
74
- const codex = doc.codex;
87
+ const codex = doc[CODEX_SECTION];
75
88
  if (codex === undefined) return undefined;
76
89
  if (!isObject(codex)) return null;
77
90
  const agents = codex.agents;
@@ -104,21 +117,33 @@ function serialize(text: string, doc: JsonObject): string {
104
117
  return text.startsWith("\ufeff") ? `\ufeff${out}` : out;
105
118
  }
106
119
 
107
- export function writeOmoRoleModel(role: string, model: string, path: string = omoJsoncPath()): OmoRoleModelWriteStatus {
120
+ /**
121
+ * `reasoning` left undefined keeps the entry's level; null removes it, so a level LazyCodex cannot
122
+ * express does not leave a stale one behind for its next install to apply.
123
+ */
124
+ export function writeOmoRoleModel(
125
+ role: string,
126
+ model: string,
127
+ path: string = omoJsoncPath(),
128
+ reasoning?: string | null,
129
+ ): OmoRoleModelWriteStatus {
108
130
  const loaded = load(path);
109
131
  if (loaded.kind !== "document") return loaded.kind === "comments" ? "skipped_comments" : loaded.kind;
110
132
  const { doc, text } = loaded;
111
133
  // Only an absent key is created; an explicit null is a value the reader calls invalid.
112
- const codex = doc.codex === undefined ? {} : doc.codex;
134
+ const codex = doc[CODEX_SECTION] === undefined ? {} : doc[CODEX_SECTION];
113
135
  if (!isObject(codex)) return "invalid";
114
136
  const agents = codex.agents === undefined ? {} : codex.agents;
115
137
  if (!isObject(agents)) return "invalid";
116
138
  const entry = agents[role] === undefined ? {} : agents[role];
117
139
  if (!isObject(entry)) return "invalid";
118
- if (entry.model === model) return "unchanged";
119
- agents[role] = { ...entry, model };
140
+ const next: JsonObject = { ...entry, model };
141
+ if (reasoning === null) delete next.reasoning;
142
+ else if (reasoning !== undefined) next.reasoning = reasoning;
143
+ if (entry.model === model && entry.reasoning === next.reasoning) return "unchanged";
144
+ agents[role] = next;
120
145
  codex.agents = agents;
121
- doc.codex = codex;
146
+ doc[CODEX_SECTION] = codex;
122
147
  assertIntegrationWriteOwnership(path);
123
148
  atomicWriteFileNoFollowUnclaimed(path, serialize(text, doc));
124
149
  return "written";
@@ -252,6 +252,13 @@ export function reconcileFreshPoolAccountPlans(runtimeConfig: OcxConfig, updates
252
252
  }
253
253
  }
254
254
 
255
+ export interface CodexAuthAccountsListOptions {
256
+ /** Finish deferred registration; only a dashboard session carries that inference consent. */
257
+ validatePending?: boolean;
258
+ /** An operator's explicit refresh command, which may retry a grant already recorded as dead. */
259
+ explicitRefresh?: boolean;
260
+ }
261
+
255
262
  export interface CodexAuthAccountsSnapshot {
256
263
  accounts: CodexAuthAccountDto[];
257
264
  mainIdentityGeneration: number;
@@ -260,7 +267,7 @@ export interface CodexAuthAccountsSnapshot {
260
267
  export async function listCodexAuthAccountsSnapshot(
261
268
  config: OcxConfig,
262
269
  forceRefresh = false,
263
- options: { validatePending?: boolean } = {},
270
+ options: CodexAuthAccountsListOptions = {},
264
271
  ): Promise<CodexAuthAccountsSnapshot> {
265
272
  const runtimeConfig = getRuntimeConfig(config);
266
273
  const poolAccounts = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
@@ -281,7 +288,8 @@ export async function listCodexAuthAccountsSnapshot(
281
288
  // identity when the switch is on and nothing has been observed yet.
282
289
  const creditsIdentity = runtimeConfig.showCodexCredits === true ? poolQuotaHistoryIdentity(account.id) ?? null : null;
283
290
  const creditsUnobserved = creditsIdentity !== null && !hasCodexCreditsObservation(account.id, creditsIdentity);
284
- quotaResult = await fetchPoolAccountQuota(account.id, forceRefresh || creditsUnobserved, account.plan, getValidCodexToken, options.validatePending === true);
291
+ quotaResult = await fetchPoolAccountQuota(account.id, forceRefresh || creditsUnobserved, account.plan, getValidCodexToken,
292
+ options.validatePending === true, undefined, undefined, options.explicitRefresh === true);
285
293
  } catch (error) {
286
294
  if (!(error instanceof PoolQuotaProbeBusyError)) throw error;
287
295
  quotaResult = {
@@ -447,7 +455,7 @@ export async function refreshCodexQuotaForActivation(config: OcxConfig, accountI
447
455
  export async function listCodexAuthAccounts(
448
456
  config: OcxConfig,
449
457
  forceRefresh = false,
450
- options: { validatePending?: boolean } = {},
458
+ options: CodexAuthAccountsListOptions = {},
451
459
  ): Promise<CodexAuthAccountDto[]> {
452
460
  return (await listCodexAuthAccountsSnapshot(config, forceRefresh, options)).accounts;
453
461
  }
@@ -0,0 +1,107 @@
1
+ import type { OcxConfig } from "../../types";
2
+ import { getConfigPath, initializePersistedConfigIfMissing, mutatePersistedConfig } from "../../config";
3
+ import { mergeConfigDefaults, readConfigFileSnapshot, validateConfigCandidate } from "../../config/diagnostics";
4
+ import { adoptPersistedCodexAccountSelection, claudeCodeBaselineArmed } from "../../config/live-reconcile";
5
+ import { deleteConfigTopLevelKey, fileBackedConfigPath, recordFileBackedConfig } from "../../config/rebase-provenance";
6
+ import { MAIN_CODEX_ACCOUNT_ID, hasLegacyMainCodexPoolAccount, isSelectableCodexPoolAccount, isValidCodexAccountId } from "../account-id";
7
+ import { isCodexAccountPaused } from "../account-pause";
8
+ import { readCodexAccountRecord } from "../account-store";
9
+ import { codexAccountPinDrainReason, resetCodexRoutingForManualSelection } from "../routing";
10
+ import { jsonResponse } from "./http";
11
+
12
+ class SelectionRejected extends Error {
13
+ constructor(message: string, readonly status: number) { super(message); }
14
+ }
15
+
16
+ function validateSelection(config: OcxConfig, accountId: string | null): void {
17
+ if (accountId === null) return;
18
+ if (accountId === MAIN_CODEX_ACCOUNT_ID && hasLegacyMainCodexPoolAccount(config.codexAccounts)) {
19
+ throw new SelectionRejected("Remove the legacy __main__ pool row before selecting the Desktop account", 409);
20
+ }
21
+ if (isCodexAccountPaused(config, accountId)) throw new SelectionRejected("Account is paused", 409);
22
+ if (accountId === MAIN_CODEX_ACCOUNT_ID) return;
23
+ if (!isValidCodexAccountId(accountId)) throw new SelectionRejected("Invalid account id format", 400);
24
+ if (!(config.codexAccounts ?? []).some(account => isSelectableCodexPoolAccount(account) && account.id === accountId)) {
25
+ throw new SelectionRejected("Account not found", 400);
26
+ }
27
+ if (readCodexAccountRecord(accountId)?.codexValidationPending) {
28
+ throw new SelectionRejected("Account validation is pending. Refresh quota after recovery to validate it.", 409);
29
+ }
30
+ }
31
+
32
+ function applySelection(config: OcxConfig, accountId: string | null): boolean {
33
+ const changed = config.activeCodexAccountId !== (accountId ?? undefined)
34
+ || config.activeCodexAccountPinned !== (accountId ?? undefined);
35
+ if (accountId === null) {
36
+ deleteConfigTopLevelKey(config, "activeCodexAccountId");
37
+ deleteConfigTopLevelKey(config, "activeCodexAccountPinned");
38
+ } else {
39
+ config.activeCodexAccountId = accountId;
40
+ config.activeCodexAccountPinned = accountId;
41
+ }
42
+ return changed;
43
+ }
44
+
45
+ /** A manual selection is an explicit command, even when equal to the live merge baseline. */
46
+ export function persistCodexAccountSelection(config: OcxConfig, accountId: string | null): Response {
47
+ const unknown = () => jsonResponse({
48
+ error: "Account selection could not be confirmed; reload settings before retrying",
49
+ code: "account_selection_unavailable",
50
+ }, 409);
51
+ const configPath = getConfigPath();
52
+ const sourcePath = fileBackedConfigPath(config);
53
+ if (sourcePath !== undefined && sourcePath !== configPath) return unknown();
54
+ let committed: OcxConfig;
55
+ let warning = false;
56
+ try {
57
+ // Do not activate a newly disk-only account that the running proxy has not adopted.
58
+ validateSelection(config, accountId);
59
+ const before = readConfigFileSnapshot();
60
+ if (before.diagnostics.source === "default" && !fileBackedConfigPath(config) && !claudeCodeBaselineArmed(config)) {
61
+ // Preserve transient/initial callers without recreating a deleted live configuration.
62
+ // The create-only owner cannot replace a competing writer's newly published config.
63
+ const normalized = validateConfigCandidate(mergeConfigDefaults(config));
64
+ if (!normalized.ok) return unknown();
65
+ const initial = normalized.config;
66
+ applySelection(initial, accountId);
67
+ initializePersistedConfigIfMissing(initial);
68
+ }
69
+ const result = mutatePersistedConfig(persisted => {
70
+ // This callback is re-run under the mutation lock immediately before publication.
71
+ // A removed or paused account must not be resurrected by a stale UI snapshot.
72
+ validateSelection(config, accountId);
73
+ validateSelection(persisted, accountId);
74
+ return { changed: applySelection(persisted, accountId), value: persisted };
75
+ });
76
+ if (result.status === "unavailable") return unknown();
77
+ committed = result.value;
78
+ } catch (error) {
79
+ if (error instanceof SelectionRejected) return jsonResponse({ error: error.message }, error.status);
80
+ // Atomic publication may succeed before generation/bookkeeping fails. Confirm the exact
81
+ // durable selection instead of rolling live state back behind an already published write.
82
+ const snapshot = readConfigFileSnapshot();
83
+ if (snapshot.raw === undefined) return unknown();
84
+ try {
85
+ const strict = validateConfigCandidate(JSON.parse(snapshot.raw.replace(/^\uFEFF/, "")));
86
+ if (!strict.ok) return unknown();
87
+ validateSelection(config, accountId);
88
+ validateSelection(strict.config, accountId);
89
+ if (strict.config.activeCodexAccountId !== (accountId ?? undefined)
90
+ || strict.config.activeCodexAccountPinned !== (accountId ?? undefined)) return unknown();
91
+ committed = strict.config;
92
+ warning = true;
93
+ } catch { return unknown(); }
94
+ }
95
+ adoptPersistedCodexAccountSelection(config, committed);
96
+ recordFileBackedConfig(config, configPath);
97
+ // No failed save may clear affinity, spend the manual preference, or move the RR cursor.
98
+ resetCodexRoutingForManualSelection(accountId ?? MAIN_CODEX_ACCOUNT_ID);
99
+ const pinDrainReason = accountId === null ? undefined : codexAccountPinDrainReason(config, accountId);
100
+ return jsonResponse({
101
+ ok: true,
102
+ activeCodexAccountId: config.activeCodexAccountId ?? null,
103
+ appliesImmediately: true,
104
+ ...(pinDrainReason !== undefined ? { pinDrained: true, pinDrainReason } : {}),
105
+ ...(warning ? { warning: "config_bookkeeping_failed" } : {}),
106
+ });
107
+ }
@@ -146,6 +146,11 @@ export async function fetchMainAccountInfo(forceRefresh = false, config?: OcxCon
146
146
  return (await fetchMainAccountInfoSnapshot(forceRefresh, config)).info;
147
147
  }
148
148
 
149
+ export interface MainAccountInfoProbeOptions {
150
+ /** Metadata-only renewal never sets or clears the traffic reauth quarantine. */
151
+ passive?: boolean;
152
+ }
153
+
149
154
  export const EMPTY_MAIN_ACCOUNT_INFO: MainAccountInfo = { email: null, plan: null, quota: null };
150
155
 
151
156
  export async function retryMainAccountInfoIfIdentityChanged(
@@ -154,12 +159,13 @@ export async function retryMainAccountInfoIfIdentityChanged(
154
159
  nativeMainLease: AdmissionLease,
155
160
  explicitRefresh: boolean,
156
161
  paced: boolean,
162
+ options: MainAccountInfoProbeOptions = {},
157
163
  ): Promise<MainAccountInfoFetchResult | null> {
158
164
  const currentAccountId = getMainChatgptAccountId();
159
165
  if (currentAccountId === null || currentAccountId === requestAccountId) return null;
160
166
  reconcileMainCodexAccountRuntimeState();
161
167
  return retriesRemaining > 0
162
- ? fetchMainAccountInfoWhileOwned(true, retriesRemaining - 1, nativeMainLease, explicitRefresh, false, paced)
168
+ ? fetchMainAccountInfoWhileOwned(true, retriesRemaining - 1, nativeMainLease, explicitRefresh, false, paced, options)
163
169
  : { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
164
170
  }
165
171
 
@@ -171,6 +177,7 @@ export async function fetchMainAccountInfoAttempt(
171
177
  explicitRefresh: boolean = forceRefresh,
172
178
  postReset = false,
173
179
  config?: OcxConfig,
180
+ options: MainAccountInfoProbeOptions = {},
174
181
  ): Promise<MainAccountInfoFetchResult> {
175
182
  const nativeMainLease = existingNativeMainLease ?? tryAcquireNativeMainProfileClaim();
176
183
  if (!nativeMainLease) {
@@ -184,7 +191,7 @@ export async function fetchMainAccountInfoAttempt(
184
191
  try {
185
192
  const paced = sharedMainQuotaPacingEnabled(config ?? loadConfig());
186
193
  const operation = async () => ({
187
- ...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease, explicitRefresh, postReset, paced),
194
+ ...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease, explicitRefresh, postReset, paced, options),
188
195
  identityGeneration: captureMainAccountIdentityGeneration(),
189
196
  });
190
197
  if (nativeMainSharedClaimHeld) return await operation();
@@ -225,6 +232,7 @@ export async function fetchMainAccountInfoWhileOwned(
225
232
  /** Reset-credit consume needs a fresh post-spend observation, not an earlier read. */
226
233
  postReset = false,
227
234
  paced = true,
235
+ options: MainAccountInfoProbeOptions = {},
228
236
  ): Promise<MainAccountInfoFetchResult> {
229
237
  const writerGeneration = captureConfigGeneration();
230
238
  reconcileMainCodexAccountRuntimeState();
@@ -287,7 +295,16 @@ export async function fetchMainAccountInfoWhileOwned(
287
295
  const current = credentialIsCurrent() && writerGeneration === captureConfigGeneration();
288
296
  if (!current) return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
289
297
  credentialChecked: true, hasCredential: true };
290
- if (explicitRefresh && (admission.result.quotaRefresh?.status === "ok"
298
+ // Single-flight shares evidence, but each caller retains its own auth behavior.
299
+ if (!options.passive && admission.result.terminalAuthFailure) {
300
+ const diagnosticStillLive = admission.result.quotaRefreshGeneration !== undefined
301
+ && isMainAccountIdentityGenerationLive(admission.result.quotaRefreshGeneration);
302
+ clearMainAccountInfoCache();
303
+ markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
304
+ return { ...admission.result, ...(diagnosticStillLive
305
+ ? { quotaRefreshGeneration: captureMainAccountIdentityGeneration() } : {}) };
306
+ }
307
+ if (!options.passive && explicitRefresh && (admission.result.quotaRefresh?.status === "ok"
291
308
  || admission.result.quotaRefresh?.status === "not_reported")) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
292
309
  return admission.result;
293
310
  }
@@ -297,13 +314,13 @@ export async function fetchMainAccountInfoWhileOwned(
297
314
  if (!resp.ok) {
298
315
  const authFailure = await classifyMainAuthResponse(resp, isMainAccountTokenVerifiablyLive());
299
316
  const terminalAuthFailure = authFailure.terminal;
300
- const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
317
+ const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced, options);
301
318
  if (retried) return retried;
302
319
  if (!isQuotaDispatchCurrent(dispatchSequence) || !credentialIsCurrent()) {
303
320
  return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
304
321
  credentialChecked: true, hasCredential: true };
305
322
  }
306
- if (terminalAuthFailure) {
323
+ if (terminalAuthFailure && !options.passive) {
307
324
  // Account for this attempt's own synchronous invalidation, never prior external drift.
308
325
  const diagnosticStillLive = isMainAccountIdentityGenerationLive(quotaRefreshGeneration);
309
326
  clearMainAccountInfoCache();
@@ -324,7 +341,7 @@ export async function fetchMainAccountInfoWhileOwned(
324
341
  quotaPhase = "body";
325
342
  const data = (await resp.json()) as WhamUsageResponse;
326
343
  quotaPhase = "publish";
327
- const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
344
+ const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced, options);
328
345
  if (retried) return retried;
329
346
  quotaPhase = "decode";
330
347
  if (data === null || typeof data !== "object" || Array.isArray(data)) {
@@ -376,7 +393,7 @@ export async function fetchMainAccountInfoWhileOwned(
376
393
  // never settled and the dashboard kept showing nothing — the symptom #327 reported.
377
394
  // An explicit refresh is an operator asking to re-evaluate, normally right after
378
395
  // signing in again, so it stays authoritative.
379
- if (explicitRefresh) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
396
+ if (explicitRefresh && !options.passive) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
380
397
  // Mirror main quota + plan into the shared stores so the rotation engine can
381
398
  // score and auto-switch the main account exactly like a pool account (Option A).
382
399
  setMainAccountPlan(result.plan);
@@ -398,7 +415,7 @@ export async function fetchMainAccountInfoWhileOwned(
398
415
  ...(freshResetCredits !== undefined ? { freshResetCredits } : {}),
399
416
  };
400
417
  } catch (error) {
401
- const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
418
+ const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced, options);
402
419
  if (retried) return retried;
403
420
  if (!credentialIsCurrent()) {
404
421
  return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,