@bitkyc08/opencodex 2.59.0 → 2.60.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 (194) hide show
  1. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  2. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +4 -1
  5. package/src/adapters/anthropic-image-codec.ts +16 -2
  6. package/src/adapters/anthropic-image-normalize.ts +49 -2
  7. package/src/adapters/anthropic.ts +4 -1
  8. package/src/adapters/base.ts +23 -0
  9. package/src/adapters/coding-agent/turn.ts +22 -2
  10. package/src/adapters/command-code.ts +50 -3
  11. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  12. package/src/adapters/cursor/discovery.ts +11 -8
  13. package/src/adapters/cursor/live-transport.ts +26 -9
  14. package/src/adapters/cursor/request-builder.ts +7 -1
  15. package/src/adapters/cursor/transport.ts +19 -0
  16. package/src/adapters/cursor.ts +3 -1
  17. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  18. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  19. package/src/adapters/devin.ts +50 -13
  20. package/src/adapters/google-antigravity-replay.ts +1 -1
  21. package/src/adapters/google-antigravity-wire.ts +29 -5
  22. package/src/adapters/google-http.ts +49 -10
  23. package/src/adapters/google-tool-schema.ts +595 -31
  24. package/src/adapters/google-wire-compiler.ts +93 -10
  25. package/src/adapters/google-wire-shape.ts +461 -0
  26. package/src/adapters/google.ts +37 -6
  27. package/src/adapters/openai-chat-images.ts +3 -1
  28. package/src/adapters/openai-chat.ts +5 -1
  29. package/src/adapters/openai-responses/image-gen.ts +8 -6
  30. package/src/adapters/openai-responses/passthrough.ts +15 -3
  31. package/src/adapters/openai-responses/reasoning.ts +7 -0
  32. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  33. package/src/bridge/sse.ts +3 -151
  34. package/src/cli/account-extended.ts +4 -4
  35. package/src/cli/dispatch.ts +3 -3
  36. package/src/cli/doctor.ts +28 -9
  37. package/src/cli/hub.ts +3 -2
  38. package/src/cli/index.ts +7 -2
  39. package/src/cli/opencode.ts +2 -2
  40. package/src/cli/provider.ts +13 -1
  41. package/src/client/machine-api.ts +2 -2
  42. package/src/client/machine-listener.ts +2 -2
  43. package/src/client/runtime.ts +26 -2
  44. package/src/codex/account-store.ts +65 -0
  45. package/src/codex/auth-api/account-list.ts +19 -11
  46. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  47. package/src/codex/catalog/gather-capture.ts +21 -2
  48. package/src/codex/catalog/model-hints.ts +29 -28
  49. package/src/codex/catalog/parsing.ts +7 -0
  50. package/src/codex/catalog/provider-models.ts +19 -2
  51. package/src/codex/catalog/retained-sync.ts +22 -26
  52. package/src/codex/catalog/routed-gather.ts +19 -0
  53. package/src/codex/context-compat.ts +5 -2
  54. package/src/codex/desired-state.ts +4 -1
  55. package/src/codex/history-job.ts +6 -6
  56. package/src/codex/history-provider.ts +20 -166
  57. package/src/codex/history-rollout-read.ts +174 -0
  58. package/src/codex/internal/catalog-writer.ts +33 -1
  59. package/src/codex/model-cache.ts +47 -0
  60. package/src/codex/model-entitlements.ts +29 -10
  61. package/src/codex/observed-model-denials.ts +101 -8
  62. package/src/codex/prompt-text-probe.ts +9 -6
  63. package/src/codex/routing.ts +7 -1
  64. package/src/codex/shim.ts +1 -1
  65. package/src/codex/subagent-model-fallback.ts +22 -4
  66. package/src/combos/failover.ts +3 -0
  67. package/src/config/admitted-identity.ts +222 -0
  68. package/src/config/diagnostics.ts +22 -1
  69. package/src/config/feature-flags.ts +5 -0
  70. package/src/config/load-degrade.ts +18 -0
  71. package/src/config/proxy-env.ts +8 -2
  72. package/src/config/schema/compaction-triggers.ts +11 -0
  73. package/src/config/schema/config-schema.ts +4 -0
  74. package/src/config/schema/leaf-validators.ts +12 -0
  75. package/src/config.ts +2 -2
  76. package/src/generated/compatibility-version.json +249 -169
  77. package/src/grok/reset-coupons.ts +38 -19
  78. package/src/images/loop.ts +6 -1
  79. package/src/integrations/aside-profile-context.ts +37 -3
  80. package/src/integrations/aside-profile-journal.ts +68 -3
  81. package/src/integrations/aside-profiles.ts +128 -3
  82. package/src/integrations/mutation-plan.ts +815 -0
  83. package/src/integrations/writer.ts +85 -99
  84. package/src/lab/live/transport.ts +4 -0
  85. package/src/lab/live/types.ts +5 -0
  86. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  87. package/src/lib/admin-secrets.ts +9 -1
  88. package/src/lib/debug-log-buffer.ts +6 -1
  89. package/src/lib/debug.ts +23 -0
  90. package/src/lib/errors.ts +79 -0
  91. package/src/lib/http-response-semantics.ts +57 -0
  92. package/src/lib/lab-live-pinned-sender.ts +26 -12
  93. package/src/lib/pinned-http.ts +142 -2
  94. package/src/lib/plain-data.ts +103 -0
  95. package/src/lib/process-control.ts +13 -5
  96. package/src/lib/provider-outbound.ts +50 -2
  97. package/src/lib/socks5-fetch.ts +136 -26
  98. package/src/lib/spend-ledger-owner.ts +364 -0
  99. package/src/lib/spend-reservation-ledger.ts +218 -27
  100. package/src/lib/windows-system-proxy.ts +16 -11
  101. package/src/oauth/callback-server.ts +4 -3
  102. package/src/oauth/generic-account-failover.ts +1 -0
  103. package/src/oauth/health.ts +12 -1
  104. package/src/oauth/index.ts +3 -107
  105. package/src/oauth/login-flow-state.ts +127 -0
  106. package/src/providers/derive.ts +34 -17
  107. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  108. package/src/providers/key-failover.ts +66 -18
  109. package/src/providers/model-rename-migration.ts +55 -1
  110. package/src/providers/model-rename-startup.ts +7 -5
  111. package/src/providers/openai-virtual-models.ts +42 -2
  112. package/src/providers/quota/antigravity.ts +22 -2
  113. package/src/providers/quota/vendor-probes-key.ts +1 -1
  114. package/src/providers/registry/entries-core.ts +39 -17
  115. package/src/providers/registry/entries-extended.ts +35 -1
  116. package/src/providers/registry/model-ids.ts +168 -0
  117. package/src/providers/registry/model-seeds.ts +9 -0
  118. package/src/providers/registry/types.ts +2 -0
  119. package/src/providers/resolved-model-policy-merge.ts +167 -0
  120. package/src/providers/resolved-model-policy.ts +406 -0
  121. package/src/providers/stale-vision-classification-migration.ts +137 -0
  122. package/src/responses/apply-patch-envelope.ts +0 -12
  123. package/src/responses/freeform-wrapper-scan.ts +279 -0
  124. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  125. package/src/responses/progressive-freeform-input.ts +130 -0
  126. package/src/responses/reasoning-envelope.ts +30 -0
  127. package/src/responses/state.ts +5 -12
  128. package/src/responses/tool-name-aliases.ts +15 -1
  129. package/src/router.ts +91 -115
  130. package/src/routing/compatibility/behavior.ts +9 -0
  131. package/src/routing/compatibility/subject.ts +16 -1
  132. package/src/server/adapter-resolve.ts +9 -0
  133. package/src/server/auth-cors.ts +3 -0
  134. package/src/server/chat-completions.ts +5 -2
  135. package/src/server/claude-messages.ts +6 -3
  136. package/src/server/effort-row.ts +11 -3
  137. package/src/server/grok-responses-control-frame.ts +160 -1
  138. package/src/server/index/serve-options.ts +86 -29
  139. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  140. package/src/server/index/websocket-handler.ts +6 -1
  141. package/src/server/index.ts +16 -13
  142. package/src/server/management/aside-profile-routes.ts +266 -7
  143. package/src/server/management/config-routes.ts +18 -2
  144. package/src/server/management/context.ts +3 -0
  145. package/src/server/management/integration-routes.ts +287 -5
  146. package/src/server/management/metrics-routes.ts +20 -0
  147. package/src/server/management/model-rows.ts +224 -12
  148. package/src/server/management/route-registry.ts +14 -0
  149. package/src/server/management/shared.ts +10 -3
  150. package/src/server/management/system-restart.ts +7 -2
  151. package/src/server/management/system-routes.ts +2 -0
  152. package/src/server/management/usage-aggregate-cache.ts +4 -0
  153. package/src/server/management-api.ts +2 -0
  154. package/src/server/management-auth.ts +15 -1
  155. package/src/server/readiness.ts +29 -10
  156. package/src/server/relay-eager.ts +24 -2
  157. package/src/server/relay.ts +119 -10
  158. package/src/server/request-log.ts +55 -2
  159. package/src/server/request-metrics.ts +236 -0
  160. package/src/server/responses/adapter-continuation.ts +3 -3
  161. package/src/server/responses/adapter-dispatch.ts +11 -6
  162. package/src/server/responses/compact.ts +32 -10
  163. package/src/server/responses/compaction-routing.ts +111 -0
  164. package/src/server/responses/core-codex-account.ts +8 -3
  165. package/src/server/responses/core-combo.ts +7 -7
  166. package/src/server/responses/core-normalize.ts +6 -12
  167. package/src/server/responses/core-opaque-recovery.ts +1 -0
  168. package/src/server/responses/core-options.ts +4 -0
  169. package/src/server/responses/encrypted-payload.ts +20 -2
  170. package/src/server/responses/passthrough-delivery.ts +31 -14
  171. package/src/server/responses/passthrough-dispatch.ts +36 -9
  172. package/src/server/responses/policy-fallback.ts +5 -13
  173. package/src/server/responses/request-prepare.ts +67 -17
  174. package/src/server/responses/request-send-budget.ts +5 -1
  175. package/src/server/responses/request-sidecar-auth.ts +1 -1
  176. package/src/server/responses/request-transport.ts +2 -2
  177. package/src/server/responses/run-turn-execution.ts +25 -3
  178. package/src/server/responses/sidecar-execution.ts +17 -2
  179. package/src/server/responses/ws-upstream.ts +14 -27
  180. package/src/server/responses-custom-tool-repair.ts +27 -54
  181. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  182. package/src/server/sse-payload-rewrite.ts +1 -1
  183. package/src/tray/windows-tray.ps1 +155 -3
  184. package/src/types/config.ts +11 -3
  185. package/src/types/provider.ts +18 -0
  186. package/src/types/request.ts +2 -0
  187. package/src/types/tools.ts +14 -0
  188. package/src/types.ts +1 -0
  189. package/src/vision/eligibility.ts +88 -9
  190. package/src/vision/plan.ts +34 -10
  191. package/src/web-search/executor.ts +41 -2
  192. package/src/web-search/loop.ts +6 -1
  193. package/gui/dist/assets/index-C5IebErG.js +0 -136
  194. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -0,0 +1,236 @@
1
+ import type { ResponsesTerminalStatus } from "../bridge";
2
+ import type { AttemptRecoveryKind } from "../usage/log";
3
+
4
+ export const REQUEST_METRICS_PROTOCOLS = Object.freeze(["responses", "chat", "messages", "unknown"] as const);
5
+ export const REQUEST_METRICS_RESULTS = Object.freeze(["completed", "failed", "incomplete", "aborted"] as const);
6
+ export const REQUEST_METRICS_RECOVERY_CLASSES = Object.freeze([
7
+ "transient",
8
+ "connection",
9
+ "credential",
10
+ "rate_limit",
11
+ "payload",
12
+ "empty_completion",
13
+ "effort_downgrade",
14
+ "other",
15
+ ] as const);
16
+
17
+ export const REQUEST_DURATION_BUCKETS_SECONDS = Object.freeze([0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60] as const);
18
+ export const REQUEST_TTFT_BUCKETS_SECONDS = Object.freeze([0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30] as const);
19
+
20
+ export type RequestMetricsProtocol = typeof REQUEST_METRICS_PROTOCOLS[number];
21
+ export type RequestMetricsResult = typeof REQUEST_METRICS_RESULTS[number];
22
+ export type RequestMetricsRecoveryClass = typeof REQUEST_METRICS_RECOVERY_CLASSES[number];
23
+
24
+ export interface RequestMetricFinalFact {
25
+ protocol?: "responses" | "chat" | "messages";
26
+ status: number;
27
+ durationMs: number;
28
+ firstOutputMs?: number;
29
+ terminalStatus?: ResponsesTerminalStatus;
30
+ closeReason?: "terminal" | "client_cancel" | "non_stream" | "body_stall" | "body_overflow";
31
+ attempts?: ReadonlyArray<{
32
+ sendCount: number;
33
+ recoveryKinds: readonly AttemptRecoveryKind[];
34
+ }>;
35
+ spendSends?: number;
36
+ }
37
+
38
+ export interface RequestMetricsRecorder {
39
+ recordFinalRequest(fact: RequestMetricFinalFact): void;
40
+ }
41
+
42
+ export interface RequestMetricsSnapshotter {
43
+ snapshot(): string;
44
+ }
45
+
46
+ export interface RequestMetricsOwner extends RequestMetricsRecorder, RequestMetricsSnapshotter {
47
+ resetForTests(): void;
48
+ }
49
+
50
+ interface HistogramCell {
51
+ buckets: number[];
52
+ count: number;
53
+ sum: number;
54
+ }
55
+
56
+ const protocolCell = (value: RequestMetricsProtocol): number => REQUEST_METRICS_PROTOCOLS.indexOf(value);
57
+ const resultCell = (value: RequestMetricsResult): number => REQUEST_METRICS_RESULTS.indexOf(value);
58
+ const recoveryCell = (value: RequestMetricsRecoveryClass): number => REQUEST_METRICS_RECOVERY_CLASSES.indexOf(value);
59
+
60
+ function matrix(rows: number, columns: number): number[][] {
61
+ return Array.from({ length: rows }, () => Array.from({ length: columns }, () => 0));
62
+ }
63
+
64
+ function histograms(bounds: readonly number[]): HistogramCell[][] {
65
+ return Array.from({ length: REQUEST_METRICS_PROTOCOLS.length }, () => (
66
+ Array.from({ length: REQUEST_METRICS_RESULTS.length }, () => ({
67
+ buckets: Array.from({ length: bounds.length + 1 }, () => 0),
68
+ count: 0,
69
+ sum: 0,
70
+ }))
71
+ ));
72
+ }
73
+
74
+ function classifyResult(fact: RequestMetricFinalFact): RequestMetricsResult {
75
+ if (fact.closeReason === "client_cancel" || fact.status === 499) return "aborted";
76
+ if (fact.terminalStatus === "failed") return "failed";
77
+ if (fact.terminalStatus === "incomplete"
78
+ || fact.closeReason === "body_stall"
79
+ || fact.closeReason === "body_overflow") return "incomplete";
80
+ if (fact.terminalStatus === "completed") return "completed";
81
+ if (fact.terminalStatus === undefined
82
+ && (fact.status === 101 || (fact.status >= 200 && fact.status < 400))) return "completed";
83
+ return "failed";
84
+ }
85
+
86
+ function recoveryClass(kind: AttemptRecoveryKind): RequestMetricsRecoveryClass {
87
+ switch (kind) {
88
+ case "transient-5xx": return "transient";
89
+ case "connection-reset": return "connection";
90
+ case "oauth-401":
91
+ case "key-401": return "credential";
92
+ case "key-429":
93
+ case "rate-limit-429":
94
+ case "anthropic-oauth-429":
95
+ case "oauth-account-429": return "rate_limit";
96
+ case "image-413":
97
+ case "console-go-upload-retry":
98
+ case "opaque-blob-rejection": return "payload";
99
+ case "empty-completion": return "empty_completion";
100
+ case "reasoning-effort-downgrade": return "effort_downgrade";
101
+ default: return "other";
102
+ }
103
+ }
104
+
105
+ function observeHistogram(cell: HistogramCell, bounds: readonly number[], value: number): void {
106
+ if (!Number.isFinite(value) || value < 0) return;
107
+ cell.count += 1;
108
+ cell.sum += value;
109
+ for (let index = 0; index < bounds.length; index += 1) {
110
+ if (value <= bounds[index]!) cell.buckets[index]! += 1;
111
+ }
112
+ cell.buckets[bounds.length]! += 1;
113
+ }
114
+
115
+ function sampleLabels(protocol: RequestMetricsProtocol, result?: RequestMetricsResult): string {
116
+ return result === undefined
117
+ ? `{protocol="${protocol}"}`
118
+ : `{protocol="${protocol}",result="${result}"}`;
119
+ }
120
+
121
+ function appendHistogram(
122
+ lines: string[],
123
+ name: string,
124
+ help: string,
125
+ values: HistogramCell[][],
126
+ bounds: readonly number[],
127
+ ): void {
128
+ lines.push(`# HELP ${name} ${help}`, `# TYPE ${name} histogram`);
129
+ for (const protocol of REQUEST_METRICS_PROTOCOLS) {
130
+ for (const result of REQUEST_METRICS_RESULTS) {
131
+ const cell = values[protocolCell(protocol)]![resultCell(result)]!;
132
+ for (let index = 0; index < bounds.length; index += 1) {
133
+ lines.push(`${name}_bucket{protocol="${protocol}",result="${result}",le="${bounds[index]}"} ${cell.buckets[index]}`);
134
+ }
135
+ lines.push(`${name}_bucket{protocol="${protocol}",result="${result}",le="+Inf"} ${cell.buckets[bounds.length]}`);
136
+ lines.push(`${name}_sum${sampleLabels(protocol, result)} ${cell.sum}`);
137
+ lines.push(`${name}_count${sampleLabels(protocol, result)} ${cell.count}`);
138
+ }
139
+ }
140
+ }
141
+
142
+ export function createRequestMetricsOwner(
143
+ processStartTimeSeconds = Date.now() / 1000,
144
+ ): RequestMetricsOwner {
145
+ let logicalRequests = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RESULTS.length);
146
+ let physicalSends = Array.from({ length: REQUEST_METRICS_PROTOCOLS.length }, () => 0);
147
+ let recoveries = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RECOVERY_CLASSES.length);
148
+ let durations = histograms(REQUEST_DURATION_BUCKETS_SECONDS);
149
+ let ttft = histograms(REQUEST_TTFT_BUCKETS_SECONDS);
150
+ let missingTtft = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RESULTS.length);
151
+
152
+ return {
153
+ recordFinalRequest(fact): void {
154
+ const protocol: RequestMetricsProtocol = fact.protocol ?? "unknown";
155
+ const result = classifyResult(fact);
156
+ const protocolIndex = protocolCell(protocol);
157
+ const resultIndex = resultCell(result);
158
+ logicalRequests[protocolIndex]![resultIndex]! += 1;
159
+
160
+ const attempts = fact.attempts;
161
+ const sends = attempts === undefined
162
+ ? (Number.isInteger(fact.spendSends) && fact.spendSends! >= 0 ? fact.spendSends! : 0)
163
+ : attempts.reduce((total, attempt) => (
164
+ Number.isInteger(attempt.sendCount) && attempt.sendCount >= 0 ? total + attempt.sendCount : total
165
+ ), 0);
166
+ physicalSends[protocolIndex]! += sends;
167
+
168
+ for (const attempt of attempts ?? []) {
169
+ for (const kind of new Set(attempt.recoveryKinds)) {
170
+ recoveries[protocolIndex]![recoveryCell(recoveryClass(kind))]! += 1;
171
+ }
172
+ }
173
+
174
+ observeHistogram(durations[protocolIndex]![resultIndex]!, REQUEST_DURATION_BUCKETS_SECONDS, fact.durationMs / 1000);
175
+ if (typeof fact.firstOutputMs === "number" && Number.isFinite(fact.firstOutputMs) && fact.firstOutputMs >= 0) {
176
+ observeHistogram(ttft[protocolIndex]![resultIndex]!, REQUEST_TTFT_BUCKETS_SECONDS, fact.firstOutputMs / 1000);
177
+ } else {
178
+ missingTtft[protocolIndex]![resultIndex]! += 1;
179
+ }
180
+ },
181
+
182
+ snapshot(): string {
183
+ const lines: string[] = [
184
+ "# HELP opencodex_logical_requests_total Finalized logical requests in this process.",
185
+ "# TYPE opencodex_logical_requests_total counter",
186
+ ];
187
+ for (const protocol of REQUEST_METRICS_PROTOCOLS) {
188
+ for (const result of REQUEST_METRICS_RESULTS) {
189
+ lines.push(`opencodex_logical_requests_total${sampleLabels(protocol, result)} ${logicalRequests[protocolCell(protocol)]![resultCell(result)]}`);
190
+ }
191
+ }
192
+ lines.push(
193
+ "# HELP opencodex_physical_sends_total Upstream sends made by finalized logical requests in this process.",
194
+ "# TYPE opencodex_physical_sends_total counter",
195
+ );
196
+ for (const protocol of REQUEST_METRICS_PROTOCOLS) {
197
+ lines.push(`opencodex_physical_sends_total${sampleLabels(protocol)} ${physicalSends[protocolCell(protocol)]}`);
198
+ }
199
+ lines.push(
200
+ "# HELP opencodex_recoveries_total Distinct recovery kinds observed per physical attempt in this process.",
201
+ "# TYPE opencodex_recoveries_total counter",
202
+ );
203
+ for (const protocol of REQUEST_METRICS_PROTOCOLS) {
204
+ for (const recovery of REQUEST_METRICS_RECOVERY_CLASSES) {
205
+ lines.push(`opencodex_recoveries_total{protocol="${protocol}",recovery="${recovery}"} ${recoveries[protocolCell(protocol)]![recoveryCell(recovery)]}`);
206
+ }
207
+ }
208
+ appendHistogram(lines, "opencodex_request_duration_seconds", "Finalized logical request duration in seconds.", durations, REQUEST_DURATION_BUCKETS_SECONDS);
209
+ appendHistogram(lines, "opencodex_ttft_seconds", "Observed time to first output in seconds.", ttft, REQUEST_TTFT_BUCKETS_SECONDS);
210
+ lines.push(
211
+ "# HELP opencodex_ttft_missing_total Finalized logical requests without an observed time to first output.",
212
+ "# TYPE opencodex_ttft_missing_total counter",
213
+ );
214
+ for (const protocol of REQUEST_METRICS_PROTOCOLS) {
215
+ for (const result of REQUEST_METRICS_RESULTS) {
216
+ lines.push(`opencodex_ttft_missing_total${sampleLabels(protocol, result)} ${missingTtft[protocolCell(protocol)]![resultCell(result)]}`);
217
+ }
218
+ }
219
+ lines.push(
220
+ "# HELP opencodex_metrics_process_start_time_seconds Unix time when this process metrics owner started.",
221
+ "# TYPE opencodex_metrics_process_start_time_seconds gauge",
222
+ `opencodex_metrics_process_start_time_seconds ${processStartTimeSeconds}`,
223
+ );
224
+ return `${lines.join("\n")}\n`;
225
+ },
226
+
227
+ resetForTests(): void {
228
+ logicalRequests = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RESULTS.length);
229
+ physicalSends = Array.from({ length: REQUEST_METRICS_PROTOCOLS.length }, () => 0);
230
+ recoveries = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RECOVERY_CLASSES.length);
231
+ durations = histograms(REQUEST_DURATION_BUCKETS_SECONDS);
232
+ ttft = histograms(REQUEST_TTFT_BUCKETS_SECONDS);
233
+ missingTtft = matrix(REQUEST_METRICS_PROTOCOLS.length, REQUEST_METRICS_RESULTS.length);
234
+ },
235
+ };
236
+ }
@@ -338,7 +338,7 @@ export function createAdapterContinuations(
338
338
  route.provider = rotated;
339
339
  invalidateSameTargetRequest();
340
340
  transportState.activeAdapter = resolveSelectionAdapter(
341
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
341
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
342
342
  config.cacheRetention,
343
343
  );
344
344
  bindRouteReasoningReplayScope({
@@ -384,7 +384,7 @@ export function createAdapterContinuations(
384
384
  invalidateSameTargetRequest();
385
385
  logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
386
386
  transportState.activeAdapter = resolveSelectionAdapter(
387
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
387
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
388
388
  config.cacheRetention,
389
389
  );
390
390
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
@@ -449,7 +449,7 @@ export function createAdapterContinuations(
449
449
  if (applied) {
450
450
  invalidateSameTargetRequest();
451
451
  transportState.activeAdapter = resolveSelectionAdapter(
452
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
452
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
453
453
  config.cacheRetention,
454
454
  );
455
455
  bindRouteReasoningReplayScope({
@@ -246,7 +246,11 @@ export async function prepareAdapterExchange(
246
246
  return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
247
247
  }
248
248
  try {
249
- initialRequest = await transportState.activeAdapter.buildRequest(parsed, { headers: requestState.selectedForwardHeaders, translatorBudget });
249
+ initialRequest = await transportState.activeAdapter.buildRequest(parsed, {
250
+ headers: requestState.selectedForwardHeaders,
251
+ translatorBudget,
252
+ abortSignal: upstream.signal,
253
+ });
250
254
  refreshRequestToolAliases(initialRequest);
251
255
  recordAdapterReasoning(logCtx, initialRequest);
252
256
  recordAdapterTier(logCtx, initialRequest);
@@ -405,6 +409,7 @@ export async function prepareAdapterExchange(
405
409
  retryRequest = await transportState.activeAdapter.buildRequest(parsed, {
406
410
  headers: requestState.selectedForwardHeaders,
407
411
  translatorBudget,
412
+ abortSignal: upstream.signal,
408
413
  ...(transportState.imageTierBias > 0 ? { imageTierBias: transportState.imageTierBias } : {}),
409
414
  });
410
415
  recordAdapterReasoning(logCtx, retryRequest);
@@ -581,7 +586,7 @@ export async function prepareAdapterExchange(
581
586
  route.provider = refreshedProvider;
582
587
  invalidateSameTargetRequest();
583
588
  transportState.activeAdapter = resolveSelectionAdapter(
584
- resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire),
589
+ resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire, route.staticPolicy),
585
590
  config.cacheRetention,
586
591
  );
587
592
  bindRouteReasoningReplayScope({
@@ -614,7 +619,7 @@ export async function prepareAdapterExchange(
614
619
  route.provider = rotated;
615
620
  invalidateSameTargetRequest();
616
621
  transportState.activeAdapter = resolveSelectionAdapter(
617
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
622
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
618
623
  config.cacheRetention,
619
624
  );
620
625
  bindRouteReasoningReplayScope({
@@ -714,7 +719,7 @@ export async function prepareAdapterExchange(
714
719
  route.provider = rotated;
715
720
  invalidateSameTargetRequest();
716
721
  transportState.activeAdapter = resolveSelectionAdapter(
717
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
722
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
718
723
  config.cacheRetention,
719
724
  );
720
725
  bindRouteReasoningReplayScope({
@@ -757,7 +762,7 @@ export async function prepareAdapterExchange(
757
762
  invalidateSameTargetRequest();
758
763
  logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
759
764
  transportState.activeAdapter = resolveSelectionAdapter(
760
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
765
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
761
766
  config.cacheRetention,
762
767
  );
763
768
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
@@ -832,7 +837,7 @@ export async function prepareAdapterExchange(
832
837
  }
833
838
  invalidateSameTargetRequest();
834
839
  transportState.activeAdapter = resolveSelectionAdapter(
835
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
840
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
836
841
  config.cacheRetention,
837
842
  );
838
843
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
@@ -10,6 +10,7 @@ import { parseRequest } from "../../responses/parser";
10
10
  import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
11
11
  import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
12
12
  import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../../responses/state";
13
+ import { repairLegacyDottedToolCallNames } from "../../responses/legacy-dotted-tool-name-repair";
13
14
  import { NoEligiblePolicyCandidateError, routeCompactionModel } from "../../router";
14
15
  import { evidenceFromBody } from "../../routing/request-evidence";
15
16
  import {
@@ -124,7 +125,12 @@ import { slugsEquivalent } from "../../providers/slug-codec";
124
125
  import { decideTier, tierValueAfterDecision } from "../../providers/fastwire";
125
126
  import { fastPolicyForModel } from "../../providers/service-tier";
126
127
  import { parseFastOnlyRowId } from "../fast-row";
127
- import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
128
+ import { captureOpenAiVirtualWirePolicy, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
129
+ import {
130
+ applyCompactionRoutingOverride,
131
+ compactionRoutingKeepsProviderIdentity,
132
+ type CompactionRoutingOverride,
133
+ } from "./compaction-routing";
128
134
  import { isUsageDebugEnabled } from "../../usage/debug";
129
135
  import {
130
136
  readJsonRequestBody,
@@ -271,6 +277,7 @@ function compactHandoffRoute(
271
277
  }
272
278
 
273
279
  export interface HandleResponsesCompactOptions {
280
+ compactionRoutingOverride?: CompactionRoutingOverride | null;
274
281
  nativeMainRefreshDependencies?: NativeMainRefreshDependencies;
275
282
  /** Release the listener's idle guard only after the complete request body is accepted. */
276
283
  onRequestBodyRead?: () => void;
@@ -603,6 +610,9 @@ export async function handleResponsesCompact(
603
610
  if (!body || typeof body !== "object" || Array.isArray(body)) {
604
611
  return formatErrorResponse(400, "invalid_request_error", "Invalid compaction request body");
605
612
  }
613
+ if (!options.compactionRoutingOverride) {
614
+ options = { ...options, compactionRoutingOverride: applyCompactionRoutingOverride(body, req.headers, config, { endpoint: "compact" }) };
615
+ }
606
616
  const raw = body as { model?: unknown; input?: unknown };
607
617
  if (typeof raw.model !== "string" || raw.model.length === 0) {
608
618
  return formatErrorResponse(400, "invalid_request_error", "compaction request requires a model");
@@ -617,11 +627,12 @@ export async function handleResponsesCompact(
617
627
  // The client's own selector, kept for the request log: `raw.model` is rewritten to the
618
628
  // base id above, and logCtx.requestedModel is assigned from it further down, so without
619
629
  // this the log would lose which id the client actually asked for.
620
- const compactRequestedModel = compactFastRow ? compactFastRow.baseId + "--fast" : raw.model;
630
+ const compactRequestedModel = options.compactionRoutingOverride?.sourceModel
631
+ ?? (compactFastRow ? compactFastRow.baseId + "--fast" : raw.model);
621
632
 
622
633
  // Recall the last completed client-visible bare model after a combo switch (#3891).
623
634
  // Configured selectors take precedence over this implicit session hint.
624
- if (typeof compactModel === "string" && !compactModel.includes("/") && !compactFastRow
635
+ if (!options.compactionRoutingOverride && typeof compactModel === "string" && !compactModel.includes("/") && !compactFastRow
625
636
  && !resolveComboId(config, compactModel)) {
626
637
  const recalledComboId = recallComboForLane(config, sessionLaneIdFromRequest(req.headers), compactModel);
627
638
  if (recalledComboId) {
@@ -684,6 +695,7 @@ export async function handleResponsesCompact(
684
695
  const virtual = resolveOpenAiCompactModel(route.providerName, selectedModelId);
685
696
  if (virtual) {
686
697
  route.modelId = virtual.wireModelId;
698
+ captureOpenAiVirtualWirePolicy(route, virtual);
687
699
  logCtx.model = virtual.selectedModelId;
688
700
  logCtx.resolvedModel = virtual.wireModelId;
689
701
  } else {
@@ -747,7 +759,12 @@ export async function handleResponsesCompact(
747
759
  // no budget at all, so `handleResponsesInner` minted a fresh four after the native attempt
748
760
  // had already spent some of the first one.
749
761
  const sendBudget: RequestExecutionBudget = options.sendBudget ?? createRequestExecutionBudget();
750
- if (supportsNativeResponsesCompactEndpoint(route.providerName, route.provider) && !accountGatedCompactWireModel && !route.combo) {
762
+ // A manual override onto another backend must not mint ciphertext the conversation model cannot replay.
763
+ const manualOverrideCrossesProvider = options.compactionRoutingOverride
764
+ ? !compactionRoutingKeepsProviderIdentity(config, options.compactionRoutingOverride, route)
765
+ : false;
766
+ if (supportsNativeResponsesCompactEndpoint(route.providerName, route.provider) && !accountGatedCompactWireModel && !route.combo
767
+ && !manualOverrideCrossesProvider) {
751
768
  if (req.signal.aborted) {
752
769
  return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
753
770
  }
@@ -845,7 +862,12 @@ export async function handleResponsesCompact(
845
862
  // The regular /v1/responses path applies sanitizeReasoningInputContent via the adapter's
846
863
  // buildRequest, but the compact endpoint forwards directly. Apply the same sanitizer here
847
864
  // so routed-model reasoning items (reasoning_text content) don't 400 the ChatGPT backend.
848
- const compactBody = sanitizeReasoningInputContent(compactBodyRaw) as typeof compactBodyRaw;
865
+ // #5095: the compact endpoint forwards `raw` directly, so it needs the same legacy dotted
866
+ // call-name repair the adapter applies. A damaged item here refuses the compaction itself,
867
+ // which is the request a long task depends on to keep going.
868
+ const compactBody = repairLegacyDottedToolCallNames(
869
+ sanitizeReasoningInputContent(compactBodyRaw),
870
+ ) as typeof compactBodyRaw;
849
871
  {
850
872
  const binding = conversationStateBindingFromAuth(authCtx, codexPoolAffinityKey(req.headers));
851
873
  if (binding) {
@@ -1303,9 +1325,9 @@ export async function handleResponsesCompact(
1303
1325
  // synthetic buffer errors are not upstream bodies and stay uninspected.
1304
1326
  if (buffered.ok) {
1305
1327
  inspectResponseLogJson(logCtx, await buffered.clone().text());
1306
- forgetCompactHandoffRoute(req, admission);
1328
+ if (!options.compactionRoutingOverride) forgetCompactHandoffRoute(req, admission);
1307
1329
  rememberServingConversationStateIssuer(outcomeCtx, codexPoolAffinityKey(req.headers));
1308
- } else if (quotaFailure && !storedPool401ReplayAttempted) {
1330
+ } else if (!options.compactionRoutingOverride && quotaFailure && !storedPool401ReplayAttempted) {
1309
1331
  const fallbackModel = compactHandoffRoute(req, admission, raw.model);
1310
1332
  if (fallbackModel && !req.signal.aborted) {
1311
1333
  const fallbackReq = new Request(req.url, {
@@ -1368,7 +1390,7 @@ export async function handleResponsesCompact(
1368
1390
  // The routed compaction turn is a handoff inside the same logical request, so it draws the
1369
1391
  // REMAINDER. Minting here is what let a native attempt spend three sends and the routed
1370
1392
  // fallback spend four more.
1371
- const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease, sendBudget, ...(admission ? { admission } : {}) });
1393
+ const response = await handleResponses(internalReq, config, logCtx, { abortSignal: req.signal, turnAdmissionLease, sendBudget, compactionRoutingOverride: options.compactionRoutingOverride, ...(admission ? { admission } : {}) });
1372
1394
  if (!response.ok) return response;
1373
1395
  let json: { output?: unknown[]; status?: unknown; error?: unknown };
1374
1396
  if (response.headers.get("content-type")?.includes("text/event-stream")) {
@@ -1438,7 +1460,7 @@ export async function handleResponsesCompact(
1438
1460
  const result = new Response(JSON.stringify({ output: compactionItems }), {
1439
1461
  headers: { "Content-Type": "application/json" },
1440
1462
  });
1441
- rememberCompactHandoffRoute(req, admission, raw.model);
1463
+ if (!options.compactionRoutingOverride) rememberCompactHandoffRoute(req, admission, raw.model);
1442
1464
  return result;
1443
1465
  }
1444
1466
  const encrypted = compactionItems[0]!.encrypted_content;
@@ -1449,6 +1471,6 @@ export async function handleResponsesCompact(
1449
1471
  }
1450
1472
  const summary = decoded;
1451
1473
  const output = buildCompactV1Output(extractCompactUserMessages(inputItems), summary);
1452
- rememberCompactHandoffRoute(req, admission, raw.model);
1474
+ if (!options.compactionRoutingOverride) rememberCompactHandoffRoute(req, admission, raw.model);
1453
1475
  return new Response(JSON.stringify({ output }), { headers: { "Content-Type": "application/json" } });
1454
1476
  }
@@ -0,0 +1,111 @@
1
+ import type { OcxConfig } from "../../types";
2
+ import { isDeclaredReasoningEffort } from "../../reasoning-effort";
3
+ import { COMPACTION_TRIGGERS } from "../../config/schema/compaction-triggers";
4
+ import { routeConcreteModel, type RouteResult } from "../../router";
5
+ import { resolveComboId } from "../../combos/identifiers";
6
+ import { recallComboForLane } from "./combo-session-recall";
7
+ import { sessionLaneIdFromRequest } from "../request-log-conversation";
8
+
9
+ /** `sourceModel` is the conversation's own selector before the rewrite. */
10
+ export interface CompactionRoutingOverride {
11
+ sourceModel: string;
12
+ /** Combo the lane remembers for a bare `sourceModel` (#3891); the conversation resumes there, not on the bare route. */
13
+ sourceCombo?: string;
14
+ /** Combo the configured override resolves to; its children route concretely but stay portable. */
15
+ targetCombo?: string;
16
+ }
17
+
18
+ function record(value: unknown): Record<string, unknown> | undefined {
19
+ return value !== null && typeof value === "object" && !Array.isArray(value)
20
+ ? value as Record<string, unknown>
21
+ : undefined;
22
+ }
23
+
24
+ /**
25
+ * Triggers this override covers. Absent means manual only, which is the narrowest
26
+ * reading of the setting and leaves automatic compaction exactly as it routes today.
27
+ * A hand-edited value the schema would have rejected disables the override rather
28
+ * than widening it, so a malformed edit can never route more than it names.
29
+ */
30
+ function configuredTriggers(value: unknown): readonly string[] | null {
31
+ if (value === undefined) return ["manual"];
32
+ if (!Array.isArray(value) || value.length === 0) return null;
33
+ if (!value.every(entry => typeof entry === "string" && (COMPACTION_TRIGGERS as readonly string[]).includes(entry))) return null;
34
+ if (new Set(value).size !== value.length) return null;
35
+ return value as readonly string[];
36
+ }
37
+
38
+ export interface CompactionRoutingOverrideOptions {
39
+ /** `responses` requires a `compaction_trigger` input item; the native compact endpoint carries none. */
40
+ endpoint?: "responses" | "compact";
41
+ transport?: "websocket";
42
+ }
43
+
44
+ export function applyCompactionRoutingOverride(
45
+ body: unknown,
46
+ headers: Headers,
47
+ config: OcxConfig,
48
+ options: CompactionRoutingOverrideOptions = {},
49
+ ): CompactionRoutingOverride | null {
50
+ const override = config.compactionRouting;
51
+ const raw = record(body);
52
+ if (!raw || typeof raw.model !== "string" || !raw.model.trim()
53
+ || typeof override?.model !== "string" || !override.model.trim()) return null;
54
+ if (override.reasoningEffort !== undefined
55
+ && (typeof override.reasoningEffort !== "string" || !isDeclaredReasoningEffort(override.reasoningEffort))) return null;
56
+ const triggers = configuredTriggers(override.triggers);
57
+ if (!triggers) return null;
58
+ if (options.endpoint !== "compact"
59
+ && !(Array.isArray(raw.input) && raw.input.some(item => record(item)?.type === "compaction_trigger"))) return null;
60
+
61
+ const metadata: unknown[] = [];
62
+ const header = headers.get("x-codex-turn-metadata");
63
+ if (options.transport !== "websocket" && header !== null) metadata.push(header);
64
+ const client = record(raw.client_metadata);
65
+ if (client && Object.hasOwn(client, "x-codex-turn-metadata")) metadata.push(client["x-codex-turn-metadata"]);
66
+ if (metadata.length === 0) return null;
67
+ let trigger: string | undefined;
68
+ for (const value of metadata) {
69
+ if (typeof value !== "string") return null;
70
+ try {
71
+ const parsed = record(JSON.parse(value));
72
+ if (parsed?.request_kind !== "compaction") return null;
73
+ const carried = record(parsed.compaction)?.trigger;
74
+ if (typeof carried !== "string" || !triggers.includes(carried)) return null;
75
+ // Copies that name different triggers are not agreement, and picking either one would
76
+ // let a caller widen an override by disagreeing with itself.
77
+ if (trigger !== undefined && trigger !== carried) return null;
78
+ trigger = carried;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+ if (trigger === undefined) return null;
84
+
85
+ const sourceModel = raw.model;
86
+ const sourceCombo = recallComboForLane(config, sessionLaneIdFromRequest(headers), sourceModel);
87
+ const targetCombo = resolveComboId(config, override.model.trim()) ?? undefined;
88
+ raw.model = override.model.trim();
89
+ if (override.reasoningEffort !== undefined) {
90
+ raw.reasoning = { ...record(raw.reasoning), effort: override.reasoningEffort };
91
+ }
92
+ return { sourceModel, ...(sourceCombo ? { sourceCombo } : {}), ...(targetCombo ? { targetCombo } : {}) };
93
+ }
94
+
95
+ /** Same provider identity keeps caller auth and may use native compact; its ciphertext replays only there. */
96
+ export function compactionRoutingKeepsProviderIdentity(
97
+ config: OcxConfig,
98
+ override: CompactionRoutingOverride,
99
+ route: RouteResult,
100
+ ): boolean {
101
+ if (route.combo || override.sourceCombo || override.targetCombo || resolveComboId(config, override.sourceModel)) return false;
102
+ let source: RouteResult;
103
+ try {
104
+ source = routeConcreteModel(config, override.sourceModel);
105
+ } catch {
106
+ return false;
107
+ }
108
+ return source.providerName === route.providerName
109
+ && source.codexAccountMode === route.codexAccountMode
110
+ && source.codexAccountNamespace === route.codexAccountNamespace;
111
+ }
@@ -328,7 +328,8 @@ export interface CodexPoolAccountRetryArgs {
328
328
  /** Sanitized caller input, before any selected Pool credential was materialized. */
329
329
  callerAuthHeaders: Headers;
330
330
  config: OcxConfig;
331
- route: { providerName: string; modelId: string; provider: OcxProviderConfig };
331
+ /** Actual routed result narrowed to the fields this retry consumes. */
332
+ route: Pick<RouteResult, "providerName" | "modelId" | "provider" | "staticPolicy">;
332
333
  parsed: OcxParsedRequest;
333
334
  logCtx: RequestLogContext;
334
335
  options: {
@@ -681,7 +682,7 @@ export async function retryCodexPoolOnAlternateAccount(
681
682
  "pool",
682
683
  );
683
684
  const retryAdapter = resolveAdapter(
684
- resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider, inboundWire),
685
+ resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider, inboundWire, route.staticPolicy),
685
686
  config.cacheRetention,
686
687
  route.providerName,
687
688
  );
@@ -841,7 +842,11 @@ export async function retryCodexPoolOnAlternateAccount(
841
842
  parsed.modelId,
842
843
  );
843
844
  if (retryModelDenial !== undefined) {
844
- recordCodexModelDenialEvidence(retryAuthCtx.accountId, retryModelDenial);
845
+ recordCodexModelDenialEvidence(
846
+ retryAuthCtx.accountId,
847
+ retryModelDenial,
848
+ retryAuthCtx.kind === "pool" ? retryAuthCtx.generation : undefined,
849
+ );
845
850
  }
846
851
  if (!retrySameConfirmedAccount || retrySendCount >= maxRetrySends) break;
847
852
  // Caller-owned main is an alternate-account replay and can never enter the bounded
@@ -29,7 +29,6 @@ import {
29
29
  import { formatErrorResponse } from "../../bridge";
30
30
  import {
31
31
  expandPreviousResponseInput,
32
- previousResponseScopeMismatch,
33
32
  previousResponseReplayFailure,
34
33
  previousResponseProviderState,
35
34
  } from "../../responses/state";
@@ -201,11 +200,12 @@ export async function executeComboResponses(
201
200
  // imageInput is disabled (and so targets see the full replayed input).
202
201
  const inboundClientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
203
202
  const body = expandPreviousResponseInput(rawBody, inboundClientThreadId);
204
- const scopeMismatch = previousResponseScopeMismatch(body);
205
- if (scopeMismatch) {
206
- console.warn("[opencodex] dropped a previous_response_id with a mismatched client task scope; continuing fresh");
203
+ const replayFailure = previousResponseReplayFailure(body);
204
+ if (replayFailure?.reason === "scope_mismatch") {
205
+ console.warn("[opencodex] refusing continuation because the client task scope does not match replay state");
207
206
  }
208
- if (previousResponseReplayFailure(body)) {
207
+ // Local replay failures require full client replay.
208
+ if (replayFailure) {
209
209
  return formatErrorResponse(
210
210
  400,
211
211
  "previous_response_not_found",
@@ -235,7 +235,7 @@ export async function executeComboResponses(
235
235
  sourceBody: body,
236
236
  previousResponseInputExpanded: body !== rawBody
237
237
  && typeof (body as { previous_response_id?: unknown }).previous_response_id === "string",
238
- providerContinuation: !scopeMismatch && body !== rawBody && requestedPreviousId
238
+ providerContinuation: body !== rawBody && requestedPreviousId
239
239
  ? previousResponseProviderState(requestedPreviousId)
240
240
  : undefined,
241
241
  recoveredPlaintext: false,
@@ -521,7 +521,7 @@ export async function executeComboResponses(
521
521
  // The live config can change while the child is streaming. Never retain credentials.
522
522
  const currentCombo = getCombo(config, comboId);
523
523
  const provider = config.providers[completedTarget.provider];
524
- if (Object.hasOwn(config.providers, completedTarget.provider)
524
+ if (!options.compactionRoutingOverride && Object.hasOwn(config.providers, completedTarget.provider)
525
525
  && provider && provider.disabled !== true
526
526
  && currentCombo?.targets.some(target => targetKey(target) === targetKey(completedTarget))) {
527
527
  rememberComboForLane(sessionLaneIdFromRequest(req.headers), comboId, completedTarget, model, writerGeneration);