@bitkyc08/opencodex 2.84.0 → 2.87.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 (518) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
  4. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  5. package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DCd-A1az.js} +1 -1
  6. package/gui/dist/assets/index-CfYDMj34.js +94 -0
  7. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  8. package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -2
  11. package/src/adapters/anthropic/beta-allowlist.ts +91 -11
  12. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  13. package/src/adapters/anthropic/client-identity.ts +1 -1
  14. package/src/adapters/anthropic/passthrough.ts +16 -3
  15. package/src/adapters/anthropic-progress.ts +84 -0
  16. package/src/adapters/anthropic.ts +108 -21
  17. package/src/adapters/azure.ts +19 -1
  18. package/src/adapters/base.ts +2 -0
  19. package/src/adapters/claude-cli/adapter.ts +111 -23
  20. package/src/adapters/claude-cli/profiles.ts +2 -1
  21. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  22. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  23. package/src/adapters/client-fingerprint.ts +24 -0
  24. package/src/adapters/coding-agent/protocol.ts +1 -1
  25. package/src/adapters/coding-agent/turn.ts +40 -11
  26. package/src/adapters/cursor/arg-codec.ts +17 -5
  27. package/src/adapters/cursor/catalog.ts +8 -0
  28. package/src/adapters/cursor/effort-map.ts +3 -0
  29. package/src/adapters/cursor/freeform-args.ts +75 -0
  30. package/src/adapters/cursor/protobuf-events.ts +36 -23
  31. package/src/adapters/dsh-account.ts +97 -0
  32. package/src/adapters/google-antigravity-wire.ts +22 -1
  33. package/src/adapters/google.ts +37 -10
  34. package/src/adapters/openai-chat/messages.ts +4 -3
  35. package/src/adapters/openai-chat/passthrough.ts +8 -2
  36. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  37. package/src/adapters/openai-chat/response-events.ts +59 -1
  38. package/src/adapters/openai-chat.ts +8 -5
  39. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  40. package/src/adapters/openai-responses/passthrough.ts +57 -125
  41. package/src/adapters/openai-responses/request-strips.ts +25 -3
  42. package/src/adapters/openai-responses/transport.ts +81 -0
  43. package/src/adapters/qoder/adapter.ts +44 -3
  44. package/src/adapters/registry.ts +5 -0
  45. package/src/adapters/zed.ts +26 -1
  46. package/src/advisor/activity.ts +55 -0
  47. package/src/advisor/budget.ts +62 -0
  48. package/src/advisor/consult.ts +223 -0
  49. package/src/advisor/context.ts +238 -0
  50. package/src/advisor/disclosure.ts +20 -0
  51. package/src/advisor/evidence-tool.ts +21 -0
  52. package/src/advisor/evidence.ts +117 -0
  53. package/src/advisor/runtime.ts +353 -0
  54. package/src/advisor/settings.ts +144 -0
  55. package/src/advisor/state.ts +369 -0
  56. package/src/advisor/synthetic-tool.ts +33 -0
  57. package/src/bridge/errors.ts +24 -1
  58. package/src/bridge/internal.ts +3 -1
  59. package/src/bridge/response-json.ts +18 -3
  60. package/src/bridge/sse.ts +58 -8
  61. package/src/bridge/terminal-refusal.ts +59 -0
  62. package/src/chat/inbound.ts +30 -6
  63. package/src/chat/outbound.ts +5 -0
  64. package/src/claude/desktop-3p-library.ts +72 -7
  65. package/src/claude/desktop-3p.ts +75 -19
  66. package/src/claude/desktop-first-party.ts +1 -1
  67. package/src/claude/desktop-picker-platform.ts +4 -0
  68. package/src/claude/desktop-picker-profile.ts +11 -4
  69. package/src/claude/desktop-picker.ts +15 -8
  70. package/src/claude/desktop-profile.ts +10 -1
  71. package/src/claude/desktop-remote-store-io.ts +5 -3
  72. package/src/claude/desktop-system-proxy.ts +50 -4
  73. package/src/claude/inbound-content-options.ts +11 -0
  74. package/src/claude/inbound.ts +154 -50
  75. package/src/claude/intercept/local-ca.ts +8 -1
  76. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  77. package/src/claude/intercept/picker-runtime.ts +6 -5
  78. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  79. package/src/claude/intercept/picker-trust.ts +19 -0
  80. package/src/claude/intercept/port-policy.ts +30 -0
  81. package/src/claude/intercept/runtime.ts +97 -94
  82. package/src/claude/model-default-settings.ts +154 -0
  83. package/src/claude/model-info.ts +7 -7
  84. package/src/claude/outbound.ts +51 -19
  85. package/src/cli/account-api.ts +1 -0
  86. package/src/cli/account-policy-dto.ts +6 -0
  87. package/src/cli/account-policy.ts +34 -5
  88. package/src/cli/account-preservation.ts +44 -0
  89. package/src/cli/account.ts +8 -2
  90. package/src/cli/advisor.ts +165 -0
  91. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  92. package/src/cli/capabilities-accounts.ts +20 -1
  93. package/src/cli/capabilities-agents-routing.ts +23 -2
  94. package/src/cli/capabilities-base.ts +39 -11
  95. package/src/cli/capabilities-observe-system.ts +63 -3
  96. package/src/cli/capabilities-provider-models.ts +11 -0
  97. package/src/cli/chatgpt-command.ts +35 -14
  98. package/src/cli/claude.ts +46 -6
  99. package/src/cli/codex-cli-update.ts +151 -41
  100. package/src/cli/codex-provider.ts +44 -0
  101. package/src/cli/combo-input.ts +10 -1
  102. package/src/cli/combo-result.ts +1 -1
  103. package/src/cli/combo.ts +2 -1
  104. package/src/cli/cross-home-owner.ts +17 -14
  105. package/src/cli/dispatch.ts +30 -5
  106. package/src/cli/doctor.ts +15 -1
  107. package/src/cli/ensure-desktop-cold-start.ts +382 -0
  108. package/src/cli/export-command.ts +1 -1
  109. package/src/cli/help.ts +4 -2
  110. package/src/cli/index.ts +14 -5
  111. package/src/cli/integration-plan-dto.ts +3 -0
  112. package/src/cli/log-history.ts +172 -0
  113. package/src/cli/models-runtime-subcommands.ts +1 -0
  114. package/src/cli/models-runtime.ts +72 -8
  115. package/src/cli/observe-stream.ts +2 -0
  116. package/src/cli/observe.ts +16 -4
  117. package/src/cli/provider.ts +27 -5
  118. package/src/cli/registry.ts +41 -9
  119. package/src/cli/restart-scope.ts +3 -1
  120. package/src/cli/runtime-api.ts +5 -3
  121. package/src/cli/system-command.ts +40 -8
  122. package/src/cli/uninstall-client-state.ts +32 -0
  123. package/src/cli/usage-compare.ts +309 -0
  124. package/src/cli/usage-model-search.ts +4 -4
  125. package/src/cli/usage-report.ts +2 -2
  126. package/src/cli/v2.ts +1 -1
  127. package/src/client/machine-listener.ts +1 -3
  128. package/src/client/runtime.ts +1 -0
  129. package/src/clients/client-path.ts +51 -0
  130. package/src/clients/config-export/contracts.ts +3 -1
  131. package/src/clients/config-export/model-metadata.ts +3 -2
  132. package/src/clients/config-export/qoder.ts +93 -0
  133. package/src/clients/config-export.ts +35 -48
  134. package/src/clients/gentle-shell-path.ts +87 -0
  135. package/src/codex/account-auto-switch.ts +2 -0
  136. package/src/codex/account-identity.ts +29 -0
  137. package/src/codex/account-label.ts +1 -1
  138. package/src/codex/account-lifecycle.ts +21 -5
  139. package/src/codex/account-preservation.ts +28 -0
  140. package/src/codex/account-use-remaining.ts +23 -0
  141. package/src/codex/app-server-processes.ts +97 -7
  142. package/src/codex/app-server-restart-service.ts +55 -4
  143. package/src/codex/auth-api/account-list.ts +4 -0
  144. package/src/codex/auth-api/account-pause-group.ts +6 -50
  145. package/src/codex/auth-api/account-preservation.ts +57 -0
  146. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  147. package/src/codex/auth-api/routes.ts +8 -0
  148. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  149. package/src/codex/auth-api.ts +0 -1
  150. package/src/codex/auth-context.ts +81 -7
  151. package/src/codex/autostart-health.ts +4 -0
  152. package/src/codex/catalog/auto-review.ts +20 -3
  153. package/src/codex/catalog/build-entries.ts +19 -2
  154. package/src/codex/catalog/combo-member.ts +14 -5
  155. package/src/codex/catalog/configured-tiers.ts +50 -0
  156. package/src/codex/catalog/effort.ts +4 -1
  157. package/src/codex/catalog/gather-capture.ts +2 -0
  158. package/src/codex/catalog/metadata.ts +11 -9
  159. package/src/codex/catalog/model-hints.ts +42 -19
  160. package/src/codex/catalog/model-visibility.ts +3 -1
  161. package/src/codex/catalog/native-ultrafast.ts +106 -0
  162. package/src/codex/catalog/parsing.ts +35 -86
  163. package/src/codex/catalog/provider-models.ts +40 -34
  164. package/src/codex/catalog/retained-sync.ts +2 -0
  165. package/src/codex/catalog/routed-gather.ts +33 -16
  166. package/src/codex/catalog/vendor-context.ts +82 -0
  167. package/src/codex/cli-install-provenance.ts +105 -17
  168. package/src/codex/cli-update-plan.ts +545 -0
  169. package/src/codex/config-write-lock.ts +63 -4
  170. package/src/codex/convergence.ts +2 -0
  171. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  172. package/src/codex/desktop-app/types.ts +2 -0
  173. package/src/codex/desktop-app/windows.ts +29 -16
  174. package/src/codex/desktop-app-restart.ts +25 -2
  175. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  176. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  177. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  178. package/src/codex/desktop-compatibility/installed-build.ts +73 -0
  179. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  180. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  181. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  182. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  183. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  184. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  185. package/src/codex/desktop-compatibility/runtime.ts +182 -0
  186. package/src/codex/desktop-compatibility/service.ts +30 -0
  187. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  188. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  189. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  190. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  191. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  192. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  193. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  194. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  195. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  196. package/src/codex/desktop-compatibility/windows-key-protection.ts +165 -0
  197. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  198. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  199. package/src/codex/features.ts +85 -45
  200. package/src/codex/history-provider.ts +1 -1
  201. package/src/codex/inject/config-write-section.ts +4 -1
  202. package/src/codex/inject/history-failure.ts +27 -0
  203. package/src/codex/inject/remove.ts +18 -6
  204. package/src/codex/inject/restore.ts +24 -2
  205. package/src/codex/inject/routing-classify.ts +4 -3
  206. package/src/codex/inject.ts +18 -27
  207. package/src/codex/journal.ts +16 -1
  208. package/src/codex/main-account-hard-lock.ts +30 -1
  209. package/src/codex/main-account.ts +99 -15
  210. package/src/codex/native-main-refresh-mode.ts +217 -0
  211. package/src/codex/native-profile-startup.ts +1 -1
  212. package/src/codex/ocx-compaction-history.ts +12 -10
  213. package/src/codex/paths.ts +86 -1
  214. package/src/codex/provider-routing.ts +174 -0
  215. package/src/codex/refresh.ts +2 -2
  216. package/src/codex/routing/health-store.ts +2 -2
  217. package/src/codex/routing/pool-recovery.ts +79 -0
  218. package/src/codex/routing/selection.ts +4 -3
  219. package/src/codex/routing/thread-affinity.ts +1 -1
  220. package/src/codex/shim-diagnostics.ts +6 -0
  221. package/src/codex/shim-overlay.ts +7 -2
  222. package/src/codex/signed-in-identity.ts +43 -0
  223. package/src/codex/subagent-model-fallback.ts +1 -1
  224. package/src/codex/sync.ts +6 -1
  225. package/src/codex/user-identity.ts +16 -5
  226. package/src/codex/windows-local-appdata.ts +84 -0
  227. package/src/combos/failover.ts +6 -1
  228. package/src/combos/index.ts +1 -0
  229. package/src/combos/resolve.ts +46 -10
  230. package/src/combos/types.ts +15 -0
  231. package/src/config/diagnostics.ts +35 -7
  232. package/src/config/live-reconcile.ts +51 -2
  233. package/src/config/load-degrade.ts +23 -1
  234. package/src/config/opencode-wire-upgrade.ts +54 -0
  235. package/src/config/opencode-zen-upgrade.ts +52 -0
  236. package/src/config/owner-registry.ts +22 -17
  237. package/src/config/paths.ts +10 -3
  238. package/src/config/provider-validation.ts +45 -2
  239. package/src/config/schema/config-schema.ts +39 -0
  240. package/src/config/schema/desktop-compatibility.ts +9 -0
  241. package/src/config/schema/leaf-validators.ts +39 -9
  242. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  243. package/src/config/schema/native-reasoning-retention.ts +26 -0
  244. package/src/config.ts +2 -0
  245. package/src/generated/compatibility-version.json +962 -350
  246. package/src/generated/model-metadata.ts +1 -1
  247. package/src/grok/sync.ts +1 -1
  248. package/src/images/loop.ts +46 -8
  249. package/src/integrations/catalog-refresh.ts +1 -1
  250. package/src/integrations/config-io.ts +20 -2
  251. package/src/integrations/journal.ts +15 -1
  252. package/src/integrations/mutation-plan.ts +2 -0
  253. package/src/integrations/registry.ts +15 -0
  254. package/src/integrations/store.ts +3 -0
  255. package/src/integrations/writer.ts +10 -1
  256. package/src/lib/advisor-activation.ts +16 -0
  257. package/src/lib/background-work.ts +65 -0
  258. package/src/lib/bounded-file-read.ts +58 -0
  259. package/src/lib/bun-path-runtime.d.mts +10 -1
  260. package/src/lib/bun-path-runtime.mjs +62 -3
  261. package/src/lib/codex-restart-contract.ts +15 -0
  262. package/src/lib/desktop-proxy-route.ts +30 -0
  263. package/src/lib/desktop-upstream-tunnel.ts +72 -0
  264. package/src/lib/empty-tool-catalog.ts +62 -0
  265. package/src/lib/errors.ts +26 -3
  266. package/src/lib/gcp-adc.ts +96 -9
  267. package/src/lib/gcp-credential-input.ts +12 -0
  268. package/src/lib/lab-live-route-production.ts +5 -0
  269. package/src/lib/local-internal-call-capability.ts +55 -0
  270. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  271. package/src/lib/package-tree-integrity.ts +2 -2
  272. package/src/lib/redact.ts +2 -2
  273. package/src/lib/socks5-fetch.ts +48 -71
  274. package/src/lib/socks5-handshake.ts +112 -0
  275. package/src/lib/translator-budget.ts +7 -1
  276. package/src/oauth/account-quota-rank.ts +69 -2
  277. package/src/oauth/anthropic-account-refusal.ts +20 -2
  278. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  279. package/src/oauth/anthropic-routing.ts +1 -21
  280. package/src/oauth/anthropic.ts +38 -7
  281. package/src/oauth/callback-server.ts +27 -1
  282. package/src/oauth/chatgpt-device.ts +1 -1
  283. package/src/oauth/chatgpt-shared.ts +83 -0
  284. package/src/oauth/chatgpt.ts +8 -72
  285. package/src/oauth/dsh.ts +203 -0
  286. package/src/oauth/generic-account-failover.ts +62 -12
  287. package/src/oauth/generic-session-affinity.ts +125 -0
  288. package/src/oauth/index.ts +188 -111
  289. package/src/oauth/kiro-device-login.ts +2 -1
  290. package/src/oauth/kiro.ts +7 -1
  291. package/src/oauth/local-token-detect.ts +21 -1
  292. package/src/oauth/login-cli-errors.ts +44 -0
  293. package/src/oauth/login-cli.ts +69 -7
  294. package/src/oauth/login-flow-state.ts +371 -23
  295. package/src/oauth/meta-muse-errors.ts +41 -0
  296. package/src/oauth/meta-muse-keychain.ts +52 -0
  297. package/src/oauth/meta-muse.ts +51 -84
  298. package/src/oauth/model-discovery-auth.ts +3 -0
  299. package/src/oauth/pool-settings-capability.ts +4 -2
  300. package/src/oauth/refresh-flight-retirement.ts +48 -0
  301. package/src/oauth/session-affinity-key.ts +23 -0
  302. package/src/oauth/store-anthropic-instance.ts +9 -2
  303. package/src/oauth/store.ts +12 -5
  304. package/src/protocols/codecs/chat-ir.ts +467 -0
  305. package/src/protocols/codecs/chat.ts +10 -1
  306. package/src/protocols/codecs/messages-ir.ts +500 -0
  307. package/src/protocols/encoders/adapter-events.ts +5 -4
  308. package/src/protocols/encoders/messages.ts +9 -6
  309. package/src/protocols/features.ts +10 -2
  310. package/src/protocols/path.ts +11 -1
  311. package/src/protocols/plan-snapshot.ts +39 -16
  312. package/src/protocols/plan.ts +9 -1
  313. package/src/protocols/provider-summary.ts +2 -1
  314. package/src/protocols/settings.ts +8 -0
  315. package/src/providers/antigravity-account-roster.ts +102 -0
  316. package/src/providers/antigravity-models.ts +85 -7
  317. package/src/providers/api-key-cooldowns.ts +49 -0
  318. package/src/providers/api-key-resolve.ts +11 -6
  319. package/src/providers/api-key-response-ownership.ts +57 -0
  320. package/src/providers/api-key-selection.ts +31 -6
  321. package/src/providers/api-keys.ts +214 -3
  322. package/src/providers/claude-cli-usage.ts +96 -0
  323. package/src/providers/dsh-account-destination.ts +31 -0
  324. package/src/providers/dsh-account-models.ts +41 -0
  325. package/src/providers/gcp-credential-cleanup.ts +59 -0
  326. package/src/providers/key-failover.ts +54 -50
  327. package/src/providers/key-store.ts +7 -2
  328. package/src/providers/model-rename-fields.ts +5 -0
  329. package/src/providers/model-rename-migration.ts +7 -5
  330. package/src/providers/openai-tiers-destination.ts +11 -2
  331. package/src/providers/openai-tiers.ts +5 -12
  332. package/src/providers/opencode-go-transport.ts +3 -1
  333. package/src/providers/quota/account-cache.ts +6 -5
  334. package/src/providers/quota/account-row.ts +34 -0
  335. package/src/providers/quota/antigravity.ts +28 -7
  336. package/src/providers/quota/dsh-account.ts +70 -0
  337. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  338. package/src/providers/quota-types.ts +2 -0
  339. package/src/providers/quota.ts +14 -20
  340. package/src/providers/registry/entries-core.ts +44 -14
  341. package/src/providers/registry/entries-extended.ts +19 -0
  342. package/src/providers/registry/types.ts +1 -1
  343. package/src/providers/request-pacing.ts +77 -19
  344. package/src/providers/routed-config.ts +339 -0
  345. package/src/providers/service-tier.ts +2 -1
  346. package/src/quota/window-mapping.ts +5 -2
  347. package/src/responses/anthropic-progress.ts +34 -0
  348. package/src/responses/assistant-replay.ts +35 -0
  349. package/src/responses/compaction-history-framing.ts +65 -0
  350. package/src/responses/compaction.ts +6 -1
  351. package/src/responses/incomplete-reason.ts +31 -0
  352. package/src/responses/parser.ts +78 -22
  353. package/src/responses/reasoning-envelope.ts +4 -0
  354. package/src/responses/reasoning-replay-cache.ts +24 -2
  355. package/src/responses/reasoning-retention.ts +173 -0
  356. package/src/responses/retained-compaction.ts +46 -0
  357. package/src/responses/schema.ts +23 -0
  358. package/src/responses/state.ts +32 -29
  359. package/src/router.ts +8 -321
  360. package/src/routing/health.ts +37 -22
  361. package/src/routing/history/build-file.ts +92 -0
  362. package/src/routing/history/indexer.ts +254 -221
  363. package/src/routing/history/ingest.ts +242 -0
  364. package/src/routing/history/rebuild-job.ts +111 -0
  365. package/src/routing/history/rebuild-protocol.ts +15 -0
  366. package/src/routing/history/rebuild-worker.ts +34 -0
  367. package/src/routing/profile.ts +1 -1
  368. package/src/server/admission-model-scope.ts +21 -1
  369. package/src/server/antigravity-direct-eligibility.ts +208 -0
  370. package/src/server/auth-cors.ts +15 -0
  371. package/src/server/chat-completions.ts +82 -10
  372. package/src/server/chat-native-reasoning.ts +123 -0
  373. package/src/server/chat-native-sse.ts +110 -7
  374. package/src/server/chat-native.ts +76 -17
  375. package/src/server/claude-messages.ts +164 -433
  376. package/src/server/effort-policy.ts +1 -1
  377. package/src/server/images.ts +15 -2
  378. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  379. package/src/server/index/model-alias-id.ts +36 -0
  380. package/src/server/index/optional-listeners.ts +2 -0
  381. package/src/server/index/serve-options.ts +11 -3
  382. package/src/server/index/startup-warnings.ts +11 -0
  383. package/src/server/index/websocket-handler.ts +9 -0
  384. package/src/server/index.ts +14 -11
  385. package/src/server/inference/advisor-diagnostics.ts +31 -0
  386. package/src/server/inference/client-encoder-delivery.ts +6 -5
  387. package/src/server/login-shell-path.ts +146 -0
  388. package/src/server/management/advisor-routes.ts +202 -0
  389. package/src/server/management/agent-settings-routes.ts +22 -4
  390. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  391. package/src/server/management/combo-routes.ts +176 -50
  392. package/src/server/management/companion-routes.ts +4 -0
  393. package/src/server/management/config-routes.ts +77 -6
  394. package/src/server/management/context.ts +4 -0
  395. package/src/server/management/cursor-integration-routes.ts +1 -1
  396. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  397. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  398. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  399. package/src/server/management/model-routes.ts +13 -8
  400. package/src/server/management/model-rows.ts +14 -0
  401. package/src/server/management/native-integration-routes.ts +2 -7
  402. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  403. package/src/server/management/oauth-account-routes.ts +87 -14
  404. package/src/server/management/protocol-settings-patch.ts +1 -0
  405. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  406. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  407. package/src/server/management/provider-overwrite-carry.ts +7 -0
  408. package/src/server/management/provider-routes.ts +25 -30
  409. package/src/server/management/route-registry.ts +21 -1
  410. package/src/server/management/sibling-guard.ts +1 -0
  411. package/src/server/management-api.ts +9 -1
  412. package/src/server/messages-antigravity-direct.ts +195 -0
  413. package/src/server/messages-native-tool-guard.ts +185 -0
  414. package/src/server/messages-native.ts +106 -22
  415. package/src/server/messages-shared.ts +431 -0
  416. package/src/server/relay.ts +1 -1
  417. package/src/server/request-log-terminal-status.ts +1 -1
  418. package/src/server/request-log.ts +13 -1
  419. package/src/server/responses/account-change-state.ts +8 -2
  420. package/src/server/responses/adapter-continuation.ts +48 -19
  421. package/src/server/responses/adapter-delivery.ts +43 -8
  422. package/src/server/responses/adapter-dispatch.ts +48 -4
  423. package/src/server/responses/advisor-plan-slot.ts +27 -0
  424. package/src/server/responses/advisor-retention.ts +117 -0
  425. package/src/server/responses/advisor-slot.ts +368 -0
  426. package/src/server/responses/code-mode-collab-surface.ts +116 -8
  427. package/src/server/responses/codex-ws-wire.ts +15 -8
  428. package/src/server/responses/compact.ts +303 -125
  429. package/src/server/responses/compaction-recovery.ts +18 -10
  430. package/src/server/responses/compaction-retention.ts +61 -0
  431. package/src/server/responses/context-overflow.ts +11 -6
  432. package/src/server/responses/core-auth.ts +6 -0
  433. package/src/server/responses/core-codex-account.ts +11 -1
  434. package/src/server/responses/core-combo-failure.ts +12 -1
  435. package/src/server/responses/core-combo.ts +141 -36
  436. package/src/server/responses/core-errors.ts +3 -3
  437. package/src/server/responses/core-lifetime.ts +7 -2
  438. package/src/server/responses/core-normalize.ts +11 -0
  439. package/src/server/responses/core-opaque-recovery.ts +35 -0
  440. package/src/server/responses/core-options.ts +51 -0
  441. package/src/server/responses/core-pipeline.ts +141 -0
  442. package/src/server/responses/core-replay.ts +25 -6
  443. package/src/server/responses/core.ts +3 -116
  444. package/src/server/responses/fetch-helpers.ts +73 -19
  445. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  446. package/src/server/responses/native-response-control.ts +4 -2
  447. package/src/server/responses/passthrough-delivery.ts +19 -11
  448. package/src/server/responses/passthrough-dispatch.ts +29 -16
  449. package/src/server/responses/request-prepare.ts +144 -17
  450. package/src/server/responses/request-sidecar-auth.ts +2 -2
  451. package/src/server/responses/request-transport.ts +120 -27
  452. package/src/server/responses/response-effects.ts +1 -0
  453. package/src/server/responses/run-turn-execution.ts +166 -38
  454. package/src/server/responses/sidecar-execution.ts +98 -5
  455. package/src/server/responses/terminal-guard.ts +4 -17
  456. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  457. package/src/server/startup-action-control.ts +16 -2
  458. package/src/server/startup-health-cache.ts +1 -0
  459. package/src/service/auth-preflight.ts +71 -0
  460. package/src/service/cli.ts +4 -2
  461. package/src/service/definition.ts +97 -0
  462. package/src/service/desktop-startup.ts +14 -0
  463. package/src/service/guards.ts +1 -61
  464. package/src/service/health-sentinel-linux.ts +112 -0
  465. package/src/service/health-sentinel-main.ts +99 -61
  466. package/src/service/health-sentinel-probe.ts +24 -7
  467. package/src/service/health.ts +5 -89
  468. package/src/service/launchd.ts +27 -7
  469. package/src/service/logging.ts +135 -0
  470. package/src/service/repair.ts +2 -1
  471. package/src/service/state.ts +7 -8
  472. package/src/service/systemd.ts +1 -1
  473. package/src/service/windows-guarded-manager.ts +2 -1
  474. package/src/service/windows-ops.ts +1 -1
  475. package/src/service/windows-scheduler.ts +2 -7
  476. package/src/service/windows-taskxml.ts +2 -2
  477. package/src/service/windows-wscript.ts +14 -0
  478. package/src/service.ts +4 -2
  479. package/src/sidecar/auth.ts +2 -0
  480. package/src/tray/windows-tray.ps1 +43 -24
  481. package/src/types/config.ts +124 -6
  482. package/src/types/opencode-go.ts +61 -0
  483. package/src/types/opencode-zen.ts +29 -0
  484. package/src/types/provider.ts +39 -6
  485. package/src/types/request.ts +38 -2
  486. package/src/types/tools.ts +2 -0
  487. package/src/types/wire.ts +29 -37
  488. package/src/types.ts +1 -0
  489. package/src/update/async-check.ts +1 -1
  490. package/src/update/badge.ts +2 -2
  491. package/src/update/index.ts +88 -15
  492. package/src/update/job-supervised-restart.ts +107 -0
  493. package/src/update/job.ts +32 -32
  494. package/src/update/notify.ts +1 -1
  495. package/src/update/npm-invocation.mjs +12 -2
  496. package/src/update/refresh-scheduler.ts +2 -2
  497. package/src/update/supervised-recovery.d.mts +27 -0
  498. package/src/update/supervised-recovery.mjs +157 -0
  499. package/src/usage/cache-diagnostic.ts +2 -0
  500. package/src/usage/cost.ts +48 -12
  501. package/src/usage/expected-prices.ts +15 -0
  502. package/src/usage/ledger-retention.ts +2 -1
  503. package/src/usage/ledger-scanner.ts +6 -5
  504. package/src/usage/log.ts +17 -8
  505. package/src/usage/summary.ts +22 -9
  506. package/src/usage/user-cost-overlays.ts +14 -19
  507. package/src/usage/user-cost-validation.ts +38 -0
  508. package/src/vision/eligibility.ts +13 -0
  509. package/src/web-search/gemini-executor.ts +2 -2
  510. package/src/web-search/loop.ts +55 -5
  511. package/src/web-search/progress-stream.ts +4 -1
  512. package/src/web-search/run-turn-loop.ts +9 -1
  513. package/assets/sponsors/packycode.png +0 -0
  514. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  515. package/gui/dist/assets/App-CZtqiGcw.js +0 -56
  516. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  517. package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
  518. package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
@@ -34,6 +34,7 @@ import type {
34
34
  CodexRestartResponse,
35
35
  } from "../lib/codex-restart-contract";
36
36
  import { getServerListenPort } from "../server/lifecycle";
37
+ import type { DesktopPickerState } from "./desktop-app/picker-freshness";
37
38
 
38
39
  async function defaultRestartDesktopApp(): Promise<CodexDesktopRestartSummary> {
39
40
  const { restartCodexDesktopApp } = await import("./desktop-app-restart");
@@ -47,9 +48,40 @@ async function defaultRestartDesktopApp(): Promise<CodexDesktopRestartSummary> {
47
48
  };
48
49
  }
49
50
 
51
+ async function defaultDesktopPickerState(): Promise<DesktopPickerState> {
52
+ const { readCodexDesktopPickerState } = await import("./desktop-app/picker-freshness");
53
+ return readCodexDesktopPickerState();
54
+ }
55
+
56
+ async function defaultCatalogBinding(): Promise<string | null> {
57
+ const { readCodexCatalogBinding } = await import("./desktop-app/picker-freshness");
58
+ return readCodexCatalogBinding();
59
+ }
60
+
61
+ async function readBinding(io: CodexRestartServiceIo): Promise<string | null | undefined> {
62
+ try {
63
+ return await (io.catalogBinding ?? defaultCatalogBinding)();
64
+ } catch {
65
+ // Distinct from null ("binds no catalog"): an unreadable binding cannot prove sameness.
66
+ return undefined;
67
+ }
68
+ }
69
+
70
+ async function readPickerState(io: CodexRestartServiceIo): Promise<DesktopPickerState> {
71
+ try {
72
+ return await (io.desktopPickerState ?? defaultDesktopPickerState)();
73
+ } catch {
74
+ return "unknown";
75
+ }
76
+ }
77
+
50
78
  export interface CodexRestartServiceIo {
51
79
  /** Desktop-restart seam, so a route test cannot terminate the developer's own Codex. */
52
80
  restartDesktopApp?: () => Promise<CodexDesktopRestartSummary>;
81
+ /** Whether the desktop picker predates the catalog; decides if the shell must restart. */
82
+ desktopPickerState?: () => Promise<DesktopPickerState>;
83
+ /** The catalog config.toml binds, read before and after the sync to catch a rebinding. */
84
+ catalogBinding?: () => Promise<string | null>;
53
85
  /** Process-layer seam, forwarded to every app-server-processes call. */
54
86
  processIo?: CodexAppServerProcessIo;
55
87
  /** Catalog refresh seam. Resolves to whether a catalog or cache write happened. */
@@ -133,6 +165,7 @@ async function runCodexRestart(io: CodexRestartServiceIo): Promise<CodexRestartR
133
165
  // roster; stopping app-servers before the write would hand the replacement the
134
166
  // same stale file it just lost.
135
167
  let synced = false;
168
+ const bindingBefore = await readBinding(io);
136
169
  try {
137
170
  const port = (io.listenPort ?? getServerListenPort)();
138
171
  synced = await (io.syncCatalog ?? defaultSyncCatalog)(port);
@@ -146,15 +179,33 @@ async function runCodexRestart(io: CodexRestartServiceIo): Promise<CodexRestartR
146
179
  (io.resetStateCache ?? resetCodexAppServerCatalogStateCache)();
147
180
  const before = (io.collectState ?? collectCodexAppServerCatalogState)(io.processIo ?? {});
148
181
 
149
- // Done here, before the early returns, because the model picker lives in the DESKTOP
150
- // app: "no app-server is running" is not a reason to leave a stale roster on screen,
151
- // and an operator who pressed restart still wants the app back on the current catalog.
182
+ // Decided here, before the early returns, because the model picker lives in the DESKTOP
183
+ // app: "no app-server is running" is not a reason to leave a stale roster on screen.
184
+ //
185
+ // The shell restarts only when its picker predates the catalog (#4761). Quitting it
186
+ // discards composer drafts, unsent picker selections and pending approvals, and when
187
+ // the picker is already current the app-server restart below is all the operator
188
+ // needs: the desktop respawns its app-server child, which rereads config and catalog.
189
+ // An unreadable picker state takes the full restart, the behaviour before #4761.
152
190
  //
153
191
  // allowHandoff is false and that is deliberate. The handoff waits for the CALLING
154
192
  // process to exit, and this runs inside a long-lived proxy that does not, so every
155
193
  // handoff started here would sit out its window and fail after the operator had
156
194
  // already been told it was handed off. An honest refusal beats that.
157
- const desktop = await (io.restartDesktopApp ?? defaultRestartDesktopApp)();
195
+ //
196
+ // A sync that rebinds model_catalog_json without rewriting the catalog bytes leaves the
197
+ // catalog mtime older than the shell, yet the shell loaded the previous binding. Seen
198
+ // across this request's own sync, that is proof of staleness, so it skips the probe.
199
+ const bindingAfter = await readBinding(io);
200
+ const rebound = bindingBefore === undefined || bindingAfter === undefined || bindingBefore !== bindingAfter;
201
+ const picker: DesktopPickerState = rebound ? "stale" : await readPickerState(io);
202
+ const desktop: CodexDesktopRestartSummary = picker === "current" || picker === "not_running"
203
+ ? {
204
+ attempted: false, stopped: [], surviving: [], relaunch: "skipped",
205
+ reason: picker === "current" ? "picker_current" : "no_targets",
206
+ pickerState: picker,
207
+ }
208
+ : { ...await (io.restartDesktopApp ?? defaultRestartDesktopApp)(), pickerState: picker };
158
209
 
159
210
  const nothingToDo = (): CodexRestartResponse => ({
160
211
  desktopApp: desktop,
@@ -10,6 +10,7 @@ import { ConfigMutationLockError, mutatePersistedConfig } from "../../config";
10
10
  import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
11
11
  import { isCodexAccountPaused, setCodexAccountPaused } from "../account-pause";
12
12
  import { codexAccountUsesCreditsAfterLimit } from "../account-credit-use";
13
+ import { codexAccountUsesRemainingQuota } from "../account-use-remaining";
13
14
  import { getCodexAccountPriority } from "../account-priority";
14
15
  import { getCodexAccountAutoSwitchThresholdOverride } from "../account-auto-switch";
15
16
  import { clearThreadAccountMapForAccount, isCodexAccountPlanExcluded, reconcileCodexActiveAfterExclusion } from "../routing";
@@ -143,6 +144,7 @@ export function poolAccountDto(
143
144
  priority,
144
145
  autoSwitchThresholdOverride: getCodexAccountAutoSwitchThresholdOverride(config, account.id),
145
146
  creditsAfterLimit: codexAccountUsesCreditsAfterLimit(config, account.id),
147
+ useRemainingQuota: codexAccountUsesRemainingQuota(config, account.id),
146
148
  quota: quota ? { ...quota } : null,
147
149
  ...codexCreditsDtoField(config, account.id, poolQuotaHistoryIdentity(account.id) ?? null),
148
150
  needsReauth: needsReauth || health.status === "reauth_required",
@@ -171,6 +173,7 @@ export interface CodexAuthAccountDto {
171
173
  autoSwitchThresholdOverride: number | null;
172
174
  /** False keeps the account out of selection while one of its usage windows is full. */
173
175
  creditsAfterLimit?: boolean;
176
+ useRemainingQuota?: boolean;
174
177
  quota: (StoredAccountQuota | (Omit<StoredAccountQuota, "updatedAt"> & { updatedAt: number })) | null;
175
178
  credits?: CodexCredits;
176
179
  needsReauth?: boolean;
@@ -405,6 +408,7 @@ export async function listCodexAuthAccountsSnapshot(
405
408
  priority: getCodexAccountPriority(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
406
409
  autoSwitchThresholdOverride: getCodexAccountAutoSwitchThresholdOverride(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
407
410
  creditsAfterLimit: codexAccountUsesCreditsAfterLimit(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
411
+ useRemainingQuota: codexAccountUsesRemainingQuota(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
408
412
  hasCredential: hasMainCredential,
409
413
  needsReauth: mainNeedsReauth,
410
414
  ...(mainReauthReason !== undefined ? { reauthReason: mainReauthReason } : {}),
@@ -1,10 +1,9 @@
1
+ import { observeSignedInCodexIdentity } from "../signed-in-identity";
1
2
  import { lstatSync } from "node:fs";
2
3
  import type { OcxConfig } from "../../types";
3
- import { inspectChatGptDomainClaim, extractEmail } from "../../oauth/chatgpt";
4
- import { MAIN_CODEX_ACCOUNT_ID, isSelectableCodexPoolAccount } from "../account-id";
5
4
  import { readCodexTokensResult } from "../auth-collision";
6
5
  import type { CodexTokenReadResult } from "../auth-collision";
7
- import { getCodexAccountCredential } from "../account-store";
6
+ import { linkedCodexAccountIds } from "../account-identity";
8
7
  import { resolveCodexHomeDir } from "../home";
9
8
  import { tryAcquireNativeMainProfileClaim } from "../native-main-admission";
10
9
  import { withNativeMainSharedClaim } from "../native-main-claim";
@@ -12,7 +11,6 @@ import { MAX_AUTH_BYTES, readBounded, resolveNativeProfileContext } from "../nat
12
11
  import { NativeProfileError } from "../native-profile-types";
13
12
  import { isNativeMainClaimUnavailable, nativeMainProfileBusyResponse } from "./http";
14
13
 
15
- type AccountIdentity = { accountId: string; email: string };
16
14
  type MainAuth = CodexTokenReadResult | { status: "api-key-only" };
17
15
 
18
16
  /** A valid API-key login has no ChatGPT identity; anything else unreadable stays unknown. */
@@ -32,50 +30,6 @@ function readMainAuth(authPath: string): MainAuth {
32
30
  }
33
31
  }
34
32
 
35
- function identity(accountId: unknown, email: unknown): AccountIdentity | undefined {
36
- const normalizedEmail = typeof email === "string" ? email.trim().toLowerCase() : undefined;
37
- return typeof accountId === "string" && accountId.trim() && normalizedEmail
38
- ? { accountId: accountId.trim(), email: normalizedEmail }
39
- : undefined;
40
- }
41
-
42
- /** Resolve only existing entries, under native-main ownership, never on the request path. */
43
- function linkedAccountIds(config: OcxConfig, selectedId: string, main: MainAuth): string[] | undefined {
44
- // Unknown main identity must not produce a successful but incomplete pause.
45
- if (main.status === "unreadable" || main.status === "invalid") return undefined;
46
- const identities = new Map<string, AccountIdentity | undefined>();
47
- if (main.status === "ok") {
48
- const tokens = main.tokens;
49
- if (typeof tokens.access_token !== "string" || typeof tokens.account_id !== "string"
50
- || (tokens.id_token !== undefined && typeof tokens.id_token !== "string")) return undefined;
51
- const claims = [tokens.id_token, tokens.access_token]
52
- .filter((token): token is string => typeof token === "string")
53
- .map(inspectChatGptDomainClaim);
54
- if (claims.some(claim => claim.kind === "invalid")) return undefined;
55
- const claimIds = claims.flatMap(claim => claim.kind === "valid" ? [claim.accountId] : []);
56
- // organizations[] describes memberships, not the selected workspace. Never use its first row.
57
- const accountId = tokens.account_id.trim() || claimIds[0];
58
- if (claimIds.some(claim => claim !== accountId)) return undefined;
59
- // Both tokens must name the same member; otherwise the bearer's identity is unknown.
60
- const emails = new Set([extractEmail(tokens.id_token), extractEmail(undefined, tokens.access_token)]
61
- .flatMap(email => email?.trim() ? [email.trim().toLowerCase()] : []));
62
- if (emails.size > 1) return undefined;
63
- identities.set(MAIN_CODEX_ACCOUNT_ID, identity(accountId, [...emails][0]));
64
- }
65
- for (const account of config.codexAccounts ?? []) {
66
- if (!isSelectableCodexPoolAccount(account)) continue;
67
- const credential = getCodexAccountCredential(account.id);
68
- identities.set(account.id, identity(credential?.chatgptAccountId, account.email));
69
- }
70
- const selected = identities.get(selectedId);
71
- // Missing identity evidence never links unrelated logins, including members of one workspace.
72
- if (!selected) return [selectedId];
73
- return [selectedId, ...[...identities].flatMap(([id, candidate]) => (
74
- id !== selectedId && candidate?.accountId === selected.accountId && candidate.email === selected.email
75
- ? [id] : []
76
- ))];
77
- }
78
-
79
33
  /** Hold admission throughout; claim the physical main whenever its home exists. */
80
34
  export async function withCodexAccountPauseGroup(
81
35
  config: OcxConfig,
@@ -92,13 +46,15 @@ export async function withCodexAccountPauseGroup(
92
46
  // Only positive absence permits Pool-only publication. A dangling symlink or access
93
47
  // failure is not absence; do not create a home just to coordinate an unused main login.
94
48
  if ((error as NodeJS.ErrnoException)?.code !== "ENOENT") return nativeMainProfileBusyResponse();
95
- const ids = linkedAccountIds(config, selectedId, { status: "missing" });
49
+ const ids = linkedCodexAccountIds(config, selectedId, { status: "missing" });
50
+ if (ids) observeSignedInCodexIdentity();
96
51
  return ids ? publish(ids) : nativeMainProfileBusyResponse();
97
52
  }
98
53
  const context = resolveNativeProfileContext({ codexHome });
99
54
  return await withNativeMainSharedClaim(context, async () => {
100
55
  const main = readMainAuth(context.authPath);
101
- const ids = linkedAccountIds(config, selectedId, main);
56
+ const ids = linkedCodexAccountIds(config, selectedId, main);
57
+ if (ids) observeSignedInCodexIdentity(main.status === "ok" ? main.tokens : undefined);
102
58
  return ids ? publish(ids) : nativeMainProfileBusyResponse();
103
59
  });
104
60
  } catch (error) {
@@ -0,0 +1,57 @@
1
+ import { getSignedInCodexIdentity } from "../signed-in-identity";
2
+ import type { OcxConfig } from "../../types";
3
+ import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
4
+ import { clearCodexAccountPin, pinnedCodexAccountId } from "../account-priority";
5
+ import { clearThreadAccountMapForAccount, previewCodexAccountForRequest } from "../routing";
6
+ import { forgetManualPreference } from "../routing/active-account";
7
+ import { withCodexAccountPauseGroup } from "./account-pause-group";
8
+ import { MAIN_CODEX_ACCOUNT_ID } from "../account-id";
9
+ import { getRuntimeConfig, saveRuntimeConfig } from "./runtime-config";
10
+ import { jsonResponse } from "./http";
11
+
12
+ /** Management owns the native-main claim; publish before releasing any runtime bindings. */
13
+ export async function handleCodexAccountPreservation(req: Request, config: OcxConfig): Promise<Response> {
14
+ let update: { enabled: boolean; applyToBound: boolean } | undefined;
15
+ if (req.method === "PUT") {
16
+ const body: unknown = await req.json().catch(() => null);
17
+ if (!body || typeof body !== "object" || Array.isArray(body)
18
+ || !("enabled" in body) || typeof body.enabled !== "boolean"
19
+ || Object.keys(body).some(key => key !== "enabled" && key !== "applyToBound")
20
+ || ("applyToBound" in body && typeof body.applyToBound !== "boolean")) {
21
+ return jsonResponse({ error: "enabled and optional applyToBound must be booleans" }, 400);
22
+ }
23
+ const applyToBound = "applyToBound" in body && body.applyToBound === true;
24
+ if (applyToBound && !body.enabled) return jsonResponse({ error: "applyToBound requires enabled: true" }, 400);
25
+ update = { enabled: body.enabled, applyToBound };
26
+ }
27
+ const runtime = getRuntimeConfig(config);
28
+ return withCodexAccountPauseGroup(runtime, MAIN_CODEX_ACCOUNT_ID, affectedAccountIds => {
29
+ const releasedPin = update?.applyToBound === true && affectedAccountIds.includes(pinnedCodexAccountId(runtime) ?? "");
30
+ if (update) {
31
+ const rollback = captureConfigTopLevelRollback(runtime, ["codexPreserveSignedInAccount", "activeCodexAccountPinned"]);
32
+ try {
33
+ runtime.codexPreserveSignedInAccount = update.enabled;
34
+ if (releasedPin) clearCodexAccountPin(runtime);
35
+ saveRuntimeConfig(config, runtime);
36
+ } catch (error) { rollback(); throw error; }
37
+ if (update.applyToBound) {
38
+ for (const id of affectedAccountIds) {
39
+ clearThreadAccountMapForAccount(id);
40
+ forgetManualPreference(id);
41
+ }
42
+ }
43
+ }
44
+ const enabled = runtime.codexPreserveSignedInAccount === true;
45
+ const nextAccountId = previewCodexAccountForRequest(null, runtime);
46
+ return jsonResponse({
47
+ ok: true, enabled, affectedAccountIds, identityKnown: getSignedInCodexIdentity() !== undefined,
48
+ nextAccountId,
49
+ nextUsesSignedInQuota: nextAccountId !== null && affectedAccountIds.includes(nextAccountId),
50
+ pinnedAccountId: pinnedCodexAccountId(runtime) ?? null,
51
+ bindings: update?.applyToBound ? "released-for-next-request" : "retained",
52
+ releasedPin,
53
+ priorityFailbackEnabled: runtime.codexAccountPriorityFailback === true,
54
+ fallbackConsumesSignedInQuota: true,
55
+ });
56
+ });
57
+ }
@@ -25,6 +25,7 @@ import type { PoolQuotaResult } from "./pool-quota-probe";
25
25
  import { fetchMainAccountInfoAttempt, fetchMainAccountInfo, MAIN_CACHE_TTL } from "./main-account-probe";
26
26
  import { fetchPoolAccountQuota, PoolQuotaProbeBusyError, POOL_CACHE_TTL, POOL_QUOTA_REFRESH_CONCURRENCY } from "./pool-quota-probe";
27
27
  import { getRuntimeConfig, configuredPoolAccount, mapWithConcurrency } from "./runtime-config";
28
+ import { trackBackgroundWork } from "../../lib/background-work";
28
29
 
29
30
  let primeInFlight: Promise<void> | null = null;
30
31
  let lastPriorityFailbackPrimeAt: number | undefined;
@@ -259,7 +260,9 @@ export async function primeCodexPoolQuotas(
259
260
  && now - lastPriorityFailbackPrimeAt < CODEX_PRIORITY_FAILBACK_REFRESH_MS) return;
260
261
  lastPriorityFailbackPrimeAt = now;
261
262
  }
262
- primeInFlight = (async () => {
263
+ // Tracked because most callers do not await it, yet it holds the native-main claim
264
+ // database open; whoever removes that directory settles it first (see background-work).
265
+ primeInFlight = trackBackgroundWork(`codex-pool-quota-prime:${reason}`, (async () => {
263
266
  const pool = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
264
267
  const stale = pool.filter(a => {
265
268
  const q = getAccountQuota(a.id);
@@ -344,7 +347,7 @@ export async function primeCodexPoolQuotas(
344
347
  if (process.env.OPENCODEX_DEBUG_QUOTA === "1") {
345
348
  console.warn(`[codex-quota] prime done (reason=${reason}, pool=${pool.length}, refreshed=${stale.length})`);
346
349
  }
347
- })().finally(() => { primeInFlight = null; });
350
+ })().finally(() => { primeInFlight = null; }));
348
351
  return primeInFlight;
349
352
  }
350
353
 
@@ -1,5 +1,7 @@
1
+ import { handleCodexAccountPreservation } from "./account-preservation";
1
2
  import { persistCodexAccountSelection } from "./account-selection";
2
3
  import { CODEX_ACCOUNT_LOG_LABEL_RE, codexAccountLogLabel } from "../account-label";
4
+ import { handleUseRemainingQuotaRoute } from "./use-remaining-route";
3
5
  import { poolQuotaHistoryIdentity, readCodexAccountRecord } from "../account-store";
4
6
  import { estimateCodexQuotaCapacity, insufficientCodexCapacity } from "../quota-capacity";
5
7
  import type { CodexCapacityResult } from "../quota-capacity";
@@ -34,6 +36,12 @@ export async function handleCodexAuthAPI(
34
36
  convergeCodexCatalog?: CodexAuthCatalogConvergence,
35
37
  principal?: import("../../server/management-auth").ManagementPrincipal,
36
38
  ): Promise<Response | null> {
39
+ if (url.pathname === "/api/codex-auth/preserve-signed-in" && (req.method === "GET" || req.method === "PUT")) {
40
+ return handleCodexAccountPreservation(req, config);
41
+ }
42
+ if (url.pathname === "/api/codex-auth/accounts/use-remaining" && (req.method === "GET" || req.method === "PUT")) {
43
+ return handleUseRemainingQuotaRoute(req, url, config);
44
+ }
37
45
  if (url.pathname === "/api/codex-auth/accounts" && req.method === "GET") {
38
46
  const forceRefresh = url.searchParams.get("refresh") === "1" || url.searchParams.get("refresh") === "true";
39
47
  return jsonResponse({ accounts: await listCodexAuthAccounts(config, forceRefresh) });
@@ -0,0 +1,36 @@
1
+ import type { OcxConfig } from "../../types";
2
+ import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
3
+ import { MAIN_CODEX_ACCOUNT_ID, isValidCodexAccountId } from "../account-id";
4
+ import { getEffectiveCodexAutoSwitchThreshold } from "../account-auto-switch";
5
+ import { codexAccountUsesRemainingQuota, setCodexAccountUseRemainingQuota } from "../account-use-remaining";
6
+ import { getMainAccountHardLockStatus } from "../main-account-hard-lock";
7
+ import { configuredPoolAccount, getRuntimeConfig, saveRuntimeConfig } from "./runtime-config";
8
+ import { jsonResponse } from "./http";
9
+
10
+ /** Called only behind the existing Codex management authentication boundary. */
11
+ export async function handleUseRemainingQuotaRoute(req: Request, url: URL, config: OcxConfig): Promise<Response> {
12
+ const parsed: unknown = req.method === "PUT" ? await req.json().catch(() => null) : { id: url.searchParams.get("id") };
13
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return jsonResponse({ error: "body must be an object" }, 400);
14
+ const body = parsed as { id?: unknown; useRemainingQuota?: unknown };
15
+ if (typeof body.id !== "string" || (body.id !== MAIN_CODEX_ACCOUNT_ID && !isValidCodexAccountId(body.id))) {
16
+ return jsonResponse({ error: "Invalid account id format" }, 400);
17
+ }
18
+ const runtime = getRuntimeConfig(config);
19
+ if (body.id !== MAIN_CODEX_ACCOUNT_ID && !configuredPoolAccount(runtime, body.id)) return jsonResponse({ error: "Account not found" }, 404);
20
+ if (req.method === "PUT") {
21
+ if (typeof body.useRemainingQuota !== "boolean") return jsonResponse({ error: "useRemainingQuota must be a boolean" }, 400);
22
+ const rollback = captureConfigTopLevelRollback(runtime, ["codexUseRemainingQuotaAccountIds"]);
23
+ try {
24
+ setCodexAccountUseRemainingQuota(runtime, body.id, body.useRemainingQuota);
25
+ saveRuntimeConfig(config, runtime);
26
+ } catch (error) {
27
+ rollback();
28
+ throw error;
29
+ }
30
+ }
31
+ return jsonResponse({ ok: true, id: body.id,
32
+ useRemainingQuota: codexAccountUsesRemainingQuota(runtime, body.id),
33
+ autoSwitchThreshold: getEffectiveCodexAutoSwitchThreshold(runtime, body.id),
34
+ ...(body.id === MAIN_CODEX_ACCOUNT_ID ? { mainAccountHardLock: getMainAccountHardLockStatus(runtime) } : {}),
35
+ });
36
+ }
@@ -33,7 +33,6 @@ export {
33
33
  } from "./auth-api/pool-mode-gate";
34
34
  export { createResetCreditWhamClient } from "./auth-api/reset-credit-service";
35
35
  export type { CodexAuthCatalogConvergence } from "./auth-api/login-flow";
36
- export { handleCodexAuthAPI } from "./auth-api/routes";
37
36
  import { getEffectiveActiveCodexAccountId } from "./routing";
38
37
  import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
39
38
  import type { OcxConfig } from "../types";
@@ -51,6 +51,7 @@ import {
51
51
  // The half-open TRANSIENT-HOLD lease (#4701). Not the quota-cooldown probe lease imported from
52
52
  // ./routing above -- different module, different domain, and a request never holds both.
53
53
  import { releaseTransientProbe } from "../routing/probe-lease";
54
+ import { trackBackgroundWork } from "../lib/background-work";
54
55
  import {
55
56
  codexConversationIdentity,
56
57
  recordCodexThreadLineage,
@@ -66,7 +67,8 @@ import {
66
67
  import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS, NATIVE_RESERVE_MODEL } from "./catalog/native-models";
67
68
  import type { CodexCooldownSource, CodexQuotaScope } from "./routing";
68
69
  import { maskAccountId } from "../lib/privacy";
69
- import { formatErrorResponse } from "../bridge";
70
+ import { formatErrorResponse, formatUsageLimitReachedResponse } from "../bridge";
71
+ import { nextCodexPoolRecoveryAt } from "./routing/pool-recovery";
70
72
  import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota, getMainPolicyQuota, parseUsageQuota, parseMainPolicyUsageQuota, setAccountQuotaFromParsed } from "./quota";
71
73
  import { codexAccountUsesCreditsAfterLimit, codexUsageLimitResetAt } from "./account-credit-use";
72
74
  import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
@@ -523,6 +525,11 @@ export class CodexAccountCooldownError extends Error {
523
525
 
524
526
  export class CodexMainAccountHardLockError extends CodexAccountCooldownError {
525
527
  readonly resetAt?: number;
528
+ /**
529
+ * Earliest known recovery of another stored pool account, set only when an unpinned pool
530
+ * selection reached main (#7046). A hint for the client's retry time; never routing input.
531
+ */
532
+ poolRecoveryAt?: number;
526
533
 
527
534
  constructor(resetAt?: number, thresholds = resolveMainAccountHardLockThresholds(undefined)) {
528
535
  super(MAIN_CODEX_ACCOUNT_ID, resetAt ?? 0);
@@ -625,7 +632,7 @@ export class CodexRecoveryWithheldError extends CodexAccountCooldownError {
625
632
 
626
633
  export type CodexAuthPolicyConfig = Readonly<Pick<OcxConfig,
627
634
  "codexMainAccountHardLock" | "codexMainAccountHardLockThresholds" | "codexDesktopAuthless" | "runtimeRole" | "pausedCodexAccountIds"
628
- | "creditCodexAccountIds" | "codexAccounts"
635
+ | "creditCodexAccountIds" | "codexAccounts" | "codexUseRemainingQuotaAccountIds"
629
636
  >>;
630
637
 
631
638
  interface CodexAuthMaterializationOptions {
@@ -776,7 +783,7 @@ function mainCreditsHoldResetAt(config: Pick<OcxConfig, "creditCodexAccountIds">
776
783
  }
777
784
 
778
785
  function assertMainAccountPolicy(
779
- config: Pick<OcxConfig, "codexMainAccountHardLock" | "codexMainAccountHardLockThresholds" | "creditCodexAccountIds"> | undefined,
786
+ config: Pick<OcxConfig, "codexMainAccountHardLock" | "codexMainAccountHardLockThresholds" | "creditCodexAccountIds" | "codexUseRemainingQuotaAccountIds"> | undefined,
780
787
  ): void {
781
788
  if (config) {
782
789
  const status = getMainAccountHardLockStatus(config);
@@ -935,16 +942,40 @@ export function cooldownErrorMessage(err: CodexAccountCooldownError, accountSele
935
942
  + ` (source: ${err.cooldownSource ?? "default"}).${recovery}`;
936
943
  }
937
944
 
945
+ /**
946
+ * The soonest a main hard-lock refusal is worth retrying: a sibling pool account's known
947
+ * recovery when it comes first, otherwise main's own reset (#7046). A pool hint is held to at
948
+ * least {@link MAIN_HARD_LOCK_MIN_POOL_RETRY_MS} so a hold about to lapse cannot turn the refusal
949
+ * into a tight retry loop. Null when neither time is known and still in the future.
950
+ */
951
+ export const MAIN_HARD_LOCK_MIN_POOL_RETRY_MS = 60_000;
952
+ export function mainHardLockRetryAt(err: CodexMainAccountHardLockError, now: number): number | null {
953
+ const future = (value: number | undefined): number | null =>
954
+ typeof value === "number" && Number.isFinite(value) && value > now ? value : null;
955
+ const main = future(err.resetAt);
956
+ const pool = future(err.poolRecoveryAt);
957
+ const poolRetryAt = pool === null ? null : Math.max(pool, now + MAIN_HARD_LOCK_MIN_POOL_RETRY_MS);
958
+ if (poolRetryAt !== null && (main === null || poolRetryAt < main)) return poolRetryAt;
959
+ return main;
960
+ }
961
+
938
962
  /** HTTP form of {@link cooldownErrorMessage}, carrying Retry-After for well-behaved clients. */
939
963
  export function cooldownErrorResponse(
940
964
  err: CodexAccountCooldownError,
941
965
  now = Date.now(),
942
966
  accountSelector?: string,
943
967
  ): Response {
968
+ if (err instanceof CodexMainAccountHardLockError) {
969
+ // A local policy refusal with a known horizon, not a transient throttle (#7046): Codex keeps
970
+ // a Goal active and sleeps through `rate_limit_exceeded` for the whole Retry-After, but
971
+ // stops on `usage_limit_reached` and shows the reset. The guard itself is unchanged.
972
+ const retryAt = mainHardLockRetryAt(err, now);
973
+ return formatUsageLimitReachedResponse(cooldownErrorMessage(err, accountSelector), retryAt,
974
+ retryAt === null ? null : Math.max(1, Math.ceil((retryAt - now) / 1000)));
975
+ }
944
976
  const res = formatErrorResponse(429, "rate_limit_error", cooldownErrorMessage(err, accountSelector));
945
977
  const headers = new Headers(res.headers);
946
- if (!(err instanceof CodexReserveUnavailableError)
947
- && (!(err instanceof CodexMainAccountHardLockError) || err.resetAt !== undefined)) {
978
+ if (!(err instanceof CodexReserveUnavailableError)) {
948
979
  headers.set("Retry-After", String(Math.max(1, Math.ceil((err.cooldownUntil - now) / 1000))));
949
980
  }
950
981
  return new Response(res.body, { status: res.status, headers });
@@ -1044,11 +1075,51 @@ async function awaitMainAccountPolicyBindingSettled(
1044
1075
  if (!settled) throw new CodexMainProfileDrainingError();
1045
1076
  }
1046
1077
 
1078
+ /**
1079
+ * Resolve the credential for one Codex turn. A main hard-lock reached through unpinned pool
1080
+ * selection is annotated with the earliest known pool recovery, so the refusal can name a retry
1081
+ * time that a sibling account may meet (#7046).
1082
+ */
1047
1083
  export async function resolveCodexAuthContext(
1048
1084
  headers: Headers,
1049
1085
  config: OcxConfig,
1050
1086
  mode: CodexAccountMode,
1051
1087
  options: ResolveCodexAuthContextOptions = {},
1088
+ ): Promise<CodexAuthContext> {
1089
+ // Filled by the inner resolver with the request's own fenced model evidence, so the hint obeys it.
1090
+ const eligibility: RecoveryHintEligibility = {};
1091
+ try {
1092
+ return await resolveCodexAuthContextUnannotated(headers, config, mode, options, eligibility);
1093
+ } catch (error) {
1094
+ if (error instanceof CodexMainAccountHardLockError && mode === "pool" && options.accountId === undefined
1095
+ && options.excludeAccountId !== MAIN_CODEX_ACCOUNT_ID
1096
+ && !requiresReserveAuthorization(options.codexAuthPolicy ?? config, options.modelId, options.admission)
1097
+ // An account-gated model with no resolved roster has no known eligible sibling.
1098
+ && (!options.modelId || !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)
1099
+ || eligibility.modelEligibleAccountIds !== undefined)) {
1100
+ try {
1101
+ const now = Date.now();
1102
+ error.poolRecoveryAt = nextCodexPoolRecoveryAt(config, now, codexQuotaScopeForModel(options.modelId), {
1103
+ deniedAccountIds: eligibility.deniedModelAccountIds,
1104
+ eligibleAccountIds: eligibility.modelEligibleAccountIds,
1105
+ });
1106
+ } catch { /* the hint is optional; the refusal stands without it */ }
1107
+ }
1108
+ throw error;
1109
+ }
1110
+ }
1111
+
1112
+ interface RecoveryHintEligibility {
1113
+ modelEligibleAccountIds?: ReadonlySet<string>;
1114
+ deniedModelAccountIds?: ReadonlySet<string>;
1115
+ }
1116
+
1117
+ async function resolveCodexAuthContextUnannotated(
1118
+ headers: Headers,
1119
+ config: OcxConfig,
1120
+ mode: CodexAccountMode,
1121
+ options: ResolveCodexAuthContextOptions = {},
1122
+ eligibility: RecoveryHintEligibility = {},
1052
1123
  ): Promise<CodexAuthContext> {
1053
1124
  const writerGeneration = captureConfigGeneration();
1054
1125
  const policy = options.codexAuthPolicy ?? config;
@@ -1256,6 +1327,7 @@ export async function resolveCodexAuthContext(
1256
1327
  const modelEligibleAccountIds = entitledAccountIds
1257
1328
  ? new Set([...entitledAccountIds].filter(candidate => !excludeAccountIds?.has(candidate)))
1258
1329
  : undefined;
1330
+ eligibility.modelEligibleAccountIds = modelEligibleAccountIds;
1259
1331
  // #4768: the flagships stay visible and never fail closed, so this is evidence routing may
1260
1332
  // ORDER by, not evidence it may refuse on. Read synchronously from rosters discovery has
1261
1333
  // already gathered -- no upstream fetch joins the request path for the most commonly
@@ -1273,6 +1345,7 @@ export async function resolveCodexAuthContext(
1273
1345
  undefined,
1274
1346
  { excludeAccountIds },
1275
1347
  );
1348
+ eligibility.deniedModelAccountIds = deniedModelAccountIds;
1276
1349
  const selectionOptions = {
1277
1350
  // Temporary switch drain keeps the candidate until the atomic claim rejects
1278
1351
  // it. Retained recovery makes main wholly ineligible so pool routing continues.
@@ -1475,9 +1548,10 @@ export async function resolveCodexAuthContext(
1475
1548
  if (options.primeCodexPoolQuotas) {
1476
1549
  void options.primeCodexPoolQuotas(config, reason).catch(() => {});
1477
1550
  } else {
1478
- import("./auth-api")
1551
+ // Tracked from the import on: the prime registers itself only once the module resolves.
1552
+ void trackBackgroundWork(`codex-pool-quota-prime-import:${reason}`, import("./auth-api")
1479
1553
  .then(({ primeCodexPoolQuotas }) => primeCodexPoolQuotas(config, reason))
1480
- .catch(() => {});
1554
+ .catch(() => {}));
1481
1555
  }
1482
1556
  }
1483
1557
  // Snapshot (not just the deadline) so a refused request can report WHY it is cooled:
@@ -35,6 +35,7 @@ export interface StartupHealthInputs {
35
35
  shimHealthy: boolean;
36
36
  platform: NodeJS.Platform;
37
37
  diagnosticStale?: boolean;
38
+ shimActivationPending?: boolean;
38
39
  routingAdoption?: RoutingAdoptionEvidence;
39
40
  }
40
41
 
@@ -55,6 +56,8 @@ export interface StartupHealth {
55
56
  serviceConflict: boolean;
56
57
  shimInstalled: boolean;
57
58
  shimHealthy: boolean;
59
+ /** Intact PATH-overlay shim this process could not see active; activate it rather than repair it. */
60
+ shimActivationPending?: boolean;
58
61
  shimCoverage: ShimCoverage;
59
62
  serviceSupported: boolean;
60
63
  platform: NodeJS.Platform;
@@ -171,6 +174,7 @@ export function collectStartupHealth(
171
174
  serviceSupported: service.supported,
172
175
  shimInstalled: shim.installed,
173
176
  shimHealthy: shim.healthy,
177
+ ...(shim.activationPending ? { shimActivationPending: true } : {}),
174
178
  platform: process.platform,
175
179
  ...(routingAdoption ? { routingAdoption } : {}),
176
180
  });
@@ -480,20 +480,37 @@ function configHasProviderAutoReview(config: Pick<OcxConfig, "providers">): bool
480
480
  provider.autoReviewModel !== undefined || provider.autoReviewModelOverrides !== undefined);
481
481
  }
482
482
 
483
+ /**
484
+ * The global override selector, when the operator enabled it and named a model.
485
+ *
486
+ * Occupies the same slot as Codex's root `auto_review_model` fallback and wins over it: an
487
+ * enabled Model-settings choice is the operator's explicit pick, while a disabled or blank block
488
+ * leaves the root selector in charge. Provider-scoped stamps keep their precedence separately.
489
+ */
490
+ export function resolveAutoReviewOverrideSelector(
491
+ config?: Pick<OcxConfig, "autoReviewOverride">,
492
+ ): string | undefined {
493
+ const block = config?.autoReviewOverride;
494
+ if (block?.enabled !== true) return undefined;
495
+ const model = typeof block.model === "string" ? block.model.trim() : "";
496
+ return model.length > 0 ? model : undefined;
497
+ }
498
+
483
499
  /** Apply the root Codex auto-review selector after the final catalog merge. */
484
500
  export function finalizeAutoReviewModelOverride(
485
501
  models: RawEntry[] | undefined,
486
502
  sourceModels: readonly RawEntry[] = [],
487
- config?: Pick<OcxConfig, "providers">,
503
+ config?: Pick<OcxConfig, "providers" | "autoReviewOverride">,
488
504
  ): AutoReviewModelOverrideResult {
489
505
  if (models && sourceModels.length > 0) preserveNativeAutoReviewModelOverrides(
490
506
  models.filter(entry => !isCodexControlPlaneModel(entry.slug)),
491
507
  sourceModels.filter(entry => !isCodexControlPlaneModel(entry.slug)),
492
508
  );
509
+ const rootValue = resolveAutoReviewOverrideSelector(config) ?? readConfiguredAutoReviewModel();
493
510
  if (config && configHasProviderAutoReview(config)) {
494
- return applyConfiguredAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), config, sourceModels);
511
+ return applyConfiguredAutoReviewModelOverride(models, rootValue, config, sourceModels);
495
512
  }
496
- return applyAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), sourceModels);
513
+ return applyAutoReviewModelOverride(models, rootValue, sourceModels);
497
514
  }
498
515
  /**
499
516
  * Why an account-gated native model stopped being offered, but only when the answer is one the