@bitkyc08/opencodex 2.81.0 → 2.83.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 (272) hide show
  1. package/bin/ocx.mjs +166 -73
  2. package/gui/dist/assets/App-Ba8uG696.js +54 -0
  3. package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
  4. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  5. package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
  6. package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic.ts +1 -1
  10. package/src/adapters/cursor/catalog.ts +2 -2
  11. package/src/adapters/cursor/effort-map.ts +1 -1
  12. package/src/adapters/devin/live-models.ts +2 -2
  13. package/src/adapters/google-truncation.ts +22 -0
  14. package/src/adapters/google.ts +7 -3
  15. package/src/adapters/ollama-native.ts +18 -14
  16. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  17. package/src/adapters/openai-responses/passthrough.ts +11 -4
  18. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  19. package/src/adapters/openai-responses/reasoning.ts +11 -1
  20. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  21. package/src/bridge/errors.ts +7 -1
  22. package/src/claude/agents-inject.ts +14 -3
  23. package/src/claude/context-windows.ts +22 -20
  24. package/src/claude/desktop-3p.ts +88 -12
  25. package/src/claude/desktop-picker.ts +1 -1
  26. package/src/claude/desktop-profile.ts +2 -1
  27. package/src/claude/gateway-cache.ts +18 -1
  28. package/src/claude/inbound.ts +77 -3
  29. package/src/claude/intercept/cli-catalog.ts +3 -0
  30. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  31. package/src/claude/intercept/picker-models.ts +14 -6
  32. package/src/claude/intercept/settings.ts +3 -0
  33. package/src/claude/long-context.ts +66 -0
  34. package/src/claude/model-info.ts +19 -16
  35. package/src/claude/outbound.ts +94 -9
  36. package/src/claude/subagent-model.ts +40 -11
  37. package/src/cli/account-auth.ts +20 -12
  38. package/src/cli/agent.ts +9 -5
  39. package/src/cli/aside-profile-recovery.ts +33 -0
  40. package/src/cli/capabilities-base.ts +5 -5
  41. package/src/cli/capabilities-integrations.ts +2 -2
  42. package/src/cli/cli-path-diagnostics.ts +132 -0
  43. package/src/cli/desktop-runtime-guidance.ts +65 -0
  44. package/src/cli/dispatch.ts +5 -0
  45. package/src/cli/doctor.ts +16 -3
  46. package/src/cli/index.ts +14 -9
  47. package/src/cli/integration-aside-sync.ts +2 -1
  48. package/src/cli/integrations.ts +16 -3
  49. package/src/cli/resolve.ts +26 -3
  50. package/src/cli/root.ts +4 -2
  51. package/src/cli/status.ts +64 -12
  52. package/src/cli/update-restart-child.ts +4 -2
  53. package/src/cli/update-restart-home.ts +28 -10
  54. package/src/cli/update-restart-service-record.ts +116 -0
  55. package/src/cli/update-restart-supervision.ts +96 -0
  56. package/src/cli/update-restart.ts +59 -13
  57. package/src/cli/v2.ts +27 -20
  58. package/src/cli/version-skew-notice.ts +77 -0
  59. package/src/clients/config-export/model-metadata.ts +16 -0
  60. package/src/clients/config-export/zcode-store.ts +46 -11
  61. package/src/clients/config-export/zcode.ts +3 -5
  62. package/src/clients/config-export.ts +1 -1
  63. package/src/clients/lazycodex.ts +1 -1
  64. package/src/clients/omo-role-models.ts +32 -7
  65. package/src/codex/auth-api/account-selection.ts +107 -0
  66. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  67. package/src/codex/auth-api/routes.ts +10 -49
  68. package/src/codex/auth-context.ts +28 -4
  69. package/src/codex/autostart-health.ts +12 -3
  70. package/src/codex/catalog/build-entries.ts +18 -0
  71. package/src/codex/catalog/bundled.ts +155 -26
  72. package/src/codex/catalog/combo-member.ts +3 -2
  73. package/src/codex/catalog/effort.ts +26 -5
  74. package/src/codex/catalog/gather-capture.ts +5 -0
  75. package/src/codex/catalog/model-hints.ts +15 -8
  76. package/src/codex/catalog/model-visibility.ts +3 -1
  77. package/src/codex/catalog/native-models.ts +1 -1
  78. package/src/codex/catalog/operator-tiers.ts +38 -0
  79. package/src/codex/catalog/provider-models.ts +68 -35
  80. package/src/codex/catalog/retained-sync.ts +7 -5
  81. package/src/codex/catalog/routed-gather.ts +15 -1
  82. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  83. package/src/codex/catalog-auto-refresh.ts +19 -0
  84. package/src/codex/catalog.ts +1 -1
  85. package/src/codex/codex-home-owner.ts +3 -0
  86. package/src/codex/config-write-lock.ts +333 -0
  87. package/src/codex/convergence.ts +2 -1
  88. package/src/codex/desktop-app/darwin.ts +1 -1
  89. package/src/codex/desktop-app/handoff.ts +1 -1
  90. package/src/codex/desktop-app/linux.ts +1 -1
  91. package/src/codex/desktop-app/lock.ts +1 -1
  92. package/src/codex/desktop-app/types.ts +1 -1
  93. package/src/codex/desktop-app/windows.ts +1 -1
  94. package/src/codex/desktop-app-restart.ts +1 -1
  95. package/src/codex/features.ts +257 -200
  96. package/src/codex/inject/config-write-section.ts +34 -0
  97. package/src/codex/inject/multi-agent-v2.ts +14 -6
  98. package/src/codex/inject/remove.ts +36 -11
  99. package/src/codex/inject/restore.ts +85 -20
  100. package/src/codex/inject-coordination.ts +15 -6
  101. package/src/codex/inject.ts +62 -20
  102. package/src/codex/journal.ts +62 -17
  103. package/src/codex/main-account-cache.ts +43 -0
  104. package/src/codex/main-account.ts +6 -7
  105. package/src/codex/paths.ts +2 -0
  106. package/src/codex/prepared-home.ts +18 -0
  107. package/src/codex/prompt-journal.ts +25 -12
  108. package/src/codex/prompt-layers.ts +49 -26
  109. package/src/codex/prompt-lock-claim.ts +134 -0
  110. package/src/codex/prompt-lock-evidence.ts +26 -0
  111. package/src/codex/prompt-lock-io.ts +16 -0
  112. package/src/codex/prompt-lock-owner.ts +130 -0
  113. package/src/codex/prompt-lock.ts +84 -53
  114. package/src/codex/routing/failure-window.ts +146 -0
  115. package/src/codex/routing/selection.ts +4 -1
  116. package/src/codex/routing.ts +13 -17
  117. package/src/codex/runtime.ts +106 -18
  118. package/src/codex/shim.ts +45 -19
  119. package/src/combos/jev-service-exchange.ts +281 -0
  120. package/src/combos/jev.ts +34 -220
  121. package/src/config/atomic-write.ts +1 -1
  122. package/src/config/codex-ws-reuse-setting.ts +10 -0
  123. package/src/config/live-reconcile.ts +17 -0
  124. package/src/config/load-degrade.ts +8 -0
  125. package/src/config/rebase-provenance.ts +11 -0
  126. package/src/config/schema/config-schema.ts +4 -0
  127. package/src/generated/compatibility-version.json +375 -227
  128. package/src/generated/model-metadata.ts +2 -2
  129. package/src/grok/inject.ts +276 -206
  130. package/src/grok/reset-coupon-ledger.ts +223 -57
  131. package/src/grok/reset-coupons.ts +13 -6
  132. package/src/grok/sync.ts +2 -1
  133. package/src/images/loop.ts +29 -11
  134. package/src/integrations/aside-profiles.ts +10 -3
  135. package/src/integrations/catalog-refresh.ts +18 -2
  136. package/src/integrations/cursor-detect.ts +1 -1
  137. package/src/integrations/writer.ts +9 -3
  138. package/src/lib/bun-path-runtime.d.mts +32 -0
  139. package/src/lib/bun-path-runtime.mjs +95 -0
  140. package/src/lib/bun-runtime-preflight.ts +140 -0
  141. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  142. package/src/lib/desktop-cli-handoff.mjs +92 -0
  143. package/src/lib/desktop-cli-record.d.mts +29 -0
  144. package/src/lib/desktop-cli-record.mjs +102 -0
  145. package/src/lib/errors.ts +25 -0
  146. package/src/lib/redact.ts +2 -2
  147. package/src/lib/request-execution-budget.ts +8 -0
  148. package/src/lib/synced-state-location.ts +46 -6
  149. package/src/lib/test-home-guard.ts +87 -1
  150. package/src/lib/translator-budget.ts +17 -4
  151. package/src/lib/windows-atomic-replace.ts +38 -16
  152. package/src/lib/windows-elevation.ts +6 -0
  153. package/src/lib/windows-secret-acl.ts +92 -25
  154. package/src/lib/winsw.ts +2 -3
  155. package/src/oauth/anthropic-account-refusal.ts +39 -1
  156. package/src/oauth/anthropic-routing.ts +1 -1
  157. package/src/oauth/anthropic-send-ownership.ts +3 -3
  158. package/src/oauth/chatgpt.ts +113 -41
  159. package/src/oauth/index.ts +7 -3
  160. package/src/oauth/meta-muse-device.ts +1 -1
  161. package/src/oauth/store.ts +1 -1
  162. package/src/oauth/types.ts +1 -1
  163. package/src/providers/anthropic-fast.ts +1 -1
  164. package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
  165. package/src/providers/azure-model-metadata.ts +115 -0
  166. package/src/providers/derive.ts +21 -0
  167. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  168. package/src/providers/devin-provider-merge-migration.ts +1 -1
  169. package/src/providers/github-copilot-auto.ts +169 -0
  170. package/src/providers/model-presets.ts +1 -1
  171. package/src/providers/muse-subscription-usage.ts +1 -1
  172. package/src/providers/quota/vendor-probes-key.ts +127 -17
  173. package/src/providers/quota.ts +1 -1
  174. package/src/providers/registry/entries-core.ts +6 -6
  175. package/src/providers/registry/entries-extended.ts +4 -0
  176. package/src/providers/registry/model-seeds.ts +3 -3
  177. package/src/providers/xai-fast-model.ts +1 -1
  178. package/src/responses/reasoning-envelope.ts +29 -1
  179. package/src/responses/reasoning-replay-cache.ts +17 -1
  180. package/src/responses/spill-store.ts +15 -2
  181. package/src/responses/state/metrics.ts +8 -0
  182. package/src/responses/state/spill-failure.ts +12 -0
  183. package/src/responses/state/spill-queue.ts +12 -5
  184. package/src/responses/state.ts +18 -10
  185. package/src/responses/task-input.ts +17 -0
  186. package/src/server/background-lifecycle.ts +27 -0
  187. package/src/server/chat-completions.ts +1 -0
  188. package/src/server/chat-native.ts +28 -12
  189. package/src/server/claude-messages.ts +43 -6
  190. package/src/server/inbound-body-admission.ts +25 -7
  191. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  192. package/src/server/index/link-listener.ts +30 -17
  193. package/src/server/index/serve-options.ts +18 -4
  194. package/src/server/index/spend-ledger-lifecycle.ts +2 -1
  195. package/src/server/index/startup-warnings.ts +19 -0
  196. package/src/server/index.ts +2 -1
  197. package/src/server/lifecycle.ts +31 -4
  198. package/src/server/management/agent-settings-routes.ts +93 -32
  199. package/src/server/management/codex-agent-role-routes.ts +4 -2
  200. package/src/server/management/codex-prompt-routes.ts +1 -0
  201. package/src/server/management/codex-role-auto-assign.ts +13 -2
  202. package/src/server/management/config-routes.ts +59 -8
  203. package/src/server/management/context.ts +2 -2
  204. package/src/server/management/grok-coupon-routes.ts +277 -52
  205. package/src/server/management/link-routes.ts +42 -20
  206. package/src/server/management/route-registry.ts +2 -2
  207. package/src/server/management/shared.ts +3 -3
  208. package/src/server/messages-native-oauth.ts +9 -8
  209. package/src/server/messages-native.ts +49 -13
  210. package/src/server/messages-response-headers.ts +14 -0
  211. package/src/server/relay-eager.ts +2 -0
  212. package/src/server/relay.ts +18 -7
  213. package/src/server/request-log-conversation.ts +48 -5
  214. package/src/server/request-log-failure-attribution.ts +3 -0
  215. package/src/server/request-log-terminal-status.ts +186 -0
  216. package/src/server/request-log.ts +15 -89
  217. package/src/server/responses/adapter-continuation.ts +11 -3
  218. package/src/server/responses/adapter-dispatch.ts +99 -19
  219. package/src/server/responses/codex-ws-exchange.ts +17 -2
  220. package/src/server/responses/codex-ws-pool.ts +100 -13
  221. package/src/server/responses/codex-ws-wire.ts +9 -0
  222. package/src/server/responses/combo-requested-effort.ts +3 -1
  223. package/src/server/responses/compact.ts +3 -2
  224. package/src/server/responses/core-codex-account.ts +22 -1
  225. package/src/server/responses/core-combo-native.ts +20 -3
  226. package/src/server/responses/core-combo.ts +22 -14
  227. package/src/server/responses/core-lifetime.ts +3 -36
  228. package/src/server/responses/core-opaque-recovery.ts +5 -5
  229. package/src/server/responses/core-options.ts +5 -0
  230. package/src/server/responses/core-replay.ts +73 -0
  231. package/src/server/responses/core.ts +9 -9
  232. package/src/server/responses/empty-completion-guard.ts +18 -2
  233. package/src/server/responses/fetch-helpers.ts +26 -3
  234. package/src/server/responses/jev-model-invoke.ts +1 -1
  235. package/src/server/responses/passthrough-delivery.ts +24 -2
  236. package/src/server/responses/passthrough-dispatch.ts +134 -62
  237. package/src/server/responses/passthrough-execution.ts +3 -3
  238. package/src/server/responses/request-prepare.ts +4 -0
  239. package/src/server/responses/request-send-budget.ts +112 -49
  240. package/src/server/responses/request-transport.ts +125 -5
  241. package/src/server/responses/response-effects.ts +1 -0
  242. package/src/server/responses/run-turn-execution.ts +15 -5
  243. package/src/server/responses/sidecar-execution.ts +30 -16
  244. package/src/server/responses/sidecar-send-budget.ts +58 -0
  245. package/src/server/responses/spend-storage-error.ts +33 -0
  246. package/src/server/responses/ws-upstream.ts +19 -6
  247. package/src/server/startup-health-cache.ts +2 -1
  248. package/src/server/workflow-refusal.ts +3 -0
  249. package/src/service/cli.ts +20 -4
  250. package/src/service/desktop-command-guard.ts +32 -0
  251. package/src/service/desktop-startup.ts +66 -40
  252. package/src/service/desktop-supervision.d.mts +26 -0
  253. package/src/service/desktop-supervision.mjs +140 -0
  254. package/src/service/managing-cli.ts +1 -0
  255. package/src/service/orchestration.ts +46 -20
  256. package/src/service/repair.ts +25 -9
  257. package/src/service/state.ts +4 -3
  258. package/src/service/systemd.ts +17 -8
  259. package/src/service/windows-ops.ts +61 -22
  260. package/src/storage/policy-job.ts +5 -1
  261. package/src/storage/policy-worker.ts +3 -1
  262. package/src/storage/policy.ts +9 -0
  263. package/src/types/config.ts +21 -0
  264. package/src/types/request.ts +10 -0
  265. package/src/types/wire.ts +1 -0
  266. package/src/update/index.ts +57 -9
  267. package/src/update/restart-ownership.ts +10 -3
  268. package/src/update/runtime-ownership.d.mts +6 -0
  269. package/src/update/runtime-ownership.mjs +23 -7
  270. package/src/usage/expected-prices.ts +1 -1
  271. package/src/web-search/loop.ts +5 -2
  272. package/gui/dist/assets/App-7yhoys8a.js +0 -54
@@ -9,7 +9,7 @@ import {
9
9
  import { poolContinuityRefusalReason, workflowRefusalResponse } from "../workflow-refusal";
10
10
  import type { AttemptRecoveryKind, AttemptRecoveryWithheld } from "../../usage/log";
11
11
  import { noteAttemptRecoveryWithheld, noteAttemptSend } from "../request-log";
12
- import { TRANSIENT_RETRY_MAX_ATTEMPTS } from "../../lib/upstream-retry";
12
+ import { TRANSIENT_RETRY_MAX_ATTEMPTS, SendBudgetExhaustedError } from "../../lib/upstream-retry";
13
13
  import type {
14
14
  DispatchDecision,
15
15
  DispatchIntent,
@@ -58,10 +58,24 @@ export function createResponsesSendBudget(
58
58
  // sends once per child seven hundred times, so every send charged to the request is charged
59
59
  // to the root as well (#4546).
60
60
  const workflowRootId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
61
- const inheritedPermit = options.compactionRecoveryPermit ?? options.comboDispatchPermit;
61
+ // A child that owns its Combo booking settles that exact permit through the reporter, even
62
+ // when its caller handed ownership over only as `comboInitialSend`.
63
+ const inheritedPermit = options.compactionRecoveryPermit ?? options.comboDispatchPermit
64
+ ?? options.comboInitialSend?.permit;
62
65
  let pendingHopPermit: SingleUseDispatchPermit | undefined = options.compactionRecoveryPermit;
66
+ let initialPermit = options.comboInitialSend?.permit;
67
+ // Count this owner's physical sends, not later bookings made by other shared-ledger owners.
68
+ // Direct callers retain sends reported before this owner was constructed.
69
+ let targetSendsUsed = initialPermit ? 0 : Math.max(0,
70
+ sendBudget.used - (options.compactionRecoveryPermit ? 1 : 0));
71
+ /** Attribute reported physical sends to this owner; the prepaid booking is spent once reported. */
72
+ const noteTargetSends = (charged: number): void => {
73
+ targetSendsUsed += charged;
74
+ if (charged > 0) initialPermit = undefined;
75
+ };
63
76
  const recordTransientSends = (used: number, permit?: SingleUseDispatchPermit): void => {
64
77
  const charged = Math.max(0, used);
78
+ noteTargetSends(charged);
65
79
  reportDispatchSends(sendBudget, charged, permit);
66
80
  options.onCompactionRecoverySendsReported?.(charged);
67
81
  chargeWorkflowSends(workflowRootId, charged);
@@ -71,6 +85,7 @@ export function createResponsesSendBudget(
71
85
  const transientSendReporter = (permit = pendingHopPermit ?? inheritedPermit) =>
72
86
  createPhysicalSendReporter(sendBudget, () => ({ poolId: logCtx.spendPoolId ?? logCtx.provider,
73
87
  identityId: logCtx.accountLogLabel }), permit, charged => {
88
+ noteTargetSends(Math.max(0, charged));
74
89
  options.onCompactionRecoverySendsReported?.(charged);
75
90
  chargeWorkflowSends(workflowRootId, charged);
76
91
  });
@@ -136,6 +151,7 @@ export function createResponsesSendBudget(
136
151
  // Ordinal 1 is skipped because the caller normally records it before dispatch. An adapter
137
152
  // that reports every send asks for it to be counted here instead, so that the first send is
138
153
  // logged where it actually happens rather than before admission could still refuse it.
154
+ targetSendsUsed += 1;
139
155
  if (send.ordinal <= 1 && options.includeFirst !== true) return;
140
156
  noteAttemptSend(logCtx.activeAttempt, inputTokens, send.recovery);
141
157
  };
@@ -191,21 +207,29 @@ export function createResponsesSendBudget(
191
207
  * The hop hands its reservation down through `pendingHopPermit`, the same seam the
192
208
  * passthrough ladder already uses, and this view spends it on the adapter's FIRST
193
209
  * reservation. Every later send in that ladder is a new physical send and is charged
194
- * normally. The refundable view below keeps an unused hop open until executor invocation;
195
- * its adapter release hands back an abandoned replay. The ordinary view retains early
196
- * settlement for existing callers.
210
+ * normally. Claiming preserves the refundable single-use booking until physical dispatch;
211
+ * an abandoned replay releases only that exact booking.
197
212
  */
198
- const claimHopPermit = (): SingleUseDispatchPermit | undefined => {
199
- const permit = pendingHopPermit;
200
- pendingHopPermit = undefined;
201
- return permit;
213
+ const permits = {
214
+ /** Transfer the initial booking once to the adapter dispatch owner. */
215
+ claimInitialPermit: () => {
216
+ const permit = initialPermit;
217
+ initialPermit = undefined;
218
+ return permit;
219
+ },
220
+ /** Transfer a pending recovery booking once, without creating a new allowance. */
221
+ claimHopPermit: () => {
222
+ const permit = pendingHopPermit;
223
+ pendingHopPermit = undefined;
224
+ return permit;
225
+ },
202
226
  };
203
- const adapterDispatchBudget: RequestExecutionBudget | undefined = adapterSendBudget === undefined
204
- ? undefined : adapterDispatchBudgetView(adapterSendBudget, { claimHopPermit });
205
- // Opt in only where the adapter's executor is the confirmation boundary. Ordinary callers
206
- // keep the existing early settlement contract, including already-settled permit fallback.
207
- const refundableAdapterDispatchBudget: RequestExecutionBudget | undefined = adapterSendBudget === undefined
208
- ? undefined : adapterDispatchBudgetView(adapterSendBudget, { claimHopPermit }, true);
227
+ const adapterDispatchBudget = adapterSendBudget === undefined
228
+ ? undefined : adapterDispatchBudgetView(adapterSendBudget, permits);
229
+ // Preserve the existing opted-in view for direct callers, including its admitted replacement
230
+ // of an already-settled hop. Exact-owned Combo and sidecar handoffs use the ordinary view.
231
+ const refundableAdapterDispatchBudget = adapterSendBudget === undefined
232
+ ? undefined : adapterDispatchBudgetView(adapterSendBudget, permits, true);
209
233
  /**
210
234
  * How many sends a recovery leg may make, and the permit that authorises the last one.
211
235
  *
@@ -292,12 +316,34 @@ export function createResponsesSendBudget(
292
316
  noteTransientSends,
293
317
  transientSendReporter,
294
318
  remainingTransientSendBudget,
319
+ /** Only this child's prepaid send, not every pending external booking, is spendable here. */
320
+ initialSendAllowance: (cap: number): number => Math.min(cap,
321
+ remainingTransientSendBudget(cap) + (initialPermit ? 1 : 0)),
322
+ /** At final executor admission, settle only the owning booking or reserve this send. */
323
+ noteInitialDispatch: (prepaid = initialPermit): void => {
324
+ if (prepaid) {
325
+ if (!prepaid.assumeCharge()) throw new SendBudgetExhaustedError();
326
+ if (prepaid === initialPermit) initialPermit = undefined;
327
+ } else if (adapterSendBudget && (options.comboInitialSend || options.compactionRecoveryPermit)) {
328
+ const decision = adapterSendBudget.reserveDispatch({ sendClass: "transient",
329
+ targetKey: adapterSendBudget.lastTargetKey ?? `${logCtx.provider}/${logCtx.model}` });
330
+ if (!decision.allowed || !decision.permit.use()) throw new SendBudgetExhaustedError();
331
+ } else {
332
+ // Legacy direct callers can prebook without handing over a permit; keep their reporter.
333
+ noteTransientSends(1);
334
+ return;
335
+ }
336
+ targetSendsUsed += 1;
337
+ options.onCompactionRecoverySendsReported?.(1);
338
+ chargeWorkflowSends(workflowRootId, 1);
339
+ },
340
+ /** Physical sends reported by this owner; unrelated shared reservations cannot consume it. */
341
+ get targetSendsUsed(): number { return targetSendsUsed; },
295
342
  /**
296
- * Physical sends this logical request has already made.
343
+ * Charged sends, including open prepaid reservations, across this logical request.
297
344
  *
298
- * A live getter, not a snapshot: it is read once per dispatch leg to resolve a configured
299
- * ladder, and a value frozen at construction would answer for a request that had sent
300
- * nothing.
345
+ * A live getter, not a completed-send snapshot. Initial ladders use the child's own
346
+ * prepaid allowance and target-local count; shared admission keeps every booking spent.
301
347
  */
302
348
  get sendsUsed(): number { return sendBudget.used; },
303
349
  adapterSendBudget,
@@ -335,8 +381,11 @@ export type ResponsesSendBudget = Exclude<ReturnType<typeof createResponsesSendB
335
381
  */
336
382
  function adapterDispatchBudgetView(
337
383
  budget: RequestExecutionBudget,
338
- hop: { claimHopPermit: () => SingleUseDispatchPermit | undefined },
339
- refundableHop = false,
384
+ hop: {
385
+ claimInitialPermit: () => SingleUseDispatchPermit | undefined;
386
+ claimHopPermit: () => SingleUseDispatchPermit | undefined;
387
+ },
388
+ legacyHopReplacement = false,
340
389
  ): RequestExecutionBudget {
341
390
  return {
342
391
  get physicalStarted() { return budget.physicalStarted; },
@@ -360,10 +409,22 @@ function adapterDispatchBudgetView(
360
409
  claimAmbiguousResend: (limit: number): boolean => budget.claimAmbiguousResend?.(limit) === true,
361
410
  get ambiguousResendSpent(): boolean { return budget.ambiguousResendSpent === true; },
362
411
  reserveDispatch(intent: DispatchIntent): DispatchDecision {
363
- const refundable = refundableHop || budget.spendEnforced === true;
364
412
  // A dispatch whose upstream state is unknown is refused on its own merits. A hop that
365
413
  // already paid does not make an unsafe replay safe, so that check stays with the budget.
366
414
  if (intent.replaySafe !== false) {
415
+ // The Combo reserves a logical provider/model identity. The child's first actual
416
+ // endpoint is not a recovery transition; retain the original refundable permit.
417
+ const initialPermit = hop.claimInitialPermit();
418
+ if (initialPermit) {
419
+ budget.bindPrepaidTarget?.(intent.targetKey);
420
+ return { allowed: true, permit: {
421
+ sendClass: initialPermit.sendClass,
422
+ execute: run => initialPermit.execute ? initialPermit.execute(run) : run(),
423
+ use: () => initialPermit.assumeCharge(),
424
+ assumeCharge: () => initialPermit.assumeCharge(),
425
+ release: () => initialPermit.release(),
426
+ } };
427
+ }
367
428
  const hopPermit = hop.claimHopPermit();
368
429
  if (hopPermit && intent.targetKey !== budget.lastTargetKey) {
369
430
  // The rotated credential can select a different regional endpoint. Replace the
@@ -372,35 +433,37 @@ function adapterDispatchBudgetView(
372
433
  hopPermit.release();
373
434
  return budget.reserveDispatch({ ...intent, sendClass: hopPermit.sendClass });
374
435
  }
375
- if (hopPermit !== undefined && (refundable || hopPermit.assumeCharge())) {
376
- let settled = false;
377
- let executionPermit = hopPermit;
378
- const confirm = (): boolean => {
379
- if (settled) return false;
380
- settled = true;
381
- // Admission can still reject this replay. Close its external booking only when
382
- // the adapter invokes its executor, leaving release() able to refund it until then.
383
- if (!refundable || hopPermit.assumeCharge()) return true;
384
- // Preserve the old fallback for a permit another leg already settled.
385
- const fresh = budget.reserveDispatch(intent);
386
- if (!fresh.allowed) return false;
387
- executionPermit = fresh.permit;
388
- return fresh.permit.assumeCharge();
389
- };
390
- return {
391
- allowed: true,
392
- permit: {
436
+ // Reservation precedes pacing and beforeDispatch. Transfer the exact open booking;
437
+ // only physical consumption closes external reporting and makes it non-refundable.
438
+ // The underlying permit enforces single use, including settlement by another owner.
439
+ if (hopPermit !== undefined) {
440
+ if (legacyHopReplacement) {
441
+ let settled = false;
442
+ let executionPermit = hopPermit;
443
+ const confirm = (): boolean => {
444
+ if (settled) return false;
445
+ settled = true;
446
+ if (hopPermit.assumeCharge()) return true;
447
+ // Preserve the old fallback for a permit another leg already settled.
448
+ const fresh = budget.reserveDispatch(intent);
449
+ if (!fresh.allowed) return false;
450
+ executionPermit = fresh.permit;
451
+ return fresh.permit.assumeCharge();
452
+ };
453
+ return { allowed: true, permit: {
393
454
  sendClass: hopPermit.sendClass,
394
455
  execute: run => executionPermit.execute ? executionPermit.execute(run) : run(),
395
- use: confirm,
396
- assumeCharge: confirm,
397
- release: (): void => {
398
- if (!refundable || settled) return;
399
- settled = true;
400
- hopPermit.release();
401
- },
402
- },
403
- };
456
+ use: confirm, assumeCharge: confirm,
457
+ release: () => { if (!settled) { settled = true; hopPermit.release(); } },
458
+ } };
459
+ }
460
+ return { allowed: true, permit: {
461
+ sendClass: hopPermit.sendClass,
462
+ execute: run => hopPermit.execute ? hopPermit.execute(run) : run(),
463
+ use: () => hopPermit.assumeCharge(),
464
+ assumeCharge: () => hopPermit.assumeCharge(),
465
+ release: () => hopPermit.release(),
466
+ } };
404
467
  }
405
468
  }
406
469
  return budget.reserveDispatch(intent);
@@ -53,6 +53,9 @@ import { classifyModelFamilyForQuota } from "../../oauth/account-quota-rank";
53
53
  import { expandInferenceOAuthSendBudget } from "../inference/context";
54
54
  import { stampOAuthAccountLabel, usesApiKeyAccount } from "../../providers/label";
55
55
  import { resolveProviderTransport } from "../../providers/xai-transport";
56
+ import { captureRouteStaticPolicy } from "../../router";
57
+ import { CopilotAutoHttpError, resolveCopilotAuto } from "../../providers/github-copilot-auto";
58
+ import { renameRoutedIdentityInContext } from "../../adapters/identity";
56
59
  import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
57
60
  import {
58
61
  providerApiKeySelectionIsCurrent,
@@ -87,6 +90,13 @@ import { resolvePassiveRouteSubjectId } from "../passive-route-linker";
87
90
  import { clientCancelledResponse } from "./core-errors";
88
91
  import { acquireAccountLease, KIRO_ACCOUNT_WAIT_MS } from "../../oauth/kiro-account-load";
89
92
 
93
+ /** Return a renewed Chat wire to the native owner before sending inference. */
94
+ export class CopilotAutoAdapterHandoff extends Error {
95
+ constructor(readonly adapter: ProviderAdapter) {
96
+ super("Copilot Auto dispatch requires the Chat adapter");
97
+ }
98
+ }
99
+
90
100
  /** Owns live credential selection and adapter bindings for one request. */
91
101
  export async function prepareResponsesTransport(
92
102
  requestContext: Pick<ResponsesRequestContext, "config" | "logCtx" | "options" | "req">,
@@ -160,6 +170,8 @@ export async function prepareResponsesTransport(
160
170
  || route.providerName === "devin"
161
171
  ) && route.provider.authMode === "oauth";
162
172
  let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
173
+ // One inference-401 replay belongs to this request, even when Auto changes exchange wire.
174
+ let oauth401ReplayAttempted = false;
163
175
  let replayOAuthCredentialSnapshot: Pick<OAuthAccessSnapshot, "accountId" | "generation"> | undefined;
164
176
  let anthropicPoolAccountId: string | null = null;
165
177
  let anthropicPoolFailovers = 0;
@@ -281,6 +293,84 @@ export async function prepareResponsesTransport(
281
293
  * the case the fence exists for. Stays 0 for every provider without a passive quota.
282
294
  */
283
295
  let passiveQuotaWriterGeneration = 0;
296
+ const copilotRequestedModel = route.modelId;
297
+ /** Project only bounded Copilot refusal metadata; upstream bodies and session tokens never reach the client. */
298
+ const copilotRefusalResponse = (error: unknown): Response | undefined => {
299
+ if (!(error instanceof CopilotAutoHttpError) || ![401, 403, 429].includes(error.status)) return;
300
+ const response = formatErrorResponse(error.status, error.status === 429 ? "rate_limit_error" : "authentication_error",
301
+ error.message);
302
+ if (error.retryAfter) response.headers.set("retry-after", error.retryAfter);
303
+ return response;
304
+ };
305
+ let copilotSessionExpiresAt = Infinity;
306
+ let copilotNegotiationRefreshed = false;
307
+ let copilotAutoAdapter: OcxProviderConfig["adapter"] | undefined;
308
+ /** The operator-configured wire, captured before the first Auto negotiation can rewrite route.provider. */
309
+ let copilotConfiguredWire: { adapter: OcxProviderConfig["adapter"]; responsesPath?: string;
310
+ chatCompletionsPath?: string; apiVersion?: string } | undefined;
311
+ /** Start every negotiation from the configured wire, so a previous credential's Auto adapter,
312
+ * endpoint and session headers never carry over to a credential that keeps the named model. */
313
+ const copilotNegotiationBase = (provider: OcxProviderConfig): OcxProviderConfig => {
314
+ if (!copilotConfiguredWire) return provider;
315
+ const headers = new Headers(provider.headers);
316
+ headers.delete("Copilot-Session-Token");
317
+ if (copilotConfiguredWire.apiVersion === undefined) headers.delete("X-GitHub-Api-Version");
318
+ else headers.set("X-GitHub-Api-Version", copilotConfiguredWire.apiVersion);
319
+ const base: OcxProviderConfig = { ...provider, adapter: copilotConfiguredWire.adapter,
320
+ headers: Object.fromEntries(headers) };
321
+ if (copilotConfiguredWire.responsesPath === undefined) delete base.responsesPath;
322
+ else base.responsesPath = copilotConfiguredWire.responsesPath;
323
+ if (copilotConfiguredWire.chatCompletionsPath === undefined) delete base.chatCompletionsPath;
324
+ else base.chatCompletionsPath = copilotConfiguredWire.chatCompletionsPath;
325
+ return base;
326
+ };
327
+ /** Resolve the request's ephemeral wire selection and recover at most one OAuth negotiation 401. */
328
+ const resolveCopilotSelection = async (requestParsed: OcxParsedRequest, insideDispatch = false): Promise<void> => {
329
+ if (route.providerName !== "github-copilot" || route.provider.authMode === "forward") return;
330
+ copilotConfiguredWire ??= {
331
+ adapter: route.provider.adapter,
332
+ responsesPath: route.provider.responsesPath,
333
+ chatCompletionsPath: route.provider.chatCompletionsPath,
334
+ apiVersion: new Headers(route.provider.headers).get("X-GitHub-Api-Version") ?? undefined,
335
+ };
336
+ /** Fence control-plane sends to the captured credential; reuse an already-held dispatch concurrency lease. */
337
+ const negotiate = () => {
338
+ const binding: DispatchBinding | undefined = oauthSelection && servingOAuthSnapshot
339
+ ? { kind: "oauth", selection: { ...oauthSelection }, snapshot: servingOAuthSnapshot }
340
+ : { kind: "api-key", provider: { ...route.provider } };
341
+ return resolveCopilotAuto(copilotNegotiationBase(route.provider), copilotRequestedModel, requestParsed,
342
+ options.abortSignal ?? req.signal, /** Fence each negotiation send against the captured credential binding before touching the network. */
343
+ () => selectionIsCurrent(binding), !insideDispatch);
344
+ };
345
+ let selected: Awaited<ReturnType<typeof resolveCopilotAuto>>;
346
+ try { selected = await negotiate(); }
347
+ catch (error) {
348
+ if (!(error instanceof CopilotAutoHttpError) || error.status !== 401 || copilotNegotiationRefreshed
349
+ || oauth401ReplayAttempted || route.provider.authMode !== "oauth" || !servingOAuthSnapshot) throw error;
350
+ copilotNegotiationRefreshed = true;
351
+ const refreshed = await refreshResolvedOAuthSelection(servingOAuthSnapshot);
352
+ route.provider = resolveProviderTransport(route.providerName,
353
+ { ...route.provider, apiKey: refreshed.accessToken }, parsed.options.promptCacheKey,
354
+ resolveCopilotApiBaseUrl(refreshed.apiBaseUrl));
355
+ sentOAuthSnapshot = refreshed;
356
+ replayOAuthCredentialSnapshot = { accountId: refreshed.accountId, generation: refreshed.generation };
357
+ selected = await negotiate();
358
+ }
359
+ copilotSessionExpiresAt = selected.expiresAt ?? Infinity;
360
+ route.provider = selected.provider;
361
+ route.modelId = selected.modelId;
362
+ route.staticPolicy = captureRouteStaticPolicy(route.providerName, route.modelId, route.provider,
363
+ route.staticPolicy.effectiveAlias, inboundWire);
364
+ copilotAutoAdapter = selected.auto ? selected.provider.adapter : undefined;
365
+ for (const target of new Set([parsed, requestParsed])) {
366
+ target.modelId = selected.modelId;
367
+ if (target._rawBody && typeof target._rawBody === "object")
368
+ (target._rawBody as { model?: string }).model = selected.modelId;
369
+ target.context = renameRoutedIdentityInContext(target.context, selected.modelId);
370
+ }
371
+ logCtx.model = selected.modelId;
372
+ if (logCtx.activeAttempt) logCtx.activeAttempt.model = selected.modelId;
373
+ };
284
374
  /**
285
375
  * Apply a rotated account's FULL credential snapshot to the live route (#2568d).
286
376
  *
@@ -306,6 +396,7 @@ export async function prepareResponsesTransport(
306
396
  const applyFailoverSnapshot = async (
307
397
  snapshot: OAuthAccessSnapshot,
308
398
  retryParsed: OcxParsedRequest = parsed,
399
+ insideDispatch = false,
309
400
  ): Promise<OAuthAccessSnapshot | null> => {
310
401
  requireAnthropicAdmission();
311
402
  if (snapshot.provider !== route.providerName) return null;
@@ -342,6 +433,9 @@ export async function prepareResponsesTransport(
342
433
  }
343
434
  if (snapshot.projectId) rotatedProvider = { ...rotatedProvider, project: snapshot.projectId };
344
435
  route.provider = rotatedProvider;
436
+ await resolveCopilotSelection(retryParsed, insideDispatch);
437
+ // Negotiation itself may have recovered a 401 with a newer committed credential.
438
+ if (route.providerName === "github-copilot" && servingOAuthSnapshot) snapshot = servingOAuthSnapshot;
345
439
  if (route.providerName === "kiro") {
346
440
  const kiroContext = { ...(snapshot.kiro ?? {}) };
347
441
  // Terminal-guard continuations are rebuilt from a shallow clone. Updating only the
@@ -411,7 +505,9 @@ export async function prepareResponsesTransport(
411
505
  && (anthropicInstance === undefined || !anthropicRouting!.getAnthropicAccountHealthSnapshot(binding.snapshot.accountId) && !anthropicRatePolicy!.anthropicRatePauseUntil(binding.snapshot.accountId) && !anthropicQuota!.anthropicFamilyRejected(binding.snapshot.accountId, route.modelId))
412
506
  && credentialGeneration(row.credential) === binding.snapshot.generation;
413
507
  };
508
+ /** Bind the negotiated wire adapter and its built requests to the serving credential's authority. */
414
509
  const resolveSelectionAdapter = (provider: OcxProviderConfig, retention = config.cacheRetention): ProviderAdapter => {
510
+ if (copilotAutoAdapter) provider = { ...provider, adapter: copilotAutoAdapter };
415
511
  const resolved = resolveAdapter(provider, retention, route.providerName);
416
512
  if (route.provider.authMode === "forward") return resolved;
417
513
  const binding: DispatchBinding | undefined = route.provider.authMode === "oauth"
@@ -468,6 +564,7 @@ export async function prepareResponsesTransport(
468
564
  }
469
565
  return resolved;
470
566
  };
567
+ /** Rebuild a stale queued selection while retaining the caller's dispatch lease and renewing Auto metadata. */
471
568
  const refreshDispatchAdapter = async (requestParsed: OcxParsedRequest): Promise<ProviderAdapter> => {
472
569
  requireAnthropicAdmission();
473
570
  if (route.provider.authMode === "oauth") {
@@ -479,7 +576,7 @@ export async function prepareResponsesTransport(
479
576
  const accountId = await anthropicRouting!.resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision, route.modelId);
480
577
  candidate = await anthropicRouting!.getAnthropicPoolAccessSnapshot(accountId);
481
578
  }
482
- if (!candidate || !await applyFailoverSnapshot(candidate, requestParsed)) {
579
+ if (!candidate || !await applyFailoverSnapshot(candidate, requestParsed, true)) {
483
580
  if (anthropicInstance !== undefined) await anthropicRouting!.resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision, route.modelId);
484
581
  throw new Error("OAuth account selection changed before dispatch");
485
582
  }
@@ -487,6 +584,7 @@ export async function prepareResponsesTransport(
487
584
  const current = resolveCurrentProviderApiKeyTransport(config, route.providerName, route.provider);
488
585
  if (!current) throw new Error("API key selection is unavailable before dispatch");
489
586
  route.provider = current;
587
+ await resolveCopilotSelection(requestParsed, true);
490
588
  }
491
589
  adapter = activeAdapter = runTurnAdapter = resolveSelectionAdapter(
492
590
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
@@ -552,13 +650,14 @@ export async function prepareResponsesTransport(
552
650
  throw new Error("Account selection changed repeatedly before turn dispatch");
553
651
  } finally { producer?.close(); }
554
652
  };
555
- const oauthDispatch = (wireRequest: AdapterRequest, requestParsed = parsed): ProviderFetchOptions["dispatchOverride"] => {
653
+ /** Fence the physical send, with bounded credential/session rebuilds and safe negotiated refusals. */
654
+ const oauthDispatch = (wireRequest: AdapterRequest, requestParsed = parsed, owner: "adapter" | "passthrough" = "adapter"): ProviderFetchOptions["dispatchOverride"] => {
556
655
  if (route.provider.authMode === "forward") return undefined;
557
- return async (input, init, execute) => {
656
+ return /** Dispatch with the current binding and session, rebuilding at most three times before returning a safe refusal. */ async (input, init, execute) => {
558
657
  let destination = input;
559
658
  let dispatchInit = init;
560
659
  for (let attempt = 0; attempt < 3; attempt++) {
561
- if (selectionIsCurrent(requestBindings.get(wireRequest))) {
660
+ if (selectionIsCurrent(requestBindings.get(wireRequest)) && copilotSessionExpiresAt > Date.now() + 1000) {
562
661
  const fetchImpl = (route.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? execute;
563
662
  const binding = requestBindings.get(wireRequest);
564
663
  const snapshot = anthropicInstance !== undefined && binding?.kind === "oauth"
@@ -622,7 +721,16 @@ export async function prepareResponsesTransport(
622
721
  if (!response.ok) await recordKeyAttemptFailure(logCtx, response, dispatchInit.signal ?? options.abortSignal);
623
722
  return response;
624
723
  }
625
- const nextAdapter = await refreshDispatchAdapter(requestParsed);
724
+ let nextAdapter: ProviderAdapter;
725
+ try { nextAdapter = await refreshDispatchAdapter(requestParsed); }
726
+ catch (error) {
727
+ const refusal = copilotRefusalResponse(error);
728
+ if (refusal) return refusal;
729
+ throw error;
730
+ }
731
+ if (owner === "passthrough" && route.providerName === "github-copilot" && nextAdapter.name === "openai-chat") {
732
+ throw new CopilotAutoAdapterHandoff(nextAdapter);
733
+ }
626
734
  // Rebind before rebuilding: the rebuild's bridged-search restore and continuation
627
735
  // restore key on the serving identity, which must be the refreshed route's, not the
628
736
  // credential whose selection just lapsed.
@@ -876,7 +984,15 @@ export async function prepareResponsesTransport(
876
984
  ? resolveCopilotApiBaseUrl(sentOAuthSnapshot?.apiBaseUrl)
877
985
  : undefined,
878
986
  );
987
+ try { await resolveCopilotSelection(parsed); }
988
+ catch (error) {
989
+ if ((options.abortSignal ?? req.signal).aborted) return clientCancelledResponse();
990
+ const refusal = copilotRefusalResponse(error);
991
+ if (refusal) return refusal;
992
+ return formatErrorResponse(502, "provider_error", "GitHub Copilot Auto negotiation failed; retry or check your Copilot account permissions.");
993
+ }
879
994
  let adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy);
995
+ if (copilotAutoAdapter) adapterProvider = { ...adapterProvider, adapter: copilotAutoAdapter };
880
996
  const stripClaudeMainAuth = options.stripClaudeMainAuthForNoncanonicalForward === true
881
997
  && !isCanonicalOpenAiForwardProvider(adapterProvider)
882
998
  && ((adapterProvider.adapter === "openai-responses" && adapterProvider.authMode === "forward")
@@ -1008,6 +1124,8 @@ export async function prepareResponsesTransport(
1008
1124
  set genericFailovers(value: typeof genericFailovers) {
1009
1125
  genericFailovers = value;
1010
1126
  },
1127
+ get oauth401ReplayAttempted(): boolean { return oauth401ReplayAttempted; },
1128
+ set oauth401ReplayAttempted(value: boolean) { oauth401ReplayAttempted = value; },
1011
1129
  get adapter(): ProviderAdapter {
1012
1130
  return adapter;
1013
1131
  },
@@ -1065,6 +1183,8 @@ export async function prepareResponsesTransport(
1065
1183
  applyFailoverSnapshot,
1066
1184
  selectionIsCurrent,
1067
1185
  resolveSelectionAdapter,
1186
+ resolveCopilotSelection,
1187
+ copilotRefusalResponse,
1068
1188
  refreshRunTurnAdapter,
1069
1189
  oauthDispatch,
1070
1190
  noteRoutedAttemptSend,
@@ -75,6 +75,7 @@ export function createResponsesEffects(
75
75
  // message, and leave Codex fataling on a missing compaction item (#422).
76
76
  const commitReasoningReplayServingRoute = (outboundHeaders?: HeadersInit): void => {
77
77
  commitReasoningReplayServingIdentity(parsed._reasoningReplayScope);
78
+ if (parsed._nativeReasoningMint) parsed._nativeReasoningMint.owner = parsed._nativeReasoningBoundOwner;
78
79
  rememberServingConversationStateIssuer(admissionState.authCtx, poolAffinityKey);
79
80
  // History has no model namespace. Record the account that actually accepted this
80
81
  // final attempt, after refresh/failover, rather than guessing from mutable affinity.
@@ -1,3 +1,4 @@
1
+ import { mapSpendLedgerStorageError } from "./spend-storage-error";
1
2
  import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
2
3
  import type { PreparedResponsesRequest } from "./request-prepare";
3
4
  import type { ResponsesTransport } from "./request-transport";
@@ -20,7 +21,7 @@ import type { AttemptRecoveryKind } from "../../usage/log";
20
21
  import { providerFetch } from "./fetch-helpers";
21
22
  import { normalizeLogConversationId } from "../request-log-conversation";
22
23
  import { normalizeDeclaredToolName, type AdapterEvent, type OcxProviderContinuationState } from "../../types";
23
- import { adapterFailureFromMessage, SEND_BUDGET_EXHAUSTED_CODE } from "../../lib/errors";
24
+ import { adapterFailureFromMessage, SEND_BUDGET_EXHAUSTED_CODE, SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE, type OcxErrorPayload } from "../../lib/errors";
24
25
  import { SendBudgetExhaustedError, markResponseNonReplayable } from "../../lib/upstream-retry";
25
26
  import {
26
27
  hasEligibleGenericOAuthFailoverTarget,
@@ -235,6 +236,7 @@ export async function executeResponsesRunTurn(
235
236
  preacquiredSlot?: ProviderRequestSlot,
236
237
  ): Promise<void> => {
237
238
  const attemptSeq = ++runTurnAttemptSeq;
239
+ if (attemptSeq === 1 && options.comboInitialSend) options.comboInitialSend.producerOwned = true;
238
240
  let pacingSlot = preacquiredSlot;
239
241
  const emit = (event: AdapterEvent) => {
240
242
  options.onCompactionRecoveryAdapterEvent?.(event);
@@ -314,7 +316,8 @@ export async function executeResponsesRunTurn(
314
316
  Object.assign(parsed, routeState);
315
317
  }
316
318
  } catch (err) {
317
- emit(err instanceof RequestPacingQueueOverloadError
319
+ const storageRefusal = mapSpendLedgerStorageError(err, logCtx);
320
+ emit(storageRefusal ? { type: "error", ...storageRefusal } : err instanceof RequestPacingQueueOverloadError
318
321
  ? {
319
322
  type: "error",
320
323
  status: 429,
@@ -339,6 +342,7 @@ export async function executeResponsesRunTurn(
339
342
  message: err instanceof Error ? err.message : String(err),
340
343
  });
341
344
  } finally {
345
+ if (attemptSeq === 1) options.comboInitialSend?.permit.release();
342
346
  devinProducers.delete(producerAbort);
343
347
  cleanupProducerAbort?.();
344
348
  releaseProviderRequestSlot(pacingSlot);
@@ -711,7 +715,8 @@ export async function executeResponsesRunTurn(
711
715
  runTurnAbort.abort();
712
716
  queue.close();
713
717
  const message = preflight.error?.message ?? "Adapter ended before producing a response";
714
- const failure = formatErrorResponse(502, "upstream_error", redactSecretString(message));
718
+ const failure = formatErrorResponse(preflight.error?.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE ? 503 : 502,
719
+ preflight.error?.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE ? SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE : "upstream_error", redactSecretString(message));
715
720
  // A replay-unsafe heartbeat means the adapter already ran a local side effect, so the
716
721
  // combo must not send this turn to another target: the failure stays with this child.
717
722
  if (preflight.replayUnsafe) markResponseNonReplayable(failure);
@@ -910,7 +915,9 @@ export async function executeResponsesRunTurn(
910
915
  const message = classifiedError?.message ?? (firstMeaningful?.type === "error"
911
916
  ? firstMeaningful.message
912
917
  : "Adapter ended before producing a response");
913
- const failure = formatErrorResponse(502, "upstream_error", redactSecretString(message));
918
+ const storageRefusal = firstMeaningful?.type === "error" && firstMeaningful.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE;
919
+ const failure = formatErrorResponse(storageRefusal ? 503 : 502,
920
+ storageRefusal ? SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE : "upstream_error", redactSecretString(message));
914
921
  if (replayUnsafe) markResponseNonReplayable(failure);
915
922
  return failure;
916
923
  }
@@ -954,7 +961,10 @@ export async function executeResponsesRunTurn(
954
961
  commitReasoningReplayServingRoute();
955
962
  }
956
963
  notifyResponseComplete(json);
957
- return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
964
+ return new Response(JSON.stringify(json), {
965
+ status: (json.error as OcxErrorPayload | undefined)?.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE ? 503 : 200,
966
+ headers: { "Content-Type": "application/json" },
967
+ });
958
968
  } catch (error) {
959
969
  if (!isTranslatorBudgetExceededError(error)) throw error;
960
970
  runTurnAbort.abort();