@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
@@ -1,9 +1,11 @@
1
- import { codexAccountUsesCreditsAfterLimit } from "../account-credit-use";
1
+ import { codexAccountUsesCreditsAfterLimit, codexUsageLimitResetAt } from "../account-credit-use";
2
2
  import { nextCodexUsageQueryAt, nextQuotaQueryDelay, pruneRemovedCodexPoolUsageAccounts } from "../quota-query-backoff";
3
3
  import { CODEX_PRIORITY_FAILBACK_REFRESH_MS } from "../account-priority";
4
4
  import { codexQuotaHasFreshUsage } from "../quota-observation-freshness";
5
5
  import { getCodexAccountCredential, getValidCodexToken, readCodexAccountRecord } from "../account-store";
6
- import { getAccountQuota, isCompleteCodexQuotaRecoverySnapshot } from "../quota";
6
+ import { getAccountQuota, getMainPolicyQuota, isCompleteCodexQuotaRecoverySnapshot } from "../quota";
7
+ import { isCodexAccountPaused } from "../account-pause";
8
+ import { hasSpendableCodexCredits } from "../quota-types";
7
9
  import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
8
10
  import { claimDueCodexQuotaRecoveryProbes, settleCodexQuotaRecoveryProbe } from "../routing";
9
11
  import { readCodexTokens } from "../auth-collision";
@@ -73,17 +75,33 @@ export async function runCodexCooldownRecoveryProbes(config: OcxConfig, now = Da
73
75
  return cooldownRecoveryInFlight;
74
76
  }
75
77
 
78
+ const MAIN_CREDIT_RENEWAL_AGE_MS = 3 * 60_000;
79
+
76
80
  let mainHardLockRecoveryInFlight: Promise<void> | null = null;
77
81
  let mainHardLockRecoveryAttempt: { identity: number; credential: number; after: number; delay: number } | undefined;
78
82
 
79
83
  /** Metadata-only recovery on the existing sweep; failures retain the observed policy block. */
80
84
  export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise<void> {
81
85
  if (mainHardLockRecoveryInFlight) return mainHardLockRecoveryInFlight;
86
+ /** Schedule a real credit observation while leaving actual-time admission unchanged. */
87
+ const creditRecoveryNeeded = (): boolean => {
88
+ const now = Date.now();
89
+ const quota = getMainPolicyQuota();
90
+ const observedAt = quota?.credits?.observedAt;
91
+ // getMainPolicyQuota binds evidence to the observed physical identity. Only renew
92
+ // valid formerly spendable funds; admission retains the actual five-minute clock.
93
+ return codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID)
94
+ && !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
95
+ && codexUsageLimitResetAt(quota, undefined, now) !== undefined
96
+ && observedAt !== undefined && observedAt > 0
97
+ && now - observedAt >= MAIN_CREDIT_RENEWAL_AGE_MS
98
+ && hasSpendableCodexCredits(quota, observedAt);
99
+ };
82
100
  const status = getMainAccountHardLockStatus(config);
83
- if (status.state !== "blocked") { mainHardLockRecoveryAttempt = undefined; return; }
101
+ if (status.state !== "blocked" && !creditRecoveryNeeded()) { mainHardLockRecoveryAttempt = undefined; return; }
84
102
  if (isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
85
103
  // A predicted reset is a scheduling hint, never proof that the hard lock can be lifted.
86
- if (status.resetAt !== undefined && status.resetAt > Date.now()) return;
104
+ if (status.state === "blocked" && status.resetAt !== undefined && status.resetAt > Date.now()) return;
87
105
  // Reconcile the owned physical credential before skipping work for an older one.
88
106
  reconcileMainCodexAccountRuntimeState();
89
107
  const physical = readCodexTokens();
@@ -98,9 +116,10 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
98
116
  if (!lease) return;
99
117
  mainHardLockRecoveryInFlight = (async () => {
100
118
  reconcileMainCodexAccountRuntimeState();
101
- if (getMainAccountHardLockStatus(config).state !== "blocked"
119
+ if ((getMainAccountHardLockStatus(config).state !== "blocked" && !creditRecoveryNeeded())
102
120
  || isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
103
121
  const identityGeneration = captureMainAccountIdentityGeneration();
122
+ const passive = getMainAccountHardLockStatus(config).state !== "blocked";
104
123
  try {
105
124
  // Refresh can require an exclusive credential claim: never hold WHAM's shared
106
125
  // claim while obtaining a valid token. The runtime lease spans both operations.
@@ -111,7 +130,12 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
111
130
  // Native refresh retains its own grant-scoped refusal; global quarantine would outlive it.
112
131
  return;
113
132
  }
114
- if (isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
133
+ const currentStatus = getMainAccountHardLockStatus(config);
134
+ if (isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)
135
+ || !isMainAccountIdentityGenerationLive(identityGeneration)
136
+ || (passive && !creditRecoveryNeeded())
137
+ || (currentStatus.state === "blocked" && currentStatus.resetAt !== undefined
138
+ && currentStatus.resetAt > Date.now())) return;
115
139
  const credential = getMainQuotaCredentialGeneration();
116
140
  // Reconcile the physical bearer before consulting the previous credential's delay.
117
141
  if (previous?.identity === identityGeneration && previous.credential === credential
@@ -123,7 +147,7 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
123
147
  after: currentQueryAfter };
124
148
  return;
125
149
  }
126
- const result = await fetchMainAccountInfoAttempt(true, 1, lease, false, false, false, config);
150
+ const result = await fetchMainAccountInfoAttempt(true, 1, lease, false, false, false, config, { passive });
127
151
  // Never charge a replacement credential for a late result from its predecessor.
128
152
  if (isMainAccountIdentityGenerationLive(identityGeneration)
129
153
  && credential === getMainQuotaCredentialGeneration()) {
@@ -134,11 +158,11 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
134
158
  const authStatus = result.quotaRefresh.status === "http_error"
135
159
  ? result.quotaRefresh.httpStatus : undefined;
136
160
  const transientAuth = (authStatus === 401 || authStatus === 403)
137
- && !isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
161
+ && !result.terminalAuthFailure && !isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
138
162
  if (transientAuth) mainHardLockRecoveryAttempt = undefined;
139
163
  else {
140
164
  const delay = nextQuotaQueryDelay(previousDelay || undefined);
141
- mainHardLockRecoveryAttempt = getMainAccountHardLockStatus(config).state === "blocked"
165
+ mainHardLockRecoveryAttempt = getMainAccountHardLockStatus(config).state === "blocked" || creditRecoveryNeeded()
142
166
  ? { identity: identityGeneration, credential, delay, after: Math.max(Date.now() + delay, queryAfter) }
143
167
  : undefined;
144
168
  }
@@ -495,6 +495,7 @@ export async function fetchPoolAccountQuota(
495
495
  validatePending = false,
496
496
  afterDispatchSequence?: number,
497
497
  recoveryProbeNow?: number,
498
+ explicitRefresh = false,
498
499
  ): Promise<PoolQuotaResult> {
499
500
  const existing = getAccountQuota(accountId);
500
501
  if (afterDispatchSequence === undefined && !forceRefresh && existing && Date.now() - existing.updatedAt < POOL_CACHE_TTL) {
@@ -504,10 +505,23 @@ export async function fetchPoolAccountQuota(
504
505
  credentialGeneration: readCodexAccountRecord(accountId)?.generation,
505
506
  };
506
507
  }
508
+ const record = readCodexAccountRecord(accountId);
509
+ // The token endpoint already called this grant revoked or expired. Only a re-login or a
510
+ // completed validation rewrites the record and drops the marker, so until then a passive read
511
+ // (dashboard poll, listing, priming, recovery probe) would only POST the dead refresh token again
512
+ // and log the same 401. Report the stored verdict the failed refresh would have produced. An
513
+ // operator's explicit refresh command from any principal, a dashboard validation, and a post-reset
514
+ // readback keep their probe, and a source-linked credential spends no grant of its own, so none
515
+ // of them is held here.
516
+ if (!explicitRefresh && !validatePending && afterDispatchSequence === undefined && record && record.deletedAt == null
517
+ && record.credential && !record.credential.sourceAuthPath
518
+ && record.lastCodexValidationTerminal === true && record.lastCodexValidationStatus === "failed") {
519
+ markAccountNeedsReauth(accountId, captureConfigGeneration(), record.generation);
520
+ return { quota: existing ?? null, needsReauth: true, reauthReason: "refresh_failed", credentialGeneration: record.generation };
521
+ }
507
522
  // A token refresh may increment the generation (and rotate the refresh token) before WHAM
508
523
  // completes. Join a flight whose starting or resolved generation is still current, but let a
509
524
  // replacement credential with the same pool id start its own request.
510
- const record = readCodexAccountRecord(accountId);
511
525
  const flights = poolQuotaRefreshInFlight.get(accountId);
512
526
  const current = flights && [...flights].find(flight => {
513
527
  const generation = flight.state.resolvedCredentialGeneration
@@ -1,3 +1,4 @@
1
+ import { persistCodexAccountSelection } from "./account-selection";
1
2
  import { CODEX_ACCOUNT_LOG_LABEL_RE, codexAccountLogLabel } from "../account-label";
2
3
  import { poolQuotaHistoryIdentity, readCodexAccountRecord } from "../account-store";
3
4
  import { estimateCodexQuotaCapacity, insufficientCodexCapacity } from "../quota-capacity";
@@ -7,13 +8,13 @@ import { getAccountQuotaHistory, listAccountQuotas } from "../quota";
7
8
  import { deleteCodexAccount } from "../account-lifecycle";
8
9
  import { isCodexAccountPaused, setCodexAccountPaused } from "../account-pause";
9
10
  import { setAllCodexAccountsCreditsAfterLimit, setCodexAccountCreditsAfterLimit } from "../account-credit-use";
10
- import { clearCodexAccountPin, isCodexAccountPriorityKey, pinnedCodexAccountId, setCodexAccountPin, setCodexAccountPriority } from "../account-priority";
11
- import { codexAccountPinDrainReason, codexQuotaScopeForModel, clearCodexAccountCooldown, clearThreadAccountMapForAccount, getEffectiveActiveCodexAccountId, isEffectiveCodexAccountPinned, resetCodexRoutingForManualSelection } from "../routing";
11
+ import { clearCodexAccountPin, isCodexAccountPriorityKey, pinnedCodexAccountId, setCodexAccountPriority } from "../account-priority";
12
+ import { codexQuotaScopeForModel, clearCodexAccountCooldown, clearThreadAccountMapForAccount, getEffectiveActiveCodexAccountId, isEffectiveCodexAccountPinned } from "../routing";
12
13
  import { DEFAULT_ACCOUNT_PRIORITY, MAX_ACCOUNT_PRIORITY, MIN_ACCOUNT_PRIORITY, normalizeAccountPoolStickyLimit, normalizeCodexAccountPoolStrategy, parseAccountPoolStickyLimit, parseCodexAccountPoolStrategy, parseAccountPriority } from "../pool-rotation";
13
14
  import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
14
15
  import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
15
16
  import type { OcxConfig } from "../../types";
16
- import { CODEX_ACCOUNT_ID_RE, hasLegacyMainCodexPoolAccount, isSelectableCodexPoolAccount, isValidCodexAccountId } from "../account-id";
17
+ import { CODEX_ACCOUNT_ID_RE, isSelectableCodexPoolAccount, isValidCodexAccountId } from "../account-id";
17
18
  import { isCodexResetCreditOperationId } from "../reset-credit-recovery";
18
19
  import { listCodexAuthAccounts, selectFallbackAfterPause, pauseExhaustedCodexAccounts } from "./account-list";
19
20
  import { jsonResponse, manualImportDisabledResponse } from "./http";
@@ -43,6 +44,7 @@ export async function handleCodexAuthAPI(
43
44
  // required by AGENTS_INSTALL.md. Raw-admin/CLI refreshes remain observational.
44
45
  return jsonResponse({ accounts: await listCodexAuthAccounts(config, true, {
45
46
  validatePending: principal === "gui-session",
47
+ explicitRefresh: true,
46
48
  }) });
47
49
  }
48
50
 
@@ -246,53 +248,13 @@ export async function handleCodexAuthAPI(
246
248
  }
247
249
 
248
250
  if (url.pathname === "/api/codex-auth/active" && req.method === "PUT") {
249
- let body: { accountId: string | null };
250
- try { body = (await req.json()) as typeof body; } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
251
- const runtimeConfig = getRuntimeConfig(config);
252
- const targetAccountId = body.accountId ?? MAIN_CODEX_ACCOUNT_ID;
253
- if (body.accountId === MAIN_CODEX_ACCOUNT_ID && hasLegacyMainCodexPoolAccount(runtimeConfig.codexAccounts)) {
254
- return jsonResponse({ error: "Remove the legacy __main__ pool row before selecting the Desktop account" }, 409);
255
- }
256
- if (body.accountId != null && isCodexAccountPaused(runtimeConfig, targetAccountId)) {
257
- return jsonResponse({ error: "Account is paused" }, 409);
258
- }
259
- if (body.accountId != null && body.accountId !== MAIN_CODEX_ACCOUNT_ID) {
260
- if (!isValidCodexAccountId(body.accountId)) return jsonResponse({ error: "Invalid account id format" }, 400);
261
- const exists = (runtimeConfig.codexAccounts ?? [])
262
- .some(account => isSelectableCodexPoolAccount(account) && account.id === body.accountId);
263
- if (!exists) return jsonResponse({ error: "Account not found" }, 400);
264
- if (readCodexAccountRecord(body.accountId)?.codexValidationPending) {
265
- return jsonResponse({ error: "Account validation is pending. Refresh quota after recovery to validate it." }, 409);
266
- }
251
+ let body: unknown;
252
+ try { body = await req.json(); } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
253
+ if (!body || typeof body !== "object" || Array.isArray(body)
254
+ || !("accountId" in body) || (body.accountId !== null && typeof body.accountId !== "string")) {
255
+ return jsonResponse({ error: "accountId must be an account id or null" }, 400);
267
256
  }
268
- runtimeConfig.activeCodexAccountId = body.accountId ?? undefined;
269
- // "Use this account now" outranks selection order until the account is spent:
270
- // persisted here rather than in resetCodexRoutingForManualSelection, which is
271
- // runtime state only. A null id clears the selection instead of making one, so it
272
- // must release the pin rather than record one: pinning the `targetAccountId`
273
- // fallback would leave a pin that no effective active account matches, which
274
- // `isEffectiveCodexAccountPinned` reports as unpinned while the tier filter still
275
- // honours it as a ceiling — invisibly capping the pool at the main account's tier.
276
- if (body.accountId == null) clearCodexAccountPin(runtimeConfig);
277
- else setCodexAccountPin(runtimeConfig, targetAccountId);
278
- resetCodexRoutingForManualSelection(targetAccountId);
279
- saveRuntimeConfig(config, runtimeConfig);
280
- // A pin this route accepts can still be dropped by the very next resolve, and saying
281
- // nothing about that is what made the setting look ignored (#4521). The checks above
282
- // refuse an account that cannot be selected at all; this reports the one remaining
283
- // outcome they do not cover, from the same predicate routing releases on, so the two
284
- // cannot drift. Absent means the pin survives — additive for existing clients.
285
- // `appliesImmediately` is unchanged: it answers whether thread affinity was cleared,
286
- // not whether the pin is durable.
287
- const pinDrainReason = body.accountId == null
288
- ? undefined
289
- : codexAccountPinDrainReason(runtimeConfig, targetAccountId);
290
- return jsonResponse({
291
- ok: true,
292
- activeCodexAccountId: body.accountId,
293
- appliesImmediately: true,
294
- ...(pinDrainReason !== undefined ? { pinDrained: true, pinDrainReason } : {}),
295
- });
257
+ return persistCodexAccountSelection(getRuntimeConfig(config), body.accountId);
296
258
  }
297
259
 
298
260
  if (url.pathname === "/api/codex-auth/active" && req.method === "GET") {
@@ -81,11 +81,13 @@ import {
81
81
  import {
82
82
  captureMainAccountIdentityGeneration,
83
83
  captureMainQuotaWriter,
84
+ captureMainQuotaDispatch,
84
85
  getObservedMainQuotaIdentityKey,
85
86
  isMainQuotaWriterLive,
86
87
  matchesMainQuotaCredential,
87
88
  observeMainQuotaCredential,
88
89
  type MainQuotaWriter,
90
+ type MainQuotaDispatch,
89
91
  } from "./main-account-cache";
90
92
  import { CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, isCodexReserveHelperUnsupported, isCodexReserveRequestEligible } from "./loopback-target";
91
93
  import type { DataPlaneAdmission } from "../server/auth-cors";
@@ -275,7 +277,11 @@ export function previewCodexPoolLineage(
275
277
  }
276
278
 
277
279
  export type CodexAuthContext =
278
- | { kind: "main"; accountId: null; reserveAuthorization?: MainReserveAuthorization }
280
+ | {
281
+ kind: "main"; accountId: null; reserveAuthorization?: MainReserveAuthorization;
282
+ /** Captured for the observed main credential actually selected for upstream. */
283
+ mainQuotaDispatch?: MainQuotaDispatch;
284
+ }
279
285
  | {
280
286
  kind: "pool";
281
287
  accountId: string;
@@ -1635,6 +1641,14 @@ export class CodexMainSubstitutionUnavailableError extends Error {
1635
1641
  }
1636
1642
  }
1637
1643
 
1644
+ /** Dispatch identity comes only from an owned observation of the selected credential. */
1645
+ function selectedMainQuotaDispatch(selected: Headers): MainQuotaDispatch | undefined {
1646
+ const bearer = selected.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
1647
+ if (!bearer) return undefined;
1648
+ const accountId = selected.get("chatgpt-account-id") ?? extractAccountId(undefined, bearer);
1649
+ return captureMainQuotaDispatch(bearer, accountId, captureConfigGeneration());
1650
+ }
1651
+
1638
1652
  /**
1639
1653
  * Build the upstream auth headers for one Codex turn.
1640
1654
  *
@@ -1654,6 +1668,7 @@ export function materializeCodexUpstreamAuth(
1654
1668
  ctx: CodexAuthContext,
1655
1669
  options: CodexAuthMaterializationOptions = {},
1656
1670
  ): Headers {
1671
+ if (ctx.kind === "main") ctx.mainQuotaDispatch = undefined;
1657
1672
  const selected = new Headers();
1658
1673
  for (const name of FORWARD_HEADERS) {
1659
1674
  const value = headers.get(name);
@@ -1695,10 +1710,12 @@ export function materializeCodexUpstreamAuth(
1695
1710
  observeSelectedMainCredential(stored, writer);
1696
1711
  assertMainAccountPolicy(options.config);
1697
1712
  assertMaterializedReserve(selected, ctx, options);
1713
+ ctx.mainQuotaDispatch = selectedMainQuotaDispatch(selected);
1698
1714
  return selected;
1699
1715
  }
1700
1716
  if (callerMatchesObservedMain(selected)) assertMainAccountPolicy(options.config);
1701
1717
  assertMaterializedReserve(selected, ctx, options);
1718
+ if (ctx.kind === "main") ctx.mainQuotaDispatch = selectedMainQuotaDispatch(selected);
1702
1719
  return selected;
1703
1720
  }
1704
1721
 
@@ -1749,6 +1766,7 @@ export async function materializeCodexUpstreamAuthAsync(
1749
1766
  ctx: CodexAuthContext,
1750
1767
  options: CodexAuthMaterializationOptions = {},
1751
1768
  ): Promise<Headers> {
1769
+ if (ctx.kind === "main") ctx.mainQuotaDispatch = undefined;
1752
1770
  if (requiresReserveAuthorization(options.config, options.modelId, options.admission)) {
1753
1771
  return materializeReserveUpstreamAuth(headers, ctx, options);
1754
1772
  }
@@ -1776,6 +1794,7 @@ export async function materializeCodexUpstreamAuthAsync(
1776
1794
  assertMainAccountPolicy(options.config);
1777
1795
  // An opt-in enabled during token refresh must not turn a proof-less context into Reserve.
1778
1796
  assertMaterializedReserve(selected, ctx, options);
1797
+ ctx.mainQuotaDispatch = selectedMainQuotaDispatch(selected);
1779
1798
  return selected;
1780
1799
  }
1781
1800
 
@@ -60,6 +60,7 @@ export interface StartupHealth {
60
60
  platform: NodeJS.Platform;
61
61
  diagnosticStale: boolean;
62
62
  recommendedCommand: string | null;
63
+ recommendedAction?: string | null;
63
64
  commands: {
64
65
  installService: string;
65
66
  repairService: string;
@@ -91,7 +92,8 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
91
92
  // An arbitrary localhost gateway has an independent lifecycle that OCX cannot repair.
92
93
  const ownsLocalRouting = inputs.routingKind === "opencodex-local";
93
94
  const desktopEffective = (inputs.platform === "darwin" || inputs.platform === "linux") && !inputs.diagnosticStale
94
- && inputs.desktop?.owned === true && inputs.desktop.loginEnabled
95
+ && inputs.desktop !== undefined && (inputs.desktop.owned || inputs.desktop.supervisor !== undefined)
96
+ && inputs.desktop.loginEnabled
95
97
  && inputs.desktop.running && inputs.desktop.viable;
96
98
  const protection: StartupProtection = ownsLocalRouting && inputs.serviceViable
97
99
  ? "service"
@@ -105,7 +107,7 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
105
107
  : rebootSafe
106
108
  ? "protected"
107
109
  : "at-risk";
108
- const recommendedCommand = status !== "at-risk" || (ownsLocalRouting && inputs.desktop?.owned)
110
+ const recommendedCommand = status !== "at-risk" || (ownsLocalRouting && inputs.desktop !== undefined)
109
111
  ? null
110
112
  : inputs.routingKind === "custom-local" || inputs.routingKind === "unknown"
111
113
  ? COMMANDS.restoreNative
@@ -117,6 +119,11 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
117
119
  // gets the registering command.
118
120
  ? (inputs.serviceInstalled && !inputs.serviceConflict ? COMMANDS.repairService : COMMANDS.installService)
119
121
  : COMMANDS.restoreNative;
122
+ const recommendedAction = status === "at-risk" && ownsLocalRouting && inputs.desktop !== undefined
123
+ ? inputs.diagnosticStale || inputs.desktop.loginEnabled
124
+ ? "Reopen OpenCodex and check Start at Login."
125
+ : "Turn on Start at Login in the OpenCodex menu so the desktop app starts this proxy after a restart."
126
+ : null;
120
127
  return {
121
128
  ...inputs,
122
129
  diagnosticStale: inputs.diagnosticStale ?? false,
@@ -127,6 +134,7 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
127
134
  protection,
128
135
  shimCoverage,
129
136
  recommendedCommand,
137
+ recommendedAction,
130
138
  commands: { ...COMMANDS },
131
139
  };
132
140
  }
@@ -211,7 +219,8 @@ function classifyStartupHealthSummary(health: StartupHealth): string {
211
219
  const command = health.recommendedCommand ?? health.commands.restoreNative;
212
220
  if (health.routingKind === "unknown") return `AT RISK after restart (Codex routing could not be verified; run '${command}')`;
213
221
  if (health.routingKind === "custom-local") return `AT RISK after restart (custom local gateway lifecycle is not managed by opencodex; run '${command}')`;
214
- if (health.desktop?.owned) return "AT RISK after restart (desktop startup could not be verified; reopen OpenCodex and check Start at Login)";
222
+ if (health.desktop?.supervisor && !health.desktop.loginEnabled && !health.diagnosticStale) return "AT RISK after restart (OpenCodex Desktop runs this proxy, but its Start at Login could not be verified; turn it on in the OpenCodex menu)";
223
+ if (health.desktop) return "AT RISK after restart (desktop startup could not be verified; reopen OpenCodex and check Start at Login)";
215
224
  if (health.shimCoverage === "cli-only") return `AT RISK for Codex Desktop after restart (launcher shim covers CLI scripts only; run '${command}')`;
216
225
  if (health.serviceConflict) return `AT RISK after restart (background service managers conflict; run '${command}')`;
217
226
  if (health.serviceStale) return `AT RISK after restart (background service files are stale; run '${command}')`;
@@ -1,3 +1,4 @@
1
+ import { isCodexControlPlaneModel } from "../control-plane-models";
1
2
  import { redactSecretString } from "../../lib/redact";
2
3
  import type { OcxConfig } from "../../types";
3
4
  import { encodeRoutedModelId } from "../../providers/slug-codec";
@@ -34,6 +35,7 @@ export type AutoReviewModelOverrideResult = "absent" | "applied" | "invalid" | "
34
35
 
35
36
  /** True when a catalog row was synthesized by opencodex instead of coming from upstream. */
36
37
  function isRoutedCatalogEntry(entry: RawEntry): boolean {
38
+ if (isCodexControlPlaneModel(entry.slug)) return false;
37
39
  const slug = typeof entry.slug === "string" ? entry.slug : "";
38
40
  return slug.includes("/")
39
41
  || (typeof entry.description === "string" && entry.description.startsWith("Routed via opencodex → "));
@@ -57,6 +59,7 @@ function clearAutoReviewOverrideValue(entry: RawEntry): void {
57
59
  * Returns the stamped values when the observed rows match that shape.
58
60
  */
59
61
  function legacyRootStampValues(observedModels: readonly RawEntry[]): ReadonlySet<string> | undefined {
62
+ observedModels = observedModels.filter(entry => !isCodexControlPlaneModel(entry?.slug));
60
63
  if (observedModels.some(entry => entry?.[AUTO_REVIEW_ROOT_MARKER] !== undefined)) return undefined;
61
64
  const configuredValues = new Set(observedModels.flatMap(entry => {
62
65
  const value = entry?.auto_review_model_override;
@@ -90,7 +93,7 @@ function clearLegacyRootStamps(models: readonly RawEntry[], sourceModels: readon
90
93
  const legacyStamp = legacyRootStampValues([...models, ...sourceModels]);
91
94
  if (legacyStamp === undefined) return;
92
95
  for (const entry of models) {
93
- if (!entry || typeof entry !== "object") continue;
96
+ if (!entry || typeof entry !== "object" || isCodexControlPlaneModel(entry.slug)) continue;
94
97
  const current = entry.auto_review_model_override;
95
98
  if (entry[AUTO_REVIEW_ROOT_MARKER] === undefined
96
99
  && typeof current === "string" && legacyStamp.has(current)) clearAutoReviewOverrideValue(entry);
@@ -107,7 +110,7 @@ function clearAutoReviewModelOverride(
107
110
  ): void {
108
111
  const legacyStamp = legacyRootStampValues([...models, ...sourceModels]);
109
112
  for (const entry of models) {
110
- if (!entry || typeof entry !== "object") continue;
113
+ if (!entry || typeof entry !== "object" || isCodexControlPlaneModel(entry.slug)) continue;
111
114
  const current = entry.auto_review_model_override;
112
115
  if (isRoutedCatalogEntry(entry)
113
116
  || (entry[AUTO_REVIEW_ROOT_MARKER] === true || rootAutoReviewStamp(entry) !== undefined)
@@ -188,6 +191,7 @@ function preserveNativeAutoReviewModelOverrides(
188
191
 
189
192
  /** Stamp a root-derived override and mark native rows so later root removal is durable. */
190
193
  function stampRootAutoReviewOverride(entry: RawEntry, target: string): void {
194
+ if (isCodexControlPlaneModel(entry.slug)) return;
191
195
  if (!isRoutedCatalogEntry(entry)) {
192
196
  const previous = rootAutoReviewStamp(entry);
193
197
  const current = entry.auto_review_model_override;
@@ -399,7 +403,7 @@ function applyRootSelectorToRemaining(
399
403
  ): AutoReviewModelOverrideResult {
400
404
  const clearRemaining = (): void => {
401
405
  for (const entry of models) {
402
- if (!entry || providerStamped.has(entry)) continue;
406
+ if (!entry || providerStamped.has(entry) || isCodexControlPlaneModel(entry.slug)) continue;
403
407
  // Native rows written by releases before the root marker cannot be told apart from upstream
404
408
  // values once provider stamps diverge. clearLegacyRootStamps sweeps the ones the legacy
405
409
  // uniform signature still recognizes before provider plans land, because provider stamping
@@ -427,7 +431,7 @@ function applyRootSelectorToRemaining(
427
431
  return "unresolved";
428
432
  }
429
433
  for (const entry of models) {
430
- if (!entry || providerStamped.has(entry)) continue;
434
+ if (!entry || providerStamped.has(entry) || isCodexControlPlaneModel(entry.slug)) continue;
431
435
  stampRootAutoReviewOverride(entry, trimmed);
432
436
  }
433
437
  return "applied";
@@ -449,7 +453,7 @@ export function applyConfiguredAutoReviewModelOverride(
449
453
  const { plans, failure } = buildProviderReviewPlans(models, config);
450
454
  const providerStamped = new Set<RawEntry>();
451
455
  for (const entry of models) {
452
- if (!entry || typeof entry !== "object") continue;
456
+ if (!entry || typeof entry !== "object" || isCodexControlPlaneModel(entry.slug)) continue;
453
457
  const provider = catalogEntryProviderName(entry);
454
458
  if (!provider) continue;
455
459
  const plan = plans.get(provider);
@@ -482,7 +486,10 @@ export function finalizeAutoReviewModelOverride(
482
486
  sourceModels: readonly RawEntry[] = [],
483
487
  config?: Pick<OcxConfig, "providers">,
484
488
  ): AutoReviewModelOverrideResult {
485
- if (models && sourceModels.length > 0) preserveNativeAutoReviewModelOverrides(models, sourceModels);
489
+ if (models && sourceModels.length > 0) preserveNativeAutoReviewModelOverrides(
490
+ models.filter(entry => !isCodexControlPlaneModel(entry.slug)),
491
+ sourceModels.filter(entry => !isCodexControlPlaneModel(entry.slug)),
492
+ );
486
493
  if (config && configHasProviderAutoReview(config)) {
487
494
  return applyConfiguredAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), config, sourceModels);
488
495
  }
@@ -1,3 +1,5 @@
1
+ import { isCodexControlPlaneModel } from "../control-plane-models";
2
+ import { withCodexControlPlaneRows } from "./control-plane";
1
3
  import { CODEX_REASONING_LEVELS, type CodexReasoningLevel } from "../../reasoning-effort";
2
4
  import { clearModelCache } from "../model-cache";
3
5
  import { routedSlug, slugEquivalenceKey } from "../../providers/slug-codec";
@@ -46,6 +48,7 @@ import { NATIVE_RESERVE_MODEL } from "./native-models";
46
48
  import { isReserveCatalogProjection, type ReserveCatalogProjection } from "./reserve";
47
49
  import { deriveEntry, finishUpstreamNativeEntry, isExactComboCatalogEntry } from "./derive-entry";
48
50
  import { PICKER_ORDER_PRIORITY_BASE, SPAWN_PRIORITY_FIELD } from "./subagent-roster";
51
+ import { preserveOperatorUltraFastTiers } from "./operator-tiers";
49
52
 
50
53
  export interface ObservedCatalogEntryBuildInput {
51
54
  readonly template: RawEntry | null;
@@ -194,7 +197,7 @@ export function buildCatalogEntriesFromObservedState({
194
197
  const comboPublicSlugs = new Set(goModels
195
198
  .filter(model => model.provider === COMBO_NAMESPACE)
196
199
  .map(catalogModelSlug));
197
- for (const slug of gptSlugs) {
200
+ for (const slug of gptSlugs.filter(slug => !isCodexControlPlaneModel(slug))) {
198
201
  const native = deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9, undefined, new Set(), openaiContextCap);
199
202
  if (rank.has(slug)) native.priority = rank.get(slug)!;
200
203
  nativeEntries.push(native);
@@ -224,7 +227,7 @@ export function buildCatalogEntriesFromObservedState({
224
227
  const selectorNativeSlugs = accountNativeSlugsBySelector?.get(selector)
225
228
  ?? accountNativeSlugs
226
229
  ?? gptSlugs;
227
- const accountNativeEntries = selectorNativeSlugs.filter(slug => slug !== NATIVE_RESERVE_MODEL).map(slug => (
230
+ const accountNativeEntries = selectorNativeSlugs.filter(slug => slug !== NATIVE_RESERVE_MODEL && !isCodexControlPlaneModel(slug)).map(slug => (
228
231
  nativeEntriesBySlug.get(slug)
229
232
  ?? deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9, undefined, new Set(), openaiContextCap)
230
233
  ));
@@ -306,7 +309,7 @@ export function buildCatalogEntriesFromObservedState({
306
309
  delete entry.prefer_websockets;
307
310
  }
308
311
  }
309
- return applyMultiAgentMode(out, multiAgentMode, multiAgentV2Enabled, {
312
+ return applyMultiAgentMode(withCodexControlPlaneRows(out, template ? [template] : [], true, wsEnabled), multiAgentMode, multiAgentV2Enabled, {
310
313
  keepNativeChatGptOnV1,
311
314
  preserveDefaultMultiAgentVersion: isReserveCatalogProjection,
312
315
  });
@@ -409,12 +412,12 @@ export function mergeCatalogModelsWithNativeRecovery(
409
412
  ): RawEntry[] {
410
413
  const merged = [...primaryCatalogModels];
411
414
  const recoveredNativeSlugs = new Set(primaryCatalogModels.flatMap(entry => {
412
- const slug = recoverableNativeSlug(entry);
415
+ const slug = isCodexControlPlaneModel(entry.slug) ? String(entry.slug) : recoverableNativeSlug(entry);
413
416
  return slug === null ? [] : [slug];
414
417
  }));
415
418
  for (const source of nativeRecoverySources) {
416
419
  for (const entry of source) {
417
- const slug = recoverableNativeSlug(entry);
420
+ const slug = isCodexControlPlaneModel(entry.slug) ? String(entry.slug) : recoverableNativeSlug(entry);
418
421
  if (slug === null || recoveredNativeSlugs.has(slug)) continue;
419
422
  merged.push(structuredClone(entry) as RawEntry);
420
423
  recoveredNativeSlugs.add(slug);
@@ -460,6 +463,7 @@ export function applyFullModelPickerOrder(entries: RawEntry[], order: readonly s
460
463
  if (!pickerOrder.some(slug => !slug.includes("/"))) return;
461
464
  const rankOf = modelPickerRank(pickerOrder);
462
465
  for (const entry of entries) {
466
+ if (isCodexControlPlaneModel(entry.slug)) continue;
463
467
  const natural = entry[SPAWN_PRIORITY_FIELD] ?? entry.priority ?? 9;
464
468
  entry[SPAWN_PRIORITY_FIELD] = natural;
465
469
  entry.priority = rankOf(String(entry.slug)) ?? pickerOrder.length + Number(natural);
@@ -467,6 +471,8 @@ export function applyFullModelPickerOrder(entries: RawEntry[], order: readonly s
467
471
  }
468
472
 
469
473
  export interface ObservedCatalogMergeInput {
474
+ /** Preserve operator-supplied Ultra Fast only from the exact persisted routed row. */
475
+ readonly ultraFastTier?: boolean;
470
476
  readonly catalogModels: readonly RawEntry[];
471
477
  readonly baselineCatalogModels: readonly RawEntry[];
472
478
  readonly routedEntries: readonly RawEntry[];
@@ -507,6 +513,7 @@ export interface ObservedCatalogMergeInput {
507
513
  * accidentally fall back to process-ambient catalog discovery or merge-policy warnings.
508
514
  */
509
515
  export function mergeCatalogEntriesFromObservedState({
516
+ ultraFastTier = false,
510
517
  catalogModels,
511
518
  baselineCatalogModels,
512
519
  routedEntries,
@@ -539,10 +546,12 @@ export function mergeCatalogEntriesFromObservedState({
539
546
  // Raw catalog rows contain nested arrays/objects that normalization mutates. Detach every row at
540
547
  // the observed-core boundary so callers can safely retain evidence objects or repeat the merge.
541
548
  const detachedCatalogModels = catalogModels
549
+ .filter(entry => !isCodexControlPlaneModel(entry.slug))
542
550
  .map(entry => restoreNativeDisplayName(structuredClone(entry) as RawEntry));
543
551
  const detachedBaselineCatalogModels = baselineCatalogModels
552
+ .filter(entry => !isCodexControlPlaneModel(entry.slug))
544
553
  .map(entry => restoreNativeDisplayName(structuredClone(entry) as RawEntry));
545
- const detachedRoutedEntries = routedEntries.map(entry => structuredClone(entry) as RawEntry);
554
+ const detachedRoutedEntries = routedEntries.filter(entry => !isCodexControlPlaneModel(entry.slug)).map(entry => structuredClone(entry) as RawEntry);
546
555
  // Track this invocation's generated custom rows, not ownership markers read from disk.
547
556
  // Their builder already finalized exact native ladders and ordinary routed mock tiers.
548
557
  const freshCustomEntries = new Set(detachedRoutedEntries.filter(entry =>
@@ -844,6 +853,20 @@ export function mergeCatalogEntriesFromObservedState({
844
853
  }
845
854
  return false;
846
855
  });
856
+ // First occurrence wins, matching enforceCatalogSlugUniqueness: a later duplicate row of the
857
+ // same slug is what the writer drops, so it must not decide which declaration survives.
858
+ const operatorRows = new Map<string, RawEntry | undefined>();
859
+ for (const entry of detachedCatalogModels) {
860
+ if (typeof entry.slug !== "string" || operatorRows.has(entry.slug)) continue;
861
+ operatorRows.set(entry.slug, trustedAccountBoundNativeCatalogSlug(entry) === undefined
862
+ && !isNativeAliasCatalogEntry(entry) && entry.owned_by !== COMBO_NAMESPACE ? entry : undefined);
863
+ }
864
+ for (const entry of finalRoutedEntries) {
865
+ if (typeof entry.slug !== "string" || !entry.slug.includes("/")
866
+ || isNativeAliasCatalogEntry(entry) || isExactComboCatalogEntry(entry, exactComboSlugs)
867
+ || trustedAccountBoundNativeCatalogSlug(entry) !== undefined) continue;
868
+ preserveOperatorUltraFastTiers(entry, operatorRows.get(entry.slug), ultraFastTier);
869
+ }
847
870
  const finalRoutedEntrySet = new Set(finalRoutedEntries);
848
871
  const degradedPreservedCount = preservedRoutedEntries.filter(entry => {
849
872
  if (!finalRoutedEntrySet.has(entry)) return false;
@@ -921,7 +944,10 @@ export function mergeCatalogEntriesFromObservedState({
921
944
  // clobber a hide flag back to list. Bare ids disable every account clone; qualified ids disable
922
945
  // only their generated account row.
923
946
  const versionedEntries = applyMultiAgentMode(
924
- applyNativeVisibility(mergedEntries, disabledModels, alignedAccountBoundEntries.length > 0, observedNativeSlugs),
947
+ withCodexControlPlaneRows(
948
+ applyNativeVisibility(mergedEntries, disabledModels, alignedAccountBoundEntries.length > 0, observedNativeSlugs),
949
+ [...catalogModels, ...baselineCatalogModels, ...routedEntries], includeNativeOpenAi, wsEnabled,
950
+ ),
925
951
  multiAgentMode,
926
952
  multiAgentV2Enabled,
927
953
  { keepNativeChatGptOnV1, preserveDefaultMultiAgentVersion: isReserveCatalogProjection, nativeDefaults: nativeMultiAgentDefaults },