@iislee/opencodex 2.31.0 → 2.35.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 (261) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  4. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -1
  7. package/src/adapters/anthropic.ts +104 -15
  8. package/src/adapters/base.ts +29 -1
  9. package/src/adapters/command-code.ts +42 -6
  10. package/src/adapters/cursor/call-id.ts +44 -0
  11. package/src/adapters/cursor/catalog.ts +541 -0
  12. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  13. package/src/adapters/cursor/cursor-errors.ts +15 -0
  14. package/src/adapters/cursor/discovery.ts +69 -18
  15. package/src/adapters/cursor/effort-map.ts +79 -1
  16. package/src/adapters/cursor/envelope-echo.ts +290 -0
  17. package/src/adapters/cursor/live-models.ts +7 -2
  18. package/src/adapters/cursor/live-transport.ts +14 -0
  19. package/src/adapters/cursor/message-mapper.ts +4 -1
  20. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  21. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  22. package/src/adapters/cursor/native-exec-network.ts +1 -1
  23. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  24. package/src/adapters/cursor/native-exec.ts +20 -0
  25. package/src/adapters/cursor/protobuf-request.ts +68 -14
  26. package/src/adapters/cursor/request-builder.ts +40 -10
  27. package/src/adapters/cursor/tool-definitions.ts +14 -4
  28. package/src/adapters/cursor/tool-result-normalize.ts +28 -5
  29. package/src/adapters/cursor/types.ts +20 -0
  30. package/src/adapters/cursor.ts +209 -38
  31. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  32. package/src/adapters/google-antigravity-replay.ts +173 -25
  33. package/src/adapters/google-antigravity-wire.ts +5 -0
  34. package/src/adapters/google-errors.ts +44 -12
  35. package/src/adapters/google.ts +136 -24
  36. package/src/adapters/image.ts +1 -1
  37. package/src/adapters/kiro-constants.ts +27 -0
  38. package/src/adapters/kiro-tools.ts +55 -16
  39. package/src/adapters/kiro.ts +182 -23
  40. package/src/adapters/openai-chat.ts +300 -242
  41. package/src/adapters/openai-responses.ts +353 -27
  42. package/src/adapters/run-turn-queue.ts +36 -1
  43. package/src/adapters/tool-catalog-nudge.ts +4 -3
  44. package/src/adapters/xai-tool-schema.ts +436 -0
  45. package/src/adapters/xai-web-search.ts +17 -16
  46. package/src/bridge.ts +90 -37
  47. package/src/chat/inbound.ts +14 -1
  48. package/src/chat/outbound.ts +15 -7
  49. package/src/claude/agents-inject.ts +8 -1
  50. package/src/claude/context-windows.ts +16 -9
  51. package/src/claude/outbound.ts +24 -11
  52. package/src/cli/access.ts +46 -3
  53. package/src/cli/account-api.ts +116 -19
  54. package/src/cli/account-extended.ts +262 -29
  55. package/src/cli/account-main.ts +12 -12
  56. package/src/cli/account.ts +69 -15
  57. package/src/cli/agent.ts +8 -1
  58. package/src/cli/alias.ts +66 -0
  59. package/src/cli/capabilities-command.ts +94 -0
  60. package/src/cli/capabilities.ts +496 -0
  61. package/src/cli/claude-desktop.ts +31 -11
  62. package/src/cli/claude.ts +26 -1
  63. package/src/cli/dispatch.ts +256 -28
  64. package/src/cli/doctor.ts +126 -14
  65. package/src/cli/help.ts +17 -6
  66. package/src/cli/index.ts +39 -9
  67. package/src/cli/init.ts +1 -0
  68. package/src/cli/inspect.ts +230 -0
  69. package/src/cli/models-runtime.ts +95 -0
  70. package/src/cli/models.ts +26 -10
  71. package/src/cli/observe.ts +31 -8
  72. package/src/cli/provider-runtime.ts +16 -2
  73. package/src/cli/provider.ts +2 -1
  74. package/src/cli/registry.ts +49 -10
  75. package/src/cli/runtime-api.ts +51 -7
  76. package/src/cli/status.ts +41 -1
  77. package/src/cli/storage.ts +234 -0
  78. package/src/cli/system-command.ts +16 -0
  79. package/src/cli/system-restart-client.ts +1 -1
  80. package/src/cli/usage-report.ts +184 -0
  81. package/src/cli/v2.ts +34 -10
  82. package/src/cli/version-skew.ts +46 -0
  83. package/src/clients/config-export.ts +85 -3
  84. package/src/clients/effective-status.ts +30 -2
  85. package/src/clients/probes/cline.ts +81 -0
  86. package/src/codex/account-label.ts +21 -0
  87. package/src/codex/account-pause.ts +2 -1
  88. package/src/codex/account-priority.ts +3 -2
  89. package/src/codex/app-server-processes.ts +83 -7
  90. package/src/codex/auth-api.ts +50 -10
  91. package/src/codex/auth-context.ts +21 -18
  92. package/src/codex/autostart-health.ts +16 -0
  93. package/src/codex/catalog/aggregation.ts +18 -0
  94. package/src/codex/catalog/effort.ts +18 -3
  95. package/src/codex/catalog/metadata.ts +27 -1
  96. package/src/codex/catalog/native-models.ts +5 -2
  97. package/src/codex/catalog/parsing.ts +72 -27
  98. package/src/codex/catalog/provider-fetch.ts +187 -31
  99. package/src/codex/catalog/sync.ts +131 -5
  100. package/src/codex/catalog.ts +1 -1
  101. package/src/codex/codex-write-lock.ts +3 -1
  102. package/src/codex/convergence-types.ts +1 -1
  103. package/src/codex/convergence.ts +27 -2
  104. package/src/codex/desired-state.ts +2 -2
  105. package/src/codex/desktop-app-restart.ts +355 -0
  106. package/src/codex/history-job.ts +32 -3
  107. package/src/codex/history-manifest.ts +112 -0
  108. package/src/codex/history-migration-guardian.ts +5 -5
  109. package/src/codex/history-provider.ts +825 -247
  110. package/src/codex/history-worker.ts +8 -5
  111. package/src/codex/inject-coordination.ts +83 -0
  112. package/src/codex/inject.ts +63 -22
  113. package/src/codex/injected-marker.ts +1 -1
  114. package/src/codex/internal/history-writer.ts +4 -3
  115. package/src/codex/log-guard/inspect.ts +22 -4
  116. package/src/codex/model-entitlements.ts +9 -2
  117. package/src/codex/native-profile-startup.ts +157 -27
  118. package/src/codex/native-residue.ts +26 -33
  119. package/src/codex/prompt-layers.ts +371 -25
  120. package/src/codex/prompt-text-probe.ts +238 -0
  121. package/src/codex/quota.ts +91 -10
  122. package/src/codex/routing.ts +9 -0
  123. package/src/codex/shim.ts +56 -3
  124. package/src/codex/subagent-model-fallback.ts +104 -25
  125. package/src/codex/transition-state.ts +116 -8
  126. package/src/combos/failover.ts +27 -0
  127. package/src/combos/types.ts +10 -0
  128. package/src/compatibility/index.ts +26 -0
  129. package/src/compatibility/manifest.ts +253 -0
  130. package/src/compatibility/openai-responses.ts +121 -0
  131. package/src/config/atomic-write.ts +219 -0
  132. package/src/config/paths.ts +40 -0
  133. package/src/config/process-state.ts +309 -0
  134. package/src/config/provider-validation.ts +177 -0
  135. package/src/config/rebase-provenance.ts +68 -0
  136. package/src/config.ts +206 -824
  137. package/src/generated/compatibility-version.json +373 -213
  138. package/src/images/loop.ts +5 -4
  139. package/src/images/plan.ts +5 -4
  140. package/src/integrations/ownership-policy.ts +160 -0
  141. package/src/integrations/ownership.ts +46 -2
  142. package/src/integrations/registry.ts +7 -0
  143. package/src/integrations/state.ts +79 -7
  144. package/src/integrations/writer.ts +26 -2
  145. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  146. package/src/lab/fabric/producer-child.ts +1 -1
  147. package/src/lib/admin-secrets.ts +24 -0
  148. package/src/lib/bounded-body.ts +14 -2
  149. package/src/lib/config-ownership.ts +20 -0
  150. package/src/lib/errors.ts +36 -3
  151. package/src/lib/package-tree-integrity.ts +101 -0
  152. package/src/lib/process-control.ts +2 -1
  153. package/src/lib/service-secrets.ts +15 -0
  154. package/src/lib/tool-argument-integers.ts +56 -5
  155. package/src/oauth/callback-server.ts +22 -2
  156. package/src/oauth/cline.ts +203 -0
  157. package/src/oauth/generic-account-failover.ts +231 -0
  158. package/src/oauth/health.ts +1 -1
  159. package/src/oauth/index.ts +65 -6
  160. package/src/oauth/key-providers.ts +10 -1
  161. package/src/oauth/kimi.ts +9 -1
  162. package/src/oauth/kiro.ts +45 -0
  163. package/src/oauth/local-token-detect.ts +2 -0
  164. package/src/oauth/open-browser-choice.ts +26 -0
  165. package/src/oauth/store.ts +14 -5
  166. package/src/oauth/types.ts +15 -0
  167. package/src/providers/auto-compact-budget.ts +65 -0
  168. package/src/providers/command-code-efforts.ts +38 -6
  169. package/src/providers/context-cap.ts +4 -3
  170. package/src/providers/default-aliases.ts +65 -0
  171. package/src/providers/derive.ts +28 -0
  172. package/src/providers/fastwire.ts +7 -1
  173. package/src/providers/label.ts +34 -1
  174. package/src/providers/model-presets.ts +119 -0
  175. package/src/providers/new-model-policy.ts +146 -0
  176. package/src/providers/provider-id-rewrite.ts +2 -1
  177. package/src/providers/quota.ts +65 -7
  178. package/src/providers/registry.ts +184 -69
  179. package/src/providers/slug-codec.ts +52 -0
  180. package/src/providers/xai-transport.ts +21 -0
  181. package/src/reasoning-effort.ts +19 -2
  182. package/src/responses/apply-patch-envelope.ts +63 -0
  183. package/src/responses/code-mode-helper-compat.ts +50 -0
  184. package/src/responses/custom-tool-compat.ts +157 -39
  185. package/src/responses/namespace-tool-compat.ts +84 -4
  186. package/src/responses/parser.ts +7 -2
  187. package/src/responses/reasoning-replay-cache.ts +81 -3
  188. package/src/responses/schema.ts +5 -1
  189. package/src/responses/state.ts +94 -4
  190. package/src/responses/thought-signature-replay.ts +17 -0
  191. package/src/responses/turn-termination.ts +107 -0
  192. package/src/router.ts +40 -2
  193. package/src/routing/analytics.ts +1 -0
  194. package/src/routing/quota.ts +10 -0
  195. package/src/server/auth-cors.ts +27 -7
  196. package/src/server/chat-completions.ts +24 -16
  197. package/src/server/chat-native-sse.ts +3 -3
  198. package/src/server/chat-native.ts +30 -11
  199. package/src/server/claude-messages.ts +1 -1
  200. package/src/server/index.ts +179 -32
  201. package/src/server/lifecycle.ts +52 -1
  202. package/src/server/local-management-read-client.ts +1 -1
  203. package/src/server/local-provider-reload-client.ts +1 -1
  204. package/src/server/management/agent-settings-routes.ts +45 -23
  205. package/src/server/management/codex-prompt-routes.ts +570 -0
  206. package/src/server/management/combo-routes.ts +2 -1
  207. package/src/server/management/config-routes.ts +59 -12
  208. package/src/server/management/context.ts +10 -1
  209. package/src/server/management/logs-usage-routes.ts +30 -24
  210. package/src/server/management/model-routes.ts +274 -4
  211. package/src/server/management/model-rows.ts +4 -0
  212. package/src/server/management/native-integration-routes.ts +2 -1
  213. package/src/server/management/oauth-account-routes.ts +10 -4
  214. package/src/server/management/provider-capability-config.ts +1 -1
  215. package/src/server/management/provider-routes.ts +81 -11
  216. package/src/server/management/route-registry.ts +332 -0
  217. package/src/server/management/routing-profile-routes.ts +2 -2
  218. package/src/server/management/system-restart.ts +1 -1
  219. package/src/server/management-api.ts +18 -16
  220. package/src/server/port-reclaim.ts +1 -1
  221. package/src/server/proxy-liveness.ts +29 -5
  222. package/src/server/relay-eager.ts +147 -20
  223. package/src/server/relay.ts +251 -19
  224. package/src/server/request-log-conversation.ts +51 -0
  225. package/src/server/request-log.ts +70 -16
  226. package/src/server/responses/codex-auth-error.ts +55 -0
  227. package/src/server/responses/collaboration.ts +37 -3
  228. package/src/server/responses/combo-stream-preflight.ts +178 -0
  229. package/src/server/responses/compact.ts +6 -21
  230. package/src/server/responses/core.ts +810 -208
  231. package/src/server/responses/empty-completion-guard.ts +63 -6
  232. package/src/server/responses/fetch-helpers.ts +17 -102
  233. package/src/server/responses/input-admission.ts +3 -1
  234. package/src/server/responses/passthrough-error.ts +33 -9
  235. package/src/server/responses/responses-field-backfill.ts +105 -13
  236. package/src/server/responses/ws-upstream.ts +75 -2
  237. package/src/server/responses-custom-tool-repair.ts +87 -6
  238. package/src/server/responses-terminal-repair.ts +25 -4
  239. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  240. package/src/server/sse-frame-buffer.ts +31 -4
  241. package/src/server/ws-bridge.ts +14 -2
  242. package/src/service.ts +42 -4
  243. package/src/storage/policy-job.ts +14 -4
  244. package/src/storage/policy.ts +88 -23
  245. package/src/types/config.ts +66 -0
  246. package/src/types/provider.ts +69 -0
  247. package/src/types/request.ts +17 -4
  248. package/src/types/tools.ts +116 -11
  249. package/src/types.ts +3 -1
  250. package/src/update/index.ts +5 -4
  251. package/src/update/job.ts +3 -1
  252. package/src/update/transactional-install.mjs +8 -1
  253. package/src/usage/expected-prices.ts +43 -24
  254. package/src/usage/log.ts +63 -12
  255. package/src/usage/summary.ts +211 -8
  256. package/src/vision/describe.ts +18 -13
  257. package/src/web-search/exa-executor.ts +40 -9
  258. package/src/web-search/executor.ts +10 -3
  259. package/src/web-search/loop.ts +5 -4
  260. package/gui/dist/assets/index-BF38heuV.js +0 -104
  261. package/gui/dist/assets/index-DMiI18Kv.css +0 -1
@@ -1,4 +1,12 @@
1
1
  import type { ResponsesTerminalStatus } from "../bridge";
2
+ import {
3
+ cyberPolicyErrorType,
4
+ CYBER_POLICY_ERROR_CODE,
5
+ CYBER_POLICY_FALLBACK_MESSAGE,
6
+ isCyberPolicyCode,
7
+ isCyberPolicyMessage,
8
+ } from "../lib/errors";
9
+ import { redactSecretString } from "../lib/redact";
2
10
  import { isTranslatorBudgetExceededError } from "../lib/translator-budget";
3
11
  import { isUsageDebugEnabled } from "../usage/debug";
4
12
  import {
@@ -16,6 +24,7 @@ import {
16
24
  joinSseFrameBytes,
17
25
  MAX_CLIENT_SSE_FRAME_BYTES,
18
26
  } from "./sse-frame-buffer";
27
+ import { replaceSseDataPayload } from "./sse-payload-rewrite";
19
28
 
20
29
  const nativePassthroughSseResponses = new WeakSet<Response>();
21
30
  const eagerRelaySseResponses = new WeakSet<Response>();
@@ -24,6 +33,30 @@ export const MAX_INSPECTION_SSE_FRAME_BYTES = MAX_CLIENT_SSE_FRAME_BYTES;
24
33
  export const MAX_COMPLETED_OUTPUT_ITEMS = 256;
25
34
  export const MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES = 8 * 1024 * 1024;
26
35
  export const MAX_TAIL_ERROR_MESSAGE_CHARS = 512;
36
+ const ADAPTER_EOF_INCOMPLETE_PAYLOAD = JSON.stringify({
37
+ type: "response.incomplete",
38
+ response: {
39
+ status: "incomplete",
40
+ incomplete_details: { reason: "adapter_eof" },
41
+ },
42
+ });
43
+ const DONE_SSE_FRAME_TEXT = "data: [DONE]\n\n";
44
+ const FAILED_TAIL_FALLBACK_PAYLOAD = JSON.stringify({
45
+ type: "response.failed",
46
+ response: {
47
+ status: "failed",
48
+ error: {
49
+ type: "upstream_error",
50
+ code: "upstream_reset",
51
+ message: "Upstream stream terminated unexpectedly",
52
+ },
53
+ last_error: {
54
+ type: "upstream_error",
55
+ code: "upstream_reset",
56
+ message: "Upstream stream terminated unexpectedly",
57
+ },
58
+ },
59
+ });
27
60
 
28
61
  export type InspectionCounters = {
29
62
  frameBufferHighWaterBytes: number;
@@ -99,6 +132,21 @@ export function buildFailedTailPayload(err: unknown): string {
99
132
  });
100
133
  }
101
134
 
135
+ function buildFailedTailPayloadOrFallback(err: unknown): string {
136
+ try {
137
+ return buildFailedTailPayload(err);
138
+ } catch {
139
+ // Error.message and String(error) may execute hostile accessors. Preserve a
140
+ // bounded protocol terminal even when diagnostic serialization is unsafe.
141
+ return FAILED_TAIL_FALLBACK_PAYLOAD;
142
+ }
143
+ }
144
+
145
+ export function failedTailFrame(encoder: TextEncoder, err: unknown): Uint8Array {
146
+ const payload = buildFailedTailPayloadOrFallback(err);
147
+ return encoder.encode(`\n\nevent: response.failed\ndata: ${payload}\n\n${DONE_SSE_FRAME_TEXT}`);
148
+ }
149
+
102
150
  export type SseTerminalOutputBoundary = {
103
151
  feed(chunk: Uint8Array): Uint8Array;
104
152
  finish(): Uint8Array;
@@ -111,13 +159,16 @@ export type SseTerminalOutputBoundary = {
111
159
  * Frame-aware client output boundary shared by both native Responses relays.
112
160
  * It buffers only the current incomplete SSE block under the same hard byte
113
161
  * cap as inspection, forwards complete blocks through the first Responses
114
- * terminal, and drops every later block/byte.
162
+ * terminal, and drops every later block/byte. A premature [DONE] is held until
163
+ * a terminal arrives so clean EOF can synthesize one terminal and one sentinel.
115
164
  */
116
165
  export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
117
166
  const decoder = new TextDecoder();
167
+ const encoder = new TextEncoder();
118
168
  const framer = new BoundedSseFrameBuffer(MAX_INSPECTION_SSE_FRAME_BYTES);
119
169
  let terminal = false;
120
170
  let done = false;
171
+ let pendingDone: { block: Uint8Array; delimiter: Uint8Array } | null = null;
121
172
  let disposed = false;
122
173
 
123
174
  const processFrames = (
@@ -129,16 +180,37 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
129
180
  for (const frame of frames) {
130
181
  const payload = sseDataPayload(decoder.decode(frame.block));
131
182
  const isDone = payload === "[DONE]";
132
- // Preserve every frame through the first Responses terminal. A [DONE]
133
- // frame is also preserved when it immediately follows that terminal in
134
- // the same upstream chunk; every later non-DONE frame is dropped.
135
- if (!responsesTerminal || isDone) output.push(frame.block, frame.delimiter);
183
+ const parsed = payload === null ? undefined : parseSsePayload(payload);
184
+ const policyError = parsed !== undefined && isPolicyRewriteType(parsed)
185
+ ? cyberPolicyTerminalError(parsed)
186
+ : undefined;
187
+ const outboundBlock = policyError
188
+ ? encoder.encode(rewritePolicyTerminalBlock(
189
+ decoder.decode(frame.block),
190
+ policyFailurePayload(policyError, parsed),
191
+ ))
192
+ : frame.block;
136
193
  if (isDone) {
137
194
  done = true;
195
+ if (responsesTerminal) {
196
+ output.push(outboundBlock, frame.delimiter);
197
+ } else if (!pendingDone) {
198
+ // Do not expose a sentinel before a Responses terminal. If EOF
199
+ // follows, the synthetic incomplete path owns the one sentinel;
200
+ // if a terminal arrives later, this pending frame is emitted then.
201
+ pendingDone = { block: outboundBlock, delimiter: frame.delimiter };
202
+ }
138
203
  continue;
139
204
  }
140
- if (!responsesTerminal && payload && terminalStatusFromSsePayload(payload)) {
205
+ // Preserve every frame through the first Responses terminal. Every
206
+ // later non-DONE frame is dropped.
207
+ if (!responsesTerminal) output.push(outboundBlock, frame.delimiter);
208
+ if (!responsesTerminal && payload && terminalStatusFromParsed(parsed)) {
141
209
  responsesTerminal = true;
210
+ if (pendingDone) {
211
+ output.push(pendingDone.block, pendingDone.delimiter);
212
+ pendingDone = null;
213
+ }
142
214
  }
143
215
  }
144
216
  if (responsesTerminal) {
@@ -155,13 +227,22 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
155
227
  },
156
228
  finish() {
157
229
  if (disposed || terminal) return new Uint8Array(0);
158
- return framer.finish();
230
+ const tail = framer.finish();
231
+ if (tail.byteLength === 0) return new Uint8Array(0);
232
+ // EOF may cut off the final SSE block before its blank-line delimiter.
233
+ // Feed it through the exact same parser/rewrite/terminal path as a
234
+ // complete frame, using a synthetic delimiter so the client receives a
235
+ // dispatchable event rather than an unterminated tail.
236
+ const tailText = decoder.decode(tail);
237
+ const delimiter = encoder.encode(tailText.includes("\r\n") ? "\r\n\r\n" : "\n\n");
238
+ return processFrames([{ block: tail, delimiter }]);
159
239
  },
160
240
  terminalSeen: () => terminal,
161
241
  doneSeen: () => done,
162
242
  dispose() {
163
243
  if (disposed) return;
164
244
  disposed = true;
245
+ pendingDone = null;
165
246
  framer.dispose();
166
247
  },
167
248
  };
@@ -198,7 +279,7 @@ export function relaySseWithFailedTail(
198
279
  // Preserve through the terminal block only, add the conventional sentinel
199
280
  // when there was no real [DONE] data event, then stop reading upstream.
200
281
  if (!terminalBoundary.doneSeen()) {
201
- controller.enqueue(encoder.encode("data: [DONE]\n\n"));
282
+ controller.enqueue(doneFrame(encoder));
202
283
  }
203
284
  closed = true;
204
285
  controller.close();
@@ -219,6 +300,16 @@ export function relaySseWithFailedTail(
219
300
  if (done) {
220
301
  const tail = terminalBoundary.finish();
221
302
  if (tail.byteLength > 0) controller.enqueue(tail);
303
+ if (terminalBoundary.terminalSeen()) {
304
+ if (!terminalBoundary.doneSeen()) controller.enqueue(doneFrame(encoder));
305
+ } else {
306
+ // A clean upstream EOF is still a failed Responses turn when no
307
+ // protocol terminal arrived. Make that state explicit so Codex
308
+ // does not treat HTTP 200 + bare EOF as a retryable disconnect.
309
+ const incomplete = adapterEofIncompleteFrame(encoder);
310
+ controller.enqueue(incomplete);
311
+ controller.enqueue(doneFrame(encoder));
312
+ }
222
313
  terminalBoundary.dispose();
223
314
  controller.close();
224
315
  return;
@@ -228,8 +319,10 @@ export function relaySseWithFailedTail(
228
319
  }
229
320
  } catch (err) {
230
321
  let partial: Uint8Array = new Uint8Array(0);
322
+ let tailTerminal = false;
231
323
  try {
232
324
  partial = terminalBoundary.finish();
325
+ tailTerminal = terminalBoundary.terminalSeen();
233
326
  } catch {
234
327
  // A near-cap ambiguous delimiter tail may itself overflow at EOF.
235
328
  // Preserve the original read/framing failure and continue emitting
@@ -237,11 +330,14 @@ export function relaySseWithFailedTail(
237
330
  }
238
331
  terminalBoundary.dispose();
239
332
  if (closed) return;
240
- const payload = buildFailedTailPayload(err);
241
333
  try {
242
334
  if (partial.byteLength > 0) controller.enqueue(partial);
243
- // Leading blank line terminates a partial SSE block so the failed frame parses cleanly.
244
- controller.enqueue(encoder.encode(`\n\nevent: response.failed\ndata: ${payload}\n\ndata: [DONE]\n\n`));
335
+ if (tailTerminal) {
336
+ if (!terminalBoundary.doneSeen()) controller.enqueue(doneFrame(encoder));
337
+ } else {
338
+ // Leading blank line terminates a partial SSE block so the failed frame parses cleanly.
339
+ controller.enqueue(failedTailFrame(encoder, err));
340
+ }
245
341
  controller.close();
246
342
  } catch { /* client already torn down */ }
247
343
  upstream.abort();
@@ -276,15 +372,139 @@ export function sseDataPayload(block: string): string | null {
276
372
  return data.length > 0 ? data.join("\n") : null;
277
373
  }
278
374
 
279
- export function terminalStatusFromSsePayload(payload: string): ResponsesTerminalStatus | null {
280
- if (payload === "[DONE]") return null;
375
+ type JsonRecord = Record<string, unknown>;
376
+
377
+ function asJsonRecord(value: unknown): JsonRecord | null {
378
+ return value !== null && typeof value === "object" && !Array.isArray(value)
379
+ ? value as JsonRecord
380
+ : null;
381
+ }
382
+
383
+ function stringField(record: JsonRecord | null, key: string): string | undefined {
384
+ const value = record?.[key];
385
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
386
+ }
387
+
388
+ /**
389
+ * Return a high-confidence policy error carried by an upstream terminal shape.
390
+ * Deliberately inspect structured error fields and known refusal copy only — a
391
+ * bare `cyber_policy` token in an unrelated payload is not sufficient.
392
+ */
393
+ function cyberPolicyTerminalError(parsed: unknown): { message: string; type?: string } | undefined {
394
+ const root = asJsonRecord(parsed);
395
+ if (!root) return undefined;
396
+ const response = asJsonRecord(root.response);
397
+ const candidates = [
398
+ asJsonRecord(root.error),
399
+ asJsonRecord(root.last_error),
400
+ asJsonRecord(response?.error),
401
+ asJsonRecord(response?.incomplete_details),
402
+ root,
403
+ response,
404
+ ];
405
+ for (const candidate of candidates) {
406
+ if (!candidate) continue;
407
+ if (isCyberPolicyCode(stringField(candidate, "code"))) {
408
+ return {
409
+ message: stringField(candidate, "message")
410
+ ?? CYBER_POLICY_FALLBACK_MESSAGE,
411
+ ...(stringField(candidate, "type") ? { type: stringField(candidate, "type") } : {}),
412
+ };
413
+ }
414
+ }
415
+ for (const candidate of candidates) {
416
+ const message = stringField(candidate, "message");
417
+ if (message && isCyberPolicyMessage(message)) {
418
+ return {
419
+ message,
420
+ ...(stringField(candidate, "type") ? { type: stringField(candidate, "type") } : {}),
421
+ };
422
+ }
423
+ }
424
+ return undefined;
425
+ }
426
+
427
+ function parseSsePayload(payload: string): unknown | undefined {
428
+ if (payload === "[DONE]") return undefined;
281
429
  try {
282
- return terminalStatusFromParsed(JSON.parse(payload));
430
+ return JSON.parse(payload);
283
431
  } catch {
284
- return null;
432
+ return undefined;
285
433
  }
286
434
  }
287
435
 
436
+ function isPolicyRewriteType(parsed: unknown): boolean {
437
+ const type = asJsonRecord(parsed)?.type;
438
+ return type === "response.failed" || type === "response.incomplete" || type === "error";
439
+ }
440
+
441
+ function rewritePolicyTerminalBlock(block: string, payload: string): string {
442
+ const newline = block.includes("\r\n") ? "\r\n" : "\n";
443
+ const rewritten = replaceSseDataPayload(block, payload);
444
+ const lines = rewritten.split(/\r?\n/);
445
+ let eventRewritten = false;
446
+ const withEvent = lines.map(line => {
447
+ if (!eventRewritten && line.startsWith("event:")) {
448
+ eventRewritten = true;
449
+ return "event: response.failed";
450
+ }
451
+ return line;
452
+ });
453
+ if (!eventRewritten) withEvent.unshift("event: response.failed");
454
+ return withEvent.join(newline);
455
+ }
456
+
457
+ function policyFailurePayload(policyError: { message: string; type?: string }, parsed: unknown): string {
458
+ const error = {
459
+ type: cyberPolicyErrorType(policyError.type),
460
+ code: CYBER_POLICY_ERROR_CODE,
461
+ message: redactSecretString(policyError.message).slice(0, MAX_TAIL_ERROR_MESSAGE_CHARS),
462
+ };
463
+ const root = asJsonRecord(parsed);
464
+ const originalResponse = asJsonRecord(root?.response);
465
+ const preservedResponse = Object.fromEntries(
466
+ Object.entries(originalResponse ?? {}).filter(([key]) => key !== "incomplete_details"),
467
+ );
468
+ const response = {
469
+ ...preservedResponse,
470
+ status: "failed",
471
+ error,
472
+ last_error: error,
473
+ retryable: false,
474
+ };
475
+ // Responses event metadata such as sequence_number is normally top-level.
476
+ // Keep it (and any other non-error, non-response fields) while replacing only
477
+ // the protocol type and error envelope.
478
+ const preservedRoot = root
479
+ ? Object.fromEntries(Object.entries(root).filter(([key]) => (
480
+ key !== "type"
481
+ && key !== "response"
482
+ && key !== "error"
483
+ && key !== "last_error"
484
+ && key !== "retryable"
485
+ )))
486
+ : {};
487
+ return JSON.stringify({
488
+ ...preservedRoot,
489
+ type: "response.failed",
490
+ retryable: false,
491
+ response,
492
+ });
493
+ }
494
+
495
+ export function adapterEofIncompleteFrame(encoder: TextEncoder): Uint8Array {
496
+ return encoder.encode(`event: response.incomplete\ndata: ${ADAPTER_EOF_INCOMPLETE_PAYLOAD}\n\n`);
497
+ }
498
+
499
+ export function doneFrame(encoder: TextEncoder): Uint8Array {
500
+ return encoder.encode(DONE_SSE_FRAME_TEXT);
501
+ }
502
+
503
+ export function terminalStatusFromSsePayload(payload: string): ResponsesTerminalStatus | null {
504
+ if (payload === "[DONE]") return null;
505
+ return terminalStatusFromParsed(parseSsePayload(payload));
506
+ }
507
+
288
508
  /** True when a native Responses SSE payload carries the FIRST kind of non-empty model output. */
289
509
  export function isFirstOutputSsePayload(payload: string | null): boolean {
290
510
  if (!payload || payload === "[DONE]") return false;
@@ -322,14 +542,16 @@ function createFirstOutputReporter(onFirstOutput?: () => void): {
322
542
  }
323
543
 
324
544
  export function terminalStatusFromParsed(parsed: unknown): ResponsesTerminalStatus | null {
325
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
326
- switch ((parsed as { type?: unknown }).type) {
545
+ const type = asJsonRecord(parsed)?.type;
546
+ switch (type) {
327
547
  case "response.completed":
328
548
  return "completed";
329
549
  case "response.failed":
330
550
  return "failed";
331
551
  case "response.incomplete":
332
- return "incomplete";
552
+ return cyberPolicyTerminalError(parsed) ? "failed" : "incomplete";
553
+ case "error":
554
+ return cyberPolicyTerminalError(parsed) ? "failed" : null;
333
555
  default:
334
556
  return null;
335
557
  }
@@ -800,6 +1022,9 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
800
1022
  }
801
1023
  reportFirstOutput.parsed(parsed);
802
1024
  const status = terminalStatusFromParsed(parsed);
1025
+ const policyTerminal = status === "failed"
1026
+ && isPolicyRewriteType(parsed)
1027
+ && cyberPolicyTerminalError(parsed) !== undefined;
803
1028
  if (status) sawTerminal = true;
804
1029
  if (!reported && handlers.onTerminal && status) {
805
1030
  try {
@@ -808,7 +1033,7 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
808
1033
  handlers.logCtx.transportPhase = "terminal_sse";
809
1034
  handlers.logCtx.terminalSource = "upstream";
810
1035
  }
811
- handlers.onTerminal(status);
1036
+ handlers.onTerminal(status, policyTerminal ? 400 : undefined);
812
1037
  } finally {
813
1038
  if (status === "failed" || status === "incomplete") clearCompletedItems();
814
1039
  }
@@ -1087,6 +1312,13 @@ function startBoundedInspectionPump(options: InspectionPumpOptions): void {
1087
1312
  }
1088
1313
  }
1089
1314
  } catch {
1315
+ // A read error can follow a final SSE block without its blank-line
1316
+ // delimiter. Flush that candidate before classifying the transport as a
1317
+ // synthetic reset; otherwise a real completed/failed/policy terminal is
1318
+ // downgraded to 502 on the inspection branch.
1319
+ if (!cancelled) {
1320
+ try { inspector.finish(); } catch { /* preserve the original read error */ }
1321
+ }
1090
1322
  if (clientGone) clientGoneWithoutTerminal = !inspector.terminalSeen();
1091
1323
  else if (!cancelled) options.onReadError?.();
1092
1324
  } finally {
@@ -61,6 +61,57 @@ export function sessionIdHeaderFromRequest(headers: Headers): string | null {
61
61
  return headers.get("session_id") ?? headers.get("session-id");
62
62
  }
63
63
 
64
+ /**
65
+ * Fixed-size logical turn lane (#820).
66
+ *
67
+ * A lane must be as SPECIFIC as the identity available, which is the opposite of what
68
+ * `codexPoolAffinityKey` wants. Affinity deliberately prefers the parent thread so a whole
69
+ * subagent fan-out pins to one account; a lane keyed that way would put every parallel
70
+ * subagent of one parent into a single lane and reject all but the first with 503 — the
71
+ * fan-out is the normal case, not an abuse.
72
+ *
73
+ * So the parent is a QUALIFIER, never the lane on its own when a child thread exists: the
74
+ * pair separates siblings while still keeping one conversation's overlapping turns together.
75
+ */
76
+ export function sessionLaneIdFromRequest(headers: Headers): string | undefined {
77
+ const parent = normalizeLogConversationId(headers.get("x-codex-parent-thread-id"));
78
+ const thread = normalizeLogConversationId(headers.get("thread-id"));
79
+ const session = normalizeLogConversationId(sessionIdHeaderFromRequest(headers));
80
+ const specific = thread ?? session;
81
+ if (parent && specific) return `${parent}\u0000${specific}`;
82
+ return specific ?? parent;
83
+ }
84
+
85
+ function firstSanitizedConversationId(
86
+ ...values: Array<string | null | undefined>
87
+ ): string | undefined {
88
+ for (const value of values) {
89
+ const sanitized = sanitizeConversationIdInput(value);
90
+ if (sanitized) return sanitized;
91
+ }
92
+ return undefined;
93
+ }
94
+
95
+ /**
96
+ * Conversation namespace for reasoning replay. Unlike the persisted log id, this stays the raw
97
+ * sanitized identity so mixed headers that carry the same conversation still hit one serving
98
+ * record. Do not hash, and do not prefer session_id over a true per-conversation thread/Cursor
99
+ * identity: session_id can be synthesized from a shared prompt_cache_key.
100
+ */
101
+ export function reasoningReplayConversationIdFromResponsesRequest(input: {
102
+ clientThreadId?: string;
103
+ threadIdHeader?: string | null;
104
+ cursorConversationId?: string;
105
+ sessionIdHeader?: string | null;
106
+ }): string | undefined {
107
+ return firstSanitizedConversationId(
108
+ input.clientThreadId,
109
+ input.threadIdHeader,
110
+ input.cursorConversationId,
111
+ input.sessionIdHeader,
112
+ );
113
+ }
114
+
64
115
  export function conversationIdFromResponsesRequest(input: {
65
116
  clientThreadId?: string;
66
117
  sessionIdHeader?: string | null;
@@ -6,6 +6,7 @@ import {
6
6
  httpStatusFromTerminalError as httpStatusFromClassifiedTerminalError,
7
7
  isClientClosedMessage,
8
8
  isCyberPolicyCode,
9
+ isCyberPolicyMessage,
9
10
  } from "../lib/errors";
10
11
  import { CODEX_CONFIG_PATH, readRootTomlString } from "../codex/paths";
11
12
  import { readCodexCatalogPath } from "../codex/catalog";
@@ -62,9 +63,18 @@ export interface RequestLogContext {
62
63
  * product: widening that enum would merge Responses and Chat Completions,
63
64
  * since both leave it undefined. */
64
65
  inboundProtocol?: "responses" | "chat" | "messages";
66
+ /**
67
+ * Set when an adapter answered the turn locally and no upstream request was made
68
+ * (`ProviderAdapter.localTerminal`). A fixed identifier naming the code path, never
69
+ * conversation-derived: it exists so a request log showing zero sends is explainable
70
+ * rather than looking like a lost request.
71
+ */
72
+ localTerminalReason?: string;
65
73
  /** Stable non-PII Codex Pool account identity for durable usage attribution. */
66
74
  accountLogLabel?: string;
67
75
  requestedModel?: string;
76
+ /** User-facing alias selector when routing resolved one; native model remains `model`. */
77
+ requestedAlias?: string;
68
78
  /** Original bare helper model when the opt-in shadow-call route rewrote this request. */
69
79
  shadowCallRewrittenFrom?: string;
70
80
  /** Internal structural combo identity; omitted from RequestLogEntry/JSONL. */
@@ -130,6 +140,12 @@ export interface RequestLogEntry {
130
140
  /** TTFT: ms from request start to the first non-empty model output delta; unset for non-streaming/tool-only. */
131
141
  firstOutputMs?: number;
132
142
  surface?: "claude" | "claude-desktop" | "grok";
143
+ /**
144
+ * Set when the proxy answered this turn locally and sent nothing upstream. Without it a zero-send
145
+ * row is indistinguishable from a request that vanished. A fixed adapter-supplied identifier,
146
+ * never conversation-derived.
147
+ */
148
+ localTerminalReason?: string;
133
149
  /** The matched configured key's id. Set ONLY for admissionKind "configured" —
134
150
  * never a sentinel, so a hand-edited entry whose id happens to be "loopback"
135
151
  * cannot absorb unrelated traffic. */
@@ -144,6 +160,7 @@ export interface RequestLogEntry {
144
160
  /** Best-effort chat/session correlation for Logs grouping (#330). */
145
161
  conversationId?: string;
146
162
  requestedModel?: string;
163
+ requestedAlias?: string;
147
164
  /** Original bare helper model when the opt-in shadow-call route rewrote this request. */
148
165
  shadowCallRewrittenFrom?: string;
149
166
  requestedEffort?: string;
@@ -259,6 +276,7 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
259
276
  ? { accountLogLabel: entry.accountLogLabel }
260
277
  : {}),
261
278
  ...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
279
+ ...(entry.requestedAlias ? { requestedAlias: entry.requestedAlias } : {}),
262
280
  ...(entry.shadowCallRewrittenFrom
263
281
  ? { shadowCallRewrittenFrom: entry.shadowCallRewrittenFrom }
264
282
  : {}),
@@ -379,6 +397,7 @@ export function addRequestLog(entry: RequestLogEntry) {
379
397
  ...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
380
398
  ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
381
399
  ...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
400
+ ...(entry.requestedAlias ? { requestedAlias: entry.requestedAlias } : {}),
382
401
  ...(entry.shadowCallRewrittenFrom
383
402
  ? { shadowCallRewrittenFrom: entry.shadowCallRewrittenFrom }
384
403
  : {}),
@@ -764,7 +783,7 @@ function captureUpstreamErrorParsed(
764
783
  last_error?: { message?: unknown };
765
784
  response?: {
766
785
  error?: { type?: unknown; code?: unknown; message?: unknown };
767
- incomplete_details?: { reason?: unknown };
786
+ incomplete_details?: { reason?: unknown; message?: unknown };
768
787
  };
769
788
  };
770
789
  captureTerminalHttpStatus(logCtx, json);
@@ -778,7 +797,8 @@ function captureUpstreamErrorParsed(
778
797
  if (logCtx.upstreamError) return;
779
798
  const message = json?.error?.message
780
799
  ?? json?.last_error?.message
781
- ?? json?.response?.error?.message;
800
+ ?? json?.response?.error?.message
801
+ ?? json?.response?.incomplete_details?.message;
782
802
  if (typeof message === "string" && message.trim()) {
783
803
  logCtx.upstreamError = redactSecretString(message).slice(0, 500);
784
804
  return;
@@ -817,25 +837,43 @@ function captureTerminalHttpStatus(
817
837
  logCtx: RequestLogContext,
818
838
  json: {
819
839
  type?: unknown;
820
- response?: { error?: { type?: unknown; code?: unknown; message?: unknown } };
840
+ code?: unknown;
841
+ message?: unknown;
842
+ error?: { type?: unknown; code?: unknown; message?: unknown };
843
+ last_error?: { type?: unknown; code?: unknown; message?: unknown };
844
+ response?: {
845
+ error?: { type?: unknown; code?: unknown; message?: unknown };
846
+ incomplete_details?: { code?: unknown; message?: unknown };
847
+ };
821
848
  },
822
849
  ): void {
823
850
  if (logCtx.terminalHttpStatus !== undefined) return;
824
- if (json.type !== "response.failed") return;
825
- const error = json.response?.error;
826
- if (!error || typeof error !== "object") return;
827
- const terminalCode = error.code === null || typeof error.code === "string"
828
- ? error.code
829
- : undefined;
830
- if (isCyberPolicyCode(terminalCode)) {
851
+ const type = json.type;
852
+ if (type !== "response.failed" && type !== "response.incomplete" && type !== "error") return;
853
+ const responseError = json.response?.error;
854
+ const responseDetails = json.response?.incomplete_details;
855
+ const candidates = [json.error, json.last_error, responseError, responseDetails, json];
856
+ const policy = candidates.some(candidate => (
857
+ candidate?.code === null || typeof candidate?.code === "string"
858
+ ) && isCyberPolicyCode(candidate.code as string | null | undefined))
859
+ || candidates.some(candidate => (
860
+ typeof candidate?.message === "string"
861
+ && candidate.message.trim().length > 0
862
+ && isCyberPolicyMessage(candidate.message)
863
+ ));
864
+ if (policy) {
831
865
  logCtx.terminalErrorCode = CYBER_POLICY_ERROR_CODE;
832
- } else {
833
- delete logCtx.terminalErrorCode;
866
+ logCtx.terminalHttpStatus = 400;
867
+ return;
834
868
  }
869
+ if (type !== "response.failed" || !responseError || typeof responseError !== "object") return;
870
+ const responseCode = responseError.code === null || typeof responseError.code === "string"
871
+ ? responseError.code
872
+ : undefined;
835
873
  logCtx.terminalHttpStatus = httpStatusFromTerminalError({
836
- type: typeof error.type === "string" ? error.type : undefined,
837
- code: terminalCode,
838
- message: typeof error.message === "string" ? error.message : undefined,
874
+ type: typeof responseError.type === "string" ? responseError.type : undefined,
875
+ code: responseCode,
876
+ message: typeof responseError.message === "string" ? responseError.message : undefined,
839
877
  });
840
878
  }
841
879
 
@@ -917,6 +955,7 @@ export function addFinalRequestLog(
917
955
  logCtx.usage,
918
956
  logCtx.usageLogInputTokens,
919
957
  contextWindowForModel(logCtx.providerAdapter ?? logCtx.provider, logCtx.model),
958
+ logCtx.localTerminalReason !== undefined,
920
959
  );
921
960
  const attempts = logCtx.attempts?.map(attempt => ({
922
961
  ...attempt,
@@ -944,11 +983,15 @@ export function addFinalRequestLog(
944
983
  ...(logCtx.apiKeyId ? { apiKeyId: logCtx.apiKeyId } : {}),
945
984
  ...(logCtx.admissionKind ? { admissionKind: logCtx.admissionKind } : {}),
946
985
  ...(logCtx.inboundProtocol ? { inboundProtocol: logCtx.inboundProtocol } : {}),
986
+ ...(logCtx.localTerminalReason
987
+ ? { localTerminalReason: sanitizeLogMetadataString(logCtx.localTerminalReason) }
988
+ : {}),
947
989
  ...(isCodexUsageAccountLogLabel(logCtx.accountLogLabel)
948
990
  ? { accountLogLabel: logCtx.accountLogLabel }
949
991
  : {}),
950
992
  ...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
951
993
  ...(logCtx.requestedModel ? { requestedModel: logCtx.requestedModel } : {}),
994
+ ...(logCtx.requestedAlias ? { requestedAlias: logCtx.requestedAlias } : {}),
952
995
  ...(shadowCallRewrittenFrom ? { shadowCallRewrittenFrom } : {}),
953
996
  ...(logCtx.requestedEffort ? { requestedEffort: logCtx.requestedEffort } : {}),
954
997
  ...(logCtx.effectiveEffort ? { effectiveEffort: logCtx.effectiveEffort } : {}),
@@ -1007,6 +1050,15 @@ export function filterRequestLogs(logs: RequestLogEntry[], params: URLSearchPara
1007
1050
  if (conversationId) {
1008
1051
  filtered = filtered.filter(entry => matchesLogConversationId(entry.conversationId, conversationId));
1009
1052
  }
1053
+ // #2704: there was no `model` clause at all, so `?model=x` was ACCEPTED and silently
1054
+ // ignored -- worse than an error, because it yields wrong conclusions from output that
1055
+ // looks correct. Attempts are matched for the same reason `provider` matches them: a
1056
+ // request that failed over should be findable by the model that actually served it.
1057
+ const model = params.get("model")?.trim();
1058
+ if (model) {
1059
+ filtered = filtered.filter(entry => entry.model === model
1060
+ || entry.attempts?.some(attempt => attempt.model === model));
1061
+ }
1010
1062
  const status = params.get("status")?.trim().toLowerCase();
1011
1063
  if (status) {
1012
1064
  filtered = /^[1-5]xx$/.test(status)
@@ -1076,6 +1128,7 @@ function finalizedUsage(
1076
1128
  usage: OcxUsage | undefined,
1077
1129
  inputTokenEstimate: number | undefined,
1078
1130
  contextWindow: number | undefined,
1131
+ locallyAnswered = false,
1079
1132
  ): FinalizedUsageResult {
1080
1133
  // The ESTIMATE itself is capped at the model's context window (codex-router PR #140). The
1081
1134
  // combined value below keeps its max(inputTokens, estimate) behavior — a provider-reported
@@ -1085,7 +1138,7 @@ function finalizedUsage(
1085
1138
  && inputTokenEstimate >= 0
1086
1139
  ? capEstimateAtContextWindow(inputTokenEstimate, contextWindow)
1087
1140
  : undefined;
1088
- const finalUsage = usageForFinalLog(adapter, usage);
1141
+ const finalUsage = usageForFinalLog(adapter, usage, locallyAnswered);
1089
1142
  const usageFallback = !finalUsage && estimate !== undefined
1090
1143
  ? { inputTokens: estimate, outputTokens: 0, estimated: true }
1091
1144
  : undefined;
@@ -1185,6 +1238,7 @@ export function finishRequestAttempt(
1185
1238
  usage ?? attempt.usage,
1186
1239
  attempt.inputTokenEstimate,
1187
1240
  contextWindowForModel(attempt.adapter, attempt.model),
1241
+ attempt.locallyAnswered === true,
1188
1242
  );
1189
1243
  attempt.status = status;
1190
1244
  attempt.durationMs = Math.max(0, durationMs);