@bitkyc08/opencodex 2.80.0 → 2.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (376) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +166 -73
  3. package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
  4. package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
  5. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  6. package/gui/dist/assets/index-D3FBCNiP.js +94 -0
  7. package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/gui/dist/provider-icons/claude-green.svg +1 -0
  10. package/package.json +3 -2
  11. package/src/adapters/anthropic-model-contract.ts +8 -2
  12. package/src/adapters/anthropic.ts +8 -3
  13. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  14. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  15. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  16. package/src/adapters/coding-agent/protocol.ts +122 -17
  17. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  18. package/src/adapters/coding-agent/turn.ts +101 -44
  19. package/src/adapters/cursor/catalog.ts +8 -0
  20. package/src/adapters/cursor/effort-map.ts +2 -0
  21. package/src/adapters/devin/live-models.ts +2 -0
  22. package/src/adapters/kiro-retry.ts +9 -4
  23. package/src/adapters/openai-chat/messages.ts +7 -6
  24. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  25. package/src/adapters/openai-responses/passthrough.ts +9 -4
  26. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  27. package/src/adapters/physical-send.ts +8 -5
  28. package/src/adapters/qoder/adapter.ts +41 -6
  29. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  30. package/src/adapters/zed.ts +16 -1
  31. package/src/claude/agents-inject.ts +14 -3
  32. package/src/claude/context-windows.ts +19 -17
  33. package/src/claude/desktop-3p.ts +88 -12
  34. package/src/claude/desktop-profile.ts +2 -1
  35. package/src/claude/gateway-cache.ts +18 -1
  36. package/src/claude/inbound-cache-stabilize.ts +4 -9
  37. package/src/claude/inbound.ts +78 -3
  38. package/src/claude/intercept/cli-catalog.ts +3 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  41. package/src/claude/intercept/picker-listener.ts +71 -4
  42. package/src/claude/intercept/picker-models.ts +14 -6
  43. package/src/claude/intercept/settings.ts +3 -0
  44. package/src/claude/long-context.ts +66 -0
  45. package/src/claude/model-info.ts +19 -16
  46. package/src/claude/outbound.ts +94 -9
  47. package/src/claude/subagent-model.ts +40 -11
  48. package/src/cli/account-anthropic-threshold.ts +14 -5
  49. package/src/cli/account-api.ts +3 -1
  50. package/src/cli/account-extended.ts +13 -9
  51. package/src/cli/account-policy-dto.ts +1 -1
  52. package/src/cli/account-policy.ts +13 -4
  53. package/src/cli/account.ts +3 -3
  54. package/src/cli/agent.ts +9 -5
  55. package/src/cli/aside-profile-recovery.ts +33 -0
  56. package/src/cli/capabilities-accounts.ts +2 -2
  57. package/src/cli/capabilities-agents-routing.ts +24 -0
  58. package/src/cli/capabilities-base.ts +5 -5
  59. package/src/cli/capabilities-integrations.ts +18 -2
  60. package/src/cli/cli-path-diagnostics.ts +132 -0
  61. package/src/cli/codex-shim-autorestore.ts +3 -0
  62. package/src/cli/desktop-runtime-guidance.ts +65 -0
  63. package/src/cli/dispatch.ts +15 -1
  64. package/src/cli/doctor.ts +16 -3
  65. package/src/cli/help.ts +4 -1
  66. package/src/cli/index.ts +25 -14
  67. package/src/cli/integration-aside-sync.ts +2 -1
  68. package/src/cli/integration-plan-dto.ts +1 -0
  69. package/src/cli/integration-preview.ts +8 -1
  70. package/src/cli/integrations.ts +48 -6
  71. package/src/cli/message-args.ts +34 -0
  72. package/src/cli/message-command.ts +43 -0
  73. package/src/cli/message-runtime.ts +6 -0
  74. package/src/cli/observe.ts +3 -1
  75. package/src/cli/provider.ts +29 -6
  76. package/src/cli/registry.ts +24 -2
  77. package/src/cli/resolve.ts +25 -2
  78. package/src/cli/root.ts +4 -2
  79. package/src/cli/status.ts +64 -12
  80. package/src/cli/update-restart-child.ts +4 -2
  81. package/src/cli/update-restart-home.ts +28 -10
  82. package/src/cli/update-restart-service-record.ts +116 -0
  83. package/src/cli/update-restart-supervision.ts +96 -0
  84. package/src/cli/update-restart.ts +59 -13
  85. package/src/cli/v2.ts +27 -20
  86. package/src/cli/version-skew-notice.ts +77 -0
  87. package/src/clients/config-export/commandcode.ts +131 -0
  88. package/src/clients/config-export/contracts.ts +12 -0
  89. package/src/clients/config-export.ts +64 -1
  90. package/src/clients/lazycodex.ts +1 -1
  91. package/src/clients/omo-role-models.ts +32 -7
  92. package/src/codex/auth-api/account-list.ts +11 -3
  93. package/src/codex/auth-api/account-selection.ts +107 -0
  94. package/src/codex/auth-api/main-account-probe.ts +25 -8
  95. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  96. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  97. package/src/codex/auth-api/routes.ts +11 -49
  98. package/src/codex/auth-context.ts +20 -1
  99. package/src/codex/autostart-health.ts +12 -3
  100. package/src/codex/catalog/auto-review.ts +13 -6
  101. package/src/codex/catalog/build-entries.ts +33 -7
  102. package/src/codex/catalog/bundled.ts +155 -26
  103. package/src/codex/catalog/combo-member.ts +3 -2
  104. package/src/codex/catalog/control-plane.ts +45 -0
  105. package/src/codex/catalog/effort.ts +37 -6
  106. package/src/codex/catalog/gather-capture.ts +10 -3
  107. package/src/codex/catalog/model-hints.ts +15 -8
  108. package/src/codex/catalog/model-visibility.ts +3 -1
  109. package/src/codex/catalog/operator-tiers.ts +38 -0
  110. package/src/codex/catalog/parsing.ts +2 -1
  111. package/src/codex/catalog/provider-models.ts +58 -44
  112. package/src/codex/catalog/retained-sync.ts +7 -5
  113. package/src/codex/catalog/routed-gather.ts +15 -1
  114. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  115. package/src/codex/catalog-auto-refresh.ts +19 -0
  116. package/src/codex/catalog.ts +1 -1
  117. package/src/codex/codex-home-owner.ts +3 -0
  118. package/src/codex/config-write-lock.ts +333 -0
  119. package/src/codex/control-plane-models.ts +6 -0
  120. package/src/codex/convergence.ts +2 -1
  121. package/src/codex/features.ts +257 -200
  122. package/src/codex/inject/config-write-section.ts +34 -0
  123. package/src/codex/inject/multi-agent-v2.ts +14 -6
  124. package/src/codex/inject/remove.ts +36 -11
  125. package/src/codex/inject/restore.ts +85 -20
  126. package/src/codex/inject-coordination.ts +15 -6
  127. package/src/codex/inject.ts +62 -20
  128. package/src/codex/journal.ts +62 -17
  129. package/src/codex/main-account-cache.ts +43 -0
  130. package/src/codex/paths.ts +2 -0
  131. package/src/codex/prepared-home.ts +18 -0
  132. package/src/codex/prompt-journal.ts +25 -12
  133. package/src/codex/prompt-layers.ts +49 -26
  134. package/src/codex/prompt-lock-claim.ts +134 -0
  135. package/src/codex/prompt-lock-evidence.ts +26 -0
  136. package/src/codex/prompt-lock-io.ts +16 -0
  137. package/src/codex/prompt-lock-owner.ts +130 -0
  138. package/src/codex/prompt-lock.ts +84 -53
  139. package/src/codex/routing/failure-window.ts +146 -0
  140. package/src/codex/routing/selection.ts +4 -1
  141. package/src/codex/routing.ts +13 -17
  142. package/src/codex/runtime.ts +106 -18
  143. package/src/codex/shim.ts +45 -19
  144. package/src/combos/jev-decision-contract.ts +25 -2
  145. package/src/combos/jev-service-exchange.ts +281 -0
  146. package/src/combos/jev.ts +34 -219
  147. package/src/combos/types.ts +1 -1
  148. package/src/config/atomic-write.ts +1 -1
  149. package/src/config/codex-ws-reuse-setting.ts +10 -0
  150. package/src/config/diagnostics.ts +27 -16
  151. package/src/config/live-reconcile.ts +17 -0
  152. package/src/config/load-degrade.ts +42 -0
  153. package/src/config/rebase-provenance.ts +11 -0
  154. package/src/config/schema/anthropic-account-pool.ts +143 -0
  155. package/src/config/schema/config-schema.ts +11 -1
  156. package/src/config/schema/leaf-validators.ts +4 -0
  157. package/src/config.ts +2 -0
  158. package/src/generated/compatibility-version.json +566 -298
  159. package/src/generated/model-metadata.ts +7 -6
  160. package/src/grok/inject.ts +50 -4
  161. package/src/grok/sync.ts +2 -1
  162. package/src/images/loop.ts +30 -11
  163. package/src/integrations/aside-profiles.ts +10 -3
  164. package/src/integrations/catalog-refresh.ts +19 -3
  165. package/src/integrations/mutation-plan.ts +7 -0
  166. package/src/integrations/registry.ts +8 -0
  167. package/src/integrations/state.ts +9 -2
  168. package/src/integrations/writer.ts +17 -5
  169. package/src/lib/bun-path-runtime.d.mts +32 -0
  170. package/src/lib/bun-path-runtime.mjs +95 -0
  171. package/src/lib/bun-runtime-preflight.ts +140 -0
  172. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  173. package/src/lib/desktop-cli-handoff.mjs +92 -0
  174. package/src/lib/desktop-cli-record.d.mts +29 -0
  175. package/src/lib/desktop-cli-record.mjs +102 -0
  176. package/src/lib/lab-live-route-production.ts +9 -1
  177. package/src/lib/request-execution-budget.ts +244 -58
  178. package/src/lib/spend-pool-alias-validation.ts +79 -0
  179. package/src/lib/spend-pool-continuity.ts +34 -0
  180. package/src/lib/spend-reservation-ledger.ts +412 -50
  181. package/src/lib/state-store-registrations.ts +20 -3
  182. package/src/lib/synced-state-location.ts +46 -6
  183. package/src/lib/test-home-guard.ts +87 -1
  184. package/src/lib/translator-budget.ts +17 -4
  185. package/src/lib/upstream-retry.ts +17 -5
  186. package/src/lib/windows-atomic-replace.ts +38 -16
  187. package/src/lib/windows-elevation.ts +6 -0
  188. package/src/lib/windows-secret-acl.ts +92 -25
  189. package/src/lib/winsw.ts +2 -3
  190. package/src/lib/workflow-budget.ts +53 -11
  191. package/src/messaging/budget.ts +45 -0
  192. package/src/messaging/discovery.ts +57 -0
  193. package/src/messaging/envelope.ts +49 -0
  194. package/src/messaging/input.ts +37 -0
  195. package/src/messaging/rpc.ts +196 -0
  196. package/src/messaging/send.ts +58 -0
  197. package/src/messaging/socket.ts +72 -0
  198. package/src/messaging/types.ts +29 -0
  199. package/src/oauth/anthropic-account-refusal.ts +90 -18
  200. package/src/oauth/anthropic-account-threshold.ts +8 -2
  201. package/src/oauth/anthropic-continuity.ts +15 -1
  202. package/src/oauth/anthropic-model-quota.ts +120 -48
  203. package/src/oauth/anthropic-model-routes.ts +11 -3
  204. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  205. package/src/oauth/anthropic-pool-config.ts +30 -0
  206. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  207. package/src/oauth/anthropic-routing.ts +1202 -866
  208. package/src/oauth/anthropic-send-ownership.ts +41 -0
  209. package/src/oauth/anthropic.ts +4 -1
  210. package/src/oauth/chatgpt.ts +113 -41
  211. package/src/oauth/generic-account-failover.ts +3 -1
  212. package/src/oauth/health.ts +8 -6
  213. package/src/oauth/index.ts +96 -175
  214. package/src/oauth/kiro.ts +22 -9
  215. package/src/oauth/login-cli.ts +2 -0
  216. package/src/oauth/model-discovery-auth.ts +43 -0
  217. package/src/oauth/pool-kernel.ts +11 -5
  218. package/src/oauth/pool-settings-capability.ts +5 -3
  219. package/src/oauth/store-anthropic-instance.ts +87 -0
  220. package/src/oauth/store.ts +68 -10
  221. package/src/oauth/token-guardian.ts +2 -0
  222. package/src/oauth/types.ts +2 -0
  223. package/src/protocols/plan-snapshot.ts +5 -0
  224. package/src/protocols/settings.ts +20 -5
  225. package/src/providers/anthropic-instance-id.ts +47 -0
  226. package/src/providers/anthropic-instance.ts +48 -0
  227. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  228. package/src/providers/azure-model-metadata.ts +115 -0
  229. package/src/providers/derive.ts +23 -0
  230. package/src/providers/kiro-models.ts +3 -0
  231. package/src/providers/label.ts +1 -1
  232. package/src/providers/model-rename-fields.ts +2 -0
  233. package/src/providers/quota/account-cache.ts +37 -11
  234. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  235. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  236. package/src/providers/quota/report-cache.ts +24 -1
  237. package/src/providers/quota/vendor-probes-key.ts +127 -17
  238. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  239. package/src/providers/quota.ts +23 -33
  240. package/src/providers/registry/entries-core.ts +35 -30
  241. package/src/providers/registry/model-ids.ts +1 -0
  242. package/src/providers/registry/model-seeds.ts +7 -4
  243. package/src/providers/registry/types.ts +3 -0
  244. package/src/providers/registry.ts +3 -3
  245. package/src/responses/reasoning-envelope.ts +29 -1
  246. package/src/responses/reasoning-replay-cache.ts +17 -1
  247. package/src/responses/spill-store.ts +20 -3
  248. package/src/responses/state/metrics.ts +95 -0
  249. package/src/responses/state/snapshot-codec.ts +6 -0
  250. package/src/responses/state/spill-failure.ts +12 -0
  251. package/src/responses/state/spill-queue.ts +13 -5
  252. package/src/responses/state/unforced-store-false.ts +55 -0
  253. package/src/responses/state.ts +68 -76
  254. package/src/responses/task-input.ts +17 -0
  255. package/src/router.ts +28 -4
  256. package/src/routing/compatibility/assemble.ts +1 -1
  257. package/src/routing/quota.ts +7 -3
  258. package/src/server/auth-cors.ts +5 -0
  259. package/src/server/background-lifecycle.ts +27 -0
  260. package/src/server/chat-completions.ts +7 -1
  261. package/src/server/chat-native.ts +64 -28
  262. package/src/server/claude-messages.ts +59 -8
  263. package/src/server/gui-static.ts +38 -4
  264. package/src/server/inbound-body-admission.ts +1 -0
  265. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  266. package/src/server/index/serve-options.ts +10 -2
  267. package/src/server/index/spend-ledger-lifecycle.ts +12 -5
  268. package/src/server/index/startup-warnings.ts +19 -0
  269. package/src/server/index.ts +5 -2
  270. package/src/server/inference/context.ts +1 -1
  271. package/src/server/lifecycle.ts +1 -1
  272. package/src/server/management/agent-settings-routes.ts +123 -38
  273. package/src/server/management/anthropic-account-threshold.ts +12 -9
  274. package/src/server/management/anthropic-pool-settings.ts +72 -0
  275. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  276. package/src/server/management/codex-agent-role-routes.ts +4 -2
  277. package/src/server/management/codex-prompt-routes.ts +1 -0
  278. package/src/server/management/codex-role-auto-assign.ts +13 -2
  279. package/src/server/management/config-routes.ts +85 -14
  280. package/src/server/management/context.ts +2 -2
  281. package/src/server/management/decision-routes.ts +2 -1
  282. package/src/server/management/integration-routes.ts +25 -2
  283. package/src/server/management/oauth-account-routes.ts +79 -98
  284. package/src/server/management/provider-instance-ownership.ts +44 -0
  285. package/src/server/management/provider-routes.ts +21 -28
  286. package/src/server/management/route-registry.ts +3 -1
  287. package/src/server/management/routing-profile-routes.ts +4 -4
  288. package/src/server/management/shared.ts +3 -3
  289. package/src/server/management/system-restart.ts +44 -14
  290. package/src/server/management/system-routes.ts +24 -2
  291. package/src/server/management/vision-sidecar-options.ts +9 -6
  292. package/src/server/management/web-search-sidecar-options.ts +51 -2
  293. package/src/server/management-auth.ts +6 -0
  294. package/src/server/messages-native-eligibility.ts +3 -2
  295. package/src/server/messages-native-oauth.ts +91 -45
  296. package/src/server/messages-native-selector.ts +39 -0
  297. package/src/server/messages-native.ts +133 -38
  298. package/src/server/messages-response-headers.ts +14 -0
  299. package/src/server/request-log-conversation.ts +48 -5
  300. package/src/server/request-log.ts +36 -2
  301. package/src/server/responses/adapter-continuation.ts +31 -19
  302. package/src/server/responses/adapter-dispatch.ts +75 -24
  303. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  304. package/src/server/responses/codex-ws-exchange.ts +17 -2
  305. package/src/server/responses/codex-ws-pool.ts +100 -13
  306. package/src/server/responses/codex-ws-wire.ts +9 -0
  307. package/src/server/responses/combo-requested-effort.ts +3 -1
  308. package/src/server/responses/compact.ts +25 -8
  309. package/src/server/responses/compaction-recovery.ts +1 -1
  310. package/src/server/responses/core-codex-account.ts +22 -1
  311. package/src/server/responses/core-combo-native.ts +24 -4
  312. package/src/server/responses/core-combo.ts +38 -25
  313. package/src/server/responses/core-lifetime.ts +3 -36
  314. package/src/server/responses/core-normalize.ts +1 -0
  315. package/src/server/responses/core-options.ts +8 -1
  316. package/src/server/responses/core-replay.ts +78 -0
  317. package/src/server/responses/core.ts +3 -3
  318. package/src/server/responses/fetch-helpers.ts +26 -3
  319. package/src/server/responses/passthrough-delivery.ts +30 -4
  320. package/src/server/responses/passthrough-dispatch.ts +39 -9
  321. package/src/server/responses/request-prepare.ts +41 -5
  322. package/src/server/responses/request-send-budget.ts +150 -50
  323. package/src/server/responses/request-spend.ts +325 -33
  324. package/src/server/responses/request-transport.ts +132 -52
  325. package/src/server/responses/response-effects.ts +1 -0
  326. package/src/server/responses/run-turn-execution.ts +13 -3
  327. package/src/server/responses/sidecar-execution.ts +113 -22
  328. package/src/server/responses/sidecar-send-budget.ts +58 -0
  329. package/src/server/responses/ws-upstream.ts +19 -6
  330. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  331. package/src/server/startup-health-cache.ts +2 -1
  332. package/src/server/workflow-refusal.ts +33 -0
  333. package/src/service/cli.ts +20 -4
  334. package/src/service/desktop-command-guard.ts +32 -0
  335. package/src/service/desktop-startup.ts +66 -40
  336. package/src/service/desktop-supervision.d.mts +26 -0
  337. package/src/service/desktop-supervision.mjs +140 -0
  338. package/src/service/managing-cli.ts +1 -0
  339. package/src/service/orchestration.ts +46 -20
  340. package/src/service/ownership-mutation-lease.d.mts +1 -1
  341. package/src/service/ownership-mutation-lease.mjs +5 -7
  342. package/src/service/repair.ts +25 -9
  343. package/src/service/state.ts +4 -3
  344. package/src/service/systemd.ts +17 -8
  345. package/src/service/windows-ops.ts +61 -22
  346. package/src/sidecar/anthropic-binding.ts +79 -0
  347. package/src/sidecar/auth.ts +76 -2
  348. package/src/storage/policy-job.ts +5 -1
  349. package/src/storage/policy-worker.ts +3 -1
  350. package/src/storage/policy.ts +9 -0
  351. package/src/types/anthropic-account-pool.ts +33 -0
  352. package/src/types/config.ts +33 -28
  353. package/src/types/provider.ts +5 -0
  354. package/src/types/request.ts +5 -0
  355. package/src/types/tools.ts +7 -0
  356. package/src/types/wire.ts +1 -0
  357. package/src/types.ts +4 -0
  358. package/src/update/index.ts +57 -9
  359. package/src/update/restart-ownership.ts +10 -3
  360. package/src/update/runtime-ownership.d.mts +6 -0
  361. package/src/update/runtime-ownership.mjs +23 -7
  362. package/src/usage/cost.ts +3 -2
  363. package/src/usage/expected-prices.ts +66 -14
  364. package/src/vision/anthropic-describe.ts +16 -3
  365. package/src/vision/index.ts +3 -0
  366. package/src/vision/plan.ts +19 -6
  367. package/src/web-search/alpha-search.ts +7 -1
  368. package/src/web-search/anthropic-executor.ts +16 -4
  369. package/src/web-search/index.ts +7 -3
  370. package/src/web-search/loop.ts +32 -7
  371. package/src/web-search/passthrough-bridge.ts +4 -1
  372. package/src/web-search/sidecar-providers.ts +4 -3
  373. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  374. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  375. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  376. package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
@@ -1,4 +1,4 @@
1
- import { rotateAnthropicAccountOnResponse } from "../../oauth/anthropic-account-refusal";
1
+ import { rotateAnthropicAccountOnResponseForInstance } from "../../oauth/anthropic-account-refusal";
2
2
  import type { ResponsesRequestContext } from "./core-options";
3
3
  import type { PreparedResponsesRequest } from "./request-prepare";
4
4
  import type { ResponsesTransport } from "./request-transport";
@@ -24,20 +24,22 @@ import {
24
24
  isGenericOAuthFailoverEnabled,
25
25
  rotateGenericOAuthAccountOn429,
26
26
  rotateGenericOAuthAccountOnRefusal,
27
+ rotateAntigravityAccountOnAuthRefusal,
27
28
  quarantineKiroSuspendedAccount,
28
29
  failoverAccountSnapshot,
29
30
  } from "../../oauth/generic-account-failover";
30
31
  import { classifyKiroRefusal } from "../../adapters/kiro-refusal";
32
+ import { isNonReplayableResponse } from "../../lib/upstream-retry";
31
33
  import { noteKiroMonthlyRefusal, noteKiroServedSuccess } from "../../providers/kiro-usage";
32
34
  import { persistKiroAccountState } from "../../providers/kiro-account-state-disk";
33
35
  import { readDisplaySafeErrorText } from "./core-errors";
34
36
  import {
35
37
  ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
36
- getAnthropicPoolAccessSnapshot,
38
+ anthropicRoutingFor,
37
39
  formatAnthropicProviderForLog,
38
40
  } from "../../oauth/anthropic-routing";
39
41
  import { resolveWireProtocolOverride } from "../adapter-resolve";
40
- import { bindRouteReasoningReplayScope, adapterNeedsForcedContinuation } from "./core-replay";
42
+ import { bindRouteReasoningReplayScope, adapterNeedsForcedContinuation, adapterNeedsToolCallContinuation } from "./core-replay";
41
43
  import { namespacedToolName } from "../../types";
42
44
  import { providerFetch } from "./fetch-helpers";
43
45
  import type { AttemptRecoveryKind } from "../../usage/log";
@@ -45,6 +47,8 @@ import { recordAdapterReasoning, recordAdapterTier } from "../request-log";
45
47
  import { normalizeLogConversationId } from "../request-log-conversation";
46
48
  import { rememberResponseState } from "../../responses/state";
47
49
  import { trackStreamLifetime } from "../lifecycle";
50
+ import { isAntigravityRawValidationRefusal } from "./antigravity-validation-refusal";
51
+ import { createSidecarSendBudget } from "./sidecar-send-budget";
48
52
 
49
53
  /** One responsibility of the Responses request pipeline; state owners are explicit. */
50
54
  export async function executeResponsesSidecars(
@@ -67,6 +71,8 @@ export async function executeResponsesSidecars(
67
71
  | "genericFailoverLimit"
68
72
  | "replayOAuthCredentialSnapshot"
69
73
  | "applyFailoverSnapshot"
74
+ | "anthropicInstance"
75
+ | "currentAnthropicRouteDecision"
70
76
  | "anthropicRouteDecision"
71
77
  | "anthropicPoolAccountId"
72
78
  | "anthropicPoolFailovers"
@@ -85,11 +91,15 @@ export async function executeResponsesSidecars(
85
91
  | "notifyResponseComplete"
86
92
  | "cancelResponseCompletion"
87
93
  >,
88
- sendBudgetState: Pick<ResponsesSendBudget, "reserveCredentialHop">,
94
+ sendBudgetState: Pick<ResponsesSendBudget, "reserveCredentialHop" | "refundableAdapterDispatchBudget" | "adapterDispatchBudget" | "noteInitialDispatch" | "noteAdapterPhysicalSend" | "noteAdapterRecoveryWithheld" | "pendingHopPermit">,
89
95
  ) {
90
96
  const { config, options, logCtx } = requestContext;
97
+ const antigravityPoolActivated = requestState.route.providerName === "google-antigravity"
98
+ && isGenericOAuthFailoverEnabled(config, requestState.route.providerName);
91
99
  const {
92
100
  applyFailoverSnapshot,
101
+ anthropicInstance,
102
+ currentAnthropicRouteDecision,
93
103
  anthropicSessionKey,
94
104
  commitResolvedOAuthSelection,
95
105
  resolveSelectionAdapter,
@@ -105,6 +115,7 @@ export async function executeResponsesSidecars(
105
115
  } = requestState;
106
116
  const { routedCompaction, openAiSidecar } = sidecarState;
107
117
  const { reserveCredentialHop } = sendBudgetState;
118
+ const sidecarBudget = createSidecarSendBudget(options, sendBudgetState, () => logCtx.usageLogInputTokens);
108
119
  const {
109
120
  commitReasoningReplayServingRoute,
110
121
  continuationStateForResponse,
@@ -182,13 +193,29 @@ export async function executeResponsesSidecars(
182
193
  || (logCtx.activeAttempt?.deliverySummary?.semanticBytes ?? 0) > 0
183
194
  || (logCtx.activeAttempt?.deliverySummary?.sideEffectEvents ?? 0) > 0;
184
195
  const noteSidecarOutput = () => { sidecarOutputStarted = true; options.onFirstOutput?.(); };
196
+ // A single request may rotate once for a verification refusal. Keep this separate from the
197
+ // broader OAuth failover count: a sidecar turn can also spend that count on rate-limit recovery.
198
+ let antigravityValidationRotationAttempted = false;
185
199
  const rotateSidecarProviderOn429 = async (
186
200
  retryAfter: string | null,
187
201
  responseHeaders?: Headers,
188
202
  retryParsed?: OcxParsedRequest,
189
203
  originalResponse?: Response,
190
204
  ): Promise<{ adapter: ProviderAdapter; recoveryKind: AttemptRecoveryKind } | null> => {
191
- if (route.providerName !== "kiro" && !(route.providerName === "anthropic" && originalResponse?.status === 403) && originalResponse && originalResponse.status !== 429) return null;
205
+ const antigravityValidationResponse = canRunWebSearch && route.providerName === "google-antigravity"
206
+ && route.provider.authMode === "oauth" && originalResponse?.status === 403;
207
+ if (route.providerName !== "kiro" && !(anthropicInstance && (originalResponse?.status === 403 || originalResponse?.status === 401))
208
+ && !antigravityValidationResponse && originalResponse && originalResponse.status !== 429) return null;
209
+ let antigravityVerification = false;
210
+ if (antigravityValidationResponse) {
211
+ if (transportState.adapter.name !== "google" || !antigravityPoolActivated
212
+ || options.abortSignal?.aborted || isNonReplayableResponse(originalResponse)
213
+ || antigravityValidationRotationAttempted
214
+ || transportState.genericFailovers >= transportState.genericFailoverLimit
215
+ || sidecarHasCommittedOutput()) return null;
216
+ antigravityVerification = await isAntigravityRawValidationRefusal(originalResponse, options.abortSignal);
217
+ if (!antigravityVerification) return null;
218
+ }
192
219
  const refusal = route.providerName === "kiro" && originalResponse
193
220
  ? classifyKiroRefusal(originalResponse.status,
194
221
  await readDisplaySafeErrorText(originalResponse.clone(), options.abortSignal ?? new AbortController().signal, "")).kind
@@ -205,6 +232,43 @@ export async function executeResponsesSidecars(
205
232
  // sidecar loops used to flatten all three to `key-429`, so an account rotation read as a key
206
233
  // rotation in the attempt row and in the Logs UI.
207
234
  let recoveryKind: AttemptRecoveryKind = "key-429";
235
+ if (antigravityVerification) {
236
+ const sent = transportState.replayOAuthCredentialSnapshot;
237
+ const failedAccountId = transportState.genericFailoverAccountId;
238
+ if (!sent || !failedAccountId || sent.accountId !== failedAccountId) return null;
239
+ if (options.abortSignal?.aborted || sidecarHasCommittedOutput()) return null;
240
+
241
+ const hop = reserveCredentialHop(
242
+ "auth-recovery",
243
+ `${route.providerName}|${route.modelId}|sidecar-oauth-verify`,
244
+ );
245
+ if (!hop.allowed) return null;
246
+ antigravityValidationRotationAttempted = true;
247
+ const nextAccountId = rotateAntigravityAccountOnAuthRefusal(
248
+ antigravityPoolActivated, sent.accountId, sent.generation, route.modelId,
249
+ );
250
+ if (!nextAccountId) {
251
+ hop.permit?.release();
252
+ return null;
253
+ }
254
+ try {
255
+ // Keep the sibling's bearer and its account-matched project together; never inherit the
256
+ // failed account's identity or project.
257
+ const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
258
+ if (options.abortSignal?.aborted || !await applyFailoverSnapshot(snapshot, retryParsed)
259
+ || transportState.replayOAuthCredentialSnapshot?.accountId !== nextAccountId) {
260
+ hop.permit?.release();
261
+ return null;
262
+ }
263
+ transportState.genericFailovers += 1;
264
+ } catch {
265
+ hop.permit?.release();
266
+ return null;
267
+ }
268
+ recoveryKind = "oauth-account-403";
269
+ if (!options.comboInitialSend) sendBudgetState.pendingHopPermit = hop.permit;
270
+ else sidecarBudget.ownCredentialHop(hop.permit);
271
+ }
208
272
  const rotated = !originalResponse || originalResponse.status === 429
209
273
  ? rotateProviderTransportOn429(config, route.providerName, route.provider, {
210
274
  retryAfter,
@@ -212,7 +276,9 @@ export async function executeResponsesSidecars(
212
276
  attemptedKey: route.provider.apiKey,
213
277
  promptCacheKey: parsed.options.promptCacheKey,
214
278
  }) : null;
215
- if (rotated) {
279
+ if (antigravityVerification) {
280
+ // `applyFailoverSnapshot` has already rebound the route to the sibling identity/project.
281
+ } else if (rotated) {
216
282
  route.provider = rotated;
217
283
  } else if (
218
284
  // A POSITIVE gate, not an early return. An early `return null` here made every later arm
@@ -225,7 +291,8 @@ export async function executeResponsesSidecars(
225
291
  ) {
226
292
  // Intersection with the request's shared budget. The sidecar replay is dispatched by the
227
293
  // web-search/image loop and never reaches `onSendsConsumed`, so this reservation is the
228
- // charge; a refusal returns null and the caller keeps the real 429 it already has.
294
+ // charge for other sidecars. Antigravity web search hands its reservation to the adapter
295
+ // physical-send owner instead; a refusal preserves the real 429.
229
296
  const hop = reserveCredentialHop(
230
297
  "auth-recovery",
231
298
  `${route.providerName}|${route.modelId}|sidecar-oauth-429`,
@@ -259,12 +326,14 @@ export async function executeResponsesSidecars(
259
326
  return null;
260
327
  }
261
328
  recoveryKind = "oauth-account-429";
262
- hop.permit?.use();
329
+ if (!options.comboInitialSend && canRunWebSearch && route.providerName === "google-antigravity")
330
+ sendBudgetState.pendingHopPermit = hop.permit;
331
+ else sidecarBudget.ownCredentialHop(hop.permit);
263
332
  } else if (
264
333
  // Anthropic's pool is excluded from generic failover, so without this arm a 429 inside a
265
334
  // web-search or image-bridge turn was terminal even with the pool fully enabled -- while
266
335
  // the very same 429 on the main response path rotated.
267
- transportState.anthropicPoolAccountId
336
+ anthropicInstance && transportState.anthropicPoolAccountId
268
337
  ) {
269
338
  // Same intersection for the Anthropic roster: its own per-request bound still applies,
270
339
  // and the shared budget decides whether this request may spend another send at all.
@@ -272,10 +341,10 @@ export async function executeResponsesSidecars(
272
341
  "auth-recovery",
273
342
  `${route.providerName}|${route.modelId}|sidecar-anthropic-429`,
274
343
  );
275
- const nextAccountId = await rotateAnthropicAccountOnResponse(
344
+ const nextAccountId = await rotateAnthropicAccountOnResponseForInstance(anthropicInstance,
276
345
  originalResponse ?? new Response(null, { status: 429, headers: responseHeaders ?? (retryAfter ? { "retry-after": retryAfter } : undefined) }), {
277
346
  config, accountId: transportState.anthropicPoolAccountId, sessionKey: anthropicSessionKey,
278
- model: route.modelId, requestKey: transportState, decision: transportState.anthropicRouteDecision, signal: options.abortSignal,
347
+ model: route.modelId, requestKey: transportState, decision: transportState.anthropicRouteDecision, currentDecision: currentAnthropicRouteDecision, signal: options.abortSignal,
279
348
  allow429Recovery: !sidecarHasCommittedOutput(), allowAccountRefusal: !sidecarHasCommittedOutput(),
280
349
  canRetry: hop.allowed && transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
281
350
  });
@@ -289,18 +358,18 @@ export async function executeResponsesSidecars(
289
358
  // carries none, and getAnthropicPoolAccessToken is what enforces its fail-closed
290
359
  // local-cli credential rule. Both existing Anthropic rotation sites apply the token the
291
360
  // same way.
292
- const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
361
+ const admitted = await commitResolvedOAuthSelection(await anthropicRoutingFor(anthropicInstance).getAnthropicPoolAccessSnapshot(nextAccountId));
293
362
  if (!admitted) throw new Error("OAuth selection changed during recovery");
294
363
  transportState.anthropicPoolAccountId = admitted.accountId;
295
364
  transportState.anthropicPoolFailovers += 1;
296
365
  route.provider = { ...route.provider, apiKey: admitted.accessToken };
297
- logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
366
+ logCtx.provider = formatAnthropicProviderForLog(anthropicInstance, admitted.accountId, config);
298
367
  } catch {
299
368
  hop.permit?.release();
300
369
  return null;
301
370
  }
302
- recoveryKind = "anthropic-oauth-429";
303
- hop.permit?.use();
371
+ recoveryKind = originalResponse?.status === 401 ? "oauth-401" : "anthropic-oauth-429";
372
+ sidecarBudget.ownCredentialHop(hop.permit);
304
373
  } else {
305
374
  // No key pool, no generic OAuth roster, no Anthropic pool could produce a replacement
306
375
  // credential. The 429 is terminal for this sidecar turn.
@@ -321,6 +390,7 @@ export async function executeResponsesSidecars(
321
390
  providerName: route.providerName,
322
391
  provider: route.provider,
323
392
  adapterName: rotatedAdapter.name,
393
+ ...(antigravityVerification ? { oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot } : {}),
324
394
  });
325
395
  }
326
396
  bindRouteReasoningReplayScope({
@@ -328,6 +398,7 @@ export async function executeResponsesSidecars(
328
398
  providerName: route.providerName,
329
399
  provider: route.provider,
330
400
  adapterName: rotatedAdapter.name,
401
+ ...(antigravityVerification ? { oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot } : {}),
331
402
  });
332
403
  return { adapter: rotatedAdapter, recoveryKind };
333
404
  };
@@ -384,11 +455,14 @@ export async function executeResponsesSidecars(
384
455
  const imageProviderFetch = providerFetch(
385
456
  route.provider,
386
457
  options.codexWsRuntimeIdentity,
387
- { providerName: route.providerName, modelId: route.modelId },
458
+ { providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(transportState.adapter) },
388
459
  );
389
460
  const imgResponse = await runWithImageBridge({
390
461
  parsed, adapter: transportState.adapter,
391
- incomingMeta: { headers: requestState.selectedForwardHeaders, providerName: route.providerName, abortSignal: options.abortSignal, translatorBudget },
462
+ incomingMeta: { headers: requestState.selectedForwardHeaders, providerName: route.providerName, abortSignal: options.abortSignal, translatorBudget, ...(options.comboInitialSend ? { providerFetch: imageProviderFetch } : {}), ...sidecarBudget.incomingMeta },
463
+ onProducerStart: sidecarBudget.takeProducerOwnership,
464
+ onProducerEnd: sidecarBudget.release,
465
+ onIterationEnd: sidecarBudget.releaseUnsentHop,
392
466
  ...(imgPlan ? { plan: imgPlan } : {}),
393
467
  ...(vidPlan ? { videoPlan: vidPlan } : {}),
394
468
  forwardHeaders: requestState.selectedForwardHeaders,
@@ -404,10 +478,10 @@ export async function executeResponsesSidecars(
404
478
  stallTimeoutSec: config.stallTimeoutSec,
405
479
  waitForRequestSlot: imageProviderFetch.waitForPacing,
406
480
  fetchImpl: imageProviderFetch.unpacedFetch ?? imageProviderFetch,
407
- fetchForRequest: (request, iterParsed) => {
481
+ fetchForRequest: (request, iterParsed, adapter) => {
408
482
  const fetch = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
409
483
  dispatchOverride: oauthDispatch(request, iterParsed),
410
- providerName: route.providerName, modelId: route.modelId,
484
+ providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(adapter),
411
485
  });
412
486
  return fetch.unpacedFetch ?? fetch;
413
487
  },
@@ -440,7 +514,10 @@ export async function executeResponsesSidecars(
440
514
  parsed._rawBody,
441
515
  response,
442
516
  continuationStateForResponse(providerState),
443
- responseStateOptions(adapterNeedsForcedContinuation(transportState.adapter.name)),
517
+ {
518
+ ...responseStateOptions(adapterNeedsForcedContinuation(transportState.adapter.name)),
519
+ retainForToolContinuation: adapterNeedsToolCallContinuation(transportState.adapter.name),
520
+ },
444
521
  );
445
522
  notifyResponseComplete(response);
446
523
  },
@@ -471,12 +548,14 @@ export async function executeResponsesSidecars(
471
548
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
472
549
  providerName: route.providerName,
473
550
  modelId: route.modelId,
551
+ ...sidecarBudget.fetchOptions(transportState.adapter),
474
552
  })(input, init)) as typeof globalThis.fetch;
475
553
  const wsResponse = await runWithWebSearch({
476
554
  parsed, adapter: transportState.adapter,
477
- fetchForRequest: (request, iterParsed) => providerFetch(route.provider, options.codexWsRuntimeIdentity, {
555
+ onIterationEnd: sidecarBudget.releaseUnsentHop,
556
+ fetchForRequest: (request, iterParsed, adapter) => providerFetch(route.provider, options.codexWsRuntimeIdentity, {
478
557
  dispatchOverride: oauthDispatch(request, iterParsed),
479
- providerName: route.providerName, modelId: route.modelId,
558
+ providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(adapter),
480
559
  }),
481
560
  incomingMeta: {
482
561
  headers: requestState.selectedForwardHeaders,
@@ -484,6 +563,11 @@ export async function executeResponsesSidecars(
484
563
  abortSignal: options.abortSignal,
485
564
  translatorBudget,
486
565
  providerFetch: routedProviderFetch,
566
+ ...(!options.comboInitialSend && route.providerName === "google-antigravity" ? {
567
+ sendBudget: sendBudgetState.refundableAdapterDispatchBudget,
568
+ onPhysicalSend: (send: { ordinal: number; recovery?: AttemptRecoveryKind }) =>
569
+ sendBudgetState.noteAdapterPhysicalSend(logCtx.usageLogInputTokens, send),
570
+ } : sidecarBudget.incomingMeta),
487
571
  },
488
572
  backend: wsPlan.backend,
489
573
  forwardProvider: wsPlan.forwardSidecar?.provider,
@@ -516,6 +600,13 @@ export async function executeResponsesSidecars(
516
600
  stallTimeoutSec: wsPlan.stallTimeoutSec,
517
601
  streamRoutedModelOutput: wsPlan.streamRoutedModelOutput,
518
602
  on429: rotateSidecarProviderOn429,
603
+ ...(!options.comboInitialSend && route.providerName === "google-antigravity" ? { onIterationEnd: () => {
604
+ // The next Antigravity adapter dispatch claims this permit. If request construction,
605
+ // admission or cancellation stopped it first, return the unused send allowance.
606
+ const permit = sendBudgetState.pendingHopPermit;
607
+ sendBudgetState.pendingHopPermit = undefined;
608
+ permit?.release();
609
+ } } : {}),
519
610
  retryOn429Policy: route.providerName === "kiro" && isGenericOAuthFailoverEnabled(config, "kiro")
520
611
  ? null : rateLimitRetryPolicyFor(route.provider),
521
612
  onCompletedResponse: response => {
@@ -0,0 +1,58 @@
1
+ import type { IncomingMeta, ProviderAdapter } from "../../adapters/base";
2
+ import type { HandleResponsesOptions } from "./core-options";
3
+ import type { ResponsesSendBudget } from "./request-send-budget";
4
+ import type { ProviderFetchOptions } from "./fetch-helpers";
5
+ import type { SingleUseDispatchPermit } from "../../lib/request-execution-budget";
6
+
7
+ /** Hosted inference consumes the same child-owned booking as ordinary dispatch. */
8
+ export function createSidecarSendBudget(
9
+ options: HandleResponsesOptions,
10
+ budget: Pick<ResponsesSendBudget, "adapterDispatchBudget" | "noteInitialDispatch" | "noteAdapterPhysicalSend" | "noteAdapterRecoveryWithheld" | "pendingHopPermit">,
11
+ inputTokens: () => number | undefined,
12
+ ) {
13
+ const initial = options.comboInitialSend;
14
+ let hopPermit: SingleUseDispatchPermit | undefined;
15
+ let producerActive = false;
16
+ /** Detach this owner’s hop before refunding its still-unused reservation. */
17
+ const releaseHop = (): void => {
18
+ const permit = hopPermit;
19
+ hopPermit = undefined;
20
+ if (budget.pendingHopPermit === permit) budget.pendingHopPermit = undefined;
21
+ permit?.release();
22
+ };
23
+ return {
24
+ /** Retain a Combo hop until dispatch; direct requests keep eager settlement. */
25
+ ownCredentialHop(permit?: SingleUseDispatchPermit): void {
26
+ if (!initial) { permit?.use(); return; } // Preserve direct callers' reporting contract.
27
+ releaseHop();
28
+ hopPermit = permit;
29
+ budget.pendingHopPermit = permit;
30
+ },
31
+ // Adapter-owned transports settle through the live dispatch view, not an HTTP receipt too.
32
+ incomingMeta: initial ? {
33
+ comboAttempt: options.comboAttempt === true,
34
+ sendBudget: budget.adapterDispatchBudget,
35
+ onPhysicalSend: send => budget.noteAdapterPhysicalSend(inputTokens(), send),
36
+ onRecoveryWithheld: budget.noteAdapterRecoveryWithheld,
37
+ } satisfies Partial<IncomingMeta> : {},
38
+ /** Attach HTTP receipts only when the adapter does not own physical dispatch. */
39
+ fetchOptions(adapter: ProviderAdapter): Pick<ProviderFetchOptions, "onPhysicalDispatch"> {
40
+ return initial && !adapter.fetchResponse && !adapter.runTurn
41
+ ? { onPhysicalDispatch: () => {
42
+ const prepaid = budget.pendingHopPermit;
43
+ budget.pendingHopPermit = undefined;
44
+ budget.noteInitialDispatch(prepaid);
45
+ } } : {};
46
+ },
47
+ /** Transfer cleanup to the asynchronous producer before it can outlive the response. */
48
+ takeProducerOwnership(): void {
49
+ if (initial) { initial.producerOwned = true; producerActive = true; }
50
+ },
51
+ /** Release unused bookings only after the owning producer has settled. */
52
+ release(): void {
53
+ initial?.permit.release(); producerActive = false; releaseHop();
54
+ },
55
+ /** An iteration may finish before runTurn; defer its refund while that producer is active. */
56
+ releaseUnsentHop(): void { if (!producerActive) releaseHop(); },
57
+ };
58
+ }
@@ -15,6 +15,7 @@ import type { NativeResponseControl } from "./native-response-control";
15
15
  // returned event frames as an SSE byte stream, so every downstream consumer
16
16
  // (passthrough relay, adapter parsers, usage sniffing) is unchanged.
17
17
 
18
+ import { codexWsReuseAcrossTurnsEnabled } from "../../config/codex-ws-reuse-setting";
18
19
  import { compareBunVersions } from "../../lib/bun-stream-caps";
19
20
  import { resolveProxyRoute, socks5ProxyFromEnv } from "../../lib/proxy-env";
20
21
  import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
@@ -26,7 +27,7 @@ import { codexWsCreateFrameExceedsLimit } from "./codex-ws-wire";
26
27
  import { isLoopbackUrl, rewriteWebSocketDial } from "../../plugins/upstream-hooks";
27
28
  export { CODEX_WS_LIVENESS_PING_INTERVAL_MS, CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS, MAX_CODEX_WS_FRAME_BYTES, MAX_CODEX_WS_QUEUE_BYTES,
28
29
  MAX_CODEX_WS_CREATE_FRAME_BYTES, CODEX_WS_CREATE_FRAME_LIMIT_BYTES, codexWsCreateFrameExceedsLimit,
29
- isCodexWsQuotaObservedResponse, isCodexWsUpstreamResponse } from "./codex-ws-wire";
30
+ isCodexWsQuotaObservedResponse, isCodexWsUpstreamResponse, isCodexWsPreludeProjection } from "./codex-ws-wire";
30
31
  export const MIN_BOUNDED_CODEX_WS_BUN_VERSION = "1.4.0";
31
32
 
32
33
  /**
@@ -99,8 +100,10 @@ export function planCodexWsDial(
99
100
  // and is authoritative; a pooled socket's upgrade copy is intentionally ignored. A rewriter
100
101
  // therefore cannot change them here, and the upgrade keeps the values the frame carries.
101
102
  const dialHeaders = { ...rewritten.headers };
103
+ const reuseAcrossTurns = codexWsReuseAcrossTurnsEnabled();
102
104
  for (const name of CODEX_WS_FRAME_HEADERS) {
103
- if (Object.hasOwn(headers, name)) dialHeaders[name] = headers[name]!;
105
+ if (reuseAcrossTurns) delete dialHeaders[name];
106
+ else if (Object.hasOwn(headers, name)) dialHeaders[name] = headers[name]!;
104
107
  else delete dialHeaders[name];
105
108
  }
106
109
  const dial = { ...rewritten, headers: dialHeaders };
@@ -148,7 +151,8 @@ export function shouldUseCodexWsUpstream(
148
151
  }
149
152
  }
150
153
 
151
- export function codexWsUpstreamFetch(
154
+ /** Select the bounded WS lane or HTTP fallback, forwarding receipts at physical dispatch. */
155
+ export async function codexWsUpstreamFetch(
152
156
  url: string,
153
157
  init: RequestInit,
154
158
  sseFallback: typeof globalThis.fetch,
@@ -157,6 +161,7 @@ export function codexWsUpstreamFetch(
157
161
  beforeDispatch?: (headers: Headers) => void,
158
162
  nativeControl?: NativeResponseControl,
159
163
  beforeContinuation?: () => Promise<void>,
164
+ onPhysicalDispatch?: () => void,
160
165
  ): Promise<Response> {
161
166
  const prepared = prepareCodexWsRequest(url, init);
162
167
  if (!prepared) return sseFallback(url, prepareCodexHttpInit(url, init));
@@ -217,8 +222,16 @@ export function codexWsUpstreamFetch(
217
222
  const dial = planCodexWsDial(wsUrl, headers, proxy);
218
223
  if (!dial) return sseFallback(url, init);
219
224
  const identity = control ? null : codexWsReuseIdentity(url, dial.headers, frameText, dial.proxy, dial.url);
220
- session = (identity ? codexWsPool.acquire(identity, dial.url, dial.headers, dial.proxy) : null)
221
- ?? new CodexWsSession(dial.url, dial.headers, false, undefined, dial.proxy);
225
+ const reuseAcrossTurns = identity !== null && codexWsReuseAcrossTurnsEnabled();
226
+ const pooled = identity
227
+ ? reuseAcrossTurns
228
+ ? await codexWsPool.acquireWaiting(identity, dial.url, dial.headers, dial.proxy, signal)
229
+ : codexWsPool.acquire(identity, dial.url, dial.headers, dial.proxy)
230
+ : null;
231
+ if (reuseAcrossTurns && signal?.aborted) {
232
+ return Promise.reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
233
+ }
234
+ session = pooled ?? new CodexWsSession(dial.url, dial.headers, false, undefined, dial.proxy);
222
235
  if (!session.busy && !session.reserve()) {
223
236
  session.dispose();
224
237
  return sseFallback(url, init);
@@ -229,7 +242,7 @@ export function codexWsUpstreamFetch(
229
242
  return codexWsExchange({
230
243
  session, url, init, prepared, sseFallback, onQuota, beforeDispatch,
231
244
  nativeControl: control,
232
- beforeContinuation,
245
+ beforeContinuation, onPhysicalDispatch,
233
246
  bunVersion: typeof runtime === "string" ? runtime : runtime.version,
234
247
  });
235
248
  }
@@ -703,6 +703,7 @@ function failedBlocks(name: string, newline: string): readonly string[] {
703
703
  * @param providerExecutedCallTypes - Call types executed by the provider.
704
704
  * @param declaredBare - Explicitly declared bare tool names without namespace provenance.
705
705
  * @param declaredCustom - Current bare custom declarations eligible for code-mode recovery.
706
+ * @param enforceDeclaredToolNames - Refuse undeclared calls; normalization remains active when false.
706
707
  * @returns An SSE block rewrite function.
707
708
  */
708
709
  export function createUndeclaredToolCallGuardBlockRewrite(
@@ -711,6 +712,7 @@ export function createUndeclaredToolCallGuardBlockRewrite(
711
712
  providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
712
713
  declaredBare?: ReadonlySet<string>,
713
714
  declaredCustom?: ReadonlySet<string>,
715
+ enforceDeclaredToolNames = true,
714
716
  ): SseBlockRewrite {
715
717
  let tripped = false;
716
718
  return (block: string) => {
@@ -723,7 +725,9 @@ export function createUndeclaredToolCallGuardBlockRewrite(
723
725
  } catch {
724
726
  return [block];
725
727
  }
726
- const name = undeclaredToolCallName(parsed, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare, declaredCustom);
728
+ const name = enforceDeclaredToolNames
729
+ ? undeclaredToolCallName(parsed, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare, declaredCustom)
730
+ : undefined;
727
731
  if (name !== undefined) {
728
732
  tripped = true;
729
733
  return failedBlocks(name, block.includes("\r\n") ? "\r\n" : "\n");
@@ -88,7 +88,8 @@ export function markStartupHealthDiagnosticStale(value: StartupHealth): StartupH
88
88
  // Mirror deriveStartupHealth's choice: an already-registered service is refreshed in
89
89
  // place. Hardcoding installService here silently undid that for every stale-cache
90
90
  // read, which is the path the dashboard hits while a probe is revalidating.
91
- recommendedCommand: value.routingKind === "opencodex-local" && value.desktop?.owned
91
+ recommendedAction: value.desktop ? "Reopen OpenCodex and check Start at Login." : value.recommendedAction,
92
+ recommendedCommand: value.routingKind === "opencodex-local" && value.desktop !== undefined
92
93
  ? null
93
94
  : value.routingKind === "custom-local" || value.routingKind === "unknown"
94
95
  ? value.commands.restoreNative
@@ -1,3 +1,4 @@
1
+ import { sharedPoolContinuityDenial } from "../lib/spend-reservation-ledger";
1
2
  /**
2
3
  * The one place that knows how this proxy refuses a turn on its own workflow budget.
3
4
  *
@@ -40,6 +41,8 @@ export interface WorkflowRefusalLog {
40
41
  readonly requestId: string;
41
42
  readonly start: number;
42
43
  readonly logCtx: RequestLogContext;
44
+ /** Receiving HTTP route's notification after this refusal's final-log call. */
45
+ readonly onLogged?: () => void;
43
46
  }
44
47
 
45
48
  /**
@@ -83,6 +86,7 @@ export function workflowRefusalResponse(
83
86
  addFinalRequestLog(refusalLog.requestId, refusalLog.start, refusalLog.logCtx, 429, {
84
87
  closeReason: "terminal",
85
88
  });
89
+ refusalLog.onLogged?.();
86
90
  }
87
91
  const refusal = formatErrorResponse(
88
92
  429,
@@ -96,6 +100,22 @@ export function workflowRefusalResponse(
96
100
  return refusal;
97
101
  }
98
102
 
103
+ /** Render the approved conservative-overlay explanation after a dispatch reservation refuses. */
104
+ export function unboundPoolSpendRefusalMessage(
105
+ logCtx: Pick<RequestLogContext, "spendRefusalDetail">,
106
+ ): string | undefined {
107
+ const detail = logCtx.spendRefusalDetail;
108
+ if (!detail?.includesUnboundPoolHistory) return undefined;
109
+ return workflowDenialSummary("workflow-spend-exhausted", detail).message;
110
+ }
111
+
112
+ /** HTTP form for pre-commit send-budget catches; the tracker already recorded the event. */
113
+ export function unboundPoolSpendRefusalResponse(logCtx: RequestLogContext): Response | undefined {
114
+ const detail = logCtx.spendRefusalDetail;
115
+ if (!detail?.includesUnboundPoolHistory) return undefined;
116
+ return workflowRefusalResponse("workflow-spend-exhausted", logCtx, undefined, undefined, detail);
117
+ }
118
+
99
119
  /**
100
120
  * Admit one HTTP turn against its root workflow budget.
101
121
  *
@@ -104,8 +124,20 @@ export function workflowRefusalResponse(
104
124
  * children. A request that names a parent is treated as that fan-out; a top-level request is
105
125
  * the conversation and may use the reserved slots.
106
126
  */
127
+ /** Read-only alias validation: keep replay corruption distinct from conflicting pool owners. */
128
+ export function poolContinuityRefusalReason(): WorkflowDenial | undefined {
129
+ const denial = sharedPoolContinuityDenial();
130
+ if (!denial) return undefined;
131
+ return denial.reason === "pool-history-unresolved" ? "workflow-pool-history-unresolved" : "workflow-spend-undurable";
132
+ }
133
+
107
134
  export function admitHttpWorkflowTurn(headers: Headers): WorkflowDecision | undefined {
108
135
  const rootId = headers.get("x-codex-parent-thread-id")?.trim() || undefined;
136
+ const continuityRefusal = poolContinuityRefusalReason();
137
+ if (continuityRefusal) {
138
+ recordWorkflowRefusalEvent(rootId, continuityRefusal);
139
+ return { admitted: false, reason: continuityRefusal, rootId: rootId ?? "" };
140
+ }
109
141
  const threadId = headers.get("thread-id")?.trim() || undefined;
110
142
  const lane: WorkflowLane = rootId !== undefined && threadId !== undefined && threadId !== rootId
111
143
  ? "worker"
@@ -132,6 +164,7 @@ export function workflowDecisionRefusalResponse(
132
164
  scope: decision.spendScope,
133
165
  limit: decision.spendLimit,
134
166
  ...(decision.spendProjected !== undefined ? { projected: decision.spendProjected } : {}),
167
+ ...(decision.spendIncludesUnboundPoolHistory ? { includesUnboundPoolHistory: true } : {}),
135
168
  }
136
169
  : undefined;
137
170
  return workflowRefusalResponse(decision.reason, logCtx, refusalLog, undefined, spend);
@@ -26,6 +26,7 @@ import type { WindowsSchedulerTaskProbe } from "./windows-scheduler";
26
26
  import { win32 } from "node:path";
27
27
  import { serviceDiagnosticsSummary } from "./diagnostics";
28
28
  import { runServiceClaim } from "./claim";
29
+ import { createSupervisionLatch, desktopServiceCommandRefusal } from "./desktop-command-guard";
29
30
 
30
31
  /**
31
32
  * `restart` is NO LONGER folded into `repair`.
@@ -214,6 +215,7 @@ export function removeServiceTokenAfterUninstall(
214
215
  /** Execute a service verb while preserving client-owned credentials during uninstall. */
215
216
  export async function serviceCommand(...args: (string | undefined)[]): Promise<void> {
216
217
  const filteredArgs = args.filter((a): a is string => Boolean(a));
218
+ const supervisionLatch = createSupervisionLatch();
217
219
  const execute = async (): Promise<void> => {
218
220
  // `claim` is not an install verb: it is deliberately outside planServiceCommand (whose
219
221
  // backend/installation checks do not apply to an ownership write) and outside
@@ -231,6 +233,12 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
231
233
  process.exit(1);
232
234
  }
233
235
  const { parsed, command } = plan;
236
+ const refusal = desktopServiceCommandRefusal(command, undefined, supervisionLatch);
237
+ if (refusal) {
238
+ console.error(refusal);
239
+ process.exitCode = 1;
240
+ return;
241
+ }
234
242
  if (command === "repair" || command === "restart") {
235
243
  const verb: ServiceRepairVerb = command === "restart" ? "restart" : "repair";
236
244
  assertServiceEnvironmentMatchesInstall();
@@ -249,7 +257,7 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
249
257
  // existing registration had been restarted, not repaired (#4914).
250
258
  let repairError: unknown;
251
259
  try {
252
- await repairService({ verb });
260
+ await repairService({ verb, supervisionLatch });
253
261
  } catch (error) {
254
262
  repairError = error;
255
263
  process.exitCode = 1;
@@ -295,12 +303,12 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
295
303
  throw new Error(`Task Scheduler state could not be verified before install: ${scheduler.detail}`);
296
304
  }
297
305
  if (scheduler.status === "absent") {
298
- await installFreshWindowsSchedulerSafely();
306
+ await installFreshWindowsSchedulerSafely({ supervisionLatch });
299
307
  } else {
300
- await installServiceSafely(backend, ops.install);
308
+ await installServiceSafely(backend, ops.install, { supervisionLatch });
301
309
  }
302
310
  } else {
303
- await installServiceSafely(backend, ops.install);
311
+ await installServiceSafely(backend, ops.install, { supervisionLatch });
304
312
  }
305
313
  } catch (error) {
306
314
  console.error(`❌ Service install cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -474,6 +482,14 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
474
482
  };
475
483
 
476
484
  const preliminary = parseServiceArgs(filteredArgs);
485
+ if (preliminary.invalid.length === 0) {
486
+ const refusal = desktopServiceCommandRefusal(preliminary.sub, undefined, supervisionLatch);
487
+ if (refusal) {
488
+ console.error(refusal);
489
+ process.exitCode = 1;
490
+ return;
491
+ }
492
+ }
477
493
  const windowsMutation = process.platform === "win32"
478
494
  && preliminary.invalid.length === 0
479
495
  && preliminary.sub !== "status";