@yansigit/opencodex 2.33.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 (196) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/index-BjCaHxdz.js +112 -0
  3. package/gui/dist/assets/index-DLkXOXLC.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +141 -23
  8. package/src/adapters/cursor/call-id.ts +44 -0
  9. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  10. package/src/adapters/cursor/discovery.ts +60 -2
  11. package/src/adapters/cursor/effort-map.ts +79 -1
  12. package/src/adapters/cursor/envelope-echo.ts +162 -0
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +17 -1
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-fs.ts +13 -12
  17. package/src/adapters/cursor/native-exec-network.ts +3 -5
  18. package/src/adapters/cursor/native-exec-policy.ts +47 -0
  19. package/src/adapters/cursor/native-exec-shell.ts +116 -31
  20. package/src/adapters/cursor/native-exec.ts +38 -10
  21. package/src/adapters/cursor/protobuf-events.ts +28 -2
  22. package/src/adapters/cursor/protobuf-request.ts +93 -41
  23. package/src/adapters/cursor/request-builder.ts +39 -10
  24. package/src/adapters/cursor/tool-definitions.ts +27 -3
  25. package/src/adapters/cursor/tool-result-normalize.ts +51 -6
  26. package/src/adapters/cursor/types.ts +23 -4
  27. package/src/adapters/cursor.ts +170 -29
  28. package/src/adapters/google-aistudio-parser.ts +49 -0
  29. package/src/adapters/google-antigravity-replay.ts +105 -25
  30. package/src/adapters/google-antigravity-wire.ts +5 -0
  31. package/src/adapters/google-errors.ts +41 -12
  32. package/src/adapters/google-http.ts +12 -11
  33. package/src/adapters/google.ts +219 -36
  34. package/src/adapters/image.ts +1 -1
  35. package/src/adapters/kiro-constants.ts +15 -0
  36. package/src/adapters/kiro-tools.ts +43 -15
  37. package/src/adapters/kiro.ts +54 -9
  38. package/src/adapters/openai-chat.ts +286 -242
  39. package/src/adapters/openai-responses.ts +335 -24
  40. package/src/adapters/run-turn-queue.ts +36 -1
  41. package/src/adapters/tool-catalog-nudge.ts +2 -2
  42. package/src/adapters/xai-tool-schema.ts +436 -0
  43. package/src/bridge.ts +67 -26
  44. package/src/chat/inbound.ts +29 -1
  45. package/src/chat/outbound.ts +15 -7
  46. package/src/claude/agents-inject.ts +8 -1
  47. package/src/claude/outbound.ts +10 -8
  48. package/src/cli/account-api.ts +27 -7
  49. package/src/cli/account-extended.ts +10 -3
  50. package/src/cli/account.ts +29 -5
  51. package/src/cli/alias.ts +66 -0
  52. package/src/cli/claude.ts +26 -1
  53. package/src/cli/dispatch.ts +13 -1
  54. package/src/cli/help.ts +1 -0
  55. package/src/cli/index.ts +6 -1
  56. package/src/cli/init.ts +1 -0
  57. package/src/cli/models-runtime.ts +95 -0
  58. package/src/cli/models.ts +13 -7
  59. package/src/cli/provider-runtime.ts +16 -2
  60. package/src/cli/registry.ts +6 -1
  61. package/src/cli/telemetry-commands.ts +25 -0
  62. package/src/cli/v2.ts +34 -10
  63. package/src/codex/account-pause.ts +2 -1
  64. package/src/codex/account-priority.ts +3 -2
  65. package/src/codex/app-server-processes.ts +80 -6
  66. package/src/codex/auth-api.ts +48 -8
  67. package/src/codex/auth-context.ts +21 -18
  68. package/src/codex/catalog/aggregation.ts +6 -0
  69. package/src/codex/catalog/model-metadata.ts +13 -1
  70. package/src/codex/catalog/native-models.ts +5 -2
  71. package/src/codex/catalog/parsing.ts +16 -0
  72. package/src/codex/catalog/provider-fetch.ts +20 -3
  73. package/src/codex/catalog/sync.ts +127 -2
  74. package/src/codex/catalog.ts +1 -1
  75. package/src/codex/codex-write-lock.ts +3 -1
  76. package/src/codex/convergence-types.ts +1 -1
  77. package/src/codex/convergence.ts +22 -2
  78. package/src/codex/desired-state.ts +2 -2
  79. package/src/codex/desktop-app-restart.ts +18 -5
  80. package/src/codex/inject-coordination.ts +83 -0
  81. package/src/codex/inject.ts +14 -1
  82. package/src/codex/log-guard/inspect.ts +22 -4
  83. package/src/codex/model-entitlements.ts +9 -2
  84. package/src/codex/prompt-layers.ts +371 -25
  85. package/src/codex/prompt-text-probe.ts +238 -0
  86. package/src/codex/quota.ts +123 -18
  87. package/src/codex/routing.ts +9 -0
  88. package/src/codex/subagent-model-fallback.ts +198 -27
  89. package/src/codex/transition-state.ts +107 -8
  90. package/src/combos/types.ts +10 -0
  91. package/src/compatibility/openai-responses.ts +33 -1
  92. package/src/config/autonomous-remediation.ts +21 -0
  93. package/src/config/provider-validation.ts +14 -0
  94. package/src/config/rebase-provenance.ts +68 -0
  95. package/src/config.ts +191 -17
  96. package/src/generated/compatibility-version.json +279 -159
  97. package/src/generated/model-metadata.ts +3 -0
  98. package/src/images/loop.ts +5 -4
  99. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  100. package/src/lab/fabric/producer-child.ts +1 -1
  101. package/src/lib/config-ownership.ts +20 -0
  102. package/src/lib/errors.ts +11 -2
  103. package/src/lib/package-tree-integrity.ts +101 -0
  104. package/src/oauth/aistudio-credentials.ts +65 -0
  105. package/src/oauth/aistudio-native-daemon.ts +116 -0
  106. package/src/oauth/aistudio-session-sync.ts +95 -0
  107. package/src/oauth/generic-account-failover.ts +231 -0
  108. package/src/oauth/google-aistudio-auth.ts +98 -0
  109. package/src/oauth/index.ts +57 -5
  110. package/src/oauth/key-providers.ts +18 -1
  111. package/src/oauth/kiro.ts +45 -0
  112. package/src/oauth/login-cli.ts +65 -1
  113. package/src/oauth/types.ts +15 -0
  114. package/src/providers/codex-capacity.ts +5 -2
  115. package/src/providers/command-code-efforts.ts +38 -6
  116. package/src/providers/context-cap.ts +4 -3
  117. package/src/providers/default-aliases.ts +65 -0
  118. package/src/providers/derive.ts +29 -1
  119. package/src/providers/fastwire.ts +7 -1
  120. package/src/providers/model-presets.ts +119 -0
  121. package/src/providers/new-model-policy.ts +146 -0
  122. package/src/providers/provider-id-rewrite.ts +2 -1
  123. package/src/providers/quota.ts +157 -46
  124. package/src/providers/registry.ts +184 -71
  125. package/src/providers/slug-codec.ts +52 -0
  126. package/src/responses/code-mode-helper-compat.ts +50 -0
  127. package/src/responses/custom-tool-compat.ts +34 -10
  128. package/src/responses/parser.ts +4 -0
  129. package/src/responses/schema.ts +5 -1
  130. package/src/responses/thought-signature-replay.ts +17 -0
  131. package/src/router.ts +43 -2
  132. package/src/routing/account-pool/cooldown.ts +8 -0
  133. package/src/routing/account-pool/index.ts +1 -0
  134. package/src/routing/analytics.ts +1 -0
  135. package/src/routing/quota.ts +10 -0
  136. package/src/server/auth-cors.ts +24 -0
  137. package/src/server/chat-completions.ts +26 -16
  138. package/src/server/chat-native-sse.ts +3 -3
  139. package/src/server/chat-native.ts +30 -11
  140. package/src/server/claude-messages.ts +1 -1
  141. package/src/server/effort-policy.ts +16 -0
  142. package/src/server/index.ts +180 -14
  143. package/src/server/lifecycle.ts +52 -1
  144. package/src/server/management/agent-settings-routes.ts +31 -15
  145. package/src/server/management/codex-prompt-routes.ts +570 -0
  146. package/src/server/management/combo-routes.ts +2 -1
  147. package/src/server/management/config-routes.ts +27 -9
  148. package/src/server/management/context.ts +9 -0
  149. package/src/server/management/logs-usage-routes.ts +11 -5
  150. package/src/server/management/model-routes.ts +266 -0
  151. package/src/server/management/oauth-account-routes.ts +13 -3
  152. package/src/server/management/provider-routes.ts +137 -3
  153. package/src/server/management/routing-profile-routes.ts +2 -2
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/port-reclaim.ts +19 -1
  156. package/src/server/relay-eager.ts +147 -20
  157. package/src/server/relay.ts +251 -19
  158. package/src/server/request-log-conversation.ts +33 -0
  159. package/src/server/request-log.ts +48 -21
  160. package/src/server/responses/collaboration.ts +42 -5
  161. package/src/server/responses/combo-stream-preflight.ts +10 -3
  162. package/src/server/responses/core.ts +575 -140
  163. package/src/server/responses/empty-completion-guard.ts +35 -0
  164. package/src/server/responses/fetch-helpers.ts +14 -6
  165. package/src/server/responses/input-admission.ts +3 -1
  166. package/src/server/responses/passthrough-error.ts +33 -9
  167. package/src/server/responses/policy-fallback.ts +1 -1
  168. package/src/server/responses/responses-field-backfill.ts +105 -13
  169. package/src/server/responses/ws-upstream.ts +35 -5
  170. package/src/server/responses-custom-tool-repair.ts +52 -7
  171. package/src/server/responses-terminal-repair.ts +25 -4
  172. package/src/server/sse-frame-buffer.ts +31 -4
  173. package/src/server/ws-bridge.ts +14 -2
  174. package/src/smoke/fingerprint-cache.ts +133 -0
  175. package/src/smoke/live-scenarios.ts +33 -0
  176. package/src/smoke/runner.ts +119 -0
  177. package/src/telemetry/dispatcher.ts +44 -0
  178. package/src/telemetry/fingerprint.ts +24 -0
  179. package/src/telemetry/hook.ts +43 -0
  180. package/src/telemetry/ledger.ts +54 -0
  181. package/src/telemetry/types.ts +23 -0
  182. package/src/types/config.ts +66 -14
  183. package/src/types/provider.ts +79 -1
  184. package/src/types/request.ts +18 -10
  185. package/src/types/tools.ts +30 -11
  186. package/src/types.ts +1 -0
  187. package/src/usage/command-code-manifest.ts +116 -0
  188. package/src/usage/cost.ts +2 -2
  189. package/src/usage/expected-prices.ts +126 -24
  190. package/src/usage/log.ts +18 -8
  191. package/src/usage/summary.ts +34 -12
  192. package/src/web-search/exa-executor.ts +40 -9
  193. package/src/web-search/index.ts +16 -8
  194. package/src/web-search/loop.ts +5 -4
  195. package/gui/dist/assets/index-DKLr4LTE.js +0 -102
  196. package/gui/dist/assets/index-DrSQdTRd.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,39 @@ export function sessionIdHeaderFromRequest(headers: Headers): string | null {
61
61
  return headers.get("session_id") ?? headers.get("session-id");
62
62
  }
63
63
 
64
+ /**
65
+ * Codex thread identity from inbound Responses headers when the parent-thread header is absent.
66
+ * Priority: x-codex-parent-thread-id > thread-id > session_id / session-id.
67
+ */
68
+ export function inboundClientThreadIdFromRequest(headers: Headers): string | undefined {
69
+ return firstSanitizedConversationId(
70
+ headers.get("x-codex-parent-thread-id"),
71
+ headers.get("thread-id"),
72
+ sessionIdHeaderFromRequest(headers),
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Fixed-size logical turn lane (#820).
78
+ *
79
+ * A lane must be as SPECIFIC as the identity available, which is the opposite of what
80
+ * `codexPoolAffinityKey` wants. Affinity deliberately prefers the parent thread so a whole
81
+ * subagent fan-out pins to one account; a lane keyed that way would put every parallel
82
+ * subagent of one parent into a single lane and reject all but the first with 503 — the
83
+ * fan-out is the normal case, not an abuse.
84
+ *
85
+ * So the parent is a QUALIFIER, never the lane on its own when a child thread exists: the
86
+ * pair separates siblings while still keeping one conversation's overlapping turns together.
87
+ */
88
+ export function sessionLaneIdFromRequest(headers: Headers): string | undefined {
89
+ const parent = normalizeLogConversationId(headers.get("x-codex-parent-thread-id"));
90
+ const thread = normalizeLogConversationId(headers.get("thread-id"));
91
+ const session = normalizeLogConversationId(sessionIdHeaderFromRequest(headers));
92
+ const specific = thread ?? session;
93
+ if (parent && specific) return `${parent}\u0000${specific}`;
94
+ return specific ?? parent;
95
+ }
96
+
64
97
  function firstSanitizedConversationId(
65
98
  ...values: Array<string | null | undefined>
66
99
  ): string | undefined {
@@ -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";
@@ -19,7 +20,7 @@ import {
19
20
  isKnownAdmissionKind,
20
21
  isKnownInboundProtocol,
21
22
  isKnownUsageSurface,
22
- isCodexUsageAccountLogLabel,
23
+ isPersistableAccountLogLabel,
23
24
  isValidReasoningWireValue,
24
25
  readRecentUsageEntries,
25
26
  usageForFinalLog,
@@ -65,6 +66,8 @@ export interface RequestLogContext {
65
66
  /** Stable non-PII Codex Pool account identity for durable usage attribution. */
66
67
  accountLogLabel?: string;
67
68
  requestedModel?: string;
69
+ /** User-facing alias selector when routing resolved one; native model remains `model`. */
70
+ requestedAlias?: string;
68
71
  /** Original bare helper model when the opt-in shadow-call route rewrote this request. */
69
72
  shadowCallRewrittenFrom?: string;
70
73
  /** Internal structural combo identity; omitted from RequestLogEntry/JSONL. */
@@ -144,6 +147,7 @@ export interface RequestLogEntry {
144
147
  /** Best-effort chat/session correlation for Logs grouping (#330). */
145
148
  conversationId?: string;
146
149
  requestedModel?: string;
150
+ requestedAlias?: string;
147
151
  /** Original bare helper model when the opt-in shadow-call route rewrote this request. */
148
152
  shadowCallRewrittenFrom?: string;
149
153
  requestedEffort?: string;
@@ -255,10 +259,11 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
255
259
  ...(entry.firstOutputMs !== undefined ? { firstOutputMs: entry.firstOutputMs } : {}),
256
260
  ...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
257
261
  ...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
258
- ...(isCodexUsageAccountLogLabel(entry.accountLogLabel)
262
+ ...(isPersistableAccountLogLabel(entry.accountLogLabel)
259
263
  ? { accountLogLabel: entry.accountLogLabel }
260
264
  : {}),
261
265
  ...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
266
+ ...(entry.requestedAlias ? { requestedAlias: entry.requestedAlias } : {}),
262
267
  ...(entry.shadowCallRewrittenFrom
263
268
  ? { shadowCallRewrittenFrom: entry.shadowCallRewrittenFrom }
264
269
  : {}),
@@ -373,12 +378,13 @@ export function addRequestLog(entry: RequestLogEntry) {
373
378
  ...(entry.apiKeyId ? { apiKeyId: entry.apiKeyId } : {}),
374
379
  ...(isKnownAdmissionKind(entry.admissionKind) ? { admissionKind: entry.admissionKind } : {}),
375
380
  ...(isKnownInboundProtocol(entry.inboundProtocol) ? { inboundProtocol: entry.inboundProtocol } : {}),
376
- ...(isCodexUsageAccountLogLabel(entry.accountLogLabel)
381
+ ...(isPersistableAccountLogLabel(entry.accountLogLabel)
377
382
  ? { accountLogLabel: entry.accountLogLabel }
378
383
  : {}),
379
384
  ...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
380
385
  ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
381
386
  ...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
387
+ ...(entry.requestedAlias ? { requestedAlias: entry.requestedAlias } : {}),
382
388
  ...(entry.shadowCallRewrittenFrom
383
389
  ? { shadowCallRewrittenFrom: entry.shadowCallRewrittenFrom }
384
390
  : {}),
@@ -765,7 +771,7 @@ function captureUpstreamErrorParsed(
765
771
  last_error?: { message?: unknown };
766
772
  response?: {
767
773
  error?: { type?: unknown; code?: unknown; message?: unknown };
768
- incomplete_details?: { reason?: unknown };
774
+ incomplete_details?: { reason?: unknown; message?: unknown };
769
775
  };
770
776
  };
771
777
  captureTerminalHttpStatus(logCtx, json);
@@ -779,7 +785,8 @@ function captureUpstreamErrorParsed(
779
785
  if (logCtx.upstreamError) return;
780
786
  const message = json?.error?.message
781
787
  ?? json?.last_error?.message
782
- ?? json?.response?.error?.message;
788
+ ?? json?.response?.error?.message
789
+ ?? json?.response?.incomplete_details?.message;
783
790
  if (typeof message === "string" && message.trim()) {
784
791
  logCtx.upstreamError = redactSecretString(message).slice(0, 500);
785
792
  return;
@@ -818,25 +825,43 @@ function captureTerminalHttpStatus(
818
825
  logCtx: RequestLogContext,
819
826
  json: {
820
827
  type?: unknown;
821
- response?: { error?: { type?: unknown; code?: unknown; message?: unknown } };
828
+ code?: unknown;
829
+ message?: unknown;
830
+ error?: { type?: unknown; code?: unknown; message?: unknown };
831
+ last_error?: { type?: unknown; code?: unknown; message?: unknown };
832
+ response?: {
833
+ error?: { type?: unknown; code?: unknown; message?: unknown };
834
+ incomplete_details?: { code?: unknown; message?: unknown };
835
+ };
822
836
  },
823
837
  ): void {
824
838
  if (logCtx.terminalHttpStatus !== undefined) return;
825
- if (json.type !== "response.failed") return;
826
- const error = json.response?.error;
827
- if (!error || typeof error !== "object") return;
828
- const terminalCode = error.code === null || typeof error.code === "string"
829
- ? error.code
830
- : undefined;
831
- if (isCyberPolicyCode(terminalCode)) {
839
+ const type = json.type;
840
+ if (type !== "response.failed" && type !== "response.incomplete" && type !== "error") return;
841
+ const responseError = json.response?.error;
842
+ const responseDetails = json.response?.incomplete_details;
843
+ const candidates = [json.error, json.last_error, responseError, responseDetails, json];
844
+ const policy = candidates.some(candidate => (
845
+ candidate?.code === null || typeof candidate?.code === "string"
846
+ ) && isCyberPolicyCode(candidate.code as string | null | undefined))
847
+ || candidates.some(candidate => (
848
+ typeof candidate?.message === "string"
849
+ && candidate.message.trim().length > 0
850
+ && isCyberPolicyMessage(candidate.message)
851
+ ));
852
+ if (policy) {
832
853
  logCtx.terminalErrorCode = CYBER_POLICY_ERROR_CODE;
833
- } else {
834
- delete logCtx.terminalErrorCode;
854
+ logCtx.terminalHttpStatus = 400;
855
+ return;
835
856
  }
857
+ if (type !== "response.failed" || !responseError || typeof responseError !== "object") return;
858
+ const responseCode = responseError.code === null || typeof responseError.code === "string"
859
+ ? responseError.code
860
+ : undefined;
836
861
  logCtx.terminalHttpStatus = httpStatusFromTerminalError({
837
- type: typeof error.type === "string" ? error.type : undefined,
838
- code: terminalCode,
839
- message: typeof error.message === "string" ? error.message : undefined,
862
+ type: typeof responseError.type === "string" ? responseError.type : undefined,
863
+ code: responseCode,
864
+ message: typeof responseError.message === "string" ? responseError.message : undefined,
840
865
  });
841
866
  }
842
867
 
@@ -945,11 +970,12 @@ export function addFinalRequestLog(
945
970
  ...(logCtx.apiKeyId ? { apiKeyId: logCtx.apiKeyId } : {}),
946
971
  ...(logCtx.admissionKind ? { admissionKind: logCtx.admissionKind } : {}),
947
972
  ...(logCtx.inboundProtocol ? { inboundProtocol: logCtx.inboundProtocol } : {}),
948
- ...(isCodexUsageAccountLogLabel(logCtx.accountLogLabel)
973
+ ...(isPersistableAccountLogLabel(logCtx.accountLogLabel)
949
974
  ? { accountLogLabel: logCtx.accountLogLabel }
950
975
  : {}),
951
976
  ...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
952
977
  ...(logCtx.requestedModel ? { requestedModel: logCtx.requestedModel } : {}),
978
+ ...(logCtx.requestedAlias ? { requestedAlias: logCtx.requestedAlias } : {}),
953
979
  ...(shadowCallRewrittenFrom ? { shadowCallRewrittenFrom } : {}),
954
980
  ...(logCtx.requestedEffort ? { requestedEffort: logCtx.requestedEffort } : {}),
955
981
  ...(logCtx.effectiveEffort ? { effectiveEffort: logCtx.effectiveEffort } : {}),
@@ -1170,7 +1196,7 @@ export function sealRequestAttemptIdentity(
1170
1196
  if (!attempt) return;
1171
1197
  attempt.provider = provider;
1172
1198
  attempt.adapter = adapter;
1173
- if (isCodexUsageAccountLogLabel(accountLogLabel)) attempt.accountLogLabel = accountLogLabel;
1199
+ if (isPersistableAccountLogLabel(accountLogLabel)) attempt.accountLogLabel = accountLogLabel;
1174
1200
  }
1175
1201
 
1176
1202
  export function noteAttemptSend(
@@ -1200,6 +1226,7 @@ export function finishRequestAttempt(
1200
1226
  status: number,
1201
1227
  durationMs: number,
1202
1228
  usage?: OcxUsage,
1229
+ upstreamError?: string,
1203
1230
  ): PersistedUsageAttempt {
1204
1231
  const finalized = finalizedUsage(
1205
1232
  attempt.adapter,
@@ -1214,7 +1241,7 @@ export function finishRequestAttempt(
1214
1241
  else delete attempt.usage;
1215
1242
  if (finalized.totalTokens !== undefined) attempt.totalTokens = finalized.totalTokens;
1216
1243
  else delete attempt.totalTokens;
1217
- const errorCode = requestLogErrorCode(status);
1244
+ const errorCode = requestLogErrorCode(status, upstreamError);
1218
1245
  if (errorCode) attempt.errorCode = errorCode;
1219
1246
  else delete attempt.errorCode;
1220
1247
  return attempt;