@bitkyc08/opencodex 2.84.0 → 2.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (518) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
  4. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  5. package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DCd-A1az.js} +1 -1
  6. package/gui/dist/assets/index-CfYDMj34.js +94 -0
  7. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  8. package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -2
  11. package/src/adapters/anthropic/beta-allowlist.ts +91 -11
  12. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  13. package/src/adapters/anthropic/client-identity.ts +1 -1
  14. package/src/adapters/anthropic/passthrough.ts +16 -3
  15. package/src/adapters/anthropic-progress.ts +84 -0
  16. package/src/adapters/anthropic.ts +108 -21
  17. package/src/adapters/azure.ts +19 -1
  18. package/src/adapters/base.ts +2 -0
  19. package/src/adapters/claude-cli/adapter.ts +111 -23
  20. package/src/adapters/claude-cli/profiles.ts +2 -1
  21. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  22. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  23. package/src/adapters/client-fingerprint.ts +24 -0
  24. package/src/adapters/coding-agent/protocol.ts +1 -1
  25. package/src/adapters/coding-agent/turn.ts +40 -11
  26. package/src/adapters/cursor/arg-codec.ts +17 -5
  27. package/src/adapters/cursor/catalog.ts +8 -0
  28. package/src/adapters/cursor/effort-map.ts +3 -0
  29. package/src/adapters/cursor/freeform-args.ts +75 -0
  30. package/src/adapters/cursor/protobuf-events.ts +36 -23
  31. package/src/adapters/dsh-account.ts +97 -0
  32. package/src/adapters/google-antigravity-wire.ts +22 -1
  33. package/src/adapters/google.ts +37 -10
  34. package/src/adapters/openai-chat/messages.ts +4 -3
  35. package/src/adapters/openai-chat/passthrough.ts +8 -2
  36. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  37. package/src/adapters/openai-chat/response-events.ts +59 -1
  38. package/src/adapters/openai-chat.ts +8 -5
  39. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  40. package/src/adapters/openai-responses/passthrough.ts +57 -125
  41. package/src/adapters/openai-responses/request-strips.ts +25 -3
  42. package/src/adapters/openai-responses/transport.ts +81 -0
  43. package/src/adapters/qoder/adapter.ts +44 -3
  44. package/src/adapters/registry.ts +5 -0
  45. package/src/adapters/zed.ts +26 -1
  46. package/src/advisor/activity.ts +55 -0
  47. package/src/advisor/budget.ts +62 -0
  48. package/src/advisor/consult.ts +223 -0
  49. package/src/advisor/context.ts +238 -0
  50. package/src/advisor/disclosure.ts +20 -0
  51. package/src/advisor/evidence-tool.ts +21 -0
  52. package/src/advisor/evidence.ts +117 -0
  53. package/src/advisor/runtime.ts +353 -0
  54. package/src/advisor/settings.ts +144 -0
  55. package/src/advisor/state.ts +369 -0
  56. package/src/advisor/synthetic-tool.ts +33 -0
  57. package/src/bridge/errors.ts +24 -1
  58. package/src/bridge/internal.ts +3 -1
  59. package/src/bridge/response-json.ts +18 -3
  60. package/src/bridge/sse.ts +58 -8
  61. package/src/bridge/terminal-refusal.ts +59 -0
  62. package/src/chat/inbound.ts +30 -6
  63. package/src/chat/outbound.ts +5 -0
  64. package/src/claude/desktop-3p-library.ts +72 -7
  65. package/src/claude/desktop-3p.ts +75 -19
  66. package/src/claude/desktop-first-party.ts +1 -1
  67. package/src/claude/desktop-picker-platform.ts +4 -0
  68. package/src/claude/desktop-picker-profile.ts +11 -4
  69. package/src/claude/desktop-picker.ts +15 -8
  70. package/src/claude/desktop-profile.ts +10 -1
  71. package/src/claude/desktop-remote-store-io.ts +5 -3
  72. package/src/claude/desktop-system-proxy.ts +50 -4
  73. package/src/claude/inbound-content-options.ts +11 -0
  74. package/src/claude/inbound.ts +154 -50
  75. package/src/claude/intercept/local-ca.ts +8 -1
  76. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  77. package/src/claude/intercept/picker-runtime.ts +6 -5
  78. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  79. package/src/claude/intercept/picker-trust.ts +19 -0
  80. package/src/claude/intercept/port-policy.ts +30 -0
  81. package/src/claude/intercept/runtime.ts +97 -94
  82. package/src/claude/model-default-settings.ts +154 -0
  83. package/src/claude/model-info.ts +7 -7
  84. package/src/claude/outbound.ts +51 -19
  85. package/src/cli/account-api.ts +1 -0
  86. package/src/cli/account-policy-dto.ts +6 -0
  87. package/src/cli/account-policy.ts +34 -5
  88. package/src/cli/account-preservation.ts +44 -0
  89. package/src/cli/account.ts +8 -2
  90. package/src/cli/advisor.ts +165 -0
  91. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  92. package/src/cli/capabilities-accounts.ts +20 -1
  93. package/src/cli/capabilities-agents-routing.ts +23 -2
  94. package/src/cli/capabilities-base.ts +39 -11
  95. package/src/cli/capabilities-observe-system.ts +63 -3
  96. package/src/cli/capabilities-provider-models.ts +11 -0
  97. package/src/cli/chatgpt-command.ts +35 -14
  98. package/src/cli/claude.ts +46 -6
  99. package/src/cli/codex-cli-update.ts +151 -41
  100. package/src/cli/codex-provider.ts +44 -0
  101. package/src/cli/combo-input.ts +10 -1
  102. package/src/cli/combo-result.ts +1 -1
  103. package/src/cli/combo.ts +2 -1
  104. package/src/cli/cross-home-owner.ts +17 -14
  105. package/src/cli/dispatch.ts +30 -5
  106. package/src/cli/doctor.ts +15 -1
  107. package/src/cli/ensure-desktop-cold-start.ts +382 -0
  108. package/src/cli/export-command.ts +1 -1
  109. package/src/cli/help.ts +4 -2
  110. package/src/cli/index.ts +14 -5
  111. package/src/cli/integration-plan-dto.ts +3 -0
  112. package/src/cli/log-history.ts +172 -0
  113. package/src/cli/models-runtime-subcommands.ts +1 -0
  114. package/src/cli/models-runtime.ts +72 -8
  115. package/src/cli/observe-stream.ts +2 -0
  116. package/src/cli/observe.ts +16 -4
  117. package/src/cli/provider.ts +27 -5
  118. package/src/cli/registry.ts +41 -9
  119. package/src/cli/restart-scope.ts +3 -1
  120. package/src/cli/runtime-api.ts +5 -3
  121. package/src/cli/system-command.ts +40 -8
  122. package/src/cli/uninstall-client-state.ts +32 -0
  123. package/src/cli/usage-compare.ts +309 -0
  124. package/src/cli/usage-model-search.ts +4 -4
  125. package/src/cli/usage-report.ts +2 -2
  126. package/src/cli/v2.ts +1 -1
  127. package/src/client/machine-listener.ts +1 -3
  128. package/src/client/runtime.ts +1 -0
  129. package/src/clients/client-path.ts +51 -0
  130. package/src/clients/config-export/contracts.ts +3 -1
  131. package/src/clients/config-export/model-metadata.ts +3 -2
  132. package/src/clients/config-export/qoder.ts +93 -0
  133. package/src/clients/config-export.ts +35 -48
  134. package/src/clients/gentle-shell-path.ts +87 -0
  135. package/src/codex/account-auto-switch.ts +2 -0
  136. package/src/codex/account-identity.ts +29 -0
  137. package/src/codex/account-label.ts +1 -1
  138. package/src/codex/account-lifecycle.ts +21 -5
  139. package/src/codex/account-preservation.ts +28 -0
  140. package/src/codex/account-use-remaining.ts +23 -0
  141. package/src/codex/app-server-processes.ts +97 -7
  142. package/src/codex/app-server-restart-service.ts +55 -4
  143. package/src/codex/auth-api/account-list.ts +4 -0
  144. package/src/codex/auth-api/account-pause-group.ts +6 -50
  145. package/src/codex/auth-api/account-preservation.ts +57 -0
  146. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  147. package/src/codex/auth-api/routes.ts +8 -0
  148. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  149. package/src/codex/auth-api.ts +0 -1
  150. package/src/codex/auth-context.ts +81 -7
  151. package/src/codex/autostart-health.ts +4 -0
  152. package/src/codex/catalog/auto-review.ts +20 -3
  153. package/src/codex/catalog/build-entries.ts +19 -2
  154. package/src/codex/catalog/combo-member.ts +14 -5
  155. package/src/codex/catalog/configured-tiers.ts +50 -0
  156. package/src/codex/catalog/effort.ts +4 -1
  157. package/src/codex/catalog/gather-capture.ts +2 -0
  158. package/src/codex/catalog/metadata.ts +11 -9
  159. package/src/codex/catalog/model-hints.ts +42 -19
  160. package/src/codex/catalog/model-visibility.ts +3 -1
  161. package/src/codex/catalog/native-ultrafast.ts +106 -0
  162. package/src/codex/catalog/parsing.ts +35 -86
  163. package/src/codex/catalog/provider-models.ts +40 -34
  164. package/src/codex/catalog/retained-sync.ts +2 -0
  165. package/src/codex/catalog/routed-gather.ts +33 -16
  166. package/src/codex/catalog/vendor-context.ts +82 -0
  167. package/src/codex/cli-install-provenance.ts +105 -17
  168. package/src/codex/cli-update-plan.ts +545 -0
  169. package/src/codex/config-write-lock.ts +63 -4
  170. package/src/codex/convergence.ts +2 -0
  171. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  172. package/src/codex/desktop-app/types.ts +2 -0
  173. package/src/codex/desktop-app/windows.ts +29 -16
  174. package/src/codex/desktop-app-restart.ts +25 -2
  175. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  176. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  177. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  178. package/src/codex/desktop-compatibility/installed-build.ts +73 -0
  179. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  180. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  181. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  182. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  183. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  184. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  185. package/src/codex/desktop-compatibility/runtime.ts +182 -0
  186. package/src/codex/desktop-compatibility/service.ts +30 -0
  187. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  188. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  189. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  190. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  191. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  192. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  193. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  194. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  195. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  196. package/src/codex/desktop-compatibility/windows-key-protection.ts +165 -0
  197. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  198. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  199. package/src/codex/features.ts +85 -45
  200. package/src/codex/history-provider.ts +1 -1
  201. package/src/codex/inject/config-write-section.ts +4 -1
  202. package/src/codex/inject/history-failure.ts +27 -0
  203. package/src/codex/inject/remove.ts +18 -6
  204. package/src/codex/inject/restore.ts +24 -2
  205. package/src/codex/inject/routing-classify.ts +4 -3
  206. package/src/codex/inject.ts +18 -27
  207. package/src/codex/journal.ts +16 -1
  208. package/src/codex/main-account-hard-lock.ts +30 -1
  209. package/src/codex/main-account.ts +99 -15
  210. package/src/codex/native-main-refresh-mode.ts +217 -0
  211. package/src/codex/native-profile-startup.ts +1 -1
  212. package/src/codex/ocx-compaction-history.ts +12 -10
  213. package/src/codex/paths.ts +86 -1
  214. package/src/codex/provider-routing.ts +174 -0
  215. package/src/codex/refresh.ts +2 -2
  216. package/src/codex/routing/health-store.ts +2 -2
  217. package/src/codex/routing/pool-recovery.ts +79 -0
  218. package/src/codex/routing/selection.ts +4 -3
  219. package/src/codex/routing/thread-affinity.ts +1 -1
  220. package/src/codex/shim-diagnostics.ts +6 -0
  221. package/src/codex/shim-overlay.ts +7 -2
  222. package/src/codex/signed-in-identity.ts +43 -0
  223. package/src/codex/subagent-model-fallback.ts +1 -1
  224. package/src/codex/sync.ts +6 -1
  225. package/src/codex/user-identity.ts +16 -5
  226. package/src/codex/windows-local-appdata.ts +84 -0
  227. package/src/combos/failover.ts +6 -1
  228. package/src/combos/index.ts +1 -0
  229. package/src/combos/resolve.ts +46 -10
  230. package/src/combos/types.ts +15 -0
  231. package/src/config/diagnostics.ts +35 -7
  232. package/src/config/live-reconcile.ts +51 -2
  233. package/src/config/load-degrade.ts +23 -1
  234. package/src/config/opencode-wire-upgrade.ts +54 -0
  235. package/src/config/opencode-zen-upgrade.ts +52 -0
  236. package/src/config/owner-registry.ts +22 -17
  237. package/src/config/paths.ts +10 -3
  238. package/src/config/provider-validation.ts +45 -2
  239. package/src/config/schema/config-schema.ts +39 -0
  240. package/src/config/schema/desktop-compatibility.ts +9 -0
  241. package/src/config/schema/leaf-validators.ts +39 -9
  242. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  243. package/src/config/schema/native-reasoning-retention.ts +26 -0
  244. package/src/config.ts +2 -0
  245. package/src/generated/compatibility-version.json +962 -350
  246. package/src/generated/model-metadata.ts +1 -1
  247. package/src/grok/sync.ts +1 -1
  248. package/src/images/loop.ts +46 -8
  249. package/src/integrations/catalog-refresh.ts +1 -1
  250. package/src/integrations/config-io.ts +20 -2
  251. package/src/integrations/journal.ts +15 -1
  252. package/src/integrations/mutation-plan.ts +2 -0
  253. package/src/integrations/registry.ts +15 -0
  254. package/src/integrations/store.ts +3 -0
  255. package/src/integrations/writer.ts +10 -1
  256. package/src/lib/advisor-activation.ts +16 -0
  257. package/src/lib/background-work.ts +65 -0
  258. package/src/lib/bounded-file-read.ts +58 -0
  259. package/src/lib/bun-path-runtime.d.mts +10 -1
  260. package/src/lib/bun-path-runtime.mjs +62 -3
  261. package/src/lib/codex-restart-contract.ts +15 -0
  262. package/src/lib/desktop-proxy-route.ts +30 -0
  263. package/src/lib/desktop-upstream-tunnel.ts +72 -0
  264. package/src/lib/empty-tool-catalog.ts +62 -0
  265. package/src/lib/errors.ts +26 -3
  266. package/src/lib/gcp-adc.ts +96 -9
  267. package/src/lib/gcp-credential-input.ts +12 -0
  268. package/src/lib/lab-live-route-production.ts +5 -0
  269. package/src/lib/local-internal-call-capability.ts +55 -0
  270. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  271. package/src/lib/package-tree-integrity.ts +2 -2
  272. package/src/lib/redact.ts +2 -2
  273. package/src/lib/socks5-fetch.ts +48 -71
  274. package/src/lib/socks5-handshake.ts +112 -0
  275. package/src/lib/translator-budget.ts +7 -1
  276. package/src/oauth/account-quota-rank.ts +69 -2
  277. package/src/oauth/anthropic-account-refusal.ts +20 -2
  278. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  279. package/src/oauth/anthropic-routing.ts +1 -21
  280. package/src/oauth/anthropic.ts +38 -7
  281. package/src/oauth/callback-server.ts +27 -1
  282. package/src/oauth/chatgpt-device.ts +1 -1
  283. package/src/oauth/chatgpt-shared.ts +83 -0
  284. package/src/oauth/chatgpt.ts +8 -72
  285. package/src/oauth/dsh.ts +203 -0
  286. package/src/oauth/generic-account-failover.ts +62 -12
  287. package/src/oauth/generic-session-affinity.ts +125 -0
  288. package/src/oauth/index.ts +188 -111
  289. package/src/oauth/kiro-device-login.ts +2 -1
  290. package/src/oauth/kiro.ts +7 -1
  291. package/src/oauth/local-token-detect.ts +21 -1
  292. package/src/oauth/login-cli-errors.ts +44 -0
  293. package/src/oauth/login-cli.ts +69 -7
  294. package/src/oauth/login-flow-state.ts +371 -23
  295. package/src/oauth/meta-muse-errors.ts +41 -0
  296. package/src/oauth/meta-muse-keychain.ts +52 -0
  297. package/src/oauth/meta-muse.ts +51 -84
  298. package/src/oauth/model-discovery-auth.ts +3 -0
  299. package/src/oauth/pool-settings-capability.ts +4 -2
  300. package/src/oauth/refresh-flight-retirement.ts +48 -0
  301. package/src/oauth/session-affinity-key.ts +23 -0
  302. package/src/oauth/store-anthropic-instance.ts +9 -2
  303. package/src/oauth/store.ts +12 -5
  304. package/src/protocols/codecs/chat-ir.ts +467 -0
  305. package/src/protocols/codecs/chat.ts +10 -1
  306. package/src/protocols/codecs/messages-ir.ts +500 -0
  307. package/src/protocols/encoders/adapter-events.ts +5 -4
  308. package/src/protocols/encoders/messages.ts +9 -6
  309. package/src/protocols/features.ts +10 -2
  310. package/src/protocols/path.ts +11 -1
  311. package/src/protocols/plan-snapshot.ts +39 -16
  312. package/src/protocols/plan.ts +9 -1
  313. package/src/protocols/provider-summary.ts +2 -1
  314. package/src/protocols/settings.ts +8 -0
  315. package/src/providers/antigravity-account-roster.ts +102 -0
  316. package/src/providers/antigravity-models.ts +85 -7
  317. package/src/providers/api-key-cooldowns.ts +49 -0
  318. package/src/providers/api-key-resolve.ts +11 -6
  319. package/src/providers/api-key-response-ownership.ts +57 -0
  320. package/src/providers/api-key-selection.ts +31 -6
  321. package/src/providers/api-keys.ts +214 -3
  322. package/src/providers/claude-cli-usage.ts +96 -0
  323. package/src/providers/dsh-account-destination.ts +31 -0
  324. package/src/providers/dsh-account-models.ts +41 -0
  325. package/src/providers/gcp-credential-cleanup.ts +59 -0
  326. package/src/providers/key-failover.ts +54 -50
  327. package/src/providers/key-store.ts +7 -2
  328. package/src/providers/model-rename-fields.ts +5 -0
  329. package/src/providers/model-rename-migration.ts +7 -5
  330. package/src/providers/openai-tiers-destination.ts +11 -2
  331. package/src/providers/openai-tiers.ts +5 -12
  332. package/src/providers/opencode-go-transport.ts +3 -1
  333. package/src/providers/quota/account-cache.ts +6 -5
  334. package/src/providers/quota/account-row.ts +34 -0
  335. package/src/providers/quota/antigravity.ts +28 -7
  336. package/src/providers/quota/dsh-account.ts +70 -0
  337. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  338. package/src/providers/quota-types.ts +2 -0
  339. package/src/providers/quota.ts +14 -20
  340. package/src/providers/registry/entries-core.ts +44 -14
  341. package/src/providers/registry/entries-extended.ts +19 -0
  342. package/src/providers/registry/types.ts +1 -1
  343. package/src/providers/request-pacing.ts +77 -19
  344. package/src/providers/routed-config.ts +339 -0
  345. package/src/providers/service-tier.ts +2 -1
  346. package/src/quota/window-mapping.ts +5 -2
  347. package/src/responses/anthropic-progress.ts +34 -0
  348. package/src/responses/assistant-replay.ts +35 -0
  349. package/src/responses/compaction-history-framing.ts +65 -0
  350. package/src/responses/compaction.ts +6 -1
  351. package/src/responses/incomplete-reason.ts +31 -0
  352. package/src/responses/parser.ts +78 -22
  353. package/src/responses/reasoning-envelope.ts +4 -0
  354. package/src/responses/reasoning-replay-cache.ts +24 -2
  355. package/src/responses/reasoning-retention.ts +173 -0
  356. package/src/responses/retained-compaction.ts +46 -0
  357. package/src/responses/schema.ts +23 -0
  358. package/src/responses/state.ts +32 -29
  359. package/src/router.ts +8 -321
  360. package/src/routing/health.ts +37 -22
  361. package/src/routing/history/build-file.ts +92 -0
  362. package/src/routing/history/indexer.ts +254 -221
  363. package/src/routing/history/ingest.ts +242 -0
  364. package/src/routing/history/rebuild-job.ts +111 -0
  365. package/src/routing/history/rebuild-protocol.ts +15 -0
  366. package/src/routing/history/rebuild-worker.ts +34 -0
  367. package/src/routing/profile.ts +1 -1
  368. package/src/server/admission-model-scope.ts +21 -1
  369. package/src/server/antigravity-direct-eligibility.ts +208 -0
  370. package/src/server/auth-cors.ts +15 -0
  371. package/src/server/chat-completions.ts +82 -10
  372. package/src/server/chat-native-reasoning.ts +123 -0
  373. package/src/server/chat-native-sse.ts +110 -7
  374. package/src/server/chat-native.ts +76 -17
  375. package/src/server/claude-messages.ts +164 -433
  376. package/src/server/effort-policy.ts +1 -1
  377. package/src/server/images.ts +15 -2
  378. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  379. package/src/server/index/model-alias-id.ts +36 -0
  380. package/src/server/index/optional-listeners.ts +2 -0
  381. package/src/server/index/serve-options.ts +11 -3
  382. package/src/server/index/startup-warnings.ts +11 -0
  383. package/src/server/index/websocket-handler.ts +9 -0
  384. package/src/server/index.ts +14 -11
  385. package/src/server/inference/advisor-diagnostics.ts +31 -0
  386. package/src/server/inference/client-encoder-delivery.ts +6 -5
  387. package/src/server/login-shell-path.ts +146 -0
  388. package/src/server/management/advisor-routes.ts +202 -0
  389. package/src/server/management/agent-settings-routes.ts +22 -4
  390. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  391. package/src/server/management/combo-routes.ts +176 -50
  392. package/src/server/management/companion-routes.ts +4 -0
  393. package/src/server/management/config-routes.ts +77 -6
  394. package/src/server/management/context.ts +4 -0
  395. package/src/server/management/cursor-integration-routes.ts +1 -1
  396. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  397. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  398. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  399. package/src/server/management/model-routes.ts +13 -8
  400. package/src/server/management/model-rows.ts +14 -0
  401. package/src/server/management/native-integration-routes.ts +2 -7
  402. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  403. package/src/server/management/oauth-account-routes.ts +87 -14
  404. package/src/server/management/protocol-settings-patch.ts +1 -0
  405. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  406. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  407. package/src/server/management/provider-overwrite-carry.ts +7 -0
  408. package/src/server/management/provider-routes.ts +25 -30
  409. package/src/server/management/route-registry.ts +21 -1
  410. package/src/server/management/sibling-guard.ts +1 -0
  411. package/src/server/management-api.ts +9 -1
  412. package/src/server/messages-antigravity-direct.ts +195 -0
  413. package/src/server/messages-native-tool-guard.ts +185 -0
  414. package/src/server/messages-native.ts +106 -22
  415. package/src/server/messages-shared.ts +431 -0
  416. package/src/server/relay.ts +1 -1
  417. package/src/server/request-log-terminal-status.ts +1 -1
  418. package/src/server/request-log.ts +13 -1
  419. package/src/server/responses/account-change-state.ts +8 -2
  420. package/src/server/responses/adapter-continuation.ts +48 -19
  421. package/src/server/responses/adapter-delivery.ts +43 -8
  422. package/src/server/responses/adapter-dispatch.ts +48 -4
  423. package/src/server/responses/advisor-plan-slot.ts +27 -0
  424. package/src/server/responses/advisor-retention.ts +117 -0
  425. package/src/server/responses/advisor-slot.ts +368 -0
  426. package/src/server/responses/code-mode-collab-surface.ts +116 -8
  427. package/src/server/responses/codex-ws-wire.ts +15 -8
  428. package/src/server/responses/compact.ts +303 -125
  429. package/src/server/responses/compaction-recovery.ts +18 -10
  430. package/src/server/responses/compaction-retention.ts +61 -0
  431. package/src/server/responses/context-overflow.ts +11 -6
  432. package/src/server/responses/core-auth.ts +6 -0
  433. package/src/server/responses/core-codex-account.ts +11 -1
  434. package/src/server/responses/core-combo-failure.ts +12 -1
  435. package/src/server/responses/core-combo.ts +141 -36
  436. package/src/server/responses/core-errors.ts +3 -3
  437. package/src/server/responses/core-lifetime.ts +7 -2
  438. package/src/server/responses/core-normalize.ts +11 -0
  439. package/src/server/responses/core-opaque-recovery.ts +35 -0
  440. package/src/server/responses/core-options.ts +51 -0
  441. package/src/server/responses/core-pipeline.ts +141 -0
  442. package/src/server/responses/core-replay.ts +25 -6
  443. package/src/server/responses/core.ts +3 -116
  444. package/src/server/responses/fetch-helpers.ts +73 -19
  445. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  446. package/src/server/responses/native-response-control.ts +4 -2
  447. package/src/server/responses/passthrough-delivery.ts +19 -11
  448. package/src/server/responses/passthrough-dispatch.ts +29 -16
  449. package/src/server/responses/request-prepare.ts +144 -17
  450. package/src/server/responses/request-sidecar-auth.ts +2 -2
  451. package/src/server/responses/request-transport.ts +120 -27
  452. package/src/server/responses/response-effects.ts +1 -0
  453. package/src/server/responses/run-turn-execution.ts +166 -38
  454. package/src/server/responses/sidecar-execution.ts +98 -5
  455. package/src/server/responses/terminal-guard.ts +4 -17
  456. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  457. package/src/server/startup-action-control.ts +16 -2
  458. package/src/server/startup-health-cache.ts +1 -0
  459. package/src/service/auth-preflight.ts +71 -0
  460. package/src/service/cli.ts +4 -2
  461. package/src/service/definition.ts +97 -0
  462. package/src/service/desktop-startup.ts +14 -0
  463. package/src/service/guards.ts +1 -61
  464. package/src/service/health-sentinel-linux.ts +112 -0
  465. package/src/service/health-sentinel-main.ts +99 -61
  466. package/src/service/health-sentinel-probe.ts +24 -7
  467. package/src/service/health.ts +5 -89
  468. package/src/service/launchd.ts +27 -7
  469. package/src/service/logging.ts +135 -0
  470. package/src/service/repair.ts +2 -1
  471. package/src/service/state.ts +7 -8
  472. package/src/service/systemd.ts +1 -1
  473. package/src/service/windows-guarded-manager.ts +2 -1
  474. package/src/service/windows-ops.ts +1 -1
  475. package/src/service/windows-scheduler.ts +2 -7
  476. package/src/service/windows-taskxml.ts +2 -2
  477. package/src/service/windows-wscript.ts +14 -0
  478. package/src/service.ts +4 -2
  479. package/src/sidecar/auth.ts +2 -0
  480. package/src/tray/windows-tray.ps1 +43 -24
  481. package/src/types/config.ts +124 -6
  482. package/src/types/opencode-go.ts +61 -0
  483. package/src/types/opencode-zen.ts +29 -0
  484. package/src/types/provider.ts +39 -6
  485. package/src/types/request.ts +38 -2
  486. package/src/types/tools.ts +2 -0
  487. package/src/types/wire.ts +29 -37
  488. package/src/types.ts +1 -0
  489. package/src/update/async-check.ts +1 -1
  490. package/src/update/badge.ts +2 -2
  491. package/src/update/index.ts +88 -15
  492. package/src/update/job-supervised-restart.ts +107 -0
  493. package/src/update/job.ts +32 -32
  494. package/src/update/notify.ts +1 -1
  495. package/src/update/npm-invocation.mjs +12 -2
  496. package/src/update/refresh-scheduler.ts +2 -2
  497. package/src/update/supervised-recovery.d.mts +27 -0
  498. package/src/update/supervised-recovery.mjs +157 -0
  499. package/src/usage/cache-diagnostic.ts +2 -0
  500. package/src/usage/cost.ts +48 -12
  501. package/src/usage/expected-prices.ts +15 -0
  502. package/src/usage/ledger-retention.ts +2 -1
  503. package/src/usage/ledger-scanner.ts +6 -5
  504. package/src/usage/log.ts +17 -8
  505. package/src/usage/summary.ts +22 -9
  506. package/src/usage/user-cost-overlays.ts +14 -19
  507. package/src/usage/user-cost-validation.ts +38 -0
  508. package/src/vision/eligibility.ts +13 -0
  509. package/src/web-search/gemini-executor.ts +2 -2
  510. package/src/web-search/loop.ts +55 -5
  511. package/src/web-search/progress-stream.ts +4 -1
  512. package/src/web-search/run-turn-loop.ts +9 -1
  513. package/assets/sponsors/packycode.png +0 -0
  514. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  515. package/gui/dist/assets/App-CZtqiGcw.js +0 -56
  516. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  517. package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
  518. package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
@@ -7,9 +7,11 @@ import { readBoundedResponseBytes } from "../../lib/bounded-body";
7
7
  import { isRequestExecutionBudget, type RequestExecutionBudget, type SingleUseDispatchPermit } from "../../lib/request-execution-budget";
8
8
  import { isNonReplayableResponse, isNonReplayableUpstreamCode, markResponseNonReplayable, TRANSIENT_RETRY_MAX_ATTEMPTS } from "../../lib/upstream-retry";
9
9
  import { isCyberPolicyCode, isTerminalRefusalCode } from "../../lib/errors";
10
- import { isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
10
+ import { isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint, supportsNativeResponsesCompactionTrigger } from "../../providers/openai-tiers";
11
11
  import { bridgeToResponsesSSE, formatErrorResponse } from "../../bridge";
12
12
  import { buildCompactV1Output, decodeCompactionSummary, encodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
13
+ import { decodeRetainedCompaction, encodeRetainedCompaction } from "../../responses/retained-compaction";
14
+ import { collectReasoningTexts } from "../../responses/reasoning-retention";
13
15
  import { finishRequestAttempt, usageFromResponsesPayload, type RequestLogContext } from "../request-log";
14
16
  import { linkRequestSessionLane } from "../request-log-conversation";
15
17
  import { isNativePassthroughSseResponse, markNativePassthroughSseResponse, isEagerRelaySseResponse, markEagerRelaySseResponse } from "../relay";
@@ -55,15 +57,17 @@ function portableBody(body: Record<string, unknown>): boolean {
55
57
  if (!Array.isArray(body.input) || body.store === true || conversationCarriesUploadedFiles(body)) return false;
56
58
  // Native ciphertext cannot be summarized by another provider. Never silently replace it with a note.
57
59
  if (body.input.some(item => record(item) && ["compaction", "compaction_summary", "context_compaction"].includes(String(item.type))
58
- && typeof item.encrypted_content === "string" && !item.encrypted_content.startsWith("ocx1:"))) return false;
60
+ && typeof item.encrypted_content === "string" && !item.encrypted_content.startsWith("ocx1:") && !decodeRetainedCompaction(item.encrypted_content))) return false;
59
61
  const input = body.input.filter(item => !record(item) || item.type !== "compaction_trigger");
60
62
  return selfContainedResponsesBody({ ...body, store: false, input });
61
63
  }
62
64
 
63
- function routed(route: RouteResult): boolean {
65
+ function routed(route: RouteResult, kind: "compaction-v1" | "compaction-v2"): boolean {
64
66
  return !route.combo && route.routeKind !== "policy" && route.routeReason !== "default-provider"
65
67
  && !isCanonicalOpenAiForwardProvider(route.provider)
66
- && !supportsNativeResponsesCompactEndpoint(route.providerName, route.provider);
68
+ && !(kind === "compaction-v1"
69
+ ? supportsNativeResponsesCompactEndpoint(route.providerName, route.provider)
70
+ : supportsNativeResponsesCompactionTrigger(route.provider));
67
71
  }
68
72
 
69
73
  /** One reader, bounded bytes, and an exact replacement body; never clone a live stream. */
@@ -89,6 +93,7 @@ export async function runWithCompactionRecovery(
89
93
  req: Request, config: OcxConfig, logCtx: RequestLogContext, options: Options, dispatch: Dispatch,
90
94
  ): Promise<Response> {
91
95
  const recovery = readCompactionRecoveryConfig(config.compactionRecovery);
96
+ const kind = options.compactionRecoveryKind ?? "compaction-v2";
92
97
  if (!recovery || options.compactionRecoveryAttempted || options.comboAttempt || (options.inboundWire && options.inboundWire !== "responses")) {
93
98
  return dispatch(req, config, logCtx, options);
94
99
  }
@@ -132,7 +137,7 @@ export async function runWithCompactionRecovery(
132
137
  },
133
138
  onCompactionRecoveryRoute(route) {
134
139
  options.onCompactionRecoveryRoute?.(route);
135
- if (snapshot && routed(route)) sourceRoute = { ...route };
140
+ if (snapshot && routed(route, kind)) sourceRoute = { ...route };
136
141
  },
137
142
  onCompactionRecoveryAdapterEvent(event) {
138
143
  options.onCompactionRecoveryAdapterEvent?.(event);
@@ -151,7 +156,7 @@ export async function runWithCompactionRecovery(
151
156
  if (!snapshot || !sourceRoute || signal.aborted || req.signal.aborted || isNonReplayableResponse(response)) return keep(response);
152
157
  let target: RouteResult;
153
158
  try { target = routeConcreteModel(config, recovery.model); } catch { return keep(response); }
154
- if (!routed(target) || identity(sourceRoute) === identity(target)) return keep(response);
159
+ if (!routed(target, kind) || identity(sourceRoute) === identity(target)) return keep(response);
155
160
  const originalModel = firstOptions.compactionRoutingOverride?.sourceModel ?? String(snapshot.model);
156
161
  // Use the established protocol commit boundary. For runTurn streams the direct event
157
162
  // observer additionally preserves side-effect heartbeats that the bridge does not publish.
@@ -289,7 +294,8 @@ export async function runWithCompactionRecovery(
289
294
  const compactions = items.filter(value => record(value) && value.type === "compaction");
290
295
  const permitted = items.every(value => record(value) && (value.type === "compaction" || value.type === "reasoning"));
291
296
  const item = permitted && compactions.length === 1 ? compactions[0] as Record<string, unknown> : undefined;
292
- const summary = item && typeof item.encrypted_content === "string" ? decodeCompactionSummary(item.encrypted_content) : null;
297
+ const structured = item && typeof item.encrypted_content === "string" ? decodeRetainedCompaction(item.encrypted_content) : null;
298
+ const summary = structured?.summary ?? (item && typeof item.encrypted_content === "string" ? decodeCompactionSummary(item.encrypted_content) : null);
293
299
  if (json?.status !== "completed" || !summary?.trim()) {
294
300
  void completed.response.body?.cancel().catch(() => undefined);
295
301
  restoreSourceLog?.(completed.response.status);
@@ -298,11 +304,13 @@ export async function runWithCompactionRecovery(
298
304
  // v1 unpacks this text through buildCompactV1Output, which already re-adds retained user
299
305
  // messages as items; embedding them here too would duplicate the same text in the output.
300
306
  const preserved = options.compactionRecoveryKind === "compaction-v1" ? summary : (() => {
301
- const retained = buildCompactV1Output(extractCompactUserMessages(snapshot.input), summary).slice(0, -1);
307
+ const usersOnly = Array.isArray(snapshot.input) ? snapshot.input.filter(item => collectReasoningTexts([item]).length === 0) : snapshot.input;
308
+ const retained = buildCompactV1Output(extractCompactUserMessages(usersOnly), summary).slice(0, -1);
302
309
  const userText = extractCompactUserMessages(retained).map((text, index) => `User message ${index + 1}:\n${text}`).join("\n\n");
303
310
  return `${summary}\n\nRetained original user messages (verbatim; preserve their goals and constraints):\n${userText}`;
304
311
  })();
305
- item!.encrypted_content = encodeCompactionSummary(preserved);
312
+ const encryptedContent = structured ? encodeRetainedCompaction(preserved, structured.reasoning) : encodeCompactionSummary(preserved);
313
+ item!.encrypted_content = encryptedContent;
306
314
  json.model = originalModel;
307
315
  void completed.response.body?.cancel().catch(() => undefined);
308
316
  void response.body?.cancel().catch(() => undefined);
@@ -310,7 +318,7 @@ export async function runWithCompactionRecovery(
310
318
  const usage = usageFromResponsesPayload(json.usage);
311
319
  async function* events(): AsyncGenerator<AdapterEvent> {
312
320
  yield { type: "text_delta", text: preserved };
313
- yield { type: "done", ...(usage ? { usage } : {}) };
321
+ yield { type: "done", compactionEncryptedContent: encryptedContent, ...(usage ? { usage } : {}) };
314
322
  }
315
323
  return new Response(bridgeToResponsesSSE(events(), originalModel, undefined, undefined, undefined, undefined, 2_000,
316
324
  { compaction: true, translatorBudget: options.translatorBudget, onCompletedResponse: () => options.onResponseComplete?.(originalModel) }), { headers: { "content-type": "text/event-stream" } });
@@ -0,0 +1,61 @@
1
+ import { rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getConfigDir } from "../../config";
4
+ import type { OcxConfig, OcxParsedRequest } from "../../types";
5
+ import type { TranslatorBudget } from "../../lib/translator-budget";
6
+ import { parseRequest } from "../../responses/parser";
7
+ import { applyReasoningRetention, appendRetentionNotice } from "../../responses/reasoning-retention";
8
+ import { encodeRetainedCompaction } from "../../responses/retained-compaction";
9
+ import { finalizeAccountLease } from "./core-lifetime";
10
+
11
+ export class InvalidRetainedCompactionSummary extends Error {}
12
+
13
+ /** One routed v2 attempt owns its local retention until its response ends or is cancelled. */
14
+ export function prepareCompactionRetention(
15
+ parsed: OcxParsedRequest,
16
+ config: OcxConfig,
17
+ contextWindow: number | undefined,
18
+ budget: TranslatorBudget,
19
+ signal: AbortSignal,
20
+ ) {
21
+ if (config.reasoningRetention?.enabled !== true) return undefined;
22
+ const raw = parsed._rawBody as Record<string, unknown>;
23
+ const retention = applyReasoningRetention(raw.input, {
24
+ archiveDir: join(getConfigDir(), "reasoning-archive"), contextWindow,
25
+ ...config.reasoningRetention,
26
+ });
27
+ if (!retention.retainedReasoning && !retention.archived) return undefined;
28
+ let committed = false;
29
+ let disposed = false;
30
+ let bytes = 0;
31
+ const dispose = () => {
32
+ if (disposed) return;
33
+ disposed = true;
34
+ signal.removeEventListener("abort", dispose);
35
+ delete parsed._compactionRetention;
36
+ budget.releaseRetained(bytes, { kind: "retained_collectors" });
37
+ if (!committed && retention.archived) rmSync(retention.archived.path, { force: true });
38
+ };
39
+ try {
40
+ const reasoning = retention.retainedReasoning?.map(item => item.content[0]!.text) ?? [];
41
+ const retainedBytes = Buffer.byteLength(JSON.stringify(reasoning));
42
+ budget.chargeRetained(retainedBytes, { kind: "retained_collectors" });
43
+ bytes = retainedBytes;
44
+ const body = { ...raw, model: parsed.modelId, input: retention.input };
45
+ const messages = parseRequest(body, { replayCacheScope: parsed._reasoningReplayScope }).context.messages;
46
+ parsed._rawBody = body;
47
+ parsed.context.messages = messages;
48
+ const callbacks: NonNullable<OcxParsedRequest["_compactionRetention"]> = {
49
+ encode(summary) {
50
+ if (disposed || signal.aborted) throw new Error("Compaction was cancelled");
51
+ if (!summary.trim()) throw new InvalidRetainedCompactionSummary("Compaction returned an empty summary");
52
+ return encodeRetainedCompaction(appendRetentionNotice(summary, retention.archived), reasoning);
53
+ },
54
+ commit() { if (!disposed && !signal.aborted) committed = true; },
55
+ };
56
+ Object.defineProperty(parsed, "_compactionRetention", { value: callbacks, configurable: true });
57
+ if (signal.aborted) { dispose(); throw signal.reason; }
58
+ signal.addEventListener("abort", dispose, { once: true });
59
+ return { dispose, deliver: (response: Response) => finalizeAccountLease(response, dispose, dispose) };
60
+ } catch (error) { dispose(); throw error; }
61
+ }
@@ -17,21 +17,25 @@ export function lateAttributionLimitResponse(): Response {
17
17
  export const PROVIDER_INPUT_TOO_LARGE_MESSAGE =
18
18
  "The provider rejected this turn because its input exceeds the provider size or context limit. Reduce the current input or compact the conversation before retrying.";
19
19
 
20
- /** Preserve non-streaming HTTP failure semantics without exposing an upstream body. */
21
- export function jsonContextOverflowResponse(): Response {
20
+ /**
21
+ * Preserve non-streaming HTTP failure semantics without exposing an upstream body. `message` is
22
+ * replaced only for the proxy's own wording, never with upstream text: when every combo target
23
+ * was refused by local input admission, no provider saw the turn and saying one did is false.
24
+ */
25
+ export function jsonContextOverflowResponse(message = PROVIDER_INPUT_TOO_LARGE_MESSAGE): Response {
22
26
  return Response.json({
23
27
  error: {
24
- message: PROVIDER_INPUT_TOO_LARGE_MESSAGE,
28
+ message,
25
29
  type: "invalid_request_error",
26
30
  code: "context_length_exceeded",
27
31
  },
28
32
  }, { status: 413, headers: { "Cache-Control": "no-store" } });
29
33
  }
30
34
 
31
- async function* contextOverflowEvents(): AsyncGenerator<AdapterEvent> {
35
+ async function* contextOverflowEvents(message: string): AsyncGenerator<AdapterEvent> {
32
36
  yield {
33
37
  type: "error",
34
- message: PROVIDER_INPUT_TOO_LARGE_MESSAGE,
38
+ message,
35
39
  status: 413,
36
40
  errorType: "invalid_request_error",
37
41
  code: "context_length_exceeded",
@@ -51,9 +55,10 @@ async function* contextOverflowEvents(): AsyncGenerator<AdapterEvent> {
51
55
  export function streamingContextOverflowResponse(
52
56
  modelId: string,
53
57
  translatorBudget: TranslatorBudget,
58
+ message = PROVIDER_INPUT_TOO_LARGE_MESSAGE,
54
59
  ): Response {
55
60
  return new Response(bridgeToResponsesSSE(
56
- contextOverflowEvents(),
61
+ contextOverflowEvents(message),
57
62
  modelId,
58
63
  undefined,
59
64
  undefined,
@@ -43,6 +43,7 @@ import { codexAuthContextLogLabel } from "../../codex/account-label";
43
43
  import { forceRefreshMainAccountToken } from "../../codex/main-account";
44
44
  import { rebindPoolCreditPolicy } from "../../codex/pool-credit-policy";
45
45
  import { safeCallerSessionId } from "../caller-session-identity";
46
+ import { copyResponsesClientMetadata } from "../../adapters/openai-responses/transport";
46
47
 
47
48
  /** Normalize caller aliases only on native egress; preserve ingress and fallback identity. */
48
49
  export function withClaudeNativeSession(headers: Headers, provider: OcxProviderConfig, sessionId?: string): Headers {
@@ -335,6 +336,11 @@ export async function resolveResponsesCodexAuth(
335
336
  headers.delete("chatgpt-account-id");
336
337
  }
337
338
  }
339
+ // Codex auth materialization keeps only its credential allowlist. A compact-v1 handoff
340
+ // must also retain the permitted client fingerprint for its routed summarizer send.
341
+ if (options.compactionRecoveryKind === "compaction-v1") {
342
+ copyResponsesClientMetadata(headers, authInputHeaders, route.provider);
343
+ }
338
344
  return {
339
345
  ok: true,
340
346
  authCtx,
@@ -32,6 +32,7 @@ import {
32
32
  type CodexWsPreOutputRefusalHold,
33
33
  } from "./codex-ws-wire";
34
34
  import type { RequestLogContext } from "../request-log";
35
+ import type { AttemptRecoveryKind } from "../../usage/log";
35
36
  import type { DataPlaneAdmission } from "../auth-cors";
36
37
  import type { InboundWire } from "../../providers/registry";
37
38
  import type { BunRuntimeGateInput } from "./ws-upstream";
@@ -909,6 +910,8 @@ export async function retryCodexPoolOnAlternateAccount(
909
910
  applyAccountChangeConversationStateScrub({
910
911
  body: parsed._rawBody,
911
912
  parsed,
913
+ preserveReasoningEncryptedContent: isCanonicalOpenAiForwardProvider(retryProvider)
914
+ && parsed._nativeReasoningRetention?.accountSwitch === true,
912
915
  bindingKey: binding.bindingKey,
913
916
  servingAccountId: binding.accountId,
914
917
  priorAccountId: firstAuthCtx.accountId,
@@ -971,6 +974,10 @@ export async function retryCodexPoolOnAlternateAccount(
971
974
  // What this diff does fix is that the rungs are now CHARGED instead of free.
972
975
  const maxRetrySends = retrySameConfirmedAccount ? 7 : 1;
973
976
  let retrySendCount = 0;
977
+ // The gated-model 400 has no roster kind; it stays unattributed as before.
978
+ const moveRecovery: AttemptRecoveryKind | undefined = outcomeStatus === 429 || outcomeStatus === 402
979
+ ? "oauth-account-429"
980
+ : isTransientUpstreamStatus(outcomeStatus) ? "transient-5xx" : undefined;
974
981
  let upstreamResponse: Response;
975
982
  try {
976
983
  while (true) {
@@ -1005,7 +1012,10 @@ export async function retryCodexPoolOnAlternateAccount(
1005
1012
  // The move is a physical send like any other, so the root workflow is charged too.
1006
1013
  chargeWorkflowSends(args.options.workflowRootId, 1);
1007
1014
  }
1008
- noteProviderAttemptSend(logCtx, route.providerName, route.provider, passthroughEstimate);
1015
+ // A move seals the first account's attempt here. Give it the status that account actually
1016
+ // answered, not the 502 the split infers, and name why the second send exists.
1017
+ if (logCtx.activeAttempt && logCtx.activeAttempt.status < 100) logCtx.activeAttempt.status = firstResponse.status;
1018
+ noteProviderAttemptSend(logCtx, route.providerName, route.provider, passthroughEstimate, moveRecovery);
1009
1019
  try {
1010
1020
  upstreamResponse = await fetchWithHeaderTimeout(
1011
1021
  request.url,
@@ -3,6 +3,7 @@ import { parseRetryAfterMs } from "../../combos";
3
3
  import type { ConsumedComboFailure, HandleResponsesOptions } from "./core-options";
4
4
  import type { OcxUsage } from "../../types";
5
5
  import { readBoundedResponseBody } from "../../lib/bounded-body";
6
+ import { carryAnthropicRefusalCredential } from "../../oauth/anthropic-account-refusal";
6
7
  import { codexQuotaFailureMessage, codexQuotaOutcomeMeta } from "./core-codex-account";
7
8
  import {
8
9
  isRateLimitOrQuotaFailureMessage,
@@ -16,6 +17,7 @@ import { resolveClientRetryAfter } from "../../lib/retry-after";
16
17
  import { formatErrorResponse } from "../../bridge";
17
18
  import { OLLAMA_LATE_ATTRIBUTION_LIMIT_MESSAGE } from "../../adapters/ollama-native";
18
19
  import { isNonReplayableResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
20
+ import { MODEL_NOT_ALLOWED_FOR_KEY } from "../admission-model-scope";
19
21
  import { usageFromResponsesPayload } from "../request-log";
20
22
  import type { ResponsesTerminalStatus } from "../../bridge";
21
23
 
@@ -49,6 +51,9 @@ export async function consumeComboFailure(
49
51
  // display-safe body carries a recognized quota message.
50
52
  let quotaConfirmedByBody = false;
51
53
  const serverError = response.status >= 500 && response.status < 600;
54
+ // A local key-scope refusal is already the client's answer: rebuilding it as an upstream_error
55
+ // would hide the stable model_not_allowed_for_key type clients branch on.
56
+ let scopeRefusalBody: string | undefined;
52
57
  try {
53
58
  const body = await readBoundedResponseBody(response, { signal, reportUtf8Validity: serverError });
54
59
  // A 5xx body counts as quota or classification evidence only when it decoded as valid
@@ -71,6 +76,9 @@ export async function consumeComboFailure(
71
76
  upstreamMessage = normalized.message;
72
77
  upstreamType = normalized.type;
73
78
  }
79
+ if (nonReplayable && response.status === 403 && upstreamType === MODEL_NOT_ALLOWED_FOR_KEY) {
80
+ scopeRefusalBody = body.text;
81
+ }
74
82
  }
75
83
  } catch (error) {
76
84
  if (signal?.aborted) throw error;
@@ -106,7 +114,9 @@ export async function consumeComboFailure(
106
114
  now,
107
115
  includeDefault: false,
108
116
  });
109
- const failureResponse = formatErrorResponse(
117
+ const failureResponse = scopeRefusalBody !== undefined
118
+ ? new Response(scopeRefusalBody, { status: 403, headers: { "Content-Type": "application/json" } })
119
+ : formatErrorResponse(
110
120
  response.status,
111
121
  cyberFailure ? (upstreamType ?? CYBER_POLICY_ERROR_CODE) : "upstream_error",
112
122
  message,
@@ -116,6 +126,7 @@ export async function consumeComboFailure(
116
126
  },
117
127
  );
118
128
  if (nonReplayable) markResponseNonReplayable(failureResponse);
129
+ carryAnthropicRefusalCredential(response, failureResponse);
119
130
  return {
120
131
  response: failureResponse,
121
132
  ...(nonReplayable ? { nonReplayable: true } : {}),
@@ -1,10 +1,12 @@
1
+ import { effectiveAzureOutputBudget } from "../../adapters/azure";
2
+ import { resolveWireProtocolOverride } from "../adapter-resolve";
1
3
  import type { CodexAccountModelRefusal } from "../../combos/failover";
2
4
  import {
3
5
  isCodexReasoningEffort,
4
6
  isDeclaredReasoningEffort,
5
7
  resolveEffortAtOrBelow,
6
8
  } from "../../reasoning-effort";
7
- import { resolveAdmissionModelScope, routeAllowedByScope } from "../admission-model-scope";
9
+ import { admissionMayUseMeteredComboTargets, resolveAdmissionModelScope, routeAllowedByScope } from "../admission-model-scope";
8
10
  import { recordAttemptRequestedEffort } from "../request-log";
9
11
  import {
10
12
  CODEX_TEXT_GUARDED_BUDGET_POLICY,
@@ -29,6 +31,7 @@ import {
29
31
  comboDefaultEffort,
30
32
  isComboTargetInCooldown,
31
33
  snapshotComboQuotaCooldowns,
34
+ comboExclusionReasons,
32
35
  noteComboSuccess,
33
36
  comboFailureDecision,
34
37
  advanceComboAfterFailure,
@@ -50,14 +53,17 @@ import {
50
53
  import { hasUnreadableEncryptedAgentTask } from "./encrypted-payload";
51
54
  import { routeConcreteModel, comboRouteDecisionTrace } from "../../router";
52
55
  import { memoryModelRouteReason } from "./memory-models";
53
- import { poolAccountProviderLabel } from "../../providers/label";
56
+ import { baseProviderLabel, poolAccountProviderLabel } from "../../providers/label";
54
57
  import { getAccountSet } from "../../oauth/store";
58
+ import { anthropicRefusalCredentialIsCurrent } from "../../oauth/anthropic-account-refusal";
55
59
  import { configuredAnthropicInstance } from "../../providers/anthropic-instance";
60
+ import { isAnthropicBillingFailure } from "../../providers/anthropic-billing-refusal";
56
61
  import {
57
62
  anthropicRoutingFor,
58
63
  formatAnthropicProviderForLog,
59
64
  } from "../../oauth/anthropic-routing";
60
- import { codexAccountLogLabel } from "../../codex/account-label";
65
+ import { codexAccountLogLabel, oauthAccountLogLabel } from "../../codex/account-label";
66
+ import { isGenericFailoverProvider, rotateGenericOAuthAccountOn429 } from "../../oauth/generic-account-failover";
61
67
  import { codexQuotaScopeForModel, getCodexQuotaHealthSnapshot } from "../../codex/routing";
62
68
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
63
69
  import type { AgentTaskRecoveryFailureReason } from "./agent-task-recovery";
@@ -133,6 +139,48 @@ function cooledPoolAccountLabel(config: OcxConfig, providerName: string, modelId
133
139
  ? label : undefined;
134
140
  }
135
141
 
142
+ /**
143
+ * The generic OAuth pool's counterpart of `cooledPoolAccountLabel`. Its 429 names one account
144
+ * through `accountLogLabel`, but the per-request account budget can end the pool's own rotation
145
+ * before the last refused account is recorded. Hand that account to the pool's rotator, which
146
+ * cools it under the pool's own Retry-After policy, and spare the target only when the pool still
147
+ * has an eligible sibling. A lone account or an all-cooled pool returns null and cools the target.
148
+ */
149
+ function cooledGenericOAuthAccountLabel(
150
+ config: OcxConfig, providerName: string, modelId: string, label: string | undefined, retryAfter: string | undefined,
151
+ ): string | undefined {
152
+ const provider = config.providers[providerName];
153
+ if (!label || !provider || !isGenericFailoverProvider(providerName, provider)) return undefined;
154
+ const base = baseProviderLabel(providerName);
155
+ const matches = getAccountSet(providerName)?.accounts.filter(account =>
156
+ oauthAccountLogLabel(account.id, base) === label) ?? [];
157
+ return matches.length === 1
158
+ && rotateGenericOAuthAccountOn429(config, providerName, matches[0]!.id, retryAfter, Date.now(), modelId)
159
+ ? label : undefined;
160
+ }
161
+
162
+ /**
163
+ * A 402 credit-balance refusal names one Anthropic OAuth account -- the evidence the pool already
164
+ * accepts on a 403 -- but the pool rotates only on 401/403/429, so the 402 reaches the combo with
165
+ * that account still selectable. Cool it under the pool's entitlement backoff and spare the target
166
+ * only while a sibling is eligible; a lone account or an exhausted pool still cools the target.
167
+ */
168
+ function creditRefusedAnthropicAccountLabel(
169
+ config: OcxConfig, providerName: string, modelId: string, label: string | undefined,
170
+ message: string, code: string | null | undefined, response: Response,
171
+ ): string | undefined {
172
+ const instance = configuredAnthropicInstance(config, providerName);
173
+ if (!label || !instance || config.providers[providerName]?.authMode !== "oauth"
174
+ || !isAnthropicBillingFailure(402, message, code)) return undefined;
175
+ const routing = anthropicRoutingFor(instance);
176
+ const matches = getAccountSet(instance)?.accounts.filter(account =>
177
+ formatAnthropicProviderForLog(instance, account.id) === label) ?? [];
178
+ return matches.length === 1
179
+ && anthropicRefusalCredentialIsCurrent(response, instance, matches[0]!.id)
180
+ && routing.recordAnthropicAccountRefusal(config, matches[0]!.id, 403, null)
181
+ && routing.getEligibleAnthropicAccounts(Date.now(), modelId).length > 0
182
+ ? label : undefined;
183
+ }
136
184
 
137
185
  /**
138
186
  * True only for the Anthropic OAuth pool's OWN "all accounts cooled" 429. Its Retry-After is the
@@ -249,6 +297,30 @@ interface JevComboChoice {
249
297
  candidate: JevCandidate;
250
298
  }
251
299
 
300
+ /** A target's JEV effort choices, or undefined when an explicit allowlist has none still supported. */
301
+ function jevChoiceEfforts(
302
+ config: OcxConfig,
303
+ target: NonNullable<ReturnType<typeof getCombo>>["targets"][number],
304
+ ): OcxComboDefaultEffort[] | undefined {
305
+ let ladder: string[] | undefined;
306
+ try {
307
+ const route = routeConcreteModel(config, targetKey(target));
308
+ ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
309
+ } catch {
310
+ // Preserve the existing routing-failure surface. Unknown capability becomes the explicit
311
+ // no-effort choice rather than broadening JEV's effort allowlist.
312
+ ladder = undefined;
313
+ }
314
+ const supportedEfforts = (ladder ?? []).filter(isCodexReasoningEffort) as OcxComboDefaultEffort[];
315
+ const configuredEfforts = target.reasoningEfforts;
316
+ const reasoningEfforts = configuredEfforts === undefined
317
+ ? supportedEfforts
318
+ : configuredEfforts.filter(effort => supportedEfforts.includes(effort));
319
+ // An explicit allowlist is restrictive. If catalog capabilities drift until no configured
320
+ // effort remains supported, omit the target instead of silently broadening JEV's choices.
321
+ return configuredEfforts !== undefined && reasoningEfforts.length === 0 ? undefined : reasoningEfforts;
322
+ }
323
+
252
324
  /** Enumerate the current ordinary Combo eligibility set without retaining attempted picks. */
253
325
  function eligibleJevComboChoices(
254
326
  config: OcxConfig,
@@ -267,23 +339,8 @@ function eligibleJevComboChoices(
267
339
  excluded.add(key);
268
340
  // The TypeSafe row owns a decision credential, not an inference transport.
269
341
  if (pick.target.provider === JEV_PROVIDER_ID) continue;
270
- let ladder: string[] | undefined;
271
- try {
272
- const route = routeConcreteModel(config, key);
273
- ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
274
- } catch {
275
- // Preserve the existing routing-failure surface. Unknown capability becomes the explicit
276
- // no-effort choice rather than broadening JEV's effort allowlist.
277
- ladder = undefined;
278
- }
279
- const supportedEfforts = (ladder ?? []).filter(isCodexReasoningEffort) as OcxComboDefaultEffort[];
280
- const configuredEfforts = pick.target.reasoningEfforts;
281
- const reasoningEfforts = configuredEfforts === undefined
282
- ? supportedEfforts
283
- : configuredEfforts.filter(effort => supportedEfforts.includes(effort));
284
- // An explicit allowlist is restrictive. If catalog capabilities drift until no configured
285
- // effort remains supported, omit the target instead of silently broadening JEV's choices.
286
- if (configuredEfforts !== undefined && reasoningEfforts.length === 0) continue;
342
+ const reasoningEfforts = jevChoiceEfforts(config, pick.target);
343
+ if (!reasoningEfforts) continue;
287
344
  choices.push({
288
345
  pick: { ...pick, attempted: [key] },
289
346
  candidate: {
@@ -315,9 +372,10 @@ export async function executeComboResponses(
315
372
  options: HandleResponsesOptions & { translatorBudget: TranslatorBudget },
316
373
  requestDispatchers: ResponsesDispatchers,
317
374
  ): Promise<Response> {
318
- const requestedModel = typeof (rawBody as { model?: unknown } | null)?.model === "string"
319
- ? (rawBody as { model: string }).model
320
- : `combo/${comboId}`;
375
+ const requestedModel = options.mainHardLockFallbackOverride?.sourceModel
376
+ ?? (typeof (rawBody as { model?: unknown } | null)?.model === "string"
377
+ ? (rawBody as { model: string }).model
378
+ : `combo/${comboId}`);
321
379
  Object.assign(logCtx, {
322
380
  requestedModel,
323
381
  model: requestedModel,
@@ -447,8 +505,10 @@ export async function executeComboResponses(
447
505
  let comboPayloadReadable = false;
448
506
  const payloadEligible = (target: (typeof combo.targets)[number]): boolean =>
449
507
  comboPayloadReadable || !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
508
+ const meteredAllowed = admissionMayUseMeteredComboTargets(config, options.admission, combo);
450
509
  const targetEligible = (target: (typeof combo.targets)[number]): boolean =>
451
- (combo.strategy !== "jev" || target.provider !== JEV_PROVIDER_ID)
510
+ (meteredAllowed || !target.metered)
511
+ && (combo.strategy !== "jev" || target.provider !== JEV_PROVIDER_ID)
452
512
  && payloadEligible(target)
453
513
  && reasoningReplayEligible(target)
454
514
  && (protocolLanes?.pickable(target) ?? true);
@@ -522,6 +582,9 @@ export async function executeComboResponses(
522
582
  now?: number;
523
583
  }) => pickComboTargetWithWait(config, comboId, {
524
584
  ...pickOptions,
585
+ // Every pick, including the recovery picks that skip `targetEligible`, keeps metered
586
+ // targets away from callers that may not spend on them.
587
+ eligible: target => (meteredAllowed || !target.metered) && (pickOptions.eligible?.(target) ?? true),
525
588
  waitForCooldownMs: combo.waitForCooldownMs,
526
589
  abortSignal: options.abortSignal,
527
590
  });
@@ -541,7 +604,8 @@ export async function executeComboResponses(
541
604
  );
542
605
  return options.abortSignal?.aborted
543
606
  ? clientCancelledResponse()
544
- : comboUnavailable(comboId);
607
+ : comboUnavailable(comboId, undefined,
608
+ comboExclusionReasons(config, comboId, target => meteredAllowed || !target.metered));
545
609
  }
546
610
  if (!(await recoverUnreadableEncryptedTask())) {
547
611
  return options.abortSignal?.aborted
@@ -557,13 +621,16 @@ export async function executeComboResponses(
557
621
  if (onlyReplayIncompatibleTargetsRemain()) return targetIncompatibleResponse();
558
622
  return options.abortSignal?.aborted
559
623
  ? clientCancelledResponse()
560
- : comboUnavailable(comboId);
624
+ : comboUnavailable(comboId, undefined, comboExclusionReasons(config, comboId, targetEligible));
561
625
  }
562
626
  let jevDecision: JevDecision | undefined;
563
627
  if (combo.strategy === "jev") {
564
628
  const choices = eligibleJevComboChoices(config, comboId, targetEligible, Date.now());
565
629
  const first = choices[0];
566
- if (!first) return comboUnavailable(comboId);
630
+ if (!first) {
631
+ return comboUnavailable(comboId, undefined, comboExclusionReasons(config, comboId, target =>
632
+ targetEligible(target) && target.provider !== JEV_PROVIDER_ID && jevChoiceEfforts(config, target) !== undefined));
633
+ }
567
634
  const resolvedFailOpenEffort = resolveEffortAtOrBelow(
568
635
  "medium",
569
636
  first.candidate.reasoningEfforts,
@@ -677,6 +744,9 @@ export async function executeComboResponses(
677
744
  // is gone, so carry the loop's own classification decision instead of re-deriving a
678
745
  // weaker one from the status alone (#4149).
679
746
  let lastFailureClassifiesOverflow = false;
747
+ // Local input admission refusals (#1524), while every failed target so far was one: such a
748
+ // target sent nothing, so an exhausted ladder must not be reported as a provider rejection.
749
+ let localAdmissionRefusals: string[] | undefined = [];
680
750
  // The first quota/rate-limit refusal of the ladder. A fallback that then refuses its own
681
751
  // credential or plan (401/403, or a 400 the ladder hopped past) says nothing about why the
682
752
  // request failed: the primary target ran out. Returning that fallback refusal told clients
@@ -712,12 +782,31 @@ export async function executeComboResponses(
712
782
  retryAfter: String(Math.max(1, Math.ceil(remainingQuotaCooldownMs / 1000))),
713
783
  }) : lastFailure;
714
784
  };
785
+ // The caller's output ceiling, captured once so a floor applied to one target never
786
+ // leaks into the next. An established parent estimate wins; otherwise (combo dispatch
787
+ // precedes request preparation) the caller's own positive cap is reserved.
788
+ const callerBudget = isPlainObject(body) ? body.max_output_tokens : undefined;
789
+ const callerOutputCeilingTokens = logCtx.spendOutputCeilingTokens
790
+ ?? (typeof callerBudget === "number" && Number.isFinite(callerBudget) && callerBudget > 0
791
+ ? Math.trunc(callerBudget) : undefined);
715
792
  while (pick) {
716
793
  if (options.abortSignal?.aborted) return clientCancelledResponse();
717
794
  const firstComboTarget = comboTargetsDispatched === 0;
718
795
  const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
719
796
  // The inherited spend tracker observes this parent log, before the child has its own label.
720
797
  logCtx.spendPoolId = targetRoute.providerName;
798
+ // Combo prepays before its child normalizes the route; the parent tracker must
799
+ // already see this target's effective cap rather than a previous target's cap.
800
+ const spendProvider = resolveWireProtocolOverride(targetRoute.providerName, targetRoute.modelId,
801
+ targetRoute.provider, options.inboundWire ?? "responses", targetRoute.staticPolicy);
802
+ const outputBudget = effectiveAzureOutputBudget(spendProvider, callerBudget);
803
+ if (outputBudget !== callerBudget && typeof outputBudget === "number" && outputBudget > 0) {
804
+ logCtx.spendOutputCeilingTokens = Math.trunc(outputBudget);
805
+ } else if (callerOutputCeilingTokens === undefined) {
806
+ delete logCtx.spendOutputCeilingTokens;
807
+ } else {
808
+ logCtx.spendOutputCeilingTokens = callerOutputCeilingTokens;
809
+ }
721
810
  // Derive the target's allowance before booking its initial send; the booking occupies it.
722
811
  const targetSendBudget = comboSendScope
723
812
  ? comboTargetSendBudget(comboSendScope, combo.targets.length - 1 - comboTargetsDispatched)
@@ -832,7 +921,8 @@ export async function executeComboResponses(
832
921
  // The live config can change while the child is streaming. Never retain credentials.
833
922
  const currentCombo = getCombo(config, comboId);
834
923
  const provider = config.providers[completedTarget.provider];
835
- if (!options.compactionRoutingOverride && Object.hasOwn(config.providers, completedTarget.provider)
924
+ if (!options.compactionRoutingOverride && !options.mainHardLockFallbackOverride
925
+ && Object.hasOwn(config.providers, completedTarget.provider)
836
926
  && provider && provider.disabled !== true
837
927
  && currentCombo?.targets.some(target => targetKey(target) === targetKey(completedTarget))) {
838
928
  rememberComboForLane(sessionLaneIdFromRequest(req.headers), comboId, completedTarget, model, writerGeneration);
@@ -1024,6 +1114,12 @@ export async function executeComboResponses(
1024
1114
  Date.now() - started,
1025
1115
  failure.usage,
1026
1116
  );
1117
+ if (failure.upstreamCode === "input_admission_refused" && attempt.sendCount === 0) {
1118
+ attempt.errorCode = failure.upstreamCode;
1119
+ localAdmissionRefusals?.push(failure.upstreamMessage ?? "");
1120
+ } else {
1121
+ localAdmissionRefusals = undefined;
1122
+ }
1027
1123
  attemptRetained = true;
1028
1124
  lastFailure = failure.response;
1029
1125
  if (!quotaFailure && (lastFailure.status === 429 || lastFailure.status === 402)) {
@@ -1107,12 +1203,16 @@ export async function executeComboResponses(
1107
1203
  const failedTargetKey = targetKey(pick.target);
1108
1204
  let failedTargetCooldownRecorded = false;
1109
1205
  // The dispatch rewrote this to name the pool account that actually served the turn.
1110
- const failedAccount = cooledPoolAccountLabel(
1111
- config,
1112
- pick.target.provider,
1113
- pick.target.model,
1114
- poolAccountProviderLabel(childLog.provider, pick.target.provider),
1115
- );
1206
+ const poolAccountLabel = poolAccountProviderLabel(childLog.provider, pick.target.provider);
1207
+ const failedAccount = failure.response.status === 402
1208
+ ? creditRefusedAnthropicAccountLabel(
1209
+ config, pick.target.provider, pick.target.model, poolAccountLabel, failure.classificationText, failure.upstreamCode,
1210
+ failure.response,
1211
+ )
1212
+ : cooledPoolAccountLabel(config, pick.target.provider, pick.target.model, poolAccountLabel)
1213
+ ?? (failure.response.status === 429 && failureCooldownScope === "target"
1214
+ ? cooledGenericOAuthAccountLabel(config, pick.target.provider, pick.target.model, childLog.accountLogLabel, failure.retryAfter)
1215
+ : undefined);
1116
1216
  // The target still cools on the pool's own refusal (the anti-hammer guard), but for the local
1117
1217
  // fallback rather than the pool's restated per-account Retry-After.
1118
1218
  const poolLocalRefusal = isAnthropicPoolLocalRefusal(
@@ -1204,9 +1304,14 @@ export async function executeComboResponses(
1204
1304
  lastFailure?.status === 413
1205
1305
  && lastFailureClassifiesOverflow
1206
1306
  ) {
1307
+ if (localAdmissionRefusals?.length) logCtx.errorCode = "input_admission_refused";
1308
+ const localMessage = localAdmissionRefusals?.length
1309
+ ? `opencodex refused this turn locally; nothing was sent upstream. No target of combo ${comboId} `
1310
+ + `that was tried can fit its input: ${localAdmissionRefusals.join(" ")} Compact the conversation before retrying.`
1311
+ : undefined;
1207
1312
  return (rawBody as { stream?: unknown } | null)?.stream === true
1208
- ? streamingContextOverflowResponse(requestedModel, options.translatorBudget)
1209
- : jsonContextOverflowResponse();
1313
+ ? streamingContextOverflowResponse(requestedModel, options.translatorBudget, localMessage)
1314
+ : jsonContextOverflowResponse(localMessage);
1210
1315
  }
1211
1316
  return failure;
1212
1317
  }
@@ -8,7 +8,7 @@ import {
8
8
  DecompressedBodyTooLargeError,
9
9
  describeInboundBodyRefusal,
10
10
  } from "../request-decompress";
11
- import { comboCooldownRetryAfterSeconds } from "../../combos";
11
+ import { comboCooldownRetryAfterSeconds, NoAvailableComboTargetsError } from "../../combos";
12
12
  import type { AgentTaskRecoveryFailureReason } from "./agent-task-recovery";
13
13
 
14
14
  /**
@@ -115,8 +115,8 @@ export function comboUnavailableResponse(
115
115
  }
116
116
 
117
117
 
118
- export function comboUnavailable(comboId: string, now = Date.now()): Response {
119
- return comboUnavailableResponse(`No available targets for combo: ${comboId}`, {
118
+ export function comboUnavailable(comboId: string, now = Date.now(), reasons = ""): Response {
119
+ return comboUnavailableResponse(new NoAvailableComboTargetsError(comboId, reasons).message, {
120
120
  retryAfter: comboCooldownRetryAfterSeconds(comboId, now),
121
121
  });
122
122
  }