@bitkyc08/opencodex 2.80.0 → 2.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (376) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +166 -73
  3. package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
  4. package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
  5. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  6. package/gui/dist/assets/index-D3FBCNiP.js +94 -0
  7. package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/gui/dist/provider-icons/claude-green.svg +1 -0
  10. package/package.json +3 -2
  11. package/src/adapters/anthropic-model-contract.ts +8 -2
  12. package/src/adapters/anthropic.ts +8 -3
  13. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  14. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  15. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  16. package/src/adapters/coding-agent/protocol.ts +122 -17
  17. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  18. package/src/adapters/coding-agent/turn.ts +101 -44
  19. package/src/adapters/cursor/catalog.ts +8 -0
  20. package/src/adapters/cursor/effort-map.ts +2 -0
  21. package/src/adapters/devin/live-models.ts +2 -0
  22. package/src/adapters/kiro-retry.ts +9 -4
  23. package/src/adapters/openai-chat/messages.ts +7 -6
  24. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  25. package/src/adapters/openai-responses/passthrough.ts +9 -4
  26. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  27. package/src/adapters/physical-send.ts +8 -5
  28. package/src/adapters/qoder/adapter.ts +41 -6
  29. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  30. package/src/adapters/zed.ts +16 -1
  31. package/src/claude/agents-inject.ts +14 -3
  32. package/src/claude/context-windows.ts +19 -17
  33. package/src/claude/desktop-3p.ts +88 -12
  34. package/src/claude/desktop-profile.ts +2 -1
  35. package/src/claude/gateway-cache.ts +18 -1
  36. package/src/claude/inbound-cache-stabilize.ts +4 -9
  37. package/src/claude/inbound.ts +78 -3
  38. package/src/claude/intercept/cli-catalog.ts +3 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  41. package/src/claude/intercept/picker-listener.ts +71 -4
  42. package/src/claude/intercept/picker-models.ts +14 -6
  43. package/src/claude/intercept/settings.ts +3 -0
  44. package/src/claude/long-context.ts +66 -0
  45. package/src/claude/model-info.ts +19 -16
  46. package/src/claude/outbound.ts +94 -9
  47. package/src/claude/subagent-model.ts +40 -11
  48. package/src/cli/account-anthropic-threshold.ts +14 -5
  49. package/src/cli/account-api.ts +3 -1
  50. package/src/cli/account-extended.ts +13 -9
  51. package/src/cli/account-policy-dto.ts +1 -1
  52. package/src/cli/account-policy.ts +13 -4
  53. package/src/cli/account.ts +3 -3
  54. package/src/cli/agent.ts +9 -5
  55. package/src/cli/aside-profile-recovery.ts +33 -0
  56. package/src/cli/capabilities-accounts.ts +2 -2
  57. package/src/cli/capabilities-agents-routing.ts +24 -0
  58. package/src/cli/capabilities-base.ts +5 -5
  59. package/src/cli/capabilities-integrations.ts +18 -2
  60. package/src/cli/cli-path-diagnostics.ts +132 -0
  61. package/src/cli/codex-shim-autorestore.ts +3 -0
  62. package/src/cli/desktop-runtime-guidance.ts +65 -0
  63. package/src/cli/dispatch.ts +15 -1
  64. package/src/cli/doctor.ts +16 -3
  65. package/src/cli/help.ts +4 -1
  66. package/src/cli/index.ts +25 -14
  67. package/src/cli/integration-aside-sync.ts +2 -1
  68. package/src/cli/integration-plan-dto.ts +1 -0
  69. package/src/cli/integration-preview.ts +8 -1
  70. package/src/cli/integrations.ts +48 -6
  71. package/src/cli/message-args.ts +34 -0
  72. package/src/cli/message-command.ts +43 -0
  73. package/src/cli/message-runtime.ts +6 -0
  74. package/src/cli/observe.ts +3 -1
  75. package/src/cli/provider.ts +29 -6
  76. package/src/cli/registry.ts +24 -2
  77. package/src/cli/resolve.ts +25 -2
  78. package/src/cli/root.ts +4 -2
  79. package/src/cli/status.ts +64 -12
  80. package/src/cli/update-restart-child.ts +4 -2
  81. package/src/cli/update-restart-home.ts +28 -10
  82. package/src/cli/update-restart-service-record.ts +116 -0
  83. package/src/cli/update-restart-supervision.ts +96 -0
  84. package/src/cli/update-restart.ts +59 -13
  85. package/src/cli/v2.ts +27 -20
  86. package/src/cli/version-skew-notice.ts +77 -0
  87. package/src/clients/config-export/commandcode.ts +131 -0
  88. package/src/clients/config-export/contracts.ts +12 -0
  89. package/src/clients/config-export.ts +64 -1
  90. package/src/clients/lazycodex.ts +1 -1
  91. package/src/clients/omo-role-models.ts +32 -7
  92. package/src/codex/auth-api/account-list.ts +11 -3
  93. package/src/codex/auth-api/account-selection.ts +107 -0
  94. package/src/codex/auth-api/main-account-probe.ts +25 -8
  95. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  96. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  97. package/src/codex/auth-api/routes.ts +11 -49
  98. package/src/codex/auth-context.ts +20 -1
  99. package/src/codex/autostart-health.ts +12 -3
  100. package/src/codex/catalog/auto-review.ts +13 -6
  101. package/src/codex/catalog/build-entries.ts +33 -7
  102. package/src/codex/catalog/bundled.ts +155 -26
  103. package/src/codex/catalog/combo-member.ts +3 -2
  104. package/src/codex/catalog/control-plane.ts +45 -0
  105. package/src/codex/catalog/effort.ts +37 -6
  106. package/src/codex/catalog/gather-capture.ts +10 -3
  107. package/src/codex/catalog/model-hints.ts +15 -8
  108. package/src/codex/catalog/model-visibility.ts +3 -1
  109. package/src/codex/catalog/operator-tiers.ts +38 -0
  110. package/src/codex/catalog/parsing.ts +2 -1
  111. package/src/codex/catalog/provider-models.ts +58 -44
  112. package/src/codex/catalog/retained-sync.ts +7 -5
  113. package/src/codex/catalog/routed-gather.ts +15 -1
  114. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  115. package/src/codex/catalog-auto-refresh.ts +19 -0
  116. package/src/codex/catalog.ts +1 -1
  117. package/src/codex/codex-home-owner.ts +3 -0
  118. package/src/codex/config-write-lock.ts +333 -0
  119. package/src/codex/control-plane-models.ts +6 -0
  120. package/src/codex/convergence.ts +2 -1
  121. package/src/codex/features.ts +257 -200
  122. package/src/codex/inject/config-write-section.ts +34 -0
  123. package/src/codex/inject/multi-agent-v2.ts +14 -6
  124. package/src/codex/inject/remove.ts +36 -11
  125. package/src/codex/inject/restore.ts +85 -20
  126. package/src/codex/inject-coordination.ts +15 -6
  127. package/src/codex/inject.ts +62 -20
  128. package/src/codex/journal.ts +62 -17
  129. package/src/codex/main-account-cache.ts +43 -0
  130. package/src/codex/paths.ts +2 -0
  131. package/src/codex/prepared-home.ts +18 -0
  132. package/src/codex/prompt-journal.ts +25 -12
  133. package/src/codex/prompt-layers.ts +49 -26
  134. package/src/codex/prompt-lock-claim.ts +134 -0
  135. package/src/codex/prompt-lock-evidence.ts +26 -0
  136. package/src/codex/prompt-lock-io.ts +16 -0
  137. package/src/codex/prompt-lock-owner.ts +130 -0
  138. package/src/codex/prompt-lock.ts +84 -53
  139. package/src/codex/routing/failure-window.ts +146 -0
  140. package/src/codex/routing/selection.ts +4 -1
  141. package/src/codex/routing.ts +13 -17
  142. package/src/codex/runtime.ts +106 -18
  143. package/src/codex/shim.ts +45 -19
  144. package/src/combos/jev-decision-contract.ts +25 -2
  145. package/src/combos/jev-service-exchange.ts +281 -0
  146. package/src/combos/jev.ts +34 -219
  147. package/src/combos/types.ts +1 -1
  148. package/src/config/atomic-write.ts +1 -1
  149. package/src/config/codex-ws-reuse-setting.ts +10 -0
  150. package/src/config/diagnostics.ts +27 -16
  151. package/src/config/live-reconcile.ts +17 -0
  152. package/src/config/load-degrade.ts +42 -0
  153. package/src/config/rebase-provenance.ts +11 -0
  154. package/src/config/schema/anthropic-account-pool.ts +143 -0
  155. package/src/config/schema/config-schema.ts +11 -1
  156. package/src/config/schema/leaf-validators.ts +4 -0
  157. package/src/config.ts +2 -0
  158. package/src/generated/compatibility-version.json +566 -298
  159. package/src/generated/model-metadata.ts +7 -6
  160. package/src/grok/inject.ts +50 -4
  161. package/src/grok/sync.ts +2 -1
  162. package/src/images/loop.ts +30 -11
  163. package/src/integrations/aside-profiles.ts +10 -3
  164. package/src/integrations/catalog-refresh.ts +19 -3
  165. package/src/integrations/mutation-plan.ts +7 -0
  166. package/src/integrations/registry.ts +8 -0
  167. package/src/integrations/state.ts +9 -2
  168. package/src/integrations/writer.ts +17 -5
  169. package/src/lib/bun-path-runtime.d.mts +32 -0
  170. package/src/lib/bun-path-runtime.mjs +95 -0
  171. package/src/lib/bun-runtime-preflight.ts +140 -0
  172. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  173. package/src/lib/desktop-cli-handoff.mjs +92 -0
  174. package/src/lib/desktop-cli-record.d.mts +29 -0
  175. package/src/lib/desktop-cli-record.mjs +102 -0
  176. package/src/lib/lab-live-route-production.ts +9 -1
  177. package/src/lib/request-execution-budget.ts +244 -58
  178. package/src/lib/spend-pool-alias-validation.ts +79 -0
  179. package/src/lib/spend-pool-continuity.ts +34 -0
  180. package/src/lib/spend-reservation-ledger.ts +412 -50
  181. package/src/lib/state-store-registrations.ts +20 -3
  182. package/src/lib/synced-state-location.ts +46 -6
  183. package/src/lib/test-home-guard.ts +87 -1
  184. package/src/lib/translator-budget.ts +17 -4
  185. package/src/lib/upstream-retry.ts +17 -5
  186. package/src/lib/windows-atomic-replace.ts +38 -16
  187. package/src/lib/windows-elevation.ts +6 -0
  188. package/src/lib/windows-secret-acl.ts +92 -25
  189. package/src/lib/winsw.ts +2 -3
  190. package/src/lib/workflow-budget.ts +53 -11
  191. package/src/messaging/budget.ts +45 -0
  192. package/src/messaging/discovery.ts +57 -0
  193. package/src/messaging/envelope.ts +49 -0
  194. package/src/messaging/input.ts +37 -0
  195. package/src/messaging/rpc.ts +196 -0
  196. package/src/messaging/send.ts +58 -0
  197. package/src/messaging/socket.ts +72 -0
  198. package/src/messaging/types.ts +29 -0
  199. package/src/oauth/anthropic-account-refusal.ts +90 -18
  200. package/src/oauth/anthropic-account-threshold.ts +8 -2
  201. package/src/oauth/anthropic-continuity.ts +15 -1
  202. package/src/oauth/anthropic-model-quota.ts +120 -48
  203. package/src/oauth/anthropic-model-routes.ts +11 -3
  204. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  205. package/src/oauth/anthropic-pool-config.ts +30 -0
  206. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  207. package/src/oauth/anthropic-routing.ts +1202 -866
  208. package/src/oauth/anthropic-send-ownership.ts +41 -0
  209. package/src/oauth/anthropic.ts +4 -1
  210. package/src/oauth/chatgpt.ts +113 -41
  211. package/src/oauth/generic-account-failover.ts +3 -1
  212. package/src/oauth/health.ts +8 -6
  213. package/src/oauth/index.ts +96 -175
  214. package/src/oauth/kiro.ts +22 -9
  215. package/src/oauth/login-cli.ts +2 -0
  216. package/src/oauth/model-discovery-auth.ts +43 -0
  217. package/src/oauth/pool-kernel.ts +11 -5
  218. package/src/oauth/pool-settings-capability.ts +5 -3
  219. package/src/oauth/store-anthropic-instance.ts +87 -0
  220. package/src/oauth/store.ts +68 -10
  221. package/src/oauth/token-guardian.ts +2 -0
  222. package/src/oauth/types.ts +2 -0
  223. package/src/protocols/plan-snapshot.ts +5 -0
  224. package/src/protocols/settings.ts +20 -5
  225. package/src/providers/anthropic-instance-id.ts +47 -0
  226. package/src/providers/anthropic-instance.ts +48 -0
  227. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  228. package/src/providers/azure-model-metadata.ts +115 -0
  229. package/src/providers/derive.ts +23 -0
  230. package/src/providers/kiro-models.ts +3 -0
  231. package/src/providers/label.ts +1 -1
  232. package/src/providers/model-rename-fields.ts +2 -0
  233. package/src/providers/quota/account-cache.ts +37 -11
  234. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  235. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  236. package/src/providers/quota/report-cache.ts +24 -1
  237. package/src/providers/quota/vendor-probes-key.ts +127 -17
  238. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  239. package/src/providers/quota.ts +23 -33
  240. package/src/providers/registry/entries-core.ts +35 -30
  241. package/src/providers/registry/model-ids.ts +1 -0
  242. package/src/providers/registry/model-seeds.ts +7 -4
  243. package/src/providers/registry/types.ts +3 -0
  244. package/src/providers/registry.ts +3 -3
  245. package/src/responses/reasoning-envelope.ts +29 -1
  246. package/src/responses/reasoning-replay-cache.ts +17 -1
  247. package/src/responses/spill-store.ts +20 -3
  248. package/src/responses/state/metrics.ts +95 -0
  249. package/src/responses/state/snapshot-codec.ts +6 -0
  250. package/src/responses/state/spill-failure.ts +12 -0
  251. package/src/responses/state/spill-queue.ts +13 -5
  252. package/src/responses/state/unforced-store-false.ts +55 -0
  253. package/src/responses/state.ts +68 -76
  254. package/src/responses/task-input.ts +17 -0
  255. package/src/router.ts +28 -4
  256. package/src/routing/compatibility/assemble.ts +1 -1
  257. package/src/routing/quota.ts +7 -3
  258. package/src/server/auth-cors.ts +5 -0
  259. package/src/server/background-lifecycle.ts +27 -0
  260. package/src/server/chat-completions.ts +7 -1
  261. package/src/server/chat-native.ts +64 -28
  262. package/src/server/claude-messages.ts +59 -8
  263. package/src/server/gui-static.ts +38 -4
  264. package/src/server/inbound-body-admission.ts +1 -0
  265. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  266. package/src/server/index/serve-options.ts +10 -2
  267. package/src/server/index/spend-ledger-lifecycle.ts +12 -5
  268. package/src/server/index/startup-warnings.ts +19 -0
  269. package/src/server/index.ts +5 -2
  270. package/src/server/inference/context.ts +1 -1
  271. package/src/server/lifecycle.ts +1 -1
  272. package/src/server/management/agent-settings-routes.ts +123 -38
  273. package/src/server/management/anthropic-account-threshold.ts +12 -9
  274. package/src/server/management/anthropic-pool-settings.ts +72 -0
  275. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  276. package/src/server/management/codex-agent-role-routes.ts +4 -2
  277. package/src/server/management/codex-prompt-routes.ts +1 -0
  278. package/src/server/management/codex-role-auto-assign.ts +13 -2
  279. package/src/server/management/config-routes.ts +85 -14
  280. package/src/server/management/context.ts +2 -2
  281. package/src/server/management/decision-routes.ts +2 -1
  282. package/src/server/management/integration-routes.ts +25 -2
  283. package/src/server/management/oauth-account-routes.ts +79 -98
  284. package/src/server/management/provider-instance-ownership.ts +44 -0
  285. package/src/server/management/provider-routes.ts +21 -28
  286. package/src/server/management/route-registry.ts +3 -1
  287. package/src/server/management/routing-profile-routes.ts +4 -4
  288. package/src/server/management/shared.ts +3 -3
  289. package/src/server/management/system-restart.ts +44 -14
  290. package/src/server/management/system-routes.ts +24 -2
  291. package/src/server/management/vision-sidecar-options.ts +9 -6
  292. package/src/server/management/web-search-sidecar-options.ts +51 -2
  293. package/src/server/management-auth.ts +6 -0
  294. package/src/server/messages-native-eligibility.ts +3 -2
  295. package/src/server/messages-native-oauth.ts +91 -45
  296. package/src/server/messages-native-selector.ts +39 -0
  297. package/src/server/messages-native.ts +133 -38
  298. package/src/server/messages-response-headers.ts +14 -0
  299. package/src/server/request-log-conversation.ts +48 -5
  300. package/src/server/request-log.ts +36 -2
  301. package/src/server/responses/adapter-continuation.ts +31 -19
  302. package/src/server/responses/adapter-dispatch.ts +75 -24
  303. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  304. package/src/server/responses/codex-ws-exchange.ts +17 -2
  305. package/src/server/responses/codex-ws-pool.ts +100 -13
  306. package/src/server/responses/codex-ws-wire.ts +9 -0
  307. package/src/server/responses/combo-requested-effort.ts +3 -1
  308. package/src/server/responses/compact.ts +25 -8
  309. package/src/server/responses/compaction-recovery.ts +1 -1
  310. package/src/server/responses/core-codex-account.ts +22 -1
  311. package/src/server/responses/core-combo-native.ts +24 -4
  312. package/src/server/responses/core-combo.ts +38 -25
  313. package/src/server/responses/core-lifetime.ts +3 -36
  314. package/src/server/responses/core-normalize.ts +1 -0
  315. package/src/server/responses/core-options.ts +8 -1
  316. package/src/server/responses/core-replay.ts +78 -0
  317. package/src/server/responses/core.ts +3 -3
  318. package/src/server/responses/fetch-helpers.ts +26 -3
  319. package/src/server/responses/passthrough-delivery.ts +30 -4
  320. package/src/server/responses/passthrough-dispatch.ts +39 -9
  321. package/src/server/responses/request-prepare.ts +41 -5
  322. package/src/server/responses/request-send-budget.ts +150 -50
  323. package/src/server/responses/request-spend.ts +325 -33
  324. package/src/server/responses/request-transport.ts +132 -52
  325. package/src/server/responses/response-effects.ts +1 -0
  326. package/src/server/responses/run-turn-execution.ts +13 -3
  327. package/src/server/responses/sidecar-execution.ts +113 -22
  328. package/src/server/responses/sidecar-send-budget.ts +58 -0
  329. package/src/server/responses/ws-upstream.ts +19 -6
  330. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  331. package/src/server/startup-health-cache.ts +2 -1
  332. package/src/server/workflow-refusal.ts +33 -0
  333. package/src/service/cli.ts +20 -4
  334. package/src/service/desktop-command-guard.ts +32 -0
  335. package/src/service/desktop-startup.ts +66 -40
  336. package/src/service/desktop-supervision.d.mts +26 -0
  337. package/src/service/desktop-supervision.mjs +140 -0
  338. package/src/service/managing-cli.ts +1 -0
  339. package/src/service/orchestration.ts +46 -20
  340. package/src/service/ownership-mutation-lease.d.mts +1 -1
  341. package/src/service/ownership-mutation-lease.mjs +5 -7
  342. package/src/service/repair.ts +25 -9
  343. package/src/service/state.ts +4 -3
  344. package/src/service/systemd.ts +17 -8
  345. package/src/service/windows-ops.ts +61 -22
  346. package/src/sidecar/anthropic-binding.ts +79 -0
  347. package/src/sidecar/auth.ts +76 -2
  348. package/src/storage/policy-job.ts +5 -1
  349. package/src/storage/policy-worker.ts +3 -1
  350. package/src/storage/policy.ts +9 -0
  351. package/src/types/anthropic-account-pool.ts +33 -0
  352. package/src/types/config.ts +33 -28
  353. package/src/types/provider.ts +5 -0
  354. package/src/types/request.ts +5 -0
  355. package/src/types/tools.ts +7 -0
  356. package/src/types/wire.ts +1 -0
  357. package/src/types.ts +4 -0
  358. package/src/update/index.ts +57 -9
  359. package/src/update/restart-ownership.ts +10 -3
  360. package/src/update/runtime-ownership.d.mts +6 -0
  361. package/src/update/runtime-ownership.mjs +23 -7
  362. package/src/usage/cost.ts +3 -2
  363. package/src/usage/expected-prices.ts +66 -14
  364. package/src/vision/anthropic-describe.ts +16 -3
  365. package/src/vision/index.ts +3 -0
  366. package/src/vision/plan.ts +19 -6
  367. package/src/web-search/alpha-search.ts +7 -1
  368. package/src/web-search/anthropic-executor.ts +16 -4
  369. package/src/web-search/index.ts +7 -3
  370. package/src/web-search/loop.ts +32 -7
  371. package/src/web-search/passthrough-bridge.ts +4 -1
  372. package/src/web-search/sidecar-providers.ts +4 -3
  373. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  374. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  375. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  376. package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
@@ -21,6 +21,9 @@ import {
21
21
  import { collectReferencedSpillFileNames, snapshotReferencedSpillFileNames } from "./state/spill-inspect";
22
22
  import { selectSnapshotEntries, snapshotPayload } from "./state/snapshot-select";
23
23
  import { clientCarriedPrefixLength, providerIssuedIdentity } from "./state/replay-fingerprint";
24
+ import { hasPendingClientToolCall, allowsUnforcedStoreFalseReplay } from "./state/unforced-store-false";
25
+ import { computeResponseStateMetrics, type ResponseStateMetrics } from "./state/metrics";
26
+ export type { ResponseStateMetrics } from "./state/metrics";
24
27
  export type { ResponseStateTempRecoveryResult, ResponseStateTempRecoveryOptions } from "./state/temp-recovery";
25
28
  export type { ResponseSpillDirInspection } from "./spill-store";
26
29
  export { recoverStaleResponseStateTemps, reclaimAbandonedResponseStateTemps, inspectAbandonedResponseStateTemps, sweepAbandonedResponseStateTemps } from "./state/temp-recovery";
@@ -28,7 +31,7 @@ import { recoverStaleResponseStateTemps } from "./state/temp-recovery";
28
31
  export type { ResponseSpillWriteFailureCode, ResponseSpillWriteStatus, ResponseSpillWriteFailureOrigin } from "./state/spill-failure";
29
32
  import type { ResponseSpillWriteFailureCode, ResponseSpillWriteStatus, ResponseSpillWriteFailureOrigin } from "./state/spill-failure";
30
33
  export { responseAdmissionCountersForTests } from "./state/spill-failure";
31
- import { admissionCounters, noteSpillWriteFailure, noteSpillWriteSuccess, spillCounters, spillWriteHealth } from "./state/spill-failure";
34
+ import { admissionCounters, noteSpillWriteFailure, noteSpillWriteSuccess, resetSpillCountersForTests, spillCounters, spillWriteHealth } from "./state/spill-failure";
32
35
  import { loadSnapshotEntry } from "./state/snapshot-codec";
33
36
  import { isBodyNonPersistable } from "./state/body-policy";
34
37
  export { isBodyNonPersistable, markBodyNonPersistable } from "./state/body-policy";
@@ -118,6 +121,7 @@ export interface ResidentResponseState {
118
121
  /** Index in `items` where provider output begins; see clientCarriedPrefixLength. */
119
122
  providerOutputStart?: number;
120
123
  providers?: OcxProviderContinuationState;
124
+ unforcedStoreFalse?: boolean;
121
125
  sizeBytes: number;
122
126
  }
123
127
 
@@ -128,6 +132,7 @@ export interface SpilledResponseState {
128
132
  /** Mirrors the spilled payload boundary so a spilled entry keeps its anchor. */
129
133
  providerOutputStart?: number;
130
134
  providers?: OcxProviderContinuationState;
135
+ unforcedStoreFalse?: boolean;
131
136
  spill: ResponseSpillRef;
132
137
  sizeBytes: number;
133
138
  }
@@ -315,6 +320,7 @@ function measureResidentEntry(id: string, entry: ResidentInput): ResidentRespons
315
320
  items: entry.items,
316
321
  ...(entry.providerOutputStart !== undefined ? { providerOutputStart: entry.providerOutputStart } : {}),
317
322
  ...(entry.providers ? { providers: entry.providers } : {}),
323
+ ...(entry.unforcedStoreFalse ? { unforcedStoreFalse: entry.unforcedStoreFalse } : {}),
318
324
  });
319
325
  return sizeBytes === null ? null : { kind: "resident", ...entry, sizeBytes };
320
326
  }
@@ -417,7 +423,9 @@ function swapResidentForSpill(id: string, expected: ResidentResponseState, ref:
417
423
  kind: "spill",
418
424
  createdAt: expected.createdAt,
419
425
  ...(expected.clientThreadId ? { clientThreadId: expected.clientThreadId } : {}),
426
+ ...(expected.providerOutputStart !== undefined ? { providerOutputStart: expected.providerOutputStart } : {}),
420
427
  ...(expected.providers ? { providers: expected.providers } : {}),
428
+ ...(expected.unforcedStoreFalse ? { unforcedStoreFalse: expected.unforcedStoreFalse } : {}),
421
429
  spill: ref,
422
430
  };
423
431
  const next: SpilledResponseState = { ...base, sizeBytes: stubSize(id, base) };
@@ -441,6 +449,7 @@ function replaceSpillEntryAtomically(
441
449
  items: candidate.items,
442
450
  ...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
443
451
  ...(candidate.providers ? { providers: candidate.providers } : {}),
452
+ ...(candidate.unforcedStoreFalse ? { unforcedStoreFalse: candidate.unforcedStoreFalse } : {}),
444
453
  });
445
454
  const base: Omit<SpilledResponseState, "sizeBytes"> = {
446
455
  kind: "spill",
@@ -448,6 +457,7 @@ function replaceSpillEntryAtomically(
448
457
  ...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
449
458
  ...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
450
459
  ...(candidate.providers ? { providers: candidate.providers } : {}),
460
+ ...(candidate.unforcedStoreFalse ? { unforcedStoreFalse: candidate.unforcedStoreFalse } : {}),
451
461
  spill: ref,
452
462
  };
453
463
  const next: SpilledResponseState = { ...base, sizeBytes: stubSize(id, base) };
@@ -533,6 +543,7 @@ function admitOversizedCandidate(
533
543
  items: candidate.items,
534
544
  ...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
535
545
  ...(candidate.providers ? { providers: candidate.providers } : {}),
546
+ ...(candidate.unforcedStoreFalse ? { unforcedStoreFalse: candidate.unforcedStoreFalse } : {}),
536
547
  });
537
548
  // Enforce the ceiling against the REAL envelope: the spill payload adds
538
549
  // the {version, responseId, ...} wrapper, so a candidate within the
@@ -547,7 +558,9 @@ function admitOversizedCandidate(
547
558
  kind: "spill",
548
559
  createdAt: candidate.createdAt,
549
560
  ...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
561
+ ...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
550
562
  ...(candidate.providers ? { providers: candidate.providers } : {}),
563
+ ...(candidate.unforcedStoreFalse ? { unforcedStoreFalse: candidate.unforcedStoreFalse } : {}),
551
564
  spill: ref,
552
565
  };
553
566
  const next: SpilledResponseState = { ...base, sizeBytes: stubSize(id, base) };
@@ -863,20 +876,29 @@ export function replayOverlapSkipsForTests(): number {
863
876
  * This ceiling therefore bounds what the store owns, which is every file it can
864
877
  * account for, and not the directory as a whole.
865
878
  */
866
- function enforceSpilledResponseBudget(): number {
879
+ function enforceSpilledResponseBudget(requiredHeadroomBytes?: number): number {
867
880
  // Price in-flight publications too: a file being created by
868
881
  // `writeResponseSpillDurablyAsync` occupies the volume before it reaches `states`.
869
882
  let spilledBytes = accountedResponseSpillBytes();
870
- if (spilledBytes <= spillByteCap()) return 0;
883
+ // Admission (#6747) passes the room its publication needs, 0 included. Evict only while that
884
+ // room is still reachable: re-read accounting after each attempt, since a failed unlink stays
885
+ // charged, and stop the moment the publication cannot fit so no unrelated continuation pays.
886
+ const admission = requiredHeadroomBytes !== undefined;
887
+ const headroom = admission && requiredHeadroomBytes > 0 ? requiredHeadroomBytes : 0;
888
+ const feasible = (): boolean => !admission
889
+ || (spilledBytes = accountedResponseSpillBytes()) - spilledResponseBytes() + headroom <= spillByteCap();
890
+ if (!feasible() || spilledBytes <= spillByteCap() - headroom) return 0;
871
891
  const before = spilledBytes;
872
892
  // Deferred generations go first. They are already superseded, so releasing one
873
893
  // costs only the crash window the queue exists to cover — the same trade
874
894
  // PENDING_SPILL_UNLINKS_MAX already makes against unbounded disk. Evicting a
875
895
  // live continuation to make room for a dead file would be the wrong order.
876
- while (spilledBytes > spillByteCap() && pendingSpillUnlinks.length > 0) {
896
+ while (spilledBytes > spillByteCap() - headroom && pendingSpillUnlinks.length > 0) {
877
897
  const ref = pendingSpillUnlinks.shift()!;
878
898
  spilledBytes -= ref.payloadBytes;
879
899
  deleteResponseSpill(ref);
900
+ if (admission) spillCounters.headroomEvictions += 1;
901
+ if (!feasible()) return before - spilledBytes;
880
902
  }
881
903
  // Ordered by createdAt, not by map order. `states` is not an age index:
882
904
  // demotion and spill replacement delete and reinsert entries, and
@@ -893,9 +915,11 @@ function enforceSpilledResponseBudget(): number {
893
915
  .sort((a, b) => a[1].createdAt - b[1].createdAt
894
916
  || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
895
917
  for (const [id, entry] of spilled) {
896
- if (spilledBytes <= spillByteCap()) break;
918
+ if (spilledBytes <= spillByteCap() - headroom) break;
897
919
  spilledBytes -= entry.spill.payloadBytes;
898
920
  deleteEntry(id);
921
+ if (admission) spillCounters.headroomEvictions += 1;
922
+ if (!feasible()) return before - spilledBytes;
899
923
  }
900
924
  return before - spilledBytes;
901
925
  }
@@ -935,6 +959,7 @@ function pruneResponses(at = now()): void {
935
959
  items: entry.items,
936
960
  ...(entry.providerOutputStart !== undefined ? { providerOutputStart: entry.providerOutputStart } : {}),
937
961
  ...(entry.providers ? { providers: entry.providers } : {}),
962
+ ...(entry.unforcedStoreFalse ? { unforcedStoreFalse: entry.unforcedStoreFalse } : {}),
938
963
  });
939
964
  if (swapResidentForSpill(oldestId, entry, ref)) noteSpillWriteSuccess();
940
965
  } catch (error) {
@@ -994,6 +1019,7 @@ export function evictOldestResponseContinuationForBudget(): number {
994
1019
  items: entry.items,
995
1020
  ...(entry.providerOutputStart !== undefined ? { providerOutputStart: entry.providerOutputStart } : {}),
996
1021
  ...(entry.providers ? { providers: entry.providers } : {}),
1022
+ ...(entry.unforcedStoreFalse ? { unforcedStoreFalse: entry.unforcedStoreFalse } : {}),
997
1023
  });
998
1024
  if (swapResidentForSpill(id, entry, ref)) noteSpillWriteSuccess();
999
1025
  } catch (error) {
@@ -1030,6 +1056,15 @@ function materializeEntry(
1030
1056
  schedulePersist();
1031
1057
  return { ok: false, failure };
1032
1058
  }
1059
+ // A restricted snapshot stub must agree with the validated spill's provenance.
1060
+ if (entry.unforcedStoreFalse && (result.payload.unforcedStoreFalse !== true
1061
+ || entry.providerOutputStart === undefined || entry.providerOutputStart > result.payload.items.length
1062
+ || entry.providerOutputStart !== result.payload.providerOutputStart)) {
1063
+ spillCounters.readFailures += 1;
1064
+ replaceWithSpillFailure(id, entry);
1065
+ schedulePersist();
1066
+ return { ok: false, failure: { code: "previous_response_not_found", reason: "spill_corrupt" } };
1067
+ }
1033
1068
  const state = measureResidentEntry(id, {
1034
1069
  createdAt: result.payload.createdAt,
1035
1070
  ...(result.payload.clientThreadId ? { clientThreadId: result.payload.clientThreadId } : {}),
@@ -1038,6 +1073,7 @@ function materializeEntry(
1038
1073
  ? { providerOutputStart: result.payload.providerOutputStart }
1039
1074
  : {}),
1040
1075
  ...(result.payload.providers ? { providers: result.payload.providers } : {}),
1076
+ ...(result.payload.unforcedStoreFalse ? { unforcedStoreFalse: result.payload.unforcedStoreFalse } : {}),
1041
1077
  });
1042
1078
  if (!state) {
1043
1079
  spillCounters.readFailures += 1;
@@ -1075,6 +1111,15 @@ export function expandPreviousResponseInput(body: unknown, clientThreadId?: stri
1075
1111
  replayScopeMismatchDrops += 1;
1076
1112
  return body;
1077
1113
  }
1114
+ const clientInput = inputItems(request.input);
1115
+ const stored = materialized.state.items;
1116
+ const carried = clientCarriedPrefixLength(stored, clientInput);
1117
+ if (
1118
+ materialized.state.unforcedStoreFalse
1119
+ && !allowsUnforcedStoreFalseReplay(stored, materialized.state.providerOutputStart, clientInput, carried)
1120
+ ) {
1121
+ return body;
1122
+ }
1078
1123
  // The client already replayed this history verbatim. Prepending the stored copy would
1079
1124
  // double it, and the doubled turn is stored again, so the next turn triples (#1412 saw
1080
1125
  // 127k of real context reach 1.3M tokens this way).
@@ -1087,10 +1132,7 @@ export function expandPreviousResponseInput(body: unknown, clientThreadId?: stri
1087
1132
  // There is no invariant that provider output always carries ids, so an entry whose output
1088
1133
  // has none simply never skips.
1089
1134
  {
1090
- const clientInput = inputItems(request.input);
1091
- const stored = materialized.state.items;
1092
1135
  const anchor = materialized.state.providerOutputStart;
1093
- const carried = clientCarriedPrefixLength(stored, clientInput);
1094
1136
  if (
1095
1137
  carried === stored.length
1096
1138
  && anchor !== undefined
@@ -1110,9 +1152,9 @@ export function expandPreviousResponseInput(body: unknown, clientThreadId?: stri
1110
1152
  }
1111
1153
  const expanded = {
1112
1154
  ...request,
1113
- input: [...materialized.state.items, ...inputItems(request.input)],
1155
+ input: [...stored, ...clientInput],
1114
1156
  };
1115
- replayedInputPrefixLengths.set(expanded, materialized.state.items.length);
1157
+ replayedInputPrefixLengths.set(expanded, stored.length);
1116
1158
  return expanded;
1117
1159
  }
1118
1160
 
@@ -1156,29 +1198,6 @@ export function previousResponseProviderState(responseId: string | undefined): O
1156
1198
  return providers ? structuredClone(providers) : undefined;
1157
1199
  }
1158
1200
 
1159
- export interface ResponseStateMetrics {
1160
- count: number;
1161
- residentCount: number;
1162
- spillStubCount: number;
1163
- tombstoneCount: number;
1164
- totalBytes: number;
1165
- spillPayloadBytes: number;
1166
- largestBytes: number;
1167
- oldestAgeMs: number;
1168
- spillWrites: number;
1169
- spillWriteFailures: number;
1170
- spillWriteStatus: ResponseSpillWriteStatus;
1171
- spillWriteConsecutiveFailures: number;
1172
- spillLastWriteFailureCode: ResponseSpillWriteFailureCode | null;
1173
- spillLastWriteFailureOrigin: ResponseSpillWriteFailureOrigin | null;
1174
- spillAclRetryReturnedTimeouts: number;
1175
- spillAclTimeoutMemoRefusals: number;
1176
- spillLastWriteFailureAt: number | null;
1177
- spillLastWriteSuccessAt: number | null;
1178
- spillReadFailures: number;
1179
- replayScopeMismatchDrops: number;
1180
- }
1181
-
1182
1201
  /**
1183
1202
  * Observe-only snapshot of the in-RAM continuation store, surfaced via GET /api/system/memory.
1184
1203
  * Additive and side-effect free — it does NOT lazy-load the disk snapshot, prune, or evict — so a
@@ -1189,50 +1208,24 @@ export interface ResponseStateMetrics {
1189
1208
  * store (JS heap) or in the runtime allocator (native).
1190
1209
  */
1191
1210
  export function responseStateMetrics(): ResponseStateMetrics {
1192
- const at = now();
1193
- let largestBytes = 0;
1194
- let oldestCreatedAt = at;
1195
- let residentCount = 0;
1196
- let spillStubCount = 0;
1197
- let tombstoneCount = 0;
1198
- let spillPayloadBytes = 0;
1199
- for (const state of states.values()) {
1200
- const bytes = state.sizeBytes;
1201
- if (bytes > largestBytes) largestBytes = bytes;
1202
- if (state.createdAt < oldestCreatedAt) oldestCreatedAt = state.createdAt;
1203
- if (state.kind === "resident") {
1204
- residentCount += 1;
1205
- } else if (state.kind === "spill") {
1206
- spillStubCount += 1;
1207
- spillPayloadBytes += state.spill.payloadBytes;
1208
- } else tombstoneCount += 1;
1209
- }
1210
- return {
1211
- count: states.size,
1212
- residentCount,
1213
- spillStubCount,
1214
- tombstoneCount,
1211
+ return computeResponseStateMetrics({
1212
+ states: states.values(),
1213
+ stateCount: states.size,
1214
+ now: now(),
1215
1215
  totalBytes: responseContinuationRetainedStoreSnapshot().bytes,
1216
- spillPayloadBytes,
1217
- largestBytes,
1218
- oldestAgeMs: states.size > 0 ? at - oldestCreatedAt : 0,
1219
1216
  spillWrites: spillCounters.writes,
1220
1217
  spillWriteFailures: spillCounters.writeFailures,
1221
- spillWriteStatus: spillWriteHealth.consecutiveFailures > 0
1222
- ? "degraded"
1223
- : spillWriteHealth.lastSuccessAt !== null
1224
- ? "healthy"
1225
- : "initial",
1226
1218
  spillWriteConsecutiveFailures: spillWriteHealth.consecutiveFailures,
1227
- spillLastWriteFailureCode: spillWriteHealth.lastFailureCode,
1228
- spillLastWriteFailureOrigin: spillWriteHealth.lastFailureOrigin,
1219
+ spillLastFailureCode: spillWriteHealth.lastFailureCode,
1220
+ spillLastFailureOrigin: spillWriteHealth.lastFailureOrigin,
1221
+ spillLastSuccessAt: spillWriteHealth.lastSuccessAt,
1229
1222
  spillAclRetryReturnedTimeouts: spillCounters.aclRetryReturnedTimeouts,
1230
1223
  spillAclTimeoutMemoRefusals: spillCounters.aclTimeoutMemoRefusals,
1231
- spillLastWriteFailureAt: spillWriteHealth.lastFailureAt,
1232
- spillLastWriteSuccessAt: spillWriteHealth.lastSuccessAt,
1224
+ spillCapacityRefusals: spillCounters.capacityRefusals, spillHeadroomEvictions: spillCounters.headroomEvictions,
1225
+ spillLastFailureAt: spillWriteHealth.lastFailureAt,
1233
1226
  spillReadFailures: spillCounters.readFailures,
1234
1227
  replayScopeMismatchDrops,
1235
- };
1228
+ });
1236
1229
  }
1237
1230
 
1238
1231
  /**
@@ -1256,7 +1249,7 @@ export function rememberResponseState(
1256
1249
  requestBody: unknown,
1257
1250
  response: { id?: unknown; output?: unknown; status?: unknown; incomplete_details?: unknown },
1258
1251
  providerState?: OcxProviderContinuationState | string,
1259
- opts?: { force?: boolean; clientThreadId?: string },
1252
+ opts?: { force?: boolean; clientThreadId?: string; retainForToolContinuation?: boolean },
1260
1253
  ): void {
1261
1254
  if (!requestBody || typeof requestBody !== "object" || Array.isArray(requestBody)) return;
1262
1255
  const request = requestBody as Record<string, unknown>;
@@ -1266,8 +1259,10 @@ export function rememberResponseState(
1266
1259
  // The passthrough branch records with force so those chains can be expanded locally; the
1267
1260
  // store stays in-memory under RESPONSE_TTL_MS, so this is a proxy-internal continuation cache, not
1268
1261
  // real server-side response storage.
1269
- if (request.store === false && !opts?.force) return;
1270
1262
  if (typeof response.id !== "string" || !Array.isArray(response.output)) return;
1263
+ // Only an explicitly opted-in adapter may retain pending client calls despite store:false.
1264
+ const unforcedStoreFalse = request.store === false && !opts?.force;
1265
+ if (unforcedStoreFalse && !(opts?.retainForToolContinuation === true && hasPendingClientToolCall(response.output))) return;
1271
1266
  if (response.status === "incomplete") {
1272
1267
  const details = response.incomplete_details;
1273
1268
  if (!details || typeof details !== "object" || Array.isArray(details)
@@ -1300,6 +1295,7 @@ export function rememberResponseState(
1300
1295
  // incomplete agent turn on the Cursor side (we suspended without a real mcpResult), so its
1301
1296
  // checkpoint must not be reused — but the conversation id string itself is still valid.
1302
1297
  ...(Object.keys(normalizedProviderState).length > 0 ? { providers: normalizedProviderState } : {}),
1298
+ ...(unforcedStoreFalse ? { unforcedStoreFalse: true } : {}),
1303
1299
  });
1304
1300
  enforceAppOwnedMemoryBudget();
1305
1301
  schedulePersist();
@@ -1337,11 +1333,7 @@ export function clearResponseStateMemoryForTests(): void {
1337
1333
  oldestResidentAt = null;
1338
1334
  stateRevision = 0;
1339
1335
  pendingSpillUnlinks.length = 0;
1340
- spillCounters.writes = 0;
1341
- spillCounters.writeFailures = 0;
1342
- spillCounters.readFailures = 0;
1343
- spillCounters.aclRetryReturnedTimeouts = 0;
1344
- spillCounters.aclTimeoutMemoRefusals = 0;
1336
+ resetSpillCountersForTests();
1345
1337
  spillWriteHealth.consecutiveFailures = 0;
1346
1338
  spillWriteHealth.lastFailureCode = null;
1347
1339
  spillWriteHealth.lastFailureOrigin = null;
@@ -54,3 +54,20 @@ export function externalTaskInputContent(item: unknown): string | OcxContentPart
54
54
  block.type === "output_text" ? { ...block, type: "input_text" } : block,
55
55
  ));
56
56
  }
57
+
58
+ /**
59
+ * The same recognition, emitted as Responses-wire user content for the raw-body repairs (#6764).
60
+ *
61
+ * The passthrough and routed-compaction paths never read the parsed context, so they used to
62
+ * send this envelope through the generic orphan repair, which prefixes
63
+ * "[tool output for unknown call]". A summarizer then read an authorized handover as a stray tool
64
+ * result. Recognition is shared with the parser so both paths agree on what is task input.
65
+ */
66
+ export function externalTaskInputResponsesContent(item: unknown): Record<string, unknown>[] | undefined {
67
+ if (externalTaskInputContent(item) === undefined) return undefined;
68
+ const output = (item as { output: string | TaskInputBlock[] }).output;
69
+ if (typeof output === "string") return [{ type: "input_text", text: output }];
70
+ return output.map(block => block.type === "input_image"
71
+ ? { type: "input_image", image_url: block.image_url, ...(block.detail ? { detail: block.detail === "original" ? "high" : block.detail } : {}) }
72
+ : { type: "input_text", text: block.text });
73
+ }
package/src/router.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { CODEX_INTERNAL_OPENAI_MODELS } from "./codex/control-plane-models";
1
2
  import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "./types";
2
3
  import { createHash } from "node:crypto";
3
4
  import { peekAuthStore } from "./oauth/store";
@@ -12,6 +13,7 @@ import {
12
13
  } from "./combos";
13
14
  import type { NormalizedComboConfig } from "./combos/types";
14
15
  import { hasOwnProvider } from "./config/provider-name";
16
+ import { anthropicInstanceRowShapeMatches } from "./providers/anthropic-instance-id";
15
17
  import { providerUsesKeyAuthOverride, resolveProviderApiKey } from "./providers/key-store";
16
18
  import { captureProviderApiKeySelection } from "./providers/api-key-selection-capture";
17
19
  import { assertProviderDestinationAllowed } from "./lib/destination-policy";
@@ -53,6 +55,14 @@ import { evaluatePolicyProfile, type PolicyRequestEvidence } from "./routing/eva
53
55
  import { assemblePolicyCandidateEvidence } from "./routing/compatibility/assemble";
54
56
  import { resolveModelPolicy, type ResolvedModelPolicy } from "./providers/resolved-model-policy";
55
57
 
58
+ /** Explicit Pool 2 admission refusal; ingress must not recover it through default routing. */
59
+ export class AnthropicSecondaryInstanceUnavailableError extends Error {
60
+ constructor() {
61
+ super("Anthropic Pool 2 requires an enabled configured provider with valid OAuth ownership");
62
+ this.name = "AnthropicSecondaryInstanceUnavailableError";
63
+ }
64
+ }
65
+
56
66
  export class UnknownRoutingPolicyError extends Error {
57
67
  constructor(readonly profileId: string) {
58
68
  super(`Unknown routing policy: ${profileId}`);
@@ -521,9 +531,18 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
521
531
  return resolved;
522
532
  }
523
533
 
534
+ /**
535
+ * Rows eligible for bare-model inference (configured default model, model list, model alias).
536
+ *
537
+ * The marked builtin Pool 2 row is excluded: it seeds the same catalog as `anthropic`, so letting it
538
+ * compete would move a bare `claude-*` request onto Pool 2 by insertion order, or make a shared alias
539
+ * ambiguous. Pool 2 is reached only through `anthropic2/<model>`, its provider alias, or an explicit
540
+ * `defaultProvider`. An unmarked custom row named `anthropic2` keeps its ordinary custom meaning.
541
+ */
524
542
  function activeProviderEntries(config: OcxConfig): [string, OcxProviderConfig][] {
525
543
  return Object.entries(config.providers)
526
- .filter(([name, provider]) => name !== LEGACY_CHATGPT_PROVIDER_ID && provider.disabled !== true);
544
+ .filter(([name, provider]) => name !== LEGACY_CHATGPT_PROVIDER_ID && provider.disabled !== true
545
+ && !(name === "anthropic2" && anthropicInstanceRowShapeMatches(name, provider)));
527
546
  }
528
547
 
529
548
  export class NoEnabledOpenAiProviderError extends Error {
@@ -564,9 +583,6 @@ export function comboRouteDecisionTrace(
564
583
  });
565
584
  }
566
585
 
567
- // Codex uses a small number of control-plane model ids that are not part of the public GPT/o
568
- // naming families. Keep this exact: a broad `codex-*` rule could capture a third-party model.
569
- const CODEX_INTERNAL_OPENAI_MODELS = new Set(["codex-auto-review"]);
570
586
  const MAX_BLOCKED_MODEL_REDIRECT_EDGES = 5;
571
587
 
572
588
  interface BlockedModelRedirectState {
@@ -620,6 +636,9 @@ function routeResult(
620
636
  routeReason: string,
621
637
  redirectState: BlockedModelRedirectState,
622
638
  ): RouteResult {
639
+ if (providerName === "anthropic2" && provider.authMode === "oauth" && !anthropicInstanceRowShapeMatches(providerName, provider)) {
640
+ throw new AnthropicSecondaryInstanceUnavailableError();
641
+ }
623
642
  const qualified = resolveBlockedModelRedirect(config, `${providerName}/${modelId}`);
624
643
  const bare = resolveBlockedModelRedirect(config, modelId);
625
644
  const crossTarget = config && (
@@ -813,6 +832,11 @@ function routeModelInternal(
813
832
  if (slash > 0) {
814
833
  const requestedProvider = modelId.slice(0, slash);
815
834
  const requestedLower = requestedProvider.toLowerCase();
835
+ // Literal B is a reserved instance selector even before its row exists. Keep other
836
+ // qualifiers, including exact uppercase custom keys, on the ordinary routing path.
837
+ if (requestedProvider === "anthropic2" && !hasOwnProvider(config.providers, requestedProvider)) {
838
+ throw new AnthropicSecondaryInstanceUnavailableError();
839
+ }
816
840
  let provName: string | undefined;
817
841
 
818
842
  if (hasOwnProvider(config.providers, requestedProvider)) {
@@ -76,7 +76,7 @@ export function assemblePolicyCandidateEvidence(
76
76
  quota: quotaEvidenceForCandidate({
77
77
  provider: candidate.provider,
78
78
  model: candidate.model,
79
- }),
79
+ }, config),
80
80
  cost: costEvidenceForCandidate({
81
81
  provider: candidate.provider,
82
82
  model: candidate.model,
@@ -1,3 +1,5 @@
1
+ import type { OcxConfig } from "../types";
2
+ import { configuredAnthropicInstance } from "../providers/anthropic-instance";
1
3
  /**
2
4
  * Quota-aware policy scoring (RI-07).
3
5
  *
@@ -98,7 +100,7 @@ export function codexPoolQuotaEvidence(accounts: readonly CodexPoolQuotaAccount[
98
100
  };
99
101
  }
100
102
 
101
- export function quotaEvidenceForCandidate(input: QuotaEvidenceInput): RouteQuotaEvidence {
103
+ export function quotaEvidenceForCandidate(input: QuotaEvidenceInput, config?: Pick<OcxConfig, "providers">): RouteQuotaEvidence {
102
104
  if (input.provider === "openai" && input.codexAccountId) {
103
105
  // listAccountQuotas() is the reconciled quota snapshot: config-generation
104
106
  // reconciliation prunes removed accounts. Other eligibility dimensions
@@ -113,8 +115,10 @@ export function quotaEvidenceForCandidate(input: QuotaEvidenceInput): RouteQuota
113
115
  return codexAccountQuotaEvidence(input.codexAccountId, input.codexAccountPlan);
114
116
  }
115
117
 
116
- if (input.provider === "anthropic" && input.accountRef) {
117
- const quota = getCachedProviderAccountQuota("anthropic", input.accountRef);
118
+ const instance = config ? configuredAnthropicInstance(config, input.provider)
119
+ : input.provider === "anthropic" ? "anthropic" : undefined;
120
+ if (instance && input.accountRef) {
121
+ const quota = getCachedProviderAccountQuota(instance, input.accountRef);
118
122
  if (quota) {
119
123
  const family = anthropicFamilyWindow(input.model, quota.customWindows ?? []);
120
124
  const percents = [quota.fiveHourPercent, quota.weeklyPercent, quota.monthlyPercent, family?.percent]
@@ -38,6 +38,7 @@ import { redactSecretString } from "../lib/redact";
38
38
  import { DECLARABLE_HOSTED_TOOL_TYPES } from "../responses/hosted-tool-policy";
39
39
  import { effectiveGoogleMode, getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry";
40
40
  import { providerConfigSeed } from "../providers/derive";
41
+ import { anthropicOAuthInstanceConfigError } from "../config/schema/anthropic-account-pool";
41
42
  import type { OcxConfig, OcxProviderConfig } from "../types";
42
43
  import { openRouterRoutingConfigError } from "../providers/openrouter-routing";
43
44
  import { modelAutoCompactTokenLimitsConfigError } from "../providers/auto-compact-budget";
@@ -785,6 +786,8 @@ export function providerManagementConfigError(
785
786
  return `provider ${name} must not include codexAccountMode`;
786
787
  }
787
788
  const typed = provider as unknown as OcxProviderConfig;
789
+ const markerError = anthropicOAuthInstanceConfigError({ providers: { [name]: provider } });
790
+ if (markerError) return markerError;
788
791
  // Every write path (POST, PUT, reload, both PATCH passes) funnels through here, so a PATCH
789
792
  // that changes authMode/baseUrl/adapter under a retained tlsProfile is refused before it
790
793
  // persists a row the config schema would later reject as document-fatal.
@@ -1057,6 +1060,8 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
1057
1060
  modelVercelGatewayRouting: "editor",
1058
1061
  authMode: "editor",
1059
1062
  oauthAccountFailover: "editor",
1063
+ anthropicAccountPool: "editor",
1064
+ anthropicOAuthInstance: "editor",
1060
1065
  keyOptional: "editor",
1061
1066
  freeTier: "editor",
1062
1067
  note: "editor",
@@ -14,6 +14,7 @@ import {
14
14
  } from "../storage/policy-scheduler";
15
15
  import { startQuotaResetPoller, stopQuotaResetPoller } from "../quota/reset-poller";
16
16
  import {
17
+ setCatalogAutoRefreshClientFanout,
17
18
  startCatalogAutoRefresh,
18
19
  stopCatalogAutoRefresh,
19
20
  syncCatalogAutoRefreshCadence,
@@ -40,6 +41,7 @@ type ProcessLoops = {
40
41
 
41
42
  type LeaseOwner = {
42
43
  token: symbol;
44
+ fanoutRevoked: boolean;
43
45
  applyPolicy: PolicyApply;
44
46
  resources: ServerResourceOwnerLease;
45
47
  lowQuota: LowQuotaRegistration;
@@ -48,6 +50,7 @@ type LeaseOwner = {
48
50
  export type ServerBackgroundLifecycleLease = {
49
51
  scheduleStartupRun(): void;
50
52
  listLowQuotaEvents(limit?: number): LowQuotaEvent[];
53
+ revokeClientFanout(): void;
51
54
  release(): Promise<void>;
52
55
  releaseAfterFailedStart(): void;
53
56
  };
@@ -61,6 +64,25 @@ function setLivePolicyOwner(applyPolicy: PolicyApply | null): void {
61
64
  setStorageCleanupPolicyJobLiveApply(applyPolicy);
62
65
  }
63
66
 
67
+ /** Resolve the current process owner at delivery time so lease hand-over cannot retain an old port. */
68
+ async function refreshConnectedClientsAfterCatalogChange(isCurrent: () => boolean) {
69
+ const captured = owners.findLast(owner => !owner.fanoutRevoked);
70
+ if (!captured) return [];
71
+ const current = () => isCurrent() && !captured.fanoutRevoked && owners.includes(captured);
72
+ const [{ readRuntimePort }, { loadConfig }, { localClientSyncAllowed }, { syncEnabledClientIntegrations }] =
73
+ await Promise.all([
74
+ import("../config/process-state"),
75
+ import("../config"),
76
+ import("../codex/desired-state"),
77
+ import("./management/config-routes"),
78
+ ]);
79
+ const runtime = readRuntimePort(process.pid);
80
+ const config = loadConfig();
81
+ // An explicit catalog tick on a hub is not permission to enroll its local clients.
82
+ if (!runtime || !current() || !localClientSyncAllowed(config)) return [];
83
+ return syncEnabledClientIntegrations(runtime.port, config, {}, { unattended: { isCurrent: current } });
84
+ }
85
+
64
86
  function startProcessLoops(applyPolicy: PolicyApply): ProcessLoops {
65
87
  let memoryWatchdog: MemoryWatchdog | null = null;
66
88
  let stateStoreSweeper: ReturnType<typeof startStateStoreSweeper> | null = null;
@@ -83,6 +105,7 @@ function startProcessLoops(applyPolicy: PolicyApply): ProcessLoops {
83
105
  // and interval timers are unref'd. The scheduler
84
106
  // module keeps every heavy import inside its tick, so naming it statically here
85
107
  // costs a module record and nothing else.
108
+ setCatalogAutoRefreshClientFanout(refreshConnectedClientsAfterCatalogChange);
86
109
  startCatalogAutoRefresh();
87
110
  // The scheduler starts at its default cadence because resolving the operator's value
88
111
  // reads the config barrel. Fire-and-forget: startup must not await an optional
@@ -106,6 +129,7 @@ function startProcessLoops(applyPolicy: PolicyApply): ProcessLoops {
106
129
  stopStorageCleanupScheduler();
107
130
  stopQuotaResetPoller();
108
131
  stopCatalogAutoRefresh();
132
+ setCatalogAutoRefreshClientFanout(null);
109
133
  setLivePolicyOwner(null);
110
134
  throw error;
111
135
  }
@@ -119,6 +143,7 @@ function stopProcessLoops(): void {
119
143
  stopStorageCleanupScheduler();
120
144
  stopQuotaResetPoller();
121
145
  stopCatalogAutoRefresh();
146
+ setCatalogAutoRefreshClientFanout(null);
122
147
  setLivePolicyOwner(null);
123
148
  }
124
149
 
@@ -181,6 +206,7 @@ export function acquireServerBackgroundLifecycle(
181
206
 
182
207
  const owner: LeaseOwner = {
183
208
  token: Symbol("server-background-lifecycle"),
209
+ fanoutRevoked: false,
184
210
  applyPolicy,
185
211
  resources: acquireServerResourceOwner(),
186
212
  lowQuota: registerCodexLowQuotaProtection(config),
@@ -206,6 +232,7 @@ export function acquireServerBackgroundLifecycle(
206
232
  return stopStoragePolicyWorker().finally(() => { cleanupInProgress = false; });
207
233
  }
208
234
  return {
235
+ revokeClientFanout() { owner.fanoutRevoked = true; },
209
236
  listLowQuotaEvents(limit) { return owner.lowQuota.listEvents(limit); },
210
237
  scheduleStartupRun() {
211
238
  if (owners.some(candidate => candidate.token === owner.token)) {
@@ -32,7 +32,7 @@ import {
32
32
  UPSTREAM_RESET_REPLAY_REFUSED_CODE,
33
33
  } from "../lib/upstream-retry";
34
34
  import { estimateTokens } from "../lib/token-estimate";
35
- import { captureRouteStaticPolicy, NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
35
+ import { AnthropicSecondaryInstanceUnavailableError, captureRouteStaticPolicy, NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
36
36
  import { evidenceFromBody } from "../routing/request-evidence";
37
37
  import { resolveWireProtocolOverride } from "./adapter-resolve";
38
38
  import { resolveOpenCodeGoTransport } from "../providers/opencode-go-transport";
@@ -113,6 +113,7 @@ export async function handleChatCompletions(
113
113
  return finalizeTranslatorBudgetResponse(
114
114
  await handleChatCompletionsWithBudget(req, config, logCtx, translatorBudget, logIds),
115
115
  translatorBudget,
116
+ req.signal,
116
117
  );
117
118
  } catch (error) {
118
119
  translatorBudget.dispose();
@@ -242,6 +243,11 @@ async function handleChatCompletionsWithBudget(
242
243
  }
243
244
  }
244
245
  } catch (err) {
246
+ if (err instanceof AnthropicSecondaryInstanceUnavailableError) {
247
+ logCtx.requestedModel = requestedModel;
248
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 401, { closeReason: "non_stream" });
249
+ return chatCompletionsErrorResponse(401, err.message, "authentication_error");
250
+ }
245
251
  if (err instanceof AdmissionModelDeniedError) {
246
252
  logCtx.requestedModel = requestedModel;
247
253
  if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 403, { closeReason: "non_stream" });