@bitkyc08/opencodex 2.83.0 → 2.86.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 (405) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-BjlttVLy.js +58 -0
  4. package/gui/dist/assets/Tray-DyR9yBee.js +1 -0
  5. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  6. package/gui/dist/assets/index-BMssP4k3.js +94 -0
  7. package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +2 -3
  10. package/src/adapters/anthropic/beta-allowlist.ts +87 -8
  11. package/src/adapters/anthropic/client-identity.ts +1 -1
  12. package/src/adapters/anthropic/passthrough.ts +33 -4
  13. package/src/adapters/azure.ts +19 -1
  14. package/src/adapters/client-fingerprint.ts +24 -0
  15. package/src/adapters/coding-agent/protocol.ts +15 -8
  16. package/src/adapters/coding-agent/turn.ts +22 -9
  17. package/src/adapters/cursor/arg-codec.ts +17 -5
  18. package/src/adapters/cursor/catalog.ts +8 -0
  19. package/src/adapters/cursor/effort-map.ts +3 -0
  20. package/src/adapters/cursor/freeform-args.ts +75 -0
  21. package/src/adapters/cursor/protobuf-events.ts +36 -23
  22. package/src/adapters/google-antigravity-wire.ts +22 -1
  23. package/src/adapters/google.ts +2 -2
  24. package/src/adapters/ollama-native.ts +29 -1
  25. package/src/adapters/openai-chat/messages.ts +8 -4
  26. package/src/adapters/openai-chat/response-events.ts +59 -1
  27. package/src/adapters/openai-chat.ts +8 -5
  28. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  29. package/src/adapters/openai-responses/passthrough.ts +3 -0
  30. package/src/adapters/openai-responses/request-strips.ts +10 -2
  31. package/src/adapters/qoder/adapter.ts +56 -15
  32. package/src/advisor/consult.ts +220 -0
  33. package/src/advisor/context.ts +226 -0
  34. package/src/advisor/disclosure.ts +20 -0
  35. package/src/advisor/runtime.ts +305 -0
  36. package/src/advisor/settings.ts +135 -0
  37. package/src/advisor/state.ts +369 -0
  38. package/src/advisor/synthetic-tool.ts +33 -0
  39. package/src/bridge/errors.ts +24 -0
  40. package/src/bridge.ts +1 -1
  41. package/src/chat/inbound.ts +30 -6
  42. package/src/chat/outbound.ts +5 -0
  43. package/src/claude/context-windows.ts +54 -1
  44. package/src/claude/desktop-3p-library.ts +43 -3
  45. package/src/claude/desktop-3p.ts +22 -8
  46. package/src/claude/desktop-picker-profile.ts +11 -4
  47. package/src/claude/desktop-profile.ts +10 -1
  48. package/src/claude/desktop-remote-store-io.ts +5 -3
  49. package/src/claude/inbound.ts +121 -50
  50. package/src/claude/intercept/connect-proxy.ts +10 -3
  51. package/src/claude/intercept/local-ca.ts +8 -1
  52. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  53. package/src/claude/intercept/picker-listener.ts +49 -1
  54. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  55. package/src/claude/intercept/settings.ts +101 -9
  56. package/src/claude/model-default-settings.ts +154 -0
  57. package/src/claude/model-info.ts +7 -7
  58. package/src/claude/outbound.ts +13 -6
  59. package/src/cli/account-api.ts +65 -0
  60. package/src/cli/account-auth.ts +26 -2
  61. package/src/cli/account.ts +41 -5
  62. package/src/cli/advisor.ts +151 -0
  63. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  64. package/src/cli/capabilities-access-remote.ts +8 -0
  65. package/src/cli/capabilities-agents-routing.ts +18 -0
  66. package/src/cli/capabilities-base.ts +14 -2
  67. package/src/cli/catalog-command-result.ts +2 -1
  68. package/src/cli/chatgpt-command.ts +35 -14
  69. package/src/cli/claude.ts +60 -11
  70. package/src/cli/config-command.ts +8 -2
  71. package/src/cli/debug.ts +8 -0
  72. package/src/cli/dispatch.ts +21 -4
  73. package/src/cli/doctor.ts +9 -0
  74. package/src/cli/ensure-desktop-cold-start.ts +329 -0
  75. package/src/cli/export-command.ts +15 -2
  76. package/src/cli/help.ts +2 -1
  77. package/src/cli/hub-disable.ts +99 -0
  78. package/src/cli/hub.ts +4 -2
  79. package/src/cli/index.ts +26 -8
  80. package/src/cli/integration-plan-dto.ts +2 -0
  81. package/src/cli/link-child.ts +104 -0
  82. package/src/cli/link-enrollment.ts +96 -0
  83. package/src/cli/link.ts +39 -24
  84. package/src/cli/observe-stream.ts +6 -1
  85. package/src/cli/observe.ts +10 -2
  86. package/src/cli/provider.ts +12 -0
  87. package/src/cli/registry.ts +20 -7
  88. package/src/cli/restart-scope.ts +3 -1
  89. package/src/cli/runtime-api.ts +27 -2
  90. package/src/cli/status.ts +104 -20
  91. package/src/client/connect.ts +39 -4
  92. package/src/client/hub-client.ts +14 -7
  93. package/src/client/hub-state.ts +25 -14
  94. package/src/client/link-home.ts +248 -0
  95. package/src/client/link-management-relay.ts +61 -0
  96. package/src/client/link-relay.ts +6 -1
  97. package/src/client/link-status.ts +14 -0
  98. package/src/client/machine-api.ts +14 -7
  99. package/src/client/machine-listener.ts +29 -4
  100. package/src/clients/config-export/contracts.ts +1 -0
  101. package/src/clients/config-export/model-metadata.ts +3 -2
  102. package/src/clients/config-export/qoder.ts +93 -0
  103. package/src/clients/config-export.ts +59 -16
  104. package/src/clients/omo-role-models.ts +6 -3
  105. package/src/codex/agent-role-models.ts +26 -8
  106. package/src/codex/auth-api/account-list.ts +17 -2
  107. package/src/codex/auth-api/account-selection.ts +17 -7
  108. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  109. package/src/codex/auth-api/routes.ts +5 -1
  110. package/src/codex/auth-context.ts +79 -5
  111. package/src/codex/autostart-health.ts +7 -1
  112. package/src/codex/catalog/build-entries.ts +11 -2
  113. package/src/codex/catalog/bundled.ts +5 -1
  114. package/src/codex/catalog/configured-tiers.ts +50 -0
  115. package/src/codex/catalog/effort.ts +6 -3
  116. package/src/codex/catalog/metadata.ts +4 -2
  117. package/src/codex/catalog/model-hints.ts +21 -3
  118. package/src/codex/catalog/native-models.ts +16 -2
  119. package/src/codex/catalog/native-ultrafast.ts +106 -0
  120. package/src/codex/catalog/parsing.ts +5 -0
  121. package/src/codex/catalog/provider-models.ts +4 -0
  122. package/src/codex/catalog/retained-sync.ts +2 -0
  123. package/src/codex/catalog/routed-gather.ts +22 -4
  124. package/src/codex/catalog/vendor-context.ts +82 -0
  125. package/src/codex/config-write-lock.ts +40 -2
  126. package/src/codex/convergence.ts +2 -0
  127. package/src/codex/desktop-app/types.ts +2 -0
  128. package/src/codex/desktop-app/windows.ts +29 -16
  129. package/src/codex/desktop-app-restart.ts +4 -2
  130. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  131. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  132. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  133. package/src/codex/desktop-compatibility/installed-build.ts +55 -0
  134. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  135. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  136. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  137. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  138. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  139. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  140. package/src/codex/desktop-compatibility/runtime.ts +173 -0
  141. package/src/codex/desktop-compatibility/service.ts +30 -0
  142. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  143. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  144. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  145. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  146. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  147. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  148. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  149. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  150. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  151. package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
  152. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  153. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  154. package/src/codex/history-provider.ts +1 -1
  155. package/src/codex/inject/config-write-section.ts +4 -1
  156. package/src/codex/inject/remove.ts +18 -6
  157. package/src/codex/journal.ts +16 -1
  158. package/src/codex/main-account.ts +99 -15
  159. package/src/codex/native-main-refresh-mode.ts +217 -0
  160. package/src/codex/native-profile-startup.ts +4 -1
  161. package/src/codex/prompt-journal.ts +20 -2
  162. package/src/codex/prompt-layers/encoding.ts +49 -0
  163. package/src/codex/prompt-layers/import-source.ts +30 -0
  164. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  165. package/src/codex/prompt-layers/toml-read.ts +52 -4
  166. package/src/codex/prompt-layers.ts +383 -20
  167. package/src/codex/prompt-lock-claim.ts +13 -3
  168. package/src/codex/prompt-lock-evidence.ts +3 -3
  169. package/src/codex/prompt-lock-owner.ts +14 -6
  170. package/src/codex/prompt-text-probe.ts +34 -5
  171. package/src/codex/quota.ts +13 -6
  172. package/src/codex/role-auto-assign.ts +2 -5
  173. package/src/codex/routing/active-account.ts +21 -0
  174. package/src/codex/routing/health-store.ts +1 -1
  175. package/src/codex/routing/pool-recovery.ts +79 -0
  176. package/src/codex/routing.ts +1 -1
  177. package/src/codex/runtime.ts +196 -8
  178. package/src/codex/shim-diagnostics.ts +6 -0
  179. package/src/codex/shim-overlay.ts +15 -6
  180. package/src/codex/shim.ts +1 -1
  181. package/src/combos/failover.ts +3 -0
  182. package/src/config/api-key-reconcile.ts +34 -0
  183. package/src/config/diagnostics.ts +18 -1
  184. package/src/config/feature-flags.ts +5 -0
  185. package/src/config/live-reconcile.ts +56 -29
  186. package/src/config/live-topology.ts +29 -0
  187. package/src/config/load-degrade.ts +8 -2
  188. package/src/config/paths.ts +13 -3
  189. package/src/config/salvage.ts +3 -1
  190. package/src/config/schema/config-schema.ts +19 -0
  191. package/src/config/schema/desktop-compatibility.ts +9 -0
  192. package/src/config/schema/leaf-validators.ts +1 -0
  193. package/src/generated/compatibility-version.json +713 -285
  194. package/src/generated/model-metadata.ts +1 -1
  195. package/src/grok/grpc-web.ts +23 -1
  196. package/src/grok/reset-coupon-ledger.ts +75 -5
  197. package/src/grok/reset-coupons.ts +4 -2
  198. package/src/integrations/catalog-refresh.ts +1 -1
  199. package/src/integrations/config-io.ts +20 -2
  200. package/src/integrations/journal.ts +15 -1
  201. package/src/integrations/mutation-plan.ts +14 -7
  202. package/src/integrations/registry.ts +52 -11
  203. package/src/integrations/state.ts +19 -7
  204. package/src/integrations/store.ts +3 -0
  205. package/src/integrations/target.ts +42 -5
  206. package/src/integrations/writer.ts +24 -7
  207. package/src/lib/advisor-activation.ts +16 -0
  208. package/src/lib/app-owned-memory-stores.ts +4 -0
  209. package/src/lib/app-owned-memory.ts +11 -0
  210. package/src/lib/background-work.ts +65 -0
  211. package/src/lib/bounded-file-read.ts +58 -0
  212. package/src/lib/bun-path-runtime.d.mts +10 -1
  213. package/src/lib/bun-path-runtime.mjs +62 -3
  214. package/src/lib/desktop-proxy-route.ts +30 -0
  215. package/src/lib/desktop-upstream-tunnel.ts +107 -0
  216. package/src/lib/errors.ts +7 -0
  217. package/src/lib/json-byte-size.ts +31 -0
  218. package/src/lib/local-internal-call-capability.ts +55 -0
  219. package/src/lib/optional-shutdown-hooks.ts +35 -0
  220. package/src/lib/outbound-credential-registry.ts +210 -0
  221. package/src/lib/package-tree-integrity.ts +2 -2
  222. package/src/lib/passive-read.ts +35 -0
  223. package/src/lib/shadow-call.ts +44 -5
  224. package/src/lib/socks5-fetch.ts +48 -71
  225. package/src/lib/socks5-handshake.ts +112 -0
  226. package/src/lib/translator-budget.ts +7 -1
  227. package/src/link/codex-settings.ts +155 -0
  228. package/src/link/management-routes.ts +89 -0
  229. package/src/link/remote-probe.ts +38 -0
  230. package/src/link/ssh-argv.ts +26 -5
  231. package/src/link/ssh-runner.ts +58 -27
  232. package/src/link/status-projection.ts +22 -4
  233. package/src/link/supervisor.ts +144 -49
  234. package/src/link/tunnel-state.ts +11 -4
  235. package/src/oauth/anthropic-continuity.ts +4 -3
  236. package/src/oauth/anthropic-identity.ts +13 -2
  237. package/src/oauth/anthropic-model-quota.ts +13 -1
  238. package/src/oauth/anthropic-routing.ts +230 -23
  239. package/src/oauth/anthropic.ts +18 -4
  240. package/src/oauth/generic-account-failover.ts +29 -5
  241. package/src/oauth/google-antigravity.ts +24 -7
  242. package/src/oauth/index.ts +28 -17
  243. package/src/oauth/kiro-account-identity.ts +106 -0
  244. package/src/oauth/kiro-device-login.ts +2 -1
  245. package/src/oauth/kiro.ts +55 -6
  246. package/src/oauth/plan.ts +29 -0
  247. package/src/oauth/store.ts +40 -3
  248. package/src/oauth/types.ts +2 -0
  249. package/src/plugins/loader.ts +11 -0
  250. package/src/protocols/codecs/chat-ir.ts +467 -0
  251. package/src/protocols/codecs/chat.ts +10 -1
  252. package/src/protocols/codecs/messages-ir.ts +500 -0
  253. package/src/protocols/features.ts +10 -2
  254. package/src/protocols/path.ts +11 -1
  255. package/src/protocols/plan-snapshot.ts +39 -16
  256. package/src/protocols/plan.ts +9 -1
  257. package/src/protocols/settings.ts +8 -0
  258. package/src/providers/anthropic-billing-ownership.ts +74 -0
  259. package/src/providers/anthropic-billing-recovery.ts +31 -0
  260. package/src/providers/anthropic-billing-refusal.ts +55 -0
  261. package/src/providers/anthropic-reset-admission.ts +62 -0
  262. package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
  263. package/src/providers/antigravity-account-roster.ts +102 -0
  264. package/src/providers/antigravity-models.ts +85 -7
  265. package/src/providers/api-key-resolve.ts +17 -2
  266. package/src/providers/key-failover.ts +52 -5
  267. package/src/providers/model-rename-fields.ts +2 -0
  268. package/src/providers/model-rename-migration.ts +5 -3
  269. package/src/providers/model-rename-startup.ts +5 -3
  270. package/src/providers/quota/account-cache.ts +11 -0
  271. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  272. package/src/providers/quota/report-cache.ts +14 -0
  273. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  274. package/src/providers/quota.ts +4 -1
  275. package/src/providers/registry/entries-core.ts +21 -20
  276. package/src/providers/registry/entries-extended.ts +19 -1
  277. package/src/providers/registry/model-seeds.ts +17 -0
  278. package/src/providers/request-pacing.ts +9 -0
  279. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  280. package/src/responses/compaction.ts +14 -0
  281. package/src/responses/parser.ts +37 -20
  282. package/src/responses/state.ts +32 -29
  283. package/src/routing/history/build-file.ts +92 -0
  284. package/src/routing/history/indexer.ts +248 -216
  285. package/src/routing/history/ingest.ts +238 -0
  286. package/src/routing/history/rebuild-job.ts +111 -0
  287. package/src/routing/history/rebuild-protocol.ts +15 -0
  288. package/src/routing/history/rebuild-worker.ts +34 -0
  289. package/src/server/admission-model-scope.ts +4 -1
  290. package/src/server/antigravity-direct-eligibility.ts +208 -0
  291. package/src/server/auth-cors.ts +46 -0
  292. package/src/server/chat-completions.ts +75 -9
  293. package/src/server/claude-messages.ts +110 -20
  294. package/src/server/configured-credentials.ts +194 -0
  295. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  296. package/src/server/index/link-management.ts +58 -0
  297. package/src/server/index/optional-listeners.ts +5 -1
  298. package/src/server/index/serve-options.ts +15 -2
  299. package/src/server/index/startup-warnings.ts +9 -0
  300. package/src/server/index/websocket-handler.ts +9 -0
  301. package/src/server/index.ts +13 -10
  302. package/src/server/inference/client-encoder-delivery.ts +6 -5
  303. package/src/server/lifecycle.ts +2 -1
  304. package/src/server/login-shell-path.ts +146 -0
  305. package/src/server/management/advisor-routes.ts +191 -0
  306. package/src/server/management/agent-settings-routes.ts +50 -14
  307. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  308. package/src/server/management/codex-agent-role-routes.ts +19 -6
  309. package/src/server/management/codex-prompt-routes.ts +136 -5
  310. package/src/server/management/codex-role-auto-assign.ts +1 -1
  311. package/src/server/management/companion-routes.ts +4 -0
  312. package/src/server/management/config-routes.ts +2 -2
  313. package/src/server/management/context.ts +4 -0
  314. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  315. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  316. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  317. package/src/server/management/grok-coupon-routes.ts +78 -27
  318. package/src/server/management/link-enrollment.ts +192 -0
  319. package/src/server/management/link-route-state.ts +185 -0
  320. package/src/server/management/link-routes.ts +82 -402
  321. package/src/server/management/model-routes.ts +2 -2
  322. package/src/server/management/model-rows.ts +3 -1
  323. package/src/server/management/protocol-settings-patch.ts +1 -0
  324. package/src/server/management/provider-overwrite-carry.ts +10 -0
  325. package/src/server/management/provider-routes.ts +1 -0
  326. package/src/server/management/route-registry.ts +16 -3
  327. package/src/server/management/shared.ts +3 -0
  328. package/src/server/management/sibling-guard.ts +1 -0
  329. package/src/server/management-api.ts +9 -1
  330. package/src/server/messages-antigravity-direct.ts +195 -0
  331. package/src/server/messages-native.ts +73 -43
  332. package/src/server/relay-eager.ts +2 -0
  333. package/src/server/request-log-filter.ts +6 -1
  334. package/src/server/request-log-terminal-status.ts +63 -6
  335. package/src/server/request-log.ts +7 -0
  336. package/src/server/responses/adapter-continuation.ts +35 -18
  337. package/src/server/responses/adapter-delivery.ts +24 -2
  338. package/src/server/responses/adapter-dispatch.ts +261 -81
  339. package/src/server/responses/advisor-plan-slot.ts +27 -0
  340. package/src/server/responses/advisor-slot.ts +336 -0
  341. package/src/server/responses/code-mode-collab-surface.ts +313 -0
  342. package/src/server/responses/codex-ws-exchange.ts +147 -6
  343. package/src/server/responses/codex-ws-wire.ts +203 -0
  344. package/src/server/responses/collaboration.ts +36 -7
  345. package/src/server/responses/compact.ts +126 -48
  346. package/src/server/responses/context-overflow.ts +13 -1
  347. package/src/server/responses/core-codex-account.ts +105 -1
  348. package/src/server/responses/core-combo-failure.ts +24 -1
  349. package/src/server/responses/core-combo.ts +28 -2
  350. package/src/server/responses/core-errors.ts +29 -1
  351. package/src/server/responses/core-lifetime.ts +2 -0
  352. package/src/server/responses/core-normalize.ts +11 -0
  353. package/src/server/responses/core-options.ts +48 -0
  354. package/src/server/responses/core-replay.ts +7 -4
  355. package/src/server/responses/core.ts +1 -1
  356. package/src/server/responses/deferred-collab-surface.ts +158 -0
  357. package/src/server/responses/deferred-send-reporter.ts +25 -0
  358. package/src/server/responses/fetch-helpers.ts +12 -1
  359. package/src/server/responses/passthrough-delivery.ts +20 -11
  360. package/src/server/responses/passthrough-dispatch.ts +29 -7
  361. package/src/server/responses/request-prepare.ts +74 -9
  362. package/src/server/responses/request-transport.ts +22 -8
  363. package/src/server/responses/run-turn-execution.ts +70 -8
  364. package/src/server/responses/sidecar-execution.ts +89 -5
  365. package/src/server/responses/terminal-guard.ts +2 -1
  366. package/src/server/responses/ws-upstream.ts +9 -1
  367. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  368. package/src/server/startup-action-control.ts +38 -4
  369. package/src/server/startup-health-cache.ts +5 -0
  370. package/src/server/system-env-shell.ts +7 -2
  371. package/src/server/system-env.ts +11 -1
  372. package/src/service/cli.ts +5 -1
  373. package/src/service/desktop-startup.ts +161 -34
  374. package/src/service/desktop-supervision.d.mts +27 -1
  375. package/src/service/desktop-supervision.mjs +111 -5
  376. package/src/service/health-sentinel-launch.ts +176 -0
  377. package/src/service/health-sentinel-main.ts +466 -0
  378. package/src/service/health-sentinel-policy.ts +241 -0
  379. package/src/service/health-sentinel-probe.ts +71 -0
  380. package/src/service/health-sentinel-windows.ts +87 -0
  381. package/src/service/linux-linger.ts +20 -0
  382. package/src/service/logging.ts +72 -0
  383. package/src/tray/windows-tray.ps1 +43 -24
  384. package/src/types/config.ts +68 -6
  385. package/src/types/provider.ts +7 -0
  386. package/src/types/request.ts +15 -0
  387. package/src/types/tools.ts +2 -0
  388. package/src/update/async-check.ts +1 -1
  389. package/src/update/badge.ts +2 -2
  390. package/src/update/index.ts +88 -15
  391. package/src/update/job-supervised-restart.ts +107 -0
  392. package/src/update/job.ts +32 -32
  393. package/src/update/notify.ts +1 -1
  394. package/src/update/refresh-scheduler.ts +2 -2
  395. package/src/update/supervised-recovery.d.mts +27 -0
  396. package/src/update/supervised-recovery.mjs +157 -0
  397. package/src/usage/cache-diagnostic.ts +2 -0
  398. package/src/usage/expected-prices.ts +16 -0
  399. package/src/usage/log.ts +19 -1
  400. package/src/web-search/gemini-executor.ts +2 -2
  401. package/assets/sponsors/packycode.png +0 -0
  402. package/gui/dist/assets/App-Ba8uG696.js +0 -54
  403. package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
  404. package/gui/dist/assets/index-BHRZdJuv.js +0 -94
  405. package/gui/dist/assets/quota-summary-DiJO6sYY.js +0 -1
@@ -24,6 +24,7 @@ import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "nod
24
24
  import { basename, dirname, isAbsolute, join, resolve, win32 } from "node:path";
25
25
  import { shouldInjectApiAuthHeader, standaloneCodexRoutingTarget } from "../codex/inject";
26
26
  import { FORMAT_MEDIA_TYPE, serializeDocument, type ConfigFormat } from "../integrations/serialize";
27
+ import { fileIO } from "../integrations/config-io";
27
28
  import { canonicalizeReasoningEfforts } from "../reasoning-effort";
28
29
  import { expandFastExportModels } from "./config-export/fast-models";
29
30
  import { probeHostname } from "../server/proxy-liveness";
@@ -81,6 +82,8 @@ import {
81
82
 
82
83
  export type { CommandCodeGeneratedConfig, CommandCodeModelEntry, CommandCodeProviderBlock };
83
84
  export { buildCommandCodeClientConfig, summarizeCommandCode, buildCommandCodeContribution, commandCodeProviderRoot };
85
+ import { buildQoderClientConfig, summarizeQoder, buildQoderContribution, qoderConfigPath } from "./config-export/qoder";
86
+ export { buildQoderClientConfig, qoderConfigPath, qoderHomeDir, type QoderGeneratedConfig } from "./config-export/qoder";
84
87
 
85
88
 
86
89
 
@@ -470,20 +473,49 @@ export function mcodeConfigPath(env: OpencodeLaunchEnv = process.env, home: stri
470
473
  return join(mcodeHomeDir(env, home), "config.yaml");
471
474
  }
472
475
 
473
- /**
474
- * ZCode (Z.ai's desktop client) keeps everything under `~/.zcode`; custom
475
- * providers live in `v2/config.json`. `ZCODE_DATA_DIR` mirrors the other
476
- * clients' override convention; relative overrides are refused for the same
477
- * reason as MCode's.
478
- */
479
- export function zcodeHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
480
- const override = env.ZCODE_DATA_DIR?.trim();
481
- if (override) return absoluteClientPath(override, home, "ZCODE_DATA_DIR");
482
- return join(home, ".zcode");
476
+ /** Bootstrap settings always stay at the default home, even after relocation. */
477
+ export type ZcodePathIO = {
478
+ readText: (path: string) => { kind: "text"; text: string } | { kind: "missing" } | { kind: "failed"; code?: string };
479
+ };
480
+ const zcodePathIO: ZcodePathIO = {
481
+ /** Shares the integration reader's four-MiB limit and regular-file-only bootstrap contract. */
482
+ readText: path => fileIO().readText(path),
483
+ };
484
+ export class ZcodeSettingsPathError extends ClientPathError {}
485
+
486
+ export function zcodeBootstrapSettingsPath(home: string = homedir()): string {
487
+ return join(home, ".zcode", "v2", "setting.json");
483
488
  }
484
489
 
485
- export function zcodeConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
486
- return join(zcodeHomeDir(env, home), "v2", "config.json");
490
+ /** The pre-relocation path is retained solely to recognize historical ownership. */
491
+ export function zcodeLegacyHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
492
+ const override = env.ZCODE_DATA_DIR?.trim();
493
+ return override ? absoluteClientPath(override, home, "ZCODE_DATA_DIR") : join(home, ".zcode");
494
+ }
495
+
496
+ /** ZCODE_DATA_DIR names the .zcode root; dataBaseDir names its parent. */
497
+ export function zcodeHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir(), io: ZcodePathIO = zcodePathIO): string {
498
+ if (env.ZCODE_DATA_DIR?.trim()) return zcodeLegacyHomeDir(env, home);
499
+ const bootstrap = zcodeBootstrapSettingsPath(home);
500
+ const read = io.readText(bootstrap);
501
+ if (read.kind === "missing") return join(home, ".zcode");
502
+ const unknown = () => new ZcodeSettingsPathError(
503
+ `ZCode's active data root is unknown: ${bootstrap} must be readable JSON with an absolute dataBaseDir (or no dataBaseDir). Repair the bootstrap setting before enabling or refreshing; no legacy provider will be applied.`,
504
+ );
505
+ if (read.kind !== "text") throw unknown();
506
+ let settings: unknown;
507
+ try { settings = JSON.parse(read.text); } catch { throw unknown(); }
508
+ if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw unknown();
509
+ const base = (settings as Record<string, unknown>).dataBaseDir;
510
+ if (base === undefined) return join(home, ".zcode");
511
+ // Match the native path's value, without cwd or tilde expansion. The native
512
+ // ZCODE_DATA_BASE_DIR selector is not an alias for our ZCODE_DATA_DIR.
513
+ if (typeof base !== "string" || !isAbsolute(base) || base.includes("\0")) throw unknown();
514
+ return join(base, ".zcode");
515
+ }
516
+
517
+ export function zcodeConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir(), io?: ZcodePathIO): string {
518
+ return join(zcodeHomeDir(env, home, io), "v2", "config.json");
487
519
  }
488
520
 
489
521
  /**
@@ -532,8 +564,7 @@ export function commandCodeConfigPath(env: OpencodeLaunchEnv = process.env, home
532
564
  * an install that has ever run, the import has already happened and never runs
533
565
  * again — every later write to `v2/config.json` is read by nobody (#5348).
534
566
  *
535
- * This project does not write this file; it names it so the integration can
536
- * tell whether its own write can still reach the client. The env override is
567
+ * The integration writes this store through its existing schema-aware writer. The env override is
537
568
  * ZCode's own (`ZCODE_PERSONAL_PROVIDER_CONFIG_FILE`), so an operator who
538
569
  * relocated the store is measured against the file their client actually opens
539
570
  * rather than the default location. A relative override is refused for the same
@@ -541,10 +572,10 @@ export function commandCodeConfigPath(env: OpencodeLaunchEnv = process.env, home
541
572
  * which file it names, and here that disagreement decides whether an apply is
542
573
  * reported as effective.
543
574
  */
544
- export function zcodeProviderStorePath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
575
+ export function zcodeProviderStorePath(env: OpencodeLaunchEnv = process.env, home: string = homedir(), io?: ZcodePathIO): string {
545
576
  const override = env.ZCODE_PERSONAL_PROVIDER_CONFIG_FILE?.trim();
546
577
  if (override) return absoluteClientPath(override, home, "ZCODE_PERSONAL_PROVIDER_CONFIG_FILE");
547
- return join(zcodeHomeDir(env, home), "v2", "provider_config.json");
578
+ return join(zcodeHomeDir(env, home, io), "v2", "provider_config.json");
548
579
  }
549
580
 
550
581
  /**
@@ -1399,6 +1430,18 @@ export function clineSettingsDir(env: OpencodeLaunchEnv = process.env, home: str
1399
1430
  }
1400
1431
 
1401
1432
  export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
1433
+ qoder: {
1434
+ id: "qoder",
1435
+ filename: "qoder-settings.json",
1436
+ destination: env => qoderConfigPath(env),
1437
+ apiKeyEnv: "",
1438
+ exportHint: "Qoder reads a non-secret placeholder from settings.json; loopback needs no key.",
1439
+ build: buildQoderClientConfig,
1440
+ format: "json",
1441
+ summarize: summarizeQoder,
1442
+ buildContribution: buildQoderContribution,
1443
+ loopbackOnly: true,
1444
+ },
1402
1445
  opencode: {
1403
1446
  id: "opencode",
1404
1447
  filename: "opencode.json",
@@ -120,10 +120,13 @@ function serialize(text: string, doc: JsonObject): string {
120
120
  /**
121
121
  * `reasoning` left undefined keeps the entry's level; null removes it, so a level LazyCodex cannot
122
122
  * express does not leave a stale one behind for its next install to apply.
123
+ * `model` left undefined keeps the entry's model, so an effort-only save of a role that inherits
124
+ * its model never adds a pin here. LazyCodex declares both keys optional, so an entry may carry
125
+ * `reasoning` alone; an absent entry is created only when there is a value to put in it.
123
126
  */
124
127
  export function writeOmoRoleModel(
125
128
  role: string,
126
- model: string,
129
+ model: string | undefined,
127
130
  path: string = omoJsoncPath(),
128
131
  reasoning?: string | null,
129
132
  ): OmoRoleModelWriteStatus {
@@ -137,10 +140,10 @@ export function writeOmoRoleModel(
137
140
  if (!isObject(agents)) return "invalid";
138
141
  const entry = agents[role] === undefined ? {} : agents[role];
139
142
  if (!isObject(entry)) return "invalid";
140
- const next: JsonObject = { ...entry, model };
143
+ const next: JsonObject = model === undefined ? { ...entry } : { ...entry, model };
141
144
  if (reasoning === null) delete next.reasoning;
142
145
  else if (reasoning !== undefined) next.reasoning = reasoning;
143
- if (entry.model === model && entry.reasoning === next.reasoning) return "unchanged";
146
+ if (entry.model === next.model && entry.reasoning === next.reasoning) return "unchanged";
144
147
  agents[role] = next;
145
148
  codex.agents = agents;
146
149
  doc[CODEX_SECTION] = codex;
@@ -1,11 +1,12 @@
1
1
  /**
2
- * The dashboard's write of one Codex agent role's model, and optionally its reasoning effort.
2
+ * The dashboard's write of one Codex agent role's model, its reasoning effort, or both.
3
3
  *
4
4
  * Codex overrides the spawn-time model with the root `model` pin in
5
5
  * `$CODEX_HOME/agents/<role>.toml`, so that pin is what decides a child's model. This module
6
- * edits exactly that value, plus the root `model_reasoning_effort` when a caller passes one, and
7
- * nothing else: every other byte of the file, including the instructions multiline string that
8
- * usually mentions `model =` in prose, is left as it was.
6
+ * edits exactly that value when a caller passes a model, plus the root `model_reasoning_effort`
7
+ * when a caller passes an effort, and nothing else: every other byte of the file, including the
8
+ * instructions multiline string that usually mentions `model =` in prose, is left as it was. An
9
+ * omitted model leaves the pin as it is, so a role that inherits its model keeps inheriting it.
9
10
  * It runs only on an explicit user action; no sync or startup path calls it.
10
11
  */
11
12
  import { lstatSync, readFileSync } from "node:fs";
@@ -97,6 +98,21 @@ export function setTomlRootReasoningEffort(content: string, effort: string): str
97
98
  return setTomlRootString(content, locateTomlReasoningEffortKey(content), "model_reasoning_effort", effort);
98
99
  }
99
100
 
101
+ export function removeTomlRootReasoningEffort(content: string): string {
102
+ const location = locateTomlReasoningEffortKey(content);
103
+ if (!location?.inRootTable) return content;
104
+ if (!location.span) {
105
+ throw new AgentRoleModelError("unsupported_model_value", "the existing model_reasoning_effort value is not a one-line string");
106
+ }
107
+ const lines = content.split("\n");
108
+ // A terminated line goes with its own newline. An unterminated last line is emptied instead,
109
+ // so the previous line keeps its terminator (and its \r in a CRLF file).
110
+ if (location.line < lines.length - 1) lines.splice(location.line, 1);
111
+ else lines[location.line] = "";
112
+ const bom = location.line === 0 && content.startsWith("\ufeff") ? "\ufeff" : "";
113
+ return bom + lines.join("\n");
114
+ }
115
+
100
116
  export function readCodexAgentRoleEffort(role: string, codexHome: string): string | null {
101
117
  try {
102
118
  const location = locateTomlReasoningEffortKey(readFileSync(roleFile(role, codexHome), "utf8"));
@@ -148,9 +164,9 @@ function assertValidRoleToml(role: string, text: string, stage: "before" | "afte
148
164
 
149
165
  export function writeCodexAgentRoleModel(
150
166
  role: string,
151
- model: string,
167
+ model: string | undefined,
152
168
  codexHome: string,
153
- effort?: string,
169
+ effort?: string | null,
154
170
  ): { status: "written" | "unchanged" } {
155
171
  requireKnownRole(role, codexHome);
156
172
  const path = roleFile(role, codexHome);
@@ -159,10 +175,12 @@ export function writeCodexAgentRoleModel(
159
175
  }
160
176
  const before = readFileSync(path, "utf8");
161
177
  assertValidRoleToml(role, before, "before");
162
- const withModel = setTomlRootModel(before, validateAgentRoleModel(model));
178
+ const withModel = model === undefined ? before : setTomlRootModel(before, validateAgentRoleModel(model));
163
179
  const after = effort === undefined
164
180
  ? withModel
165
- : setTomlRootReasoningEffort(withModel, validateAgentRoleEffort(effort));
181
+ : effort === null
182
+ ? removeTomlRootReasoningEffort(withModel)
183
+ : setTomlRootReasoningEffort(withModel, validateAgentRoleEffort(effort));
166
184
  if (after === before) return { status: "unchanged" };
167
185
  assertValidRoleToml(role, after, "after");
168
186
  assertIntegrationWriteOwnership(path);
@@ -33,6 +33,7 @@ import { fetchMainAccountInfoAttempt, EMPTY_MAIN_ACCOUNT_INFO, mainResetCreditsF
33
33
  import { fetchPoolAccountQuota, PoolQuotaProbeBusyError, POOL_QUOTA_REFRESH_CONCURRENCY } from "./pool-quota-probe";
34
34
  import type { PoolQuotaResult } from "./pool-quota-probe";
35
35
  import { getRuntimeConfig, configuredPoolAccount, mapWithConcurrency } from "./runtime-config";
36
+ import { isPassiveRead } from "../../lib/passive-read";
36
37
 
37
38
  export function quotaForPlan<T extends Omit<StoredAccountQuota, "updatedAt"> | StoredAccountQuota | null>(
38
39
  quota: T,
@@ -274,13 +275,25 @@ export async function listCodexAuthAccountsSnapshot(
274
275
  pruneCodexCredits([MAIN_CODEX_ACCOUNT_ID, ...poolAccounts.map(account => account.id)]);
275
276
  // One redaction decision for the whole snapshot, read once from the operator's config (#3859).
276
277
  const maskEmails = emailMaskingEnabled(runtimeConfig);
277
- const mainResult = await fetchMainAccountInfoAttempt(forceRefresh, 1, undefined, false,
278
- forceRefresh, false, runtimeConfig);
278
+ // A passive read (a Remote Link Child's, #6959) reports what this machine already observed: no
279
+ // token refresh, no WHAM probe, and so none of the plan or credential writes they make.
280
+ const passive = isPassiveRead();
281
+ const mainResult: Awaited<ReturnType<typeof fetchMainAccountInfoAttempt>> = passive
282
+ ? { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: false, hasCredential: false,
283
+ identityGeneration: captureMainAccountIdentityGeneration() }
284
+ : await fetchMainAccountInfoAttempt(forceRefresh, 1, undefined, false, forceRefresh, false, runtimeConfig);
279
285
  const refreshedPool = await mapWithConcurrency(poolAccounts, POOL_QUOTA_REFRESH_CONCURRENCY, async account => {
280
286
  const cred = getCodexAccountCredential(account.id);
281
287
  let quotaResult: PoolQuotaResult;
282
288
  if (!cred) {
283
289
  quotaResult = { quota: null, needsReauth: true };
290
+ } else if (passive) {
291
+ quotaResult = {
292
+ quota: getAccountQuota(account.id),
293
+ needsReauth: false,
294
+ credentialGeneration: readCodexAccountRecord(account.id)?.generation,
295
+ quotaProbeSkipped: true,
296
+ };
284
297
  } else {
285
298
  try {
286
299
  // Credits are process-local while pool quota is hydrated from disk, so right after a
@@ -410,6 +423,8 @@ export async function listCodexAuthAccountsSnapshot(
410
423
  };
411
424
  // Project the refusal and DTO in the same owned synchronous observation. A same-account grant
412
425
  // replacement during earlier pool probes must not inherit the previous grant's refusal.
426
+ // A passive read opens no claim database (its lock file is hardened on open).
427
+ if (passive) return projectSnapshot();
413
428
  const projectionLease = tryAcquireNativeMainProfileClaim();
414
429
  if (!projectionLease) return projectSnapshot();
415
430
  try {
@@ -6,11 +6,11 @@ import { deleteConfigTopLevelKey, fileBackedConfigPath, recordFileBackedConfig }
6
6
  import { MAIN_CODEX_ACCOUNT_ID, hasLegacyMainCodexPoolAccount, isSelectableCodexPoolAccount, isValidCodexAccountId } from "../account-id";
7
7
  import { isCodexAccountPaused } from "../account-pause";
8
8
  import { readCodexAccountRecord } from "../account-store";
9
- import { codexAccountPinDrainReason, resetCodexRoutingForManualSelection } from "../routing";
9
+ import { codexAccountPinDrainReason, resetCodexRoutingForClearedSelection, resetCodexRoutingForManualSelection } from "../routing";
10
10
  import { jsonResponse } from "./http";
11
11
 
12
12
  class SelectionRejected extends Error {
13
- constructor(message: string, readonly status: number) { super(message); }
13
+ constructor(message: string, readonly status: number, readonly code?: string) { super(message); }
14
14
  }
15
15
 
16
16
  function validateSelection(config: OcxConfig, accountId: string | null): void {
@@ -42,8 +42,14 @@ function applySelection(config: OcxConfig, accountId: string | null): boolean {
42
42
  return changed;
43
43
  }
44
44
 
45
+ function validateExpectedPin(config: OcxConfig, expectedPinnedAccountId: string | undefined): void {
46
+ if (expectedPinnedAccountId !== undefined && config.activeCodexAccountPinned !== expectedPinnedAccountId) {
47
+ throw new SelectionRejected("The pin changed; reload accounts before releasing it", 409, "account_pin_changed");
48
+ }
49
+ }
50
+
45
51
  /** A manual selection is an explicit command, even when equal to the live merge baseline. */
46
- export function persistCodexAccountSelection(config: OcxConfig, accountId: string | null): Response {
52
+ export function persistCodexAccountSelection(config: OcxConfig, accountId: string | null, expectedPinnedAccountId?: string): Response {
47
53
  const unknown = () => jsonResponse({
48
54
  error: "Account selection could not be confirmed; reload settings before retrying",
49
55
  code: "account_selection_unavailable",
@@ -63,7 +69,9 @@ export function persistCodexAccountSelection(config: OcxConfig, accountId: strin
63
69
  const normalized = validateConfigCandidate(mergeConfigDefaults(config));
64
70
  if (!normalized.ok) return unknown();
65
71
  const initial = normalized.config;
66
- applySelection(initial, accountId);
72
+ validateExpectedPin(initial, expectedPinnedAccountId);
73
+ // Conditional release must keep the baseline pin until the locked mutation checks it.
74
+ if (expectedPinnedAccountId === undefined) applySelection(initial, accountId);
67
75
  initializePersistedConfigIfMissing(initial);
68
76
  }
69
77
  const result = mutatePersistedConfig(persisted => {
@@ -71,12 +79,13 @@ export function persistCodexAccountSelection(config: OcxConfig, accountId: strin
71
79
  // A removed or paused account must not be resurrected by a stale UI snapshot.
72
80
  validateSelection(config, accountId);
73
81
  validateSelection(persisted, accountId);
82
+ validateExpectedPin(persisted, expectedPinnedAccountId);
74
83
  return { changed: applySelection(persisted, accountId), value: persisted };
75
84
  });
76
85
  if (result.status === "unavailable") return unknown();
77
86
  committed = result.value;
78
87
  } catch (error) {
79
- if (error instanceof SelectionRejected) return jsonResponse({ error: error.message }, error.status);
88
+ if (error instanceof SelectionRejected) return jsonResponse({ error: error.message, ...(error.code ? { code: error.code } : {}) }, error.status);
80
89
  // Atomic publication may succeed before generation/bookkeeping fails. Confirm the exact
81
90
  // durable selection instead of rolling live state back behind an already published write.
82
91
  const snapshot = readConfigFileSnapshot();
@@ -95,12 +104,13 @@ export function persistCodexAccountSelection(config: OcxConfig, accountId: strin
95
104
  adoptPersistedCodexAccountSelection(config, committed);
96
105
  recordFileBackedConfig(config, configPath);
97
106
  // No failed save may clear affinity, spend the manual preference, or move the RR cursor.
98
- resetCodexRoutingForManualSelection(accountId ?? MAIN_CODEX_ACCOUNT_ID);
107
+ if (accountId === null) resetCodexRoutingForClearedSelection();
108
+ else resetCodexRoutingForManualSelection(accountId);
99
109
  const pinDrainReason = accountId === null ? undefined : codexAccountPinDrainReason(config, accountId);
100
110
  return jsonResponse({
101
111
  ok: true,
102
112
  activeCodexAccountId: config.activeCodexAccountId ?? null,
103
- appliesImmediately: true,
113
+ appliesImmediately: accountId !== null,
104
114
  ...(pinDrainReason !== undefined ? { pinDrained: true, pinDrainReason } : {}),
105
115
  ...(warning ? { warning: "config_bookkeeping_failed" } : {}),
106
116
  });
@@ -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
 
@@ -254,7 +254,11 @@ export async function handleCodexAuthAPI(
254
254
  || !("accountId" in body) || (body.accountId !== null && typeof body.accountId !== "string")) {
255
255
  return jsonResponse({ error: "accountId must be an account id or null" }, 400);
256
256
  }
257
- return persistCodexAccountSelection(getRuntimeConfig(config), body.accountId);
257
+ const expectedPin = "expectedPinnedAccountId" in body ? body.expectedPinnedAccountId : undefined;
258
+ if (expectedPin !== undefined && (body.accountId !== null || !isCodexAccountPriorityKey(expectedPin))) {
259
+ return jsonResponse({ error: "expectedPinnedAccountId must be a valid account id for a release" }, 400);
260
+ }
261
+ return persistCodexAccountSelection(getRuntimeConfig(config), body.accountId, expectedPin as string | undefined);
258
262
  }
259
263
 
260
264
  if (url.pathname === "/api/codex-auth/active" && req.method === "GET") {
@@ -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);
@@ -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
  });
@@ -215,7 +219,9 @@ function classifyStartupHealthSummary(health: StartupHealth): string {
215
219
  ? "custom remote Codex routing (no local restart dependency)"
216
220
  : "native Codex routing (no opencodex restart dependency)";
217
221
  if (health.protection === "service") return "protected by background service";
218
- if (health.protection === "desktop") return "protected by desktop app at login and its proxy supervisor";
222
+ if (health.protection === "desktop") return health.desktop?.loginRegistration === "registered-not-loaded"
223
+ ? "protected by desktop app at next login and its proxy supervisor (Start at Login registered; not loaded in this session)"
224
+ : "protected by desktop app at login and its proxy supervisor";
219
225
  const command = health.recommendedCommand ?? health.commands.restoreNative;
220
226
  if (health.routingKind === "unknown") return `AT RISK after restart (Codex routing could not be verified; run '${command}')`;
221
227
  if (health.routingKind === "custom-local") return `AT RISK after restart (custom local gateway lifecycle is not managed by opencodex; run '${command}')`;
@@ -48,7 +48,8 @@ import { NATIVE_RESERVE_MODEL } from "./native-models";
48
48
  import { isReserveCatalogProjection, type ReserveCatalogProjection } from "./reserve";
49
49
  import { deriveEntry, finishUpstreamNativeEntry, isExactComboCatalogEntry } from "./derive-entry";
50
50
  import { PICKER_ORDER_PRIORITY_BASE, SPAWN_PRIORITY_FIELD } from "./subagent-roster";
51
- import { preserveOperatorUltraFastTiers } from "./operator-tiers";
51
+ import { applyConfiguredUltraFast } from "./configured-tiers";
52
+ import { applyNativeUltraFastTier } from "./native-ultrafast";
52
53
 
53
54
  export interface ObservedCatalogEntryBuildInput {
54
55
  readonly template: RawEntry | null;
@@ -473,6 +474,8 @@ export function applyFullModelPickerOrder(entries: RawEntry[], order: readonly s
473
474
  export interface ObservedCatalogMergeInput {
474
475
  /** Preserve operator-supplied Ultra Fast only from the exact persisted routed row. */
475
476
  readonly ultraFastTier?: boolean;
477
+ /** Current captured wire declarations; disk rows and templates never populate this set. */
478
+ readonly configuredUltraFastSlugs?: ReadonlySet<string>;
476
479
  readonly catalogModels: readonly RawEntry[];
477
480
  readonly baselineCatalogModels: readonly RawEntry[];
478
481
  readonly routedEntries: readonly RawEntry[];
@@ -514,6 +517,7 @@ export interface ObservedCatalogMergeInput {
514
517
  */
515
518
  export function mergeCatalogEntriesFromObservedState({
516
519
  ultraFastTier = false,
520
+ configuredUltraFastSlugs = new Set(),
517
521
  catalogModels,
518
522
  baselineCatalogModels,
519
523
  routedEntries,
@@ -865,7 +869,7 @@ export function mergeCatalogEntriesFromObservedState({
865
869
  if (typeof entry.slug !== "string" || !entry.slug.includes("/")
866
870
  || isNativeAliasCatalogEntry(entry) || isExactComboCatalogEntry(entry, exactComboSlugs)
867
871
  || trustedAccountBoundNativeCatalogSlug(entry) !== undefined) continue;
868
- preserveOperatorUltraFastTiers(entry, operatorRows.get(entry.slug), ultraFastTier);
872
+ applyConfiguredUltraFast(entry, operatorRows.get(entry.slug), configuredUltraFastSlugs.has(entry.slug), ultraFastTier);
869
873
  }
870
874
  const finalRoutedEntrySet = new Set(finalRoutedEntries);
871
875
  const degradedPreservedCount = preservedRoutedEntries.filter(entry => {
@@ -932,6 +936,11 @@ export function mergeCatalogEntriesFromObservedState({
932
936
  );
933
937
  }
934
938
  }
939
+ // Native Astra/Sol rows (bare and account-bound) gain or lose the documented Ultrafast
940
+ // choice with the switch; aliases, combos and reserve projections keep their own tiers.
941
+ if (!reserveProjection && !exactCombo && m.owned_by !== COMBO_NAMESPACE && !isNativeAliasCatalogEntry(e)) {
942
+ applyNativeUltraFastTier(e, ultraFastTier);
943
+ }
935
944
  if (wsEnabled) e.supports_websockets = true;
936
945
  else {
937
946
  delete e.supports_websockets;
@@ -28,6 +28,7 @@ import {
28
28
  import type { NormalizedComboConfig } from "../../combos/types";
29
29
  import { providerDestinationResolvedError } from "../../lib/destination-policy";
30
30
  import { redactSecretString } from "../../lib/redact";
31
+ import { isPassiveRead } from "../../lib/passive-read";
31
32
  import upstreamModelsSnapshot from "../data/upstream-models.json";
32
33
  import { decodeOverlayState } from "../shim-state-file";
33
34
 
@@ -313,7 +314,8 @@ function* bundledCatalogSteps(
313
314
  const output = yield command;
314
315
  if (!current()) return null;
315
316
  const catalog = output === null ? null : parseCatalogJson(output);
316
- if (catalog && findNativeTemplate(catalog)) {
317
+ if (catalog && catalog.models?.every(row => row !== null && typeof row === "object" && !Array.isArray(row))
318
+ && findNativeTemplate(catalog)) {
317
319
  if (cacheKey) {
318
320
  publishBundledCatalogCache(
319
321
  cacheKey,
@@ -377,6 +379,8 @@ export function loadBundledCodexCatalogAsync(
377
379
  if (useCache && bundledCatalogFlight?.inputs === inputs && bundledCatalogFlight.epoch === epoch) {
378
380
  return view(bundledCatalogFlight.promise);
379
381
  }
382
+ // A passive read (a Remote Link Child's, #6959) runs no Codex binary and persists no runtime.
383
+ if (isPassiveRead()) return Promise.resolve(null);
380
384
  const controller = new AbortController();
381
385
  const flight = { inputs, epoch, controller, promise: Promise.resolve<ReadonlyRawCatalog | null>(null) };
382
386
  if (useCache && !signal && isCurrent === catalogAlwaysCurrent) {