@bitkyc08/opencodex 2.67.0 → 2.68.0-preview.20260927

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 (257) hide show
  1. package/bin/ocx.mjs +3 -0
  2. package/gui/dist/assets/App-B5-cAEd2.js +51 -0
  3. package/gui/dist/assets/App-DCBismRi.css +1 -0
  4. package/gui/dist/assets/Tray-DFnMiyD0.js +1 -0
  5. package/gui/dist/assets/{index-DtNmX7hW.css → index-BDUBS8PW.css} +1 -1
  6. package/gui/dist/assets/index-BcXblvem.js +86 -0
  7. package/gui/dist/assets/quota-summary-i84eOU3o.js +1 -0
  8. package/gui/dist/index.html +21 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic-image-guard.ts +13 -1
  11. package/src/adapters/anthropic-image-normalize.ts +3 -2
  12. package/src/adapters/anthropic-output-schema.ts +36 -0
  13. package/src/adapters/anthropic.ts +10 -2
  14. package/src/adapters/base.ts +7 -0
  15. package/src/adapters/codebuddy/live-models.ts +180 -0
  16. package/src/adapters/coding-agent/protocol.ts +91 -12
  17. package/src/adapters/coding-agent/turn.ts +32 -27
  18. package/src/adapters/devin/cloud-direct/index.ts +1 -0
  19. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +18 -4
  20. package/src/adapters/devin.ts +9 -6
  21. package/src/adapters/google-http.ts +4 -4
  22. package/src/adapters/google.ts +93 -3
  23. package/src/adapters/kiro/adapter.ts +6 -2
  24. package/src/adapters/kiro/stream.ts +12 -2
  25. package/src/adapters/kiro/usage.ts +3 -1
  26. package/src/adapters/kiro-errors.ts +11 -1
  27. package/src/adapters/kiro-events.ts +27 -1
  28. package/src/adapters/kiro-refusal.ts +30 -0
  29. package/src/adapters/kiro-retry.ts +71 -31
  30. package/src/adapters/openai-chat/deepseek-artifact-schema.ts +45 -0
  31. package/src/adapters/openai-chat/messages.ts +31 -4
  32. package/src/adapters/openai-chat/serialized-tool-call-content.ts +52 -7
  33. package/src/adapters/openai-chat/tool-call-id-remint.ts +65 -0
  34. package/src/adapters/openai-chat/tool-schema.ts +6 -2
  35. package/src/adapters/openai-chat.ts +2 -2
  36. package/src/adapters/openai-responses/muse-tool-choice.ts +31 -0
  37. package/src/adapters/openai-responses/passthrough.ts +9 -3
  38. package/src/adapters/opencode-go-additional-tools.ts +19 -10
  39. package/src/adapters/physical-send.ts +10 -3
  40. package/src/adapters/registry.ts +2 -1
  41. package/src/adapters/responses-tool-schema.ts +31 -3
  42. package/src/adapters/run-turn-queue.ts +63 -23
  43. package/src/adapters/unique-tool-call-ids.ts +63 -0
  44. package/src/adapters/xai-web-search.ts +32 -2
  45. package/src/bridge/sse.ts +14 -2
  46. package/src/chat/inbound.ts +20 -6
  47. package/src/claude/agents-inject.ts +4 -0
  48. package/src/claude/desktop-3p.ts +53 -13
  49. package/src/claude/desktop-profile.ts +41 -4
  50. package/src/claude/inbound-model-options.ts +14 -2
  51. package/src/claude/inbound.ts +1 -1
  52. package/src/claude/intercept/connect-proxy.ts +17 -1
  53. package/src/claude/intercept/local-ca.ts +7 -1
  54. package/src/claude/message-threads.ts +28 -0
  55. package/src/cli/account-api.ts +13 -0
  56. package/src/cli/account-auth.ts +49 -2
  57. package/src/cli/account-extended.ts +4 -2
  58. package/src/cli/account.ts +4 -2
  59. package/src/cli/capabilities.ts +17 -0
  60. package/src/cli/claude.ts +3 -1
  61. package/src/cli/dispatch.ts +62 -5
  62. package/src/cli/index.ts +84 -79
  63. package/src/cli/minimax.ts +4 -2
  64. package/src/cli/opencode.ts +6 -1
  65. package/src/cli/restart-handoff.ts +110 -0
  66. package/src/cli/status.ts +51 -0
  67. package/src/client/connect.ts +34 -15
  68. package/src/client/link-ingress.ts +102 -0
  69. package/src/client/link-join.ts +37 -16
  70. package/src/client/link-relay.ts +228 -45
  71. package/src/client/link-state.ts +54 -0
  72. package/src/client/link-status.ts +39 -0
  73. package/src/client/link-teardown.ts +2 -2
  74. package/src/client/link-tunnel.ts +615 -86
  75. package/src/client/machine-api.ts +10 -4
  76. package/src/client/machine-listener.ts +57 -10
  77. package/src/client/runtime.ts +210 -35
  78. package/src/clients/config-export/constants.ts +1 -12
  79. package/src/clients/config-export/contracts.ts +2 -0
  80. package/src/clients/config-export/model-metadata.ts +9 -3
  81. package/src/clients/config-export/omp.ts +1 -1
  82. package/src/clients/config-export/zcode-store.ts +2 -2
  83. package/src/clients/config-export.ts +36 -3
  84. package/src/codex/app-server-processes.ts +32 -0
  85. package/src/codex/app-server-restart-service.ts +20 -3
  86. package/src/codex/catalog/access-programs.ts +50 -0
  87. package/src/codex/catalog/build-entries.ts +13 -0
  88. package/src/codex/catalog/derive-entry.ts +1 -0
  89. package/src/codex/catalog/model-hints.ts +11 -0
  90. package/src/codex/catalog/parsing.ts +6 -2
  91. package/src/codex/catalog/provider-models.ts +81 -0
  92. package/src/codex/catalog/retained-sync.ts +3 -0
  93. package/src/codex/codex-write-lock.ts +2 -2
  94. package/src/codex/convergence.ts +2 -0
  95. package/src/codex/desired-state.ts +14 -4
  96. package/src/codex/home.ts +21 -3
  97. package/src/codex/inject/restore.ts +17 -0
  98. package/src/codex/inject/routing-classify.ts +3 -2
  99. package/src/codex/inject-coordination.ts +4 -1
  100. package/src/codex/inject.ts +8 -1
  101. package/src/codex/journal.ts +6 -2
  102. package/src/codex/management-convergence.ts +9 -0
  103. package/src/codex/model-entitlements.ts +28 -5
  104. package/src/codex/routing/idle-window.ts +58 -0
  105. package/src/codex/routing-drift.ts +134 -0
  106. package/src/codex/routing-healer.ts +419 -0
  107. package/src/codex/routing.ts +10 -0
  108. package/src/codex/runtime.ts +188 -57
  109. package/src/codex/sibling-handoff.ts +71 -0
  110. package/src/codex/sibling-start.ts +139 -0
  111. package/src/codex/sync.ts +5 -3
  112. package/src/combos/failover.ts +32 -5
  113. package/src/combos/request.ts +11 -3
  114. package/src/combos/reset-window.ts +10 -5
  115. package/src/combos/resolve.ts +36 -6
  116. package/src/config/diagnostics.ts +2 -1
  117. package/src/config/load-degrade.ts +2 -0
  118. package/src/config/paths.ts +7 -1
  119. package/src/config/process-state.ts +10 -1
  120. package/src/config/schema/compaction-recovery.ts +15 -0
  121. package/src/config/schema/config-schema.ts +2 -0
  122. package/src/config/schema/leaf-validators.ts +9 -3
  123. package/src/generated/compatibility-version.json +360 -204
  124. package/src/images/loop.ts +47 -32
  125. package/src/integrations/catalog-refresh.ts +4 -0
  126. package/src/integrations/omp-yaml-source.ts +1 -0
  127. package/src/lib/claude-request-projection.ts +101 -0
  128. package/src/lib/local-account-switch-capability.ts +48 -0
  129. package/src/lib/local-upstream.ts +146 -0
  130. package/src/lib/package-tree-integrity.ts +25 -3
  131. package/src/lib/package-tree-retarget.ts +130 -0
  132. package/src/lib/process-control.ts +4 -1
  133. package/src/lib/system-restart-contract.ts +106 -0
  134. package/src/lib/upstream-retry.ts +57 -1
  135. package/src/link/ports.ts +9 -0
  136. package/src/link/ssh-argv.ts +16 -0
  137. package/src/link/ssh-runner.ts +70 -2
  138. package/src/link/tunnel-state.ts +58 -9
  139. package/src/oauth/account-quota-rank.ts +29 -18
  140. package/src/oauth/generic-account-failover.ts +179 -22
  141. package/src/oauth/index.ts +17 -4
  142. package/src/oauth/kiro-account-load.ts +107 -0
  143. package/src/oauth/kiro-device-login.ts +312 -0
  144. package/src/oauth/kiro-terminal-failover.ts +28 -0
  145. package/src/oauth/login-flow-state.ts +7 -1
  146. package/src/oauth/pool-settings-capability.ts +23 -14
  147. package/src/oauth/store.ts +56 -3
  148. package/src/oauth/types.ts +4 -0
  149. package/src/plugins/loader.ts +350 -0
  150. package/src/plugins/upstream-hooks.ts +119 -0
  151. package/src/protocols/encoders/adapter-events.ts +9 -2
  152. package/src/providers/account-quota-disk.ts +42 -4
  153. package/src/providers/codebuddy-models.ts +6 -2
  154. package/src/providers/command-code-efforts.ts +33 -10
  155. package/src/providers/kiro-account-state-disk.ts +44 -0
  156. package/src/providers/kiro-model-catalog.ts +162 -0
  157. package/src/providers/kiro-models.ts +5 -4
  158. package/src/providers/kiro-quota-metrics.ts +35 -0
  159. package/src/providers/kiro-usage.ts +118 -14
  160. package/src/providers/quota/account-cache.ts +43 -7
  161. package/src/providers/quota/antigravity.ts +8 -3
  162. package/src/providers/quota/kiro-account-probe.ts +12 -0
  163. package/src/providers/quota/vendor-probes-key.ts +31 -26
  164. package/src/providers/quota/vendor-probes-oauth.ts +16 -7
  165. package/src/providers/quota-types.ts +14 -0
  166. package/src/providers/quota.ts +21 -21
  167. package/src/providers/registry/entries-extended.ts +5 -3
  168. package/src/providers/request-pacing.ts +410 -12
  169. package/src/remote-control/workspace-codex-runtime.ts +4 -3
  170. package/src/responses/citation-markers.ts +132 -65
  171. package/src/responses/hosted-tool-policy.ts +14 -3
  172. package/src/responses/parser-content.ts +3 -2
  173. package/src/responses/parser.ts +4 -1
  174. package/src/responses/schema.ts +4 -0
  175. package/src/responses/visualization-directives.ts +182 -0
  176. package/src/server/chat-native-sse.ts +24 -12
  177. package/src/server/chat-native.ts +137 -68
  178. package/src/server/claude-messages.ts +147 -12
  179. package/src/server/index/link-listener.ts +7 -0
  180. package/src/server/index/optional-listeners.ts +4 -1
  181. package/src/server/index/package-tree-guard.ts +35 -16
  182. package/src/server/index/serve-options.ts +4 -2
  183. package/src/server/index/startup-warnings.ts +6 -0
  184. package/src/server/index.ts +7 -7
  185. package/src/server/inference/client-encoder-delivery.ts +3 -0
  186. package/src/server/inference/context.ts +30 -2
  187. package/src/server/local-account-switch-auth.ts +79 -0
  188. package/src/server/management/agent-settings-routes.ts +11 -3
  189. package/src/server/management/config-routes.ts +28 -11
  190. package/src/server/management/context.ts +2 -0
  191. package/src/server/management/link-routes.ts +123 -39
  192. package/src/server/management/logs-usage-routes.ts +9 -42
  193. package/src/server/management/model-rows.ts +1 -0
  194. package/src/server/management/oauth-account-routes.ts +67 -12
  195. package/src/server/management/route-registry.ts +5 -5
  196. package/src/server/management/sibling-guard.ts +60 -0
  197. package/src/server/management/storage-log-guard-routes.ts +72 -6
  198. package/src/server/management/system-restart.ts +92 -135
  199. package/src/server/management/system-routes.ts +1 -1
  200. package/src/server/management-api.ts +31 -3
  201. package/src/server/management-auth.ts +3 -0
  202. package/src/server/port-reclaim.ts +111 -1
  203. package/src/server/proxy-liveness.ts +16 -0
  204. package/src/server/relay.ts +16 -15
  205. package/src/server/request-log-filter.ts +28 -27
  206. package/src/server/request-log.ts +7 -3
  207. package/src/server/request-metrics.ts +28 -0
  208. package/src/server/responses/adapter-continuation.ts +151 -21
  209. package/src/server/responses/adapter-delivery.ts +21 -7
  210. package/src/server/responses/adapter-dispatch.ts +239 -51
  211. package/src/server/responses/codex-ws-pool.ts +5 -4
  212. package/src/server/responses/codex-ws-request.ts +4 -1
  213. package/src/server/responses/compact.ts +8 -4
  214. package/src/server/responses/compaction-recovery-policy.ts +116 -0
  215. package/src/server/responses/compaction-recovery.ts +330 -0
  216. package/src/server/responses/core-combo-failure.ts +1 -0
  217. package/src/server/responses/core-combo.ts +5 -1
  218. package/src/server/responses/core-lifetime.ts +22 -0
  219. package/src/server/responses/core-options.ts +16 -1
  220. package/src/server/responses/core.ts +14 -4
  221. package/src/server/responses/empty-completion-guard.ts +2 -0
  222. package/src/server/responses/fetch-helpers.ts +82 -31
  223. package/src/server/responses/passthrough-delivery.ts +3 -2
  224. package/src/server/responses/passthrough-dispatch.ts +12 -2
  225. package/src/server/responses/passthrough-execution.ts +2 -2
  226. package/src/server/responses/request-send-budget.ts +21 -5
  227. package/src/server/responses/request-transport.ts +116 -18
  228. package/src/server/responses/reset-replay.ts +32 -0
  229. package/src/server/responses/run-turn-execution.ts +136 -19
  230. package/src/server/responses/sidecar-execution.ts +41 -8
  231. package/src/server/responses/terminal-guard.ts +2 -0
  232. package/src/server/responses/ws-upstream.ts +44 -4
  233. package/src/server/restart-replacement.ts +382 -0
  234. package/src/server/startup-health-cache.ts +23 -4
  235. package/src/server/stop-teardown.ts +35 -1
  236. package/src/server/system-env.ts +3 -0
  237. package/src/service/windows-taskxml.ts +11 -7
  238. package/src/service/windows-wrapper-exit.ts +8 -0
  239. package/src/stall-timeout.ts +35 -10
  240. package/src/storage/policy-job.ts +4 -0
  241. package/src/storage/scanner.ts +177 -29
  242. package/src/storage/storage-mutation-coordinator.ts +17 -0
  243. package/src/tray/windows-tray.ps1 +50 -23
  244. package/src/types/config.ts +4 -0
  245. package/src/types/provider.ts +5 -1
  246. package/src/types/request.ts +13 -2
  247. package/src/update/index.ts +3 -1
  248. package/src/update/job.ts +6 -2
  249. package/src/update/mise-launcher-target.ts +100 -0
  250. package/src/update/worker-launch.ts +46 -0
  251. package/src/usage/log.ts +2 -0
  252. package/src/web-search/loop.ts +5 -2
  253. package/gui/dist/assets/App-BJsT8Icc.css +0 -1
  254. package/gui/dist/assets/App-D3pNGiN4.js +0 -51
  255. package/gui/dist/assets/Tray-yb05wCjb.js +0 -1
  256. package/gui/dist/assets/index-CouvvtMV.js +0 -86
  257. package/gui/dist/assets/tray-data-CYGdjCJ7.js +0 -1
@@ -0,0 +1,116 @@
1
+ /** Pure eligibility policy. The caller owns admission, buffering, dispatch and the shared budget. */
2
+ export interface CompactionRecoveryConfig {
3
+ enabled: true;
4
+ model: string;
5
+ allowDevinInvalidArgument?: boolean;
6
+ }
7
+
8
+ /** Evidence must describe the failed attempt, never request text or a guessed error message. */
9
+ export interface CompactionRecoveryEvidence {
10
+ requestKind: "ordinary" | "compaction-v1" | "compaction-v2";
11
+ recoveryAttempts: number;
12
+ cancelled: boolean;
13
+ nonReplayable: boolean;
14
+ /** Any semantic output observed, including output retained privately before delivery. */
15
+ partialOutput: boolean;
16
+ toolEffects: boolean;
17
+ remainingSends: number;
18
+ /** Canonical serving identities resolved by the caller, including provider/account identity. */
19
+ originalModel: string;
20
+ fallbackModel: string;
21
+ /** Actual serving adapter/provider, not a prefix inferred from the requested selector. */
22
+ provider: string;
23
+ httpStatus?: number;
24
+ /** A completed result is never retried. Output validation belongs to the caller. */
25
+ responseStatus: "completed" | "failed" | "incomplete" | "unknown";
26
+ errorCode?: string;
27
+ errorType?: string;
28
+ authenticationDenied: boolean;
29
+ policyDenied: boolean;
30
+ budgetDenied: boolean;
31
+ refusal: boolean;
32
+ upstreamFailure: boolean;
33
+ }
34
+
35
+ export type CompactionRecoveryDecision =
36
+ | { recover: true; model: string; reason: "context-overflow" | "compaction-output" | "upstream-unavailable" | "devin-invalid-argument" }
37
+ | { recover: false; reason: "disabled" | "invalid-evidence" | "ordinary-request" | "already-attempted" | "cancelled" | "unsafe-replay" | "protected-failure" | "budget-exhausted" | "same-model" | "succeeded" | "unclassified-failure" };
38
+
39
+ const MODEL_LIMIT = 512;
40
+ const safeName = (value: unknown, limit: number): value is string =>
41
+ typeof value === "string" && value.length > 0 && value.length <= limit
42
+ && value === value.trim() && !/[\u0000-\u0020\u007f-\u009f]/.test(value);
43
+
44
+ /** Invalid persisted values disable recovery rather than widening it. No configurable retry count. */
45
+ export function readCompactionRecoveryConfig(value: unknown): CompactionRecoveryConfig | null {
46
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
47
+ const raw = value as Record<string, unknown>;
48
+ if (Object.keys(raw).some(key => !["enabled", "model", "allowDevinInvalidArgument"].includes(key))
49
+ || raw.enabled !== true || !safeName(raw.model, MODEL_LIMIT)
50
+ || (raw.allowDevinInvalidArgument !== undefined && typeof raw.allowDevinInvalidArgument !== "boolean")) return null;
51
+ return {
52
+ enabled: true,
53
+ model: raw.model,
54
+ ...(raw.allowDevinInvalidArgument !== undefined ? { allowDevinInvalidArgument: raw.allowDevinInvalidArgument } : {}),
55
+ };
56
+ }
57
+
58
+ // Structured tokens only. The normalizer must also supply the explicit denial flags above.
59
+ const PROTECTED_CODE = /(?:^|_)(?:auth|authentication|authorization|unauthenticated|unauthorized|permission|forbidden|policy|refusal|refused|budget|quota|billing|safety|content_filter|origin_rejected|admission|scope)(?:_|$)/;
60
+ const CONTEXT_CODES = new Set(["context_length_exceeded", "context_window_exceeded", "input_too_long"]);
61
+ const COMPACTION_CODES = new Set(["compaction_failed", "invalid_compaction_output", "empty_compaction_output"]);
62
+ const SERVER_CODES = new Set(["upstream_error", "upstream_server_error", "server_is_overloaded", "internal", "internal_error", "server_error", "unavailable", "service_unavailable", "gateway_timeout"]);
63
+
64
+ /** Decides eligibility only; returning true neither spends nor grants another send. */
65
+ export function decideCompactionRecovery(
66
+ value: unknown,
67
+ evidence: CompactionRecoveryEvidence,
68
+ ): CompactionRecoveryDecision {
69
+ const config = readCompactionRecoveryConfig(value);
70
+ if (!config) return { recover: false, reason: "disabled" };
71
+ const flags = [evidence.cancelled, evidence.nonReplayable, evidence.partialOutput, evidence.toolEffects,
72
+ evidence.authenticationDenied, evidence.policyDenied, evidence.budgetDenied, evidence.refusal, evidence.upstreamFailure];
73
+ if (flags.some(flag => typeof flag !== "boolean")
74
+ || !Number.isSafeInteger(evidence.recoveryAttempts) || evidence.recoveryAttempts < 0
75
+ || !Number.isSafeInteger(evidence.remainingSends) || evidence.remainingSends < 0
76
+ || !safeName(evidence.originalModel, MODEL_LIMIT) || !safeName(evidence.fallbackModel, MODEL_LIMIT)
77
+ || !safeName(evidence.provider, 128)
78
+ || !["ordinary", "compaction-v1", "compaction-v2"].includes(evidence.requestKind)
79
+ || !["completed", "failed", "incomplete", "unknown"].includes(evidence.responseStatus)
80
+ || (evidence.httpStatus !== undefined && (!Number.isInteger(evidence.httpStatus) || evidence.httpStatus < 100 || evidence.httpStatus > 599))
81
+ || [evidence.errorCode, evidence.errorType].some(code => code !== undefined && !safeName(code, 128))) {
82
+ return { recover: false, reason: "invalid-evidence" };
83
+ }
84
+ if (evidence.requestKind === "ordinary") return { recover: false, reason: "ordinary-request" };
85
+ if (evidence.recoveryAttempts !== 0) return { recover: false, reason: "already-attempted" };
86
+ const status = evidence.httpStatus;
87
+ const codes = [evidence.errorCode, evidence.errorType].filter((code): code is string => code !== undefined);
88
+ if (evidence.cancelled || status === 499 || codes.some(code => code === "cancelled" || code === "canceled" || code === "client_cancelled")) {
89
+ return { recover: false, reason: "cancelled" };
90
+ }
91
+ if (evidence.nonReplayable || evidence.partialOutput || evidence.toolEffects) return { recover: false, reason: "unsafe-replay" };
92
+ if (evidence.authenticationDenied || evidence.policyDenied || evidence.budgetDenied || evidence.refusal
93
+ || status === 401 || status === 403 || status === 402 || status === 429
94
+ || codes.some(code => PROTECTED_CODE.test(code.toLowerCase()) || code === "failed_precondition")) {
95
+ return { recover: false, reason: "protected-failure" };
96
+ }
97
+ if (evidence.remainingSends === 0) return { recover: false, reason: "budget-exhausted" };
98
+ if (evidence.originalModel === evidence.fallbackModel) return { recover: false, reason: "same-model" };
99
+ if (evidence.responseStatus === "completed") return { recover: false, reason: "succeeded" };
100
+ const failedHttp = status !== undefined && status >= 400;
101
+ if (!evidence.upstreamFailure || (!failedHttp && evidence.responseStatus !== "failed")) {
102
+ return { recover: false, reason: "unclassified-failure" };
103
+ }
104
+ const code = evidence.errorCode;
105
+ const requestFailure = status === undefined || status === 200 || status === 400 || status === 413 || status === 422 || status >= 500;
106
+ if (requestFailure && code && CONTEXT_CODES.has(code)) return { recover: true, model: config.model, reason: "context-overflow" };
107
+ if (requestFailure && code && COMPACTION_CODES.has(code)) return { recover: true, model: config.model, reason: "compaction-output" };
108
+ if (config.allowDevinInvalidArgument === true && evidence.provider === "devin" && code === "invalid_argument"
109
+ && (evidence.responseStatus === "failed" || (status !== undefined && status >= 400 && status < 500))) {
110
+ return { recover: true, model: config.model, reason: "devin-invalid-argument" };
111
+ }
112
+ if (status !== undefined && status >= 500 && (code === undefined || SERVER_CODES.has(code))) {
113
+ return { recover: true, model: config.model, reason: "upstream-unavailable" };
114
+ }
115
+ return { recover: false, reason: "unclassified-failure" };
116
+ }
@@ -0,0 +1,330 @@
1
+ import type { AdapterEvent, OcxConfig } from "../../types";
2
+ import { routeConcreteModel, type RouteResult } from "../../router";
3
+ import { copyPlainData } from "../../lib/plain-data";
4
+ import { jsonUtf8Bytes } from "../../lib/json-byte-size";
5
+ import type { TranslatorBudget } from "../../lib/translator-budget";
6
+ import { readBoundedResponseBytes } from "../../lib/bounded-body";
7
+ import { isRequestExecutionBudget, type RequestExecutionBudget, type SingleUseDispatchPermit } from "../../lib/request-execution-budget";
8
+ import { isNonReplayableResponse, isNonReplayableUpstreamCode, markResponseNonReplayable, TRANSIENT_RETRY_MAX_ATTEMPTS } from "../../lib/upstream-retry";
9
+ import { isCyberPolicyCode, isTerminalRefusalCode } from "../../lib/errors";
10
+ import { isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
11
+ import { bridgeToResponsesSSE, formatErrorResponse } from "../../bridge";
12
+ import { buildCompactV1Output, decodeCompactionSummary, encodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
13
+ import { finishRequestAttempt, usageFromResponsesPayload, type RequestLogContext } from "../request-log";
14
+ import { linkRequestSessionLane } from "../request-log-conversation";
15
+ import { isNativePassthroughSseResponse, markNativePassthroughSseResponse, isEagerRelaySseResponse, markEagerRelaySseResponse } from "../relay";
16
+ import type { HandleResponsesOptions } from "./core-options";
17
+ import { consumeComboFailure, createChildPassthroughCallbackGate } from "./core-combo-failure";
18
+ import { preflightComboStreamResponse } from "./combo-stream-preflight";
19
+ import { conversationCarriesUploadedFiles } from "./account-change-state";
20
+ import { selfContainedResponsesBody } from "./reset-replay";
21
+ import { decideCompactionRecovery, readCompactionRecoveryConfig } from "./compaction-recovery-policy";
22
+
23
+ type Options = HandleResponsesOptions & { translatorBudget: TranslatorBudget };
24
+ type Dispatch = (req: Request, config: OcxConfig, log: RequestLogContext, options: Options) => Promise<Response>;
25
+ const MAX_BYTES = 32 * 1024 * 1024;
26
+ const RETAINED_USER_CHARS = 80_000;
27
+ const record = (value: unknown): value is Record<string, unknown> => !!value && typeof value === "object" && !Array.isArray(value);
28
+ const token = (value: unknown): string | undefined => typeof value === "string" && /^[a-zA-Z0-9_-]{1,128}$/.test(value) ? value : undefined;
29
+
30
+ function identity(route: RouteResult): string {
31
+ return JSON.stringify([route.providerName, route.modelId, route.codexAccountMode ?? "", route.codexAccountNamespace ?? ""]);
32
+ }
33
+
34
+ function physicalSends(log: RequestLogContext): number {
35
+ // activeAttempt normally also belongs to attempts: count each receipt exactly once.
36
+ const attempts = new Set([...(log.attempts ?? []), ...(log.activeAttempt ? [log.activeAttempt] : [])]);
37
+ return [...attempts].reduce((sum, attempt) => sum + Math.max(0, attempt.sendCount), 0);
38
+ }
39
+
40
+ /** Reconcile only this leg's physical receipts; never charge already-booked adapter sends again. */
41
+ function settlePhysicalSends(log: RequestLogContext, budget: RequestExecutionBudget, beforeSends: number, beforeUsed: number, reported: number, permit?: SingleUseDispatchPermit): number {
42
+ const sent = Math.max(0, physicalSends(log) - beforeSends);
43
+ // A legacy fetch leg does not claim the hop through adapterDispatchBudget. Settle its
44
+ // prepaid booking explicitly; an adapter-owned leg already claimed it, making this a no-op.
45
+ if (sent > 0) permit?.assumeCharge();
46
+ else permit?.release();
47
+ // An external report may settle a prepaid booking without changing used. Its explicit
48
+ // receipt outranks the numeric delta, or the same source send would be charged twice.
49
+ const unreported = Math.max(0, sent - Math.max(reported, Math.max(0, budget.used - beforeUsed)));
50
+ if (unreported > 0) budget.used += unreported;
51
+ return sent;
52
+ }
53
+
54
+ function portableBody(body: Record<string, unknown>): boolean {
55
+ if (!Array.isArray(body.input) || body.store === true || conversationCarriesUploadedFiles(body)) return false;
56
+ // Native ciphertext cannot be summarized by another provider. Never silently replace it with a note.
57
+ 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;
59
+ const input = body.input.filter(item => !record(item) || item.type !== "compaction_trigger");
60
+ return selfContainedResponsesBody({ ...body, store: false, input });
61
+ }
62
+
63
+ function routed(route: RouteResult): boolean {
64
+ return !route.combo && route.routeKind !== "policy" && route.routeReason !== "default-provider"
65
+ && !isCanonicalOpenAiForwardProvider(route.provider)
66
+ && !supportsNativeResponsesCompactEndpoint(route.providerName, route.provider);
67
+ }
68
+
69
+ /** One reader, bounded bytes, and an exact replacement body; never clone a live stream. */
70
+ async function bufferedJson(response: Response, signal: AbortSignal): Promise<{ response: Response; json?: Record<string, unknown> }> {
71
+ const bytes = await readBoundedResponseBytes(response, { signal, maxBytes: MAX_BYTES, inactivityTimeoutMs: 300_000 });
72
+ if (bytes.oversized) return { response: formatErrorResponse(502, "translation_buffer_limit", "Compaction recovery response exceeded its byte limit") };
73
+ const headers = new Headers(response.headers);
74
+ headers.delete("content-length");
75
+ headers.delete("content-encoding");
76
+ const replacement = new Response(bytes.bytes, { status: response.status, statusText: response.statusText, headers });
77
+ if (isNonReplayableResponse(response)) markResponseNonReplayable(replacement);
78
+ try {
79
+ const json: unknown = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.bytes));
80
+ return { response: replacement, ...(record(json) ? { json } : {}) };
81
+ } catch { return { response: replacement }; }
82
+ }
83
+
84
+ /**
85
+ * Opt-in recovery for routed compaction only. Native compact/ciphertext, stored continuations,
86
+ * policy/combo routes and hosted tools retain their original failure. This owns no credentials.
87
+ */
88
+ export async function runWithCompactionRecovery(
89
+ req: Request, config: OcxConfig, logCtx: RequestLogContext, options: Options, dispatch: Dispatch,
90
+ ): Promise<Response> {
91
+ const recovery = readCompactionRecoveryConfig(config.compactionRecovery);
92
+ if (!recovery || options.compactionRecoveryAttempted || options.comboAttempt || (options.inboundWire && options.inboundWire !== "responses")) {
93
+ return dispatch(req, config, logCtx, options);
94
+ }
95
+ let snapshot: Record<string, unknown> | undefined;
96
+ let snapshotBytes = 0;
97
+ let sourceRoute: RouteResult | undefined;
98
+ let partialOutput = false;
99
+ let replayUnsafe = false;
100
+ let adapterError: Extract<AdapterEvent, { type: "error" }> | undefined;
101
+ let sourceFailure: Response | undefined;
102
+ let recoveryPermit: SingleUseDispatchPermit | undefined;
103
+ let restoreSourceLog: ((attemptStatus?: number) => void) | undefined;
104
+ let sourceReportedSends = 0;
105
+ const gate = createChildPassthroughCallbackGate(options);
106
+ const signal = options.abortSignal ?? req.signal;
107
+ const spentBefore = options.sendBudget?.used ?? 0;
108
+ const sendsBefore = physicalSends(logCtx);
109
+ const firstOptions: Options = {
110
+ ...options,
111
+ onCompactionRecoverySendsReported(count) {
112
+ sourceReportedSends += count;
113
+ options.onCompactionRecoverySendsReported?.(count);
114
+ },
115
+ onRequestBodyParsed(body) {
116
+ options.onRequestBodyParsed?.(body);
117
+ if (!record(body) || !Array.isArray(body.input) || typeof body.model !== "string"
118
+ || !body.input.some(item => record(item) && item.type === "compaction_trigger") || !portableBody(body)) return;
119
+ const users = extractCompactUserMessages(body.input);
120
+ if ((users.at(-1)?.length ?? 0) > RETAINED_USER_CHARS) return;
121
+ try {
122
+ const bytes = jsonUtf8Bytes(body, MAX_BYTES);
123
+ const reservation = options.translatorBudget.reserveTransient(bytes, { kind: "request_copies" });
124
+ try {
125
+ const copy = copyPlainData(body);
126
+ if (!copy.ok) return;
127
+ snapshot = copy.value;
128
+ snapshotBytes = bytes;
129
+ reservation.commitRetained();
130
+ } finally { reservation.release(); }
131
+ } catch { /* Optional recovery cannot reject an otherwise valid original request. */ }
132
+ },
133
+ onCompactionRecoveryRoute(route) {
134
+ options.onCompactionRecoveryRoute?.(route);
135
+ if (snapshot && routed(route)) sourceRoute = { ...route };
136
+ },
137
+ onCompactionRecoveryAdapterEvent(event) {
138
+ options.onCompactionRecoveryAdapterEvent?.(event);
139
+ if (!snapshot) return;
140
+ if (event.type === "heartbeat") replayUnsafe ||= event.replayUnsafe === true;
141
+ else if (event.type === "error") adapterError = event;
142
+ else if (event.type !== "done") partialOutput = true;
143
+ },
144
+ onResponseComplete: model => snapshot ? gate.onResponseComplete(model) : options.onResponseComplete?.(model),
145
+ onNativePassthroughTerminal: status => snapshot ? gate.onTerminal(status) : options.onNativePassthroughTerminal?.(status),
146
+ onNativePassthroughCancel: () => snapshot ? gate.onCancel() : options.onNativePassthroughCancel?.(),
147
+ };
148
+ try {
149
+ let response = await dispatch(req, config, logCtx, firstOptions);
150
+ const keep = (value: Response) => { gate.commit(); return value; };
151
+ if (!snapshot || !sourceRoute || signal.aborted || req.signal.aborted || isNonReplayableResponse(response)) return keep(response);
152
+ let target: RouteResult;
153
+ try { target = routeConcreteModel(config, recovery.model); } catch { return keep(response); }
154
+ if (!routed(target) || identity(sourceRoute) === identity(target)) return keep(response);
155
+ const originalModel = firstOptions.compactionRoutingOverride?.sourceModel ?? String(snapshot.model);
156
+ // Use the established protocol commit boundary. For runTurn streams the direct event
157
+ // observer additionally preserves side-effect heartbeats that the bridge does not publish.
158
+ if (response.ok && response.headers.get("content-type")?.includes("text/event-stream")) {
159
+ const native = isNativePassthroughSseResponse(response);
160
+ const eager = isEagerRelaySseResponse(response);
161
+ const preflight = await preflightComboStreamResponse(response, logCtx);
162
+ response = preflight.response;
163
+ if (preflight.kind !== "failed") {
164
+ if (native) markNativePassthroughSseResponse(response);
165
+ if (eager) markEagerRelaySseResponse(response);
166
+ return keep(response);
167
+ }
168
+ } else if (response.ok) {
169
+ const buffered = await bufferedJson(response, signal);
170
+ response = buffered.response;
171
+ const json = buffered.json;
172
+ if (!json || json.status !== "failed" || (Array.isArray(json.output) && json.output.length > 0)) return keep(response);
173
+ // HTTP 200 can carry a failed terminal. The original structured error remains intact.
174
+ response = Response.json({ error: json.error, response: json }, { status: adapterError?.status ?? 502 });
175
+ }
176
+ if (response.ok || replayUnsafe || partialOutput || signal.aborted || req.signal.aborted) {
177
+ if (replayUnsafe) markResponseNonReplayable(response);
178
+ return keep(response);
179
+ }
180
+ const failure = await consumeComboFailure(response, signal);
181
+ response = failure.response;
182
+ const code = token(adapterError?.code) ?? token(failure.upstreamCode);
183
+ const errorType = token(adapterError?.errorType) ?? token(failure.upstreamType);
184
+ const budget = options.sendBudget;
185
+ // Reset-only fetch legs report their physical receipt but historically leave used alone.
186
+ // Reconcile only a failed, eligible compaction; successful/disabled requests stay unchanged.
187
+ const sourceSends = budget && isRequestExecutionBudget(budget)
188
+ ? settlePhysicalSends(logCtx, budget, sendsBefore, spentBefore, sourceReportedSends) : 0;
189
+ const decision = decideCompactionRecovery(recovery, {
190
+ requestKind: options.compactionRecoveryKind ?? "compaction-v2", recoveryAttempts: 0,
191
+ cancelled: signal.aborted || req.signal.aborted, nonReplayable: !!failure.nonReplayable || isNonReplayableUpstreamCode(code),
192
+ partialOutput, toolEffects: replayUnsafe,
193
+ remainingSends: budget && isRequestExecutionBudget(budget) ? budget.remainingBaseSends(TRANSIENT_RETRY_MAX_ATTEMPTS) : 0,
194
+ originalModel: identity(sourceRoute), fallbackModel: identity(target), provider: sourceRoute.provider.adapter,
195
+ httpStatus: adapterError?.status ?? response.status, responseStatus: "failed", errorCode: code, errorType,
196
+ authenticationDenied: response.status === 401 || response.status === 403,
197
+ policyDenied: isCyberPolicyCode(code), budgetDenied: code === "translation_buffer_limit",
198
+ refusal: isTerminalRefusalCode(code), upstreamFailure: sourceSends > 0,
199
+ });
200
+ if (!decision.recover || !budget || !isRequestExecutionBudget(budget)) return keep(response);
201
+ const fallbackBeforeSends = physicalSends(logCtx);
202
+ const fallbackBeforeUsed = budget.used;
203
+ const reservation = budget.reserveDispatch({ sendClass: "combo-failover", targetKey: `compaction:${identity(target)}`, countedExternally: true, replaySafe: true });
204
+ if (!reservation.allowed) return keep(response);
205
+ recoveryPermit = reservation.permit;
206
+ sourceFailure = response;
207
+ gate.discard();
208
+ // Finish the first physical attempt while retaining its receipt in attempts[].
209
+ if (logCtx.activeAttempt) finishRequestAttempt(logCtx.activeAttempt, response.status,
210
+ Math.max(0, Date.now() - (logCtx.activeAttemptStartedAt ?? Date.now())), logCtx.activeAttempt.usage ?? logCtx.usage);
211
+ // Snapshot the original log fields before the fallback rewrites them: a failed fallback
212
+ // returns the original failure, so the log must keep describing that failure, not the
213
+ // fallback's model, provider, route decision or terminal error.
214
+ const SOURCE_LOG_FIELDS = [
215
+ "model", "provider", "providerAdapter", "requestedAlias", "servedModel", "wireModel",
216
+ "resolvedModel", "routeDecision", "tierOutcome", "activeTierMetadata", "usage",
217
+ "usageFromBridge", "upstreamError", "terminalHttpStatus", "terminalErrorCode",
218
+ "terminalIncompleteReason", "errorCode",
219
+ ] as const;
220
+ const sourceLog: Partial<Record<keyof RequestLogContext, unknown>> = {};
221
+ const logFields = logCtx as unknown as Record<string, unknown>;
222
+ for (const field of SOURCE_LOG_FIELDS) sourceLog[field] = logCtx[field];
223
+ restoreSourceLog = (attemptStatus?: number) => {
224
+ if (logCtx.activeAttempt) finishRequestAttempt(logCtx.activeAttempt, attemptStatus ?? sourceFailure!.status,
225
+ Math.max(0, Date.now() - (logCtx.activeAttemptStartedAt ?? Date.now())), logCtx.activeAttempt.usage ?? logCtx.usage);
226
+ delete logCtx.activeAttempt;
227
+ delete logCtx.activeAttemptStartedAt;
228
+ for (const field of SOURCE_LOG_FIELDS) {
229
+ if (sourceLog[field] === undefined) delete logFields[field];
230
+ else logFields[field] = sourceLog[field];
231
+ }
232
+ };
233
+ delete logCtx.activeAttempt;
234
+ delete logCtx.activeAttemptStartedAt;
235
+ delete logCtx.usage;
236
+ delete logCtx.usageFromBridge;
237
+ delete logCtx.upstreamError;
238
+ delete logCtx.terminalHttpStatus;
239
+ delete logCtx.terminalErrorCode;
240
+ delete logCtx.terminalIncompleteReason;
241
+ const headers = new Headers(req.headers);
242
+ headers.delete("authorization");
243
+ headers.delete("chatgpt-account-id");
244
+ headers.delete("content-length");
245
+ headers.delete("content-encoding");
246
+ headers.set("content-type", "application/json");
247
+ const nextBody = { ...snapshot, model: decision.model, stream: false, store: false };
248
+ const bytes = jsonUtf8Bytes(nextBody, MAX_BYTES);
249
+ const serialization = options.translatorBudget.reserveTransient(bytes, { kind: "request_copies" });
250
+ let fallback: Response;
251
+ let fallbackReportedSends = 0;
252
+ try {
253
+ const child = new Request(req.url, { method: "POST", headers, body: JSON.stringify(nextBody), signal: req.signal });
254
+ linkRequestSessionLane(req, child);
255
+ // The source send is finished. The child reuses this account-load holder and may
256
+ // select the same Kiro account, so return its lease before child admission can
257
+ // acquire and replace the holder's reference.
258
+ if (options.accountLoad?.lease) {
259
+ options.accountLoad.lease.release();
260
+ options.accountLoad.lease = null;
261
+ }
262
+ fallback = await dispatch(child, config, logCtx, {
263
+ ...options, compactionRecoveryAttempted: true, compactionRecoveryPermit: recoveryPermit,
264
+ compactionRoutingOverride: { sourceModel: originalModel },
265
+ onRequestBodyRead: undefined, onRequestBodyParsed: undefined,
266
+ onCompactionRecoveryRoute: undefined, onCompactionRecoveryAdapterEvent: undefined,
267
+ onCompactionRecoverySendsReported: count => {
268
+ fallbackReportedSends += count;
269
+ options.onCompactionRecoverySendsReported?.(count);
270
+ },
271
+ onResponseComplete: undefined, onNativePassthroughTerminal: undefined, onNativePassthroughCancel: undefined,
272
+ });
273
+ } finally {
274
+ settlePhysicalSends(logCtx, budget, fallbackBeforeSends, fallbackBeforeUsed, fallbackReportedSends, recoveryPermit);
275
+ serialization.release();
276
+ }
277
+ if (signal.aborted || req.signal.aborted) {
278
+ void fallback.body?.cancel().catch(() => undefined);
279
+ return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
280
+ }
281
+ if (!fallback.ok) {
282
+ void fallback.body?.cancel().catch(() => undefined);
283
+ restoreSourceLog?.(fallback.status);
284
+ return response;
285
+ }
286
+ const completed = await bufferedJson(fallback, signal);
287
+ const json = completed.json;
288
+ const items = json && Array.isArray(json.output) ? json.output : [];
289
+ const compactions = items.filter(value => record(value) && value.type === "compaction");
290
+ const permitted = items.every(value => record(value) && (value.type === "compaction" || value.type === "reasoning"));
291
+ 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;
293
+ if (json?.status !== "completed" || !summary?.trim()) {
294
+ void completed.response.body?.cancel().catch(() => undefined);
295
+ restoreSourceLog?.(completed.response.status);
296
+ return response;
297
+ }
298
+ // v1 unpacks this text through buildCompactV1Output, which already re-adds retained user
299
+ // messages as items; embedding them here too would duplicate the same text in the output.
300
+ const preserved = options.compactionRecoveryKind === "compaction-v1" ? summary : (() => {
301
+ const retained = buildCompactV1Output(extractCompactUserMessages(snapshot.input), summary).slice(0, -1);
302
+ const userText = extractCompactUserMessages(retained).map((text, index) => `User message ${index + 1}:\n${text}`).join("\n\n");
303
+ return `${summary}\n\nRetained original user messages (verbatim; preserve their goals and constraints):\n${userText}`;
304
+ })();
305
+ item!.encrypted_content = encodeCompactionSummary(preserved);
306
+ json.model = originalModel;
307
+ void completed.response.body?.cancel().catch(() => undefined);
308
+ void response.body?.cancel().catch(() => undefined);
309
+ if (snapshot.stream === true) {
310
+ const usage = usageFromResponsesPayload(json.usage);
311
+ async function* events(): AsyncGenerator<AdapterEvent> {
312
+ yield { type: "text_delta", text: preserved };
313
+ yield { type: "done", ...(usage ? { usage } : {}) };
314
+ }
315
+ return new Response(bridgeToResponsesSSE(events(), originalModel, undefined, undefined, undefined, undefined, 2_000,
316
+ { compaction: true, translatorBudget: options.translatorBudget, onCompletedResponse: () => options.onResponseComplete?.(originalModel) }), { headers: { "content-type": "text/event-stream" } });
317
+ }
318
+ options.onResponseComplete?.(originalModel);
319
+ return Response.json(json);
320
+ } catch (error) {
321
+ gate.discard();
322
+ if (signal.aborted || req.signal.aborted) return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
323
+ if (sourceFailure) { restoreSourceLog?.(); return sourceFailure; }
324
+ throw error;
325
+ } finally {
326
+ if (snapshotBytes) options.translatorBudget.releaseRetained(snapshotBytes, { kind: "request_copies" });
327
+ recoveryPermit?.release();
328
+ snapshot = undefined;
329
+ }
330
+ }
@@ -117,6 +117,7 @@ export async function consumeComboFailure(
117
117
  ...(nonReplayable ? { nonReplayable: true } : {}),
118
118
  classificationText,
119
119
  ...(normalizedUpstreamCode !== undefined ? { upstreamCode: normalizedUpstreamCode } : {}),
120
+ ...(upstreamType !== undefined ? { upstreamType } : {}),
120
121
  ...(!cyberFailure && cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
121
122
  // The EFFECTIVE classification decides, not the raw status. An upstream that wraps a quota
122
123
  // refusal in a 5xx still carries `x-codex-*-reset-at`, and gating on 402/429 alone threw
@@ -667,7 +667,11 @@ export async function executeComboResponses(
667
667
  const attempt = beginRequestAttempt(
668
668
  (logCtx.attempts?.length ?? 0) + 1,
669
669
  pick.target.provider,
670
- pick.target.model,
670
+ // The id the child wire will actually send, not the selector the combo named. A target may
671
+ // be an alias, and `routeConcreteModel` above is where it becomes the provider's native id;
672
+ // recording the alias here would describe a request that never left (the adapter resolves
673
+ // the id before it reads any per-model list).
674
+ targetRoute.modelId,
671
675
  config.providers[pick.target.provider]!.adapter,
672
676
  );
673
677
  childLog.activeAttempt = attempt;
@@ -80,6 +80,28 @@ export function finalizeOwnedTranslatorBudget(response: Response, budget: Transl
80
80
  return finalizedResponse;
81
81
  }
82
82
 
83
+ /** Release a serving-account slot when the client body ends, errors or is cancelled. */
84
+ export function finalizeAccountLease(response: Response, release: () => void): Response {
85
+ if (!response.body) { release(); return response; }
86
+ const reader = response.body.getReader();
87
+ let done = false;
88
+ const finish = () => { if (!done) { done = true; release(); } };
89
+ const body = new ReadableStream<Uint8Array>({
90
+ async pull(controller) {
91
+ try {
92
+ const next = await reader.read();
93
+ if (next.done) { finish(); controller.close(); }
94
+ else controller.enqueue(next.value);
95
+ } catch (error) { finish(); controller.error(error); }
96
+ },
97
+ async cancel(reason) { try { await reader.cancel(reason); } finally { finish(); } },
98
+ });
99
+ const wrapped = new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
100
+ if (isNativePassthroughSseResponse(response)) markNativePassthroughSseResponse(wrapped);
101
+ if (isEagerRelaySseResponse(response)) markEagerRelaySseResponse(wrapped);
102
+ return wrapped;
103
+ }
104
+
83
105
 
84
106
 
85
107
 
@@ -1,5 +1,7 @@
1
1
  import type { NativeResponseControl } from "./native-response-control";
2
- import type { OcxUsage, OcxProviderContinuationState, OcxConfig } from "../../types";
2
+ import type { AdapterEvent, OcxUsage, OcxProviderContinuationState, OcxConfig } from "../../types";
3
+ import type { RouteResult } from "../../router";
4
+ import type { SingleUseDispatchPermit } from "../../lib/request-execution-budget";
3
5
  import type { CodexAuthPolicyConfig, CodexAuthContext } from "../../codex/auth-context";
4
6
  import type { AdmissionLease } from "../../lib/admission";
5
7
  import type { DataPlaneAdmission } from "../auth-cors";
@@ -16,12 +18,15 @@ import type { TranslatorBudget } from "../../lib/translator-budget";
16
18
  import type { TransientSendBudget } from "../../lib/upstream-retry";
17
19
  import type { RequestLogContext } from "../request-log";
18
20
  import type { UpstreamHostAdmissionLease } from "../../codex/upstream-host-health";
21
+ import type { AccountLease } from "../../oauth/kiro-account-load";
19
22
 
20
23
  export interface ConsumedComboFailure {
21
24
  response: Response;
22
25
  classificationText: string;
23
26
  /** Structured upstream `error.code` when present in the failure body. */
24
27
  upstreamCode?: string;
28
+ /** Complete structured provider type, retained for conservative recovery classification. */
29
+ upstreamType?: string;
25
30
  /** Valid numeric/date value used only for cooldown calculation. */
26
31
  retryAfter?: string;
27
32
  /** Upstream Codex quota-window reset timestamps used for combo cooldowns. */
@@ -52,6 +57,16 @@ export interface ClientEncoderOption {
52
57
  }
53
58
 
54
59
  export interface HandleResponsesOptions {
60
+ /** Internal routed-compaction recovery: one logical request, one emergency target. */
61
+ compactionRecoveryAttempted?: boolean;
62
+ compactionRecoveryPermit?: SingleUseDispatchPermit;
63
+ compactionRecoveryKind?: "compaction-v1" | "compaction-v2";
64
+ onCompactionRecoveryRoute?: (route: RouteResult) => void;
65
+ onCompactionRecoveryAdapterEvent?: (event: AdapterEvent) => void;
66
+ /** Physical-send reports already delivered to the shared used setter, including booking settlement. */
67
+ onCompactionRecoverySendsReported?: (count: number) => void;
68
+ /** Private holder for the Kiro serving-account lease. */
69
+ accountLoad?: { lease: AccountLease | null };
55
70
  /** Internal Claude replay identity; consumed only by the final canonical Go transport. */
56
71
  claudeGoAffinity?: { sessionLane?: string };
57
72
  /** Validated Claude metadata identity; projected only into final canonical attempt headers. */
@@ -10,7 +10,7 @@ import { createTranslatorBudget } from "../../lib/translator-budget";
10
10
  import { captureExplicitOpenAiCallerAuth } from "../../providers/openai-sidecar";
11
11
  import { captureCallerDirectAuth } from "../../providers/caller-authorization";
12
12
  import { createInferenceSendBudget } from "../inference/context";
13
- import { finalizeOwnedTranslatorBudget } from "./core-lifetime";
13
+ import { finalizeOwnedTranslatorBudget, finalizeAccountLease } from "./core-lifetime";
14
14
  import type { TranslatorBudget } from "../../lib/translator-budget";
15
15
  import { executeComboResponses } from "./core-combo";
16
16
  import { prepareResponsesRequest } from "./request-prepare";
@@ -27,6 +27,7 @@ import { createAdapterContinuations } from "./adapter-continuation";
27
27
  import { deliverAdapterResponse } from "./adapter-delivery";
28
28
  import { releaseUpstreamHostAdmission } from "../../codex/upstream-host-health";
29
29
  import { releaseCodexAuthContextProbeLease } from "../../codex/auth-context";
30
+ import { runWithCompactionRecovery } from "./compaction-recovery";
30
31
 
31
32
  /** Public Responses entry and compatibility exports. Implementations live with their owners. */
32
33
 
@@ -42,8 +43,12 @@ export async function handleResponses(
42
43
  ): Promise<Response> {
43
44
  const ownsBudget = options.translatorBudget === undefined;
44
45
  const translatorBudget = options.translatorBudget ?? createTranslatorBudget();
46
+ const accountLoad = { lease: null as import("../../oauth/kiro-account-load").AccountLease | null };
47
+ const abortSignal = options.abortSignal ?? req.signal;
48
+ const release = () => { accountLoad.lease?.release(); accountLoad.lease = null; abortSignal.removeEventListener("abort", release); };
49
+ abortSignal.addEventListener("abort", release, { once: true });
45
50
  try {
46
- const response = await handleResponsesInner(req, config, logCtx, {
51
+ const response = await runWithCompactionRecovery(req, config, logCtx, {
47
52
  ...options,
48
53
  openAiSidecarAuth: options.openAiSidecarAuth === undefined
49
54
  ? captureExplicitOpenAiCallerAuth(req.headers, config) : options.openAiSidecarAuth,
@@ -55,11 +60,15 @@ export async function handleResponses(
55
60
  visionDescribeTerminal: options.visionDescribeTerminal === true
56
61
  || req.headers.get("x-opencodex-vision-describe") === "1",
57
62
  translatorBudget,
63
+ accountLoad,
58
64
  // Once at ingress, spend observer included: a combo child inherits the parent's holder.
59
65
  sendBudget: options.sendBudget ?? createInferenceSendBudget(req, logCtx),
60
- });
61
- return ownsBudget ? finalizeOwnedTranslatorBudget(response, translatorBudget) : response;
66
+ }, handleResponsesInner);
67
+ const finalResponse = ownsBudget ? finalizeOwnedTranslatorBudget(response, translatorBudget) : response;
68
+ if (!accountLoad.lease) { release(); return finalResponse; }
69
+ return finalizeAccountLease(finalResponse, release);
62
70
  } catch (error) {
71
+ release();
63
72
  if (ownsBudget) translatorBudget.dispose();
64
73
  throw error;
65
74
  }
@@ -101,6 +110,7 @@ async function handleResponsesInner(
101
110
  if (requestState instanceof Response) return requestState;
102
111
  const transportState = await prepareResponsesTransport(requestContext, admissionState, requestState);
103
112
  if (transportState instanceof Response) return transportState;
113
+ options.onCompactionRecoveryRoute?.(requestState.route);
104
114
  const sidecarState = await prepareResponsesSidecarAuth(requestContext, requestState, transportState);
105
115
  if (sidecarState instanceof Response) return sidecarState;
106
116
  const responseEffects = createResponsesEffects(
@@ -173,6 +173,7 @@ export function mergeUsage(
173
173
  const cacheReadInputTokens = sumOptional("cacheReadInputTokens");
174
174
  const cacheCreationInputTokens = sumOptional("cacheCreationInputTokens");
175
175
  const reasoningOutputTokens = sumOptional("reasoningOutputTokens");
176
+ const providerCredits = sumOptional("providerCredits");
176
177
  const contextTotalTokens = second.contextTotalTokens ?? first.contextTotalTokens;
177
178
  const inputTokens = first.inputTokens + second.inputTokens;
178
179
  const outputTokens = first.outputTokens + second.outputTokens;
@@ -188,6 +189,7 @@ export function mergeUsage(
188
189
  ...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {}),
189
190
  ...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
190
191
  ...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
192
+ ...(providerCredits !== undefined ? { providerCredits } : {}),
191
193
  ...(first.estimated || second.estimated ? { estimated: true } : {}),
192
194
  ...(rawUsage !== undefined ? { rawUsage } : {}),
193
195
  };