@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
@@ -72,10 +72,25 @@ export type { AnthropicAccountPoolConfig } from "../types/anthropic-account-pool
72
72
 
73
73
  /** Local admission refusal across async dispatch; never treat it as provider reachability evidence. */
74
74
  export class AnthropicAccountCooldownError extends Error {
75
- constructor(readonly retryAfterSeconds: number | null, readonly routePosition?: number) {
76
- super(routePosition === undefined
77
- ? "All Anthropic OAuth accounts are temporarily rate-limited"
78
- : "Anthropic OAuth accounts for this model route are temporarily rate-limited");
75
+ /** The retryable wording, kept unchanged for combo children: combo failover classifies by prose too. */
76
+ readonly retryableMessage: string;
77
+ constructor(
78
+ readonly retryAfterSeconds: number | null,
79
+ readonly routePosition?: number,
80
+ /**
81
+ * Set only when no account the route can use is admissible and every held one is out on
82
+ * spent quota (#6991). `resetsAt` is the earliest upstream window reset among them (epoch
83
+ * ms), or null when any held account's reset is unknown. Null overall means at least one hold
84
+ * is a transient throttle or an entitlement refusal, which a client retry may outlast.
85
+ */
86
+ readonly usageLimit: { readonly resetsAt: number | null } | null = null,
87
+ ) {
88
+ const scope = routePosition === undefined ? "All Anthropic OAuth accounts" : "Anthropic OAuth accounts for this model route";
89
+ const retryable = `${scope} are temporarily rate-limited`;
90
+ super(usageLimit === null ? retryable
91
+ : usageLimit.resetsAt === null ? `${scope} have reached their usage limit`
92
+ : `${scope} have reached their usage limit; the first one resets at ${new Date(usageLimit.resetsAt).toISOString()}`);
93
+ this.retryableMessage = retryable;
79
94
  this.name = "AnthropicAccountCooldownError";
80
95
  }
81
96
  }
@@ -89,7 +104,7 @@ export class AnthropicAccountCooldownError extends Error {
89
104
  * `retry-after` would report a drained five-hour window as request-rate throttling.
90
105
  */
91
106
  type AnthropicCooldownSource = "retry-after" | "reset-derived" | "default";
92
- type AnthropicQuotaWindow = "five-hour" | "weekly";
107
+ export type AnthropicQuotaWindow = "five-hour" | "weekly";
93
108
 
94
109
  interface AccountHealth {
95
110
  cooldownUntil: number;
@@ -98,6 +113,24 @@ interface AccountHealth {
98
113
  rejectedQuotaWindows?: AnthropicQuotaWindow[];
99
114
  /** Monotonic fence so an older quota probe cannot erase a newer refusal. */
100
115
  cooldownGeneration: number;
116
+ /** When the refusal was recorded; a reset settled earlier must not lift it. */
117
+ recordedAt: number;
118
+ /** 429 (shared quota) or 403 (entitlement); only a quota refusal can be lifted by a reset grant. */
119
+ refusalStatus: 429 | 403;
120
+ /** Windows a 429 reported as rejected, whatever set the deadline; empty when it named none. */
121
+ refusedQuotaWindows: AnthropicQuotaWindow[];
122
+ /**
123
+ * When the rejected windows reopen according to upstream's reset headers, kept even when
124
+ * Retry-After set the deadline. A usage-limit answer reports this, never an admission deadline.
125
+ */
126
+ quotaResetAt?: number;
127
+ }
128
+
129
+ /** See `anthropicAccountHold`: the latest hold on one account and its known window reset. */
130
+ interface AnthropicAccountHold {
131
+ until: number;
132
+ quota: boolean;
133
+ resetAt?: number;
101
134
  }
102
135
 
103
136
  interface AffinityEntry {
@@ -127,6 +160,14 @@ function parseRetryAfterMs(value: string | null | undefined, now: number): numbe
127
160
  return delayUntil(Date.parse(text), now);
128
161
  }
129
162
 
163
+ /** Windows whose unified status a refusal reported as rejected, independent of any reset header. */
164
+ function rejectedQuotaWindowsIn(headers: AnthropicRateLimitHeaders | null | undefined): AnthropicQuotaWindow[] {
165
+ if (!headers) return [];
166
+ return ([["5h", "five-hour"], ["7d", "weekly"]] as const)
167
+ .filter(([window]) => headers.get(`anthropic-ratelimit-unified-${window}-status`)?.trim() === "rejected")
168
+ .map(([, quotaWindow]) => quotaWindow);
169
+ }
170
+
130
171
  function parseRateLimitReset(
131
172
  headers: AnthropicRateLimitHeaders | null | undefined,
132
173
  now: number,
@@ -152,6 +193,32 @@ function parseRateLimitReset(
152
193
  return { delayMs: latest - now, rejectedQuotaWindows };
153
194
  }
154
195
 
196
+ /**
197
+ * When every window a refusal reported as rejected reopens, per upstream's reset headers; undefined
198
+ * unless each rejected window carries a usable reset. Unlike `parseRateLimitReset`, which picks the
199
+ * best available admission deadline, this is reported to the client as the usage-limit reset, so a
200
+ * weekly window with no reset cannot hide behind a five-hour one that has one.
201
+ */
202
+ function rejectedWindowsResetAt(headers: AnthropicRateLimitHeaders | null | undefined, now: number): number | undefined {
203
+ if (!headers) return undefined;
204
+ const usableReset = (header: string): number | undefined => {
205
+ const resetAt = Number(headers.get(header)?.trim()) * 1000;
206
+ return Number.isFinite(resetAt) && delayUntil(resetAt, now) !== undefined ? resetAt : undefined;
207
+ };
208
+ const rejected = rejectedQuotaWindowsIn(headers);
209
+ if (!rejected.length) {
210
+ return headers.get("anthropic-ratelimit-unified-status")?.trim() === "rejected"
211
+ ? usableReset("anthropic-ratelimit-unified-reset") : undefined;
212
+ }
213
+ let latest = 0;
214
+ for (const window of rejected) {
215
+ const resetAt = usableReset(`anthropic-ratelimit-unified-${window === "five-hour" ? "5h" : "7d"}-reset`);
216
+ if (resetAt === undefined) return undefined;
217
+ latest = Math.max(latest, resetAt);
218
+ }
219
+ return latest;
220
+ }
221
+
155
222
  interface ScoredAccount {
156
223
  accountId: string;
157
224
  hasKnownUsage: boolean;
@@ -286,6 +353,7 @@ type RoutingFacadeSymbol =
286
353
  | "settleAnthropicCooldownRecovery"
287
354
  | "getAnthropicAccountHealthSnapshot"
288
355
  | "clearAnthropicAccountCooldown"
356
+ | "clearAnthropicAccountCooldownForReset"
289
357
  | "sweepExpiredAnthropicRoutingHealth"
290
358
  | "clearAnthropicAccountPoolState"
291
359
  | "anthropicSessionAffinitySizeForTests"
@@ -293,6 +361,8 @@ type RoutingFacadeSymbol =
293
361
  | "hasAnthropicFailoverQuorum"
294
362
  | "forgetAnthropicFailoverQuorum"
295
363
  | "getAnthropicPoolRetryAfterSeconds"
364
+ | "anthropicPoolCooldownError"
365
+ | "anthropicPoolUsageLimitAfterRefusal"
296
366
  | "pickAlternateAnthropicAccount"
297
367
  | "resolveAnthropicAccountForSession"
298
368
  | "resolveAnthropicDispatchAccountId"
@@ -315,7 +385,7 @@ const handlers = new Map<AnthropicInstanceId, RoutingHandlers>();
315
385
  function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting {
316
386
  const PROVIDER = instance;
317
387
  const poolKey = anthropicPoolKey(instance);
318
- const { anthropicFamilyRejected, anthropicFamilyRetryAt, anthropicModelExhausted, clearAnthropicFamilyQuota } = anthropicModelQuotaFor(instance);
388
+ const { anthropicFamilyRejected, anthropicFamilyRetryAt, anthropicFamilyHoldState, anthropicModelExhausted, clearAnthropicFamilyQuota } = anthropicModelQuotaFor(instance);
319
389
  const { anthropicRatePauseUntil, clearAnthropicRatePauses, pauseAnthropicRateAdmission } = anthropicRatePolicyFor(instance);
320
390
  const { anthropicCooldownGeneration, clearAnthropicCooldownGenerations, noteAnthropicCooldownMutation } = anthropicCooldownRecoveryFor(instance);
321
391
  // Request-owned config authorizes selection/commit. Namespace-only credential and roster
@@ -356,7 +426,26 @@ function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting
356
426
 
357
427
 
358
428
  /**
359
- * Capture the exact reset-derived refusal a fresh usage probe is allowed to recover.
429
+ * The windows a fresh usage read must show open before it may lift this cooldown, or null when
430
+ * no usage read can lift it.
431
+ *
432
+ * Only a 429 is quota evidence: `recordAnthropicAccountRefusal` keeps a 429 only after
433
+ * classifying it as shared quota, and a 403 is an entitlement refusal that usage cannot answer.
434
+ * The deadline source does not matter (#6989). Anthropic sends Retry-After on a spent window as
435
+ * well, set to that window's reset, so a `retry-after` cooldown that named rejected windows
436
+ * carries exactly the evidence a `reset-derived` one does; the read decides, not the header.
437
+ * A refusal that named no window can only be the aggregate unified status. It stays claimable
438
+ * when reset-derived, as before, but no usage reading covers it, so it is always retained.
439
+ */
440
+ function recoverableQuotaWindows(entry: AccountHealth): readonly AnthropicQuotaWindow[] | null {
441
+ if (entry.refusalStatus !== 429) return null;
442
+ if (entry.rejectedQuotaWindows?.length) return entry.rejectedQuotaWindows;
443
+ if (entry.refusedQuotaWindows.length) return entry.refusedQuotaWindows;
444
+ return entry.cooldownSource === "reset-derived" ? [] : null;
445
+ }
446
+
447
+ /**
448
+ * Capture the exact quota refusal a fresh usage probe is allowed to recover.
360
449
  *
361
450
  * The generation fence matters because the usage request is asynchronous: a newer 429 may
362
451
  * arrive while it is in flight, and an older answer must never erase that newer refusal.
@@ -366,13 +455,14 @@ function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting
366
455
  now = Date.now(),
367
456
  ): AnthropicCooldownRecoveryClaim | null {
368
457
  const entry = upstreamHealth.get(accountId);
369
- if (!entry || entry.cooldownUntil <= now || entry.cooldownSource !== "reset-derived") return null;
458
+ if (!entry || entry.cooldownUntil <= now || recoverableQuotaWindows(entry) === null) return null;
370
459
  return { instance, accountId, cooldownGeneration: entry.cooldownGeneration, claimedAt: now };
371
460
  }
372
461
 
373
462
  /**
374
- * Clear only the claimed reset-derived cooldown when a fresh, complete usage result proves
375
- * headroom in every window that upstream previously reported as rejected.
463
+ * Clear only the claimed quota cooldown when a fresh, complete usage result proves headroom in
464
+ * every window that upstream previously reported as rejected. A result read before the claim
465
+ * (`updatedAt < claimedAt`) is stale and settles nothing.
376
466
  */
377
467
  function settleAnthropicCooldownRecovery(
378
468
  claim: AnthropicCooldownRecoveryClaim,
@@ -380,12 +470,12 @@ function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting
380
470
  ): AnthropicCooldownRecoverySettlement {
381
471
  if (claim.instance !== instance) return "superseded";
382
472
  const entry = upstreamHealth.get(claim.accountId);
383
- if (!entry || entry.cooldownSource !== "reset-derived"
473
+ const windows = entry ? recoverableQuotaWindows(entry) : null;
474
+ if (!entry || windows === null
384
475
  || entry.cooldownGeneration !== claim.cooldownGeneration
385
476
  || anthropicCooldownGeneration(claim.accountId) !== claim.cooldownGeneration
386
477
  || !Number.isFinite(quota.updatedAt) || quota.updatedAt < claim.claimedAt) return "superseded";
387
- const windows = entry.rejectedQuotaWindows;
388
- if (!windows?.length) return "retained";
478
+ if (!windows.length) return "retained";
389
479
  const recovered = windows.every(window => {
390
480
  const percent = window === "five-hour" ? quota.fiveHourPercent : quota.weeklyPercent;
391
481
  return typeof percent === "number" && Number.isFinite(percent) && percent >= 0 && percent < 100;
@@ -416,6 +506,34 @@ function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting
416
506
  return cleared;
417
507
  }
418
508
 
509
+ /**
510
+ * Lift a quota cooldown for a reset grant. `claimedAt` is when the claim was first journaled,
511
+ * before it was sent: a refusal recorded at or after it may already describe the post-reset
512
+ * account (a tie within one clock tick cannot be ordered, so it stays too). The windows the
513
+ * grant confirmed cleared must cover every window the refusal named; a refusal that named none
514
+ * needs both windows cleared, and an entitlement (403) refusal is never lifted by a reset.
515
+ */
516
+ function clearAnthropicAccountCooldownForReset(
517
+ accountId: string,
518
+ claimedAt: number,
519
+ clearedWindows: ReadonlySet<AnthropicQuotaWindow>,
520
+ ): "cleared" | "absent" | "newer" | "uncovered" {
521
+ const entry = upstreamHealth.get(accountId);
522
+ if (!entry) return "absent";
523
+ if (entry.recordedAt >= claimedAt) return "newer";
524
+ const named = entry.rejectedQuotaWindows?.length ? entry.rejectedQuotaWindows : entry.refusedQuotaWindows;
525
+ const required: readonly AnthropicQuotaWindow[] = named.length ? named : ["five-hour", "weekly"];
526
+ if (entry.refusalStatus !== 429 || !required.every(window => clearedWindows.has(window))) {
527
+ // Kept, but reads that started before the grant are now stale. Move the entry to the same
528
+ // new generation so a fresh usage read can still recover a 429 quota cooldown.
529
+ entry.cooldownGeneration = noteAnthropicCooldownMutation(accountId);
530
+ return "uncovered";
531
+ }
532
+ upstreamHealth.delete(accountId);
533
+ noteAnthropicCooldownMutation(accountId);
534
+ return "cleared";
535
+ }
536
+
419
537
  function sweepExpiredAnthropicRoutingHealth(now = Date.now()): number {
420
538
  let removed = 0;
421
539
  for (const [accountId, health] of upstreamHealth) {
@@ -603,23 +721,90 @@ function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting
603
721
 
604
722
  /** Earliest remaining cooldown among cooled Anthropic accounts, for client Retry-After. */
605
723
  function getAnthropicPoolRetryAfterSeconds(now = Date.now(), decision: AnthropicRouteDecision | null = null, model?: string): number | null {
724
+ return anthropicPoolCooldownError(now, decision, model).retryAfterSeconds;
725
+ }
726
+
727
+ /**
728
+ * What keeps one account out of admission, and whether that is spent quota (#6991).
729
+ *
730
+ * Every hold must lift before the account can serve, so the latest one decides. A 429 health
731
+ * entry is quota by construction (only a shared-quota classification records one), whatever its
732
+ * deadline source; a 403 entry is an entitlement refusal; an admission pause is a transient
733
+ * throttle; a family exclusion is a spent family window unless a revalidation send is already
734
+ * in flight. A tie goes to the transient hold, because then a retry at that instant may succeed.
735
+ *
736
+ * The reported reset is computed apart from that admission order: the account's usage reopens
737
+ * when the last of its quota holds' upstream windows resets, and it is unknown when any of them
738
+ * lacks a known reset. Admission deadlines are not resets: they may be Retry-After, the
739
+ * sixty-second default or the thirty-minute family evidence lifetime.
740
+ */
741
+ function anthropicAccountHold(accountId: string, now: number, model?: string): AnthropicAccountHold | null {
742
+ const latest = { until: 0, quota: false };
743
+ let quotaResetAt = 0;
744
+ let quotaResetKnown = true;
745
+ const consider = (until: number | undefined, quota: boolean, reset?: number) => {
746
+ if (until === undefined || until <= now) return;
747
+ if (quota) {
748
+ if (reset === undefined || reset <= now) quotaResetKnown = false;
749
+ else quotaResetAt = Math.max(quotaResetAt, reset);
750
+ }
751
+ if (until < latest.until || (until === latest.until && quota)) return;
752
+ latest.until = until;
753
+ latest.quota = quota;
754
+ };
755
+ const entry = getAnthropicAccountHealthSnapshot(accountId, now) ? upstreamHealth.get(accountId) : undefined;
756
+ consider(entry?.cooldownUntil, entry?.refusalStatus === 429, entry?.quotaResetAt);
757
+ consider(anthropicRatePauseUntil(accountId, now), false);
758
+ const family = anthropicFamilyHoldState(accountId, model);
759
+ consider(anthropicFamilyRetryAt(accountId, model, now), family?.revalidating === false, family?.resetAt);
760
+ if (latest.until === 0) return null;
761
+ return { until: latest.until, quota: latest.quota,
762
+ ...(latest.quota && quotaResetKnown && quotaResetAt > 0 ? { resetAt: quotaResetAt } : {}) };
763
+ }
764
+
765
+ /**
766
+ * The local refusal for a pool (or model route) with no admissible account. Retry-After is the
767
+ * earliest hold among accounts the route can use once they recover. The refusal is a usage limit
768
+ * only when no account the route can use is admissible and every held one is out on spent quota;
769
+ * then a client retry before that instant cannot succeed, and Codex should stop and show the
770
+ * reset instead of retrying. The reset is the earliest known window reset, and is omitted when
771
+ * any held account's reset is unknown, since that account could reopen first.
772
+ */
773
+ function anthropicPoolCooldownError(now = Date.now(), decision: AnthropicRouteDecision | null = null, model?: string): AnthropicAccountCooldownError {
606
774
  const set = getAccountSet(PROVIDER);
607
- if (!set) return null;
775
+ if (!set) return new AnthropicAccountCooldownError(null, decision?.position);
608
776
  // Once an explicit fallback route has no eligible declared account, selection may use
609
777
  // the ordinary pool. Its earliest usable cooldown must determine the advertised wait.
610
778
  const fallbackExpanded = decision?.fallback === true
611
779
  && !getEligibleAnthropicAccounts(now, model).some(id => decision.accounts.includes(id));
612
780
  let earliest: number | null = null;
781
+ let allQuota = true;
782
+ let earliestReset: number | null = null;
783
+ let resetsKnown = true;
613
784
  for (const account of set.accounts) {
614
785
  if (account.paused === true || account.needsReauth === true || !isPoolCredentialUsable(account.id, now)) continue;
615
786
  if (decision && !fallbackExpanded && !decision.accounts.includes(account.id)) continue;
616
- const snap = getAnthropicAccountHealthSnapshot(account.id, now);
617
- const until = Math.max(snap?.cooldownUntil ?? 0, anthropicRatePauseUntil(account.id, now) ?? 0, anthropicFamilyRetryAt(account.id, model, now) ?? 0);
618
- if (until <= now) continue;
619
- if (earliest === null || until < earliest) earliest = until;
787
+ const hold = anthropicAccountHold(account.id, now, model);
788
+ if (!hold) continue;
789
+ if (!hold.quota) allQuota = false;
790
+ else if (hold.resetAt === undefined) resetsKnown = false;
791
+ else if (earliestReset === null || hold.resetAt < earliestReset) earliestReset = hold.resetAt;
792
+ if (earliest === null || hold.until < earliest) earliest = hold.until;
620
793
  }
621
- if (earliest === null || earliest <= now) return null;
622
- return Math.max(1, Math.ceil((earliest - now) / 1000));
794
+ if (earliest === null) return new AnthropicAccountCooldownError(null, decision?.position);
795
+ const exhausted = allQuota && routeCandidates(getEligibleAnthropicAccounts(now, model), decision).length === 0;
796
+ return new AnthropicAccountCooldownError(Math.max(1, Math.ceil((earliest - now) / 1000)), decision?.position,
797
+ exhausted ? { resetsAt: resetsKnown ? earliestReset : null } : null);
798
+ }
799
+
800
+ /**
801
+ * After an upstream refusal found no replacement: the usage-limit refusal when the pool is
802
+ * exhausted as defined above, else null. A refusal that left an admissible sibling (a send
803
+ * budget ran out, a request-scoped 429) or a sibling on a transient hold is not exhaustion.
804
+ */
805
+ function anthropicPoolUsageLimitAfterRefusal(now = Date.now(), decision: AnthropicRouteDecision | null = null, model?: string): AnthropicAccountCooldownError | null {
806
+ const refusal = anthropicPoolCooldownError(now, decision, model);
807
+ return refusal.usageLimit === null ? null : refusal;
623
808
  }
624
809
 
625
810
  function pickLowestUsage(config: OcxConfig, excludeId: string | undefined, now: number, decision: AnthropicRouteDecision | null = null, model?: string, excludedAccountIds?: ReadonlySet<string>): string | null {
@@ -950,7 +1135,7 @@ function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting
950
1135
  const now = Date.now();
951
1136
  const selection = resolveAnthropicAccountForSession(sessionKey, config, now, decision, model);
952
1137
  if (selection.reason === "all-cooled") {
953
- throw new AnthropicAccountCooldownError(getAnthropicPoolRetryAfterSeconds(now, decision, model), decision?.position);
1138
+ throw anthropicPoolCooldownError(now, decision, model);
954
1139
  }
955
1140
  if (selection.reason === "paused" || !selection.accountId || !getEligibleAnthropicAccounts(now, model).includes(selection.accountId)) {
956
1141
  const { OAuthAccountPausedError, OAuthLoginRequiredError } = await import("./index");
@@ -1084,6 +1269,7 @@ function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting
1084
1269
  // account is back in the rotation a minute later.
1085
1270
  const parsedRetry = parseRetryAfterMs(retryAfterHeader, now);
1086
1271
  const resetDerived = status === 429 && parsedRetry === undefined ? parseRateLimitReset(rateLimitHeaders, now) : undefined;
1272
+ const quotaResetAt = status === 429 ? rejectedWindowsResetAt(rateLimitHeaders, now) : undefined;
1087
1273
  const cooldownMs = parsedRetry ?? resetDerived?.delayMs ?? (status === 403 ? ACCOUNT_REFUSAL_COOLDOWN_MS : DEFAULT_COOLDOWN_MS);
1088
1274
  const cooldownGeneration = noteAnthropicCooldownMutation(failedAccountId);
1089
1275
  upstreamHealth.set(failedAccountId, {
@@ -1093,6 +1279,10 @@ function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting
1093
1279
  : resetDerived !== undefined ? "reset-derived" : "default",
1094
1280
  ...(resetDerived ? { rejectedQuotaWindows: resetDerived.rejectedQuotaWindows } : {}),
1095
1281
  cooldownGeneration,
1282
+ recordedAt: now,
1283
+ refusalStatus: status,
1284
+ refusedQuotaWindows: status === 429 ? rejectedQuotaWindowsIn(rateLimitHeaders) : [],
1285
+ ...(quotaResetAt !== undefined ? { quotaResetAt } : {}),
1096
1286
  });
1097
1287
  sweepExpiredOnWrite(now);
1098
1288
  clearAnthropicSessionAffinityForAccount(failedAccountId);
@@ -1272,9 +1462,10 @@ function createAnthropicRouting(instance: AnthropicInstanceId): AnthropicRouting
1272
1462
  instance, captureAnthropicManualSelectionGeneration, anthropicAccountPoolConfig,
1273
1463
  isAnthropicAccountPoolEnabled, anthropicAutoSwitchThreshold, anthropicAccountAutoSwitchThreshold,
1274
1464
  captureAnthropicCooldownRecovery, settleAnthropicCooldownRecovery, getAnthropicAccountHealthSnapshot,
1275
- clearAnthropicAccountCooldown, sweepExpiredAnthropicRoutingHealth, clearAnthropicAccountPoolState,
1465
+ clearAnthropicAccountCooldown, clearAnthropicAccountCooldownForReset, sweepExpiredAnthropicRoutingHealth, clearAnthropicAccountPoolState,
1276
1466
  anthropicSessionAffinitySizeForTests, getEligibleAnthropicAccounts, hasAnthropicFailoverQuorum,
1277
- forgetAnthropicFailoverQuorum, getAnthropicPoolRetryAfterSeconds, pickAlternateAnthropicAccount,
1467
+ forgetAnthropicFailoverQuorum, getAnthropicPoolRetryAfterSeconds, anthropicPoolCooldownError,
1468
+ anthropicPoolUsageLimitAfterRefusal, pickAlternateAnthropicAccount,
1278
1469
  resolveAnthropicAccountForSession, resolveAnthropicDispatchAccountId, bindAnthropicSessionAffinity,
1279
1470
  clearAnthropicSessionAffinityForAccount, rotateAnthropicAccountOn429, rotateAnthropicAccountOnRefusal,
1280
1471
  recordAnthropicAccount429, recordAnthropicAccountRefusal, promoteAnthropicActiveAccount,
@@ -1352,6 +1543,14 @@ export function clearAnthropicAccountCooldown(accountId: string): boolean {
1352
1543
  return anthropicRoutingFor("anthropic").clearAnthropicAccountCooldown(accountId);
1353
1544
  }
1354
1545
 
1546
+ export function clearAnthropicAccountCooldownForReset(
1547
+ accountId: string,
1548
+ claimedAt: number,
1549
+ clearedWindows: ReadonlySet<AnthropicQuotaWindow>,
1550
+ ): "cleared" | "absent" | "newer" | "uncovered" {
1551
+ return anthropicRoutingFor("anthropic").clearAnthropicAccountCooldownForReset(accountId, claimedAt, clearedWindows);
1552
+ }
1553
+
1355
1554
  export function sweepExpiredAnthropicRoutingHealth(now = Date.now()): number {
1356
1555
  return anthropicRoutingFor("anthropic").sweepExpiredAnthropicRoutingHealth(now);
1357
1556
  }
@@ -1380,6 +1579,14 @@ export function getAnthropicPoolRetryAfterSeconds(now = Date.now(), decision: An
1380
1579
  return anthropicRoutingFor("anthropic").getAnthropicPoolRetryAfterSeconds(now, decision, model);
1381
1580
  }
1382
1581
 
1582
+ export function anthropicPoolCooldownError(now = Date.now(), decision: AnthropicRouteDecision | null = null, model?: string): AnthropicAccountCooldownError {
1583
+ return anthropicRoutingFor("anthropic").anthropicPoolCooldownError(now, decision, model);
1584
+ }
1585
+
1586
+ export function anthropicPoolUsageLimitAfterRefusal(now = Date.now(), decision: AnthropicRouteDecision | null = null, model?: string): AnthropicAccountCooldownError | null {
1587
+ return anthropicRoutingFor("anthropic").anthropicPoolUsageLimitAfterRefusal(now, decision, model);
1588
+ }
1589
+
1383
1590
  export function pickAlternateAnthropicAccount(
1384
1591
  config: OcxConfig,
1385
1592
  excludeId: string,
@@ -1,6 +1,6 @@
1
1
  /** Anthropic OAuth flow (Claude Pro/Max). Ported from jawcode oauth/anthropic.ts. */
2
2
  import { OAuthCallbackFlow } from "./callback-server";
3
- import { bindAnthropicIdentity, resolveAnthropicAccountIdentity } from "./anthropic-identity";
3
+ import { bindAnthropicIdentity, normalizeAnthropicIdentity, resolveAnthropicAccountIdentity } from "./anthropic-identity";
4
4
  import { generatePKCE } from "./pkce";
5
5
  import type { LocalTokenImportMode, OAuthController, OAuthCredentials } from "./types";
6
6
  import { outboundProxyConfigured, startupOutboundProxyConfigured } from "../lib/proxy-env";
@@ -12,6 +12,8 @@ const AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
12
12
  const TOKEN_URL = "https://api.anthropic.com/v1/oauth/token";
13
13
  const CALLBACK_PORT = 54545;
14
14
  const CALLBACK_PATH = "/callback";
15
+ /** Profile enrichment budget on token refresh; login paths keep the resolver's own deadline. */
16
+ const REFRESH_PROFILE_TIMEOUT_MS = 2_000;
15
17
  const SCOPES = "org:create_api_key user:profile user:inference";
16
18
 
17
19
  // ── OAuth-request requirements applied by the anthropic adapter when authMode==="oauth" ──
@@ -130,6 +132,15 @@ function credsFrom(data: AnthropicTokenResponse, refreshFallback?: string): OAut
130
132
  };
131
133
  }
132
134
 
135
+ async function withAccountProfile(credential: OAuthCredentials, signal?: AbortSignal): Promise<OAuthCredentials> {
136
+ const profile = await resolveAnthropicAccountIdentity(credential.access, signal);
137
+ if (!profile) return credential;
138
+ // Token-response identity remains authoritative here; enrichment changes display metadata only.
139
+ const identity = normalizeAnthropicIdentity(credential.anthropicIdentity, credential.access);
140
+ if (identity && identity.accountUuid !== profile.accountUuid) return credential;
141
+ return { ...credential, plan: profile.plan ?? null };
142
+ }
143
+
133
144
  export class AnthropicOAuthFlow extends OAuthCallbackFlow {
134
145
  #verifier = "";
135
146
 
@@ -174,7 +185,7 @@ export class AnthropicOAuthFlow extends OAuthCallbackFlow {
174
185
  redirect_uri: redirectUri,
175
186
  code_verifier: this.#verifier,
176
187
  });
177
- return credsFrom(parseTokenResponse(responseBody));
188
+ return withAccountProfile(credsFrom(parseTokenResponse(responseBody)), this.ctrl.signal);
178
189
  }
179
190
  }
180
191
 
@@ -192,7 +203,8 @@ export async function loginAnthropic(
192
203
  if (local.expires >= Date.now() + 60_000) {
193
204
  const identity = await resolveAnthropicAccountIdentity(local.access, ctrl.signal);
194
205
  ctrl.signal?.throwIfAborted();
195
- return { ...local, ...(identity ? { accountId: identity.accountUuid, anthropicIdentity: identity } : {}) };
206
+ return { ...local, ...(identity ? { accountId: identity.accountUuid,
207
+ anthropicIdentity: normalizeAnthropicIdentity(identity, local.access), plan: identity.plan ?? null } : {}) };
196
208
  }
197
209
  try {
198
210
  return { ...(await refreshAnthropicToken(local.refresh)), source: "local-cli" };
@@ -218,5 +230,7 @@ export async function refreshAnthropicToken(refreshToken: string): Promise<OAuth
218
230
  client_id: CLIENT_ID,
219
231
  refresh_token: refreshToken,
220
232
  });
221
- return credsFrom(parseTokenResponse(responseBody), refreshToken);
233
+ // Refresh can sit on a request path, so the profile enrichment gets a short deadline. A
234
+ // missed read leaves plan unobserved and the stored label survives through mergeOAuthPlan.
235
+ return withAccountProfile(credsFrom(parseTokenResponse(responseBody), refreshToken), AbortSignal.timeout(REFRESH_PROFILE_TIMEOUT_MS));
222
236
  }
@@ -41,6 +41,7 @@ import type { KiroRefusalKind } from "../adapters/kiro-refusal";
41
41
  import { kiroAccountEvidence } from "../providers/kiro-usage";
42
42
  import { kiroEvidenceIdentity } from "../providers/kiro-account-state-disk";
43
43
  import { kiroAccountSupportsModel } from "../providers/kiro-model-catalog";
44
+ import { antigravityAccountSupportsModel } from "../providers/antigravity-account-roster";
44
45
  import { ACCOUNT_QUOTA_TTL_MS } from "../providers/quota-wire";
45
46
  import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
46
47
  import { subscribeOAuthAccountPauseChanges } from "../lib/account-selection-events";
@@ -248,11 +249,31 @@ export function eligibleFailoverAccounts(providerName: string, now = Date.now(),
248
249
  return eligibleIdsIn(getAccountSet(providerName), providerName, now, family);
249
250
  }
250
251
 
251
- /** Model evidence only ranks accounts already eligible and below any configured cap. */
252
+ /** Whether an account's own model listing includes `model`; undefined when unknown. */
253
+ function accountListsModel(providerName: string, accountId: string, model: string): boolean | undefined {
254
+ if (providerName === "kiro") return kiroAccountSupportsModel(accountId, model);
255
+ if (providerName === "google-antigravity") return antigravityAccountSupportsModel(providerName, accountId, model);
256
+ return undefined;
257
+ }
258
+
259
+ /**
260
+ * Model evidence only ranks accounts already eligible and below any configured cap.
261
+ *
262
+ * Kiro and Antigravity both gate models per account. For Antigravity (Claude 4.6 vs 5.5, Argon)
263
+ * an account whose discovered roster lists the model wins; an account whose roster omits it is
264
+ * skipped while any other remains; an account with no roster yet is unknown and stays usable.
265
+ */
252
266
  export function preferKiroModelSupport(
253
267
  providerName: string, ids: string[], model?: string | null, config?: OcxConfig,
254
268
  ): string[] {
255
- if (providerName !== "kiro" || !model) return ids;
269
+ if (!model) return ids;
270
+ if (providerName === "google-antigravity") {
271
+ const positive = ids.filter(id => accountListsModel(providerName, id, model) === true);
272
+ if (positive.length) return positive;
273
+ const notListedAgainst = ids.filter(id => accountListsModel(providerName, id, model) !== false);
274
+ return notListedAgainst.length ? notListedAgainst : ids;
275
+ }
276
+ if (providerName !== "kiro") return ids;
256
277
  const cap = config?.providers?.kiro?.oauthAccountFailover?.maxConcurrentPerAccount;
257
278
  const withRoom = typeof cap === "number" && Number.isInteger(cap) && cap >= 1 && cap <= 100
258
279
  ? ids.filter(id => accountInFlight("kiro", id) < cap) : ids;
@@ -631,10 +652,13 @@ export function preferredInitialAccount(
631
652
  const modelEligible = preferKiroModelSupport(providerName,
632
653
  eligibleFailoverAccounts(providerName, now, classifyModelFamilyForQuota(providerName, requestedModelId)),
633
654
  requestedModelId, config);
634
- const activeLacks = providerName === "kiro" && !!active && !!requestedModelId
635
- && kiroAccountSupportsModel(active, requestedModelId) === false;
655
+ const activeLacks = (providerName === "kiro" || providerName === "google-antigravity")
656
+ && !!active && !!requestedModelId
657
+ && accountListsModel(providerName, active, requestedModelId) === false;
636
658
  const listingSibling = activeLacks
637
- ? modelEligible.find(id => id !== active && kiroModelListingHasRoom(config, id, requestedModelId!))
659
+ ? modelEligible.find(id => id !== active && (providerName === "kiro"
660
+ ? kiroModelListingHasRoom(config, id, requestedModelId!)
661
+ : accountListsModel(providerName, id, requestedModelId!) === true))
638
662
  : undefined;
639
663
  if (listingSibling) return listingSibling;
640
664
 
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server";
13
13
  import { generatePKCE } from "./pkce";
14
+ import { normalizeOAuthPlan } from "./plan";
14
15
  import type { OAuthController, OAuthCredentials } from "./types";
15
16
  import { antigravityUserAgent, ANTIGRAVITY_IDE_VERSION } from "../adapters/client-fingerprint";
16
17
 
@@ -94,7 +95,7 @@ function extractProjectId(data: Record<string, unknown> | undefined): string | u
94
95
  return undefined;
95
96
  }
96
97
 
97
- async function loadCodeAssistProject(accessToken: string, signal?: AbortSignal): Promise<string | undefined> {
98
+ async function loadCodeAssistProject(accessToken: string, signal?: AbortSignal): Promise<{ projectId?: string; plan: string | null } | undefined> {
98
99
  const response = await fetch(`${PROD_API}/${API_VERSION}:loadCodeAssist`, {
99
100
  method: "POST",
100
101
  headers: { Authorization: `Bearer ${accessToken}`, Accept: "*/*", "Content-Type": "application/json", "User-Agent": antigravityUserAgent() },
@@ -102,7 +103,12 @@ async function loadCodeAssistProject(accessToken: string, signal?: AbortSignal):
102
103
  signal: requestSignal(signal),
103
104
  });
104
105
  if (!response.ok) return undefined;
105
- return extractProjectId((await response.json().catch(() => undefined)) as Record<string, unknown> | undefined);
106
+ const data: unknown = await response.json().catch(() => undefined);
107
+ if (!data || typeof data !== "object" || Array.isArray(data)) return undefined;
108
+ const payload = data as Record<string, unknown>;
109
+ const tier = (value: unknown) => value && typeof value === "object" && "id" in value
110
+ ? normalizeOAuthPlan(value.id) : undefined;
111
+ return { projectId: extractProjectId(payload), plan: tier(payload.paidTier) ?? tier(payload.currentTier) ?? null };
106
112
  }
107
113
 
108
114
  async function onboardProject(accessToken: string, signal?: AbortSignal): Promise<string | undefined> {
@@ -138,7 +144,18 @@ async function onboardProject(accessToken: string, signal?: AbortSignal): Promis
138
144
 
139
145
  /** Discover the CCA project for an access token (loadCodeAssist → onboardUser fallback). */
140
146
  export async function discoverAntigravityProject(accessToken: string, signal?: AbortSignal): Promise<string | undefined> {
141
- return (await loadCodeAssistProject(accessToken, signal)) ?? (await onboardProject(accessToken, signal));
147
+ return (await discoverAntigravityAccount(accessToken, signal)).projectId;
148
+ }
149
+
150
+ async function discoverAntigravityAccount(accessToken: string, signal?: AbortSignal): Promise<{ projectId?: string; plan?: string | null }> {
151
+ const account = await loadCodeAssistProject(accessToken, signal);
152
+ // Once loadCodeAssist answered, an onboarding failure must not discard its tier observation;
153
+ // a missing project still fails login, and cancellation still propagates.
154
+ const projectId = account?.projectId ?? await (account
155
+ ? onboardProject(accessToken, signal).catch(error => { if (signal?.aborted) throw error; return undefined; })
156
+ : onboardProject(accessToken, signal));
157
+ // An unanswered loadCodeAssist observed nothing; only a parsed body may clear a stored label.
158
+ return { projectId, ...(account ? { plan: account.plan } : {}) };
142
159
  }
143
160
 
144
161
  function credentialsFromPayload(payload: GoogleTokenPayload, refreshFallback = ""): OAuthCredentials {
@@ -207,13 +224,13 @@ class AntigravityOAuthFlow extends OAuthCallbackFlow {
207
224
  }, this.ctrl.signal);
208
225
  const creds = credentialsFromPayload(payload);
209
226
  this.ctrl.onProgress?.("Discovering Cloud Code Assist project");
210
- const projectId = await discoverAntigravityProject(creds.access, this.ctrl.signal);
227
+ const { projectId, plan } = await discoverAntigravityAccount(creds.access, this.ctrl.signal);
211
228
  if (!projectId) {
212
229
  // Fail the login rather than persisting a credential that every request would reject for a
213
230
  // missing CCA project — otherwise status shows "logged in" while all calls fail closed.
214
231
  throw new Error("Antigravity login could not discover a Cloud Code Assist project for this account. Ensure the account has Antigravity/Cloud Code Assist access and try again.");
215
232
  }
216
- return { ...creds, projectId };
233
+ return { ...creds, projectId, plan };
217
234
  }
218
235
  }
219
236
 
@@ -231,8 +248,8 @@ export async function refreshAntigravityToken(refreshToken: string, signal?: Abo
231
248
  }, signal);
232
249
  const creds = credentialsFromPayload(payload, refreshToken);
233
250
  // Re-discover the project on refresh so a newly-onboarded account fills in projectId.
234
- const projectId = await discoverAntigravityProject(creds.access, signal).catch(() => undefined);
235
- return projectId ? { ...creds, projectId } : creds;
251
+ const account = await discoverAntigravityAccount(creds.access, signal).catch(() => undefined);
252
+ return account ? { ...creds, ...account } : creds;
236
253
  }
237
254
 
238
255
  /**