@bitkyc08/opencodex 2.10.2 → 2.11.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 (121) hide show
  1. package/README.md +31 -0
  2. package/bin/ocx.mjs +10 -0
  3. package/gui/dist/assets/index-Bk-PN-70.css +1 -0
  4. package/gui/dist/assets/index-BynIEIV-.js +70 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +4 -2
  7. package/src/adapters/cursor/effort-map.ts +11 -0
  8. package/src/adapters/cursor/live-transport.ts +11 -0
  9. package/src/adapters/cursor/native-exec-fs.ts +9 -6
  10. package/src/adapters/cursor/native-exec.ts +4 -2
  11. package/src/adapters/cursor/protobuf-events.ts +176 -4
  12. package/src/adapters/cursor/request-builder.ts +15 -4
  13. package/src/adapters/cursor/tool-definitions.ts +118 -2
  14. package/src/adapters/google.ts +15 -5
  15. package/src/adapters/openai-chat.ts +24 -2
  16. package/src/adapters/openai-responses.ts +2 -1
  17. package/src/bridge.ts +9 -5
  18. package/src/chat/outbound.ts +4 -3
  19. package/src/claude/desktop-3p.ts +222 -2
  20. package/src/claude/outbound.ts +15 -6
  21. package/src/cli/account-api.ts +4 -0
  22. package/src/cli/account-extended.ts +112 -0
  23. package/src/cli/account.ts +23 -6
  24. package/src/cli/claude-desktop.ts +26 -3
  25. package/src/cli/config-command.ts +9 -0
  26. package/src/cli/help.ts +18 -2
  27. package/src/cli/index.ts +277 -55
  28. package/src/cli/models.ts +5 -1
  29. package/src/cli/provider.ts +8 -2
  30. package/src/cli/ready.ts +301 -0
  31. package/src/cli/system-restart-client.ts +146 -0
  32. package/src/cli/tray-proxy.ts +153 -6
  33. package/src/clients/config-export.ts +12 -19
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-namespaces.ts +49 -3
  36. package/src/codex/account-priority.ts +83 -0
  37. package/src/codex/auth-api.ts +83 -0
  38. package/src/codex/auth-context.ts +5 -2
  39. package/src/codex/catalog/provider-fetch.ts +11 -0
  40. package/src/codex/catalog/sync.ts +23 -1
  41. package/src/codex/codex-write-lock.ts +16 -4
  42. package/src/codex/desired-state.ts +37 -4
  43. package/src/codex/history-job.ts +15 -5
  44. package/src/codex/history-provider.ts +31 -14
  45. package/src/codex/history-worker.ts +28 -4
  46. package/src/codex/inject-coordination.ts +13 -1
  47. package/src/codex/inject.ts +360 -66
  48. package/src/codex/internal/history-writer.ts +1 -1
  49. package/src/codex/native-main-lock-file.ts +5 -1
  50. package/src/codex/native-main-owner.ts +17 -3
  51. package/src/codex/native-profile-manager.ts +19 -0
  52. package/src/codex/native-profile-startup.ts +8 -0
  53. package/src/codex/native-residue.ts +140 -27
  54. package/src/codex/pool-rotation.ts +74 -4
  55. package/src/codex/refresh.ts +7 -0
  56. package/src/codex/routing.ts +177 -36
  57. package/src/codex/subagent-model-fallback.ts +34 -4
  58. package/src/codex/sync.ts +61 -0
  59. package/src/codex/upstream-host-health.ts +329 -31
  60. package/src/combos/request.ts +2 -0
  61. package/src/config.ts +221 -2
  62. package/src/images/loop.ts +1 -1
  63. package/src/integrations/native/ownership-preflight.ts +39 -2
  64. package/src/lib/bun-stream-caps.ts +3 -3
  65. package/src/lib/sse-decoder.ts +41 -0
  66. package/src/lib/system-restart-contract.ts +73 -0
  67. package/src/lib/windows-secret-acl.ts +141 -39
  68. package/src/lib/windows-user-principal.ts +283 -0
  69. package/src/lib/winsw.ts +18 -2
  70. package/src/oauth/key-providers.ts +12 -0
  71. package/src/providers/derive.ts +54 -2
  72. package/src/providers/free-directory.ts +6 -5
  73. package/src/providers/model-discovery.ts +9 -3
  74. package/src/providers/quota.ts +592 -0
  75. package/src/providers/registry.ts +316 -13
  76. package/src/responses/parser.ts +26 -10
  77. package/src/responses/reasoning-replay-cache.ts +1 -0
  78. package/src/routing/profile-namespace.ts +15 -0
  79. package/src/routing/profile.ts +2 -1
  80. package/src/server/auth-cors.ts +44 -13
  81. package/src/server/chat-completions.ts +0 -4
  82. package/src/server/claude-messages.ts +73 -15
  83. package/src/server/github-copilot-responses-repair.ts +338 -0
  84. package/src/server/index.ts +328 -111
  85. package/src/server/lifecycle.ts +36 -0
  86. package/src/server/management/agent-settings-routes.ts +147 -56
  87. package/src/server/management/config-routes.ts +7 -2
  88. package/src/server/management/context.ts +4 -0
  89. package/src/server/management/native-integration-routes.ts +199 -20
  90. package/src/server/management/provider-routes.ts +41 -0
  91. package/src/server/management/routing-profile-routes.ts +234 -5
  92. package/src/server/management/system-restart.ts +12 -10
  93. package/src/server/management/system-routes.ts +20 -0
  94. package/src/server/management-auth.ts +51 -3
  95. package/src/server/ports.ts +41 -1
  96. package/src/server/proxy-liveness.ts +129 -4
  97. package/src/server/readiness.ts +99 -0
  98. package/src/server/relay.ts +113 -97
  99. package/src/server/request-log.ts +10 -4
  100. package/src/server/responses/compact.ts +107 -12
  101. package/src/server/responses/core.ts +220 -39
  102. package/src/server/responses-item-id-repair.ts +22 -3
  103. package/src/server/responses-model-rewrite.ts +29 -0
  104. package/src/server/sse-frame-buffer.ts +292 -0
  105. package/src/server/sse-payload-rewrite.ts +25 -14
  106. package/src/server/ws-bridge.ts +27 -22
  107. package/src/service-manager-probe.ts +520 -10
  108. package/src/service.ts +134 -2
  109. package/src/storage/worker-lifecycle.ts +14 -14
  110. package/src/tray/windows-tray.ps1 +74 -9
  111. package/src/types.ts +68 -2
  112. package/src/update/index.ts +12 -0
  113. package/src/update/job.ts +392 -18
  114. package/src/update/npm-cache-preflight.d.mts +47 -0
  115. package/src/update/npm-cache-preflight.mjs +201 -0
  116. package/src/usage/log.ts +1 -1
  117. package/src/vision/index.ts +77 -2
  118. package/src/web-search/loop.ts +1 -1
  119. package/src/web-search/parse.ts +4 -1
  120. package/gui/dist/assets/index-BKVqyYqT.js +0 -70
  121. package/gui/dist/assets/index-Ca_3269W.css +0 -1
@@ -6,17 +6,21 @@ import {
6
6
  addFinalRequestLog,
7
7
  httpStatusForRequestLogTerminal,
8
8
  inspectResponseLogJson,
9
- inspectResponseLogSsePayload,
10
9
  inspectResponseLogSsePayloadParsed,
11
10
  recordFirstOutput,
12
11
  type RequestLogContext,
13
12
  type RequestLogEntry,
14
13
  } from "./request-log";
14
+ import {
15
+ BoundedSseFrameBuffer,
16
+ joinSseFrameBytes,
17
+ MAX_CLIENT_SSE_FRAME_BYTES,
18
+ } from "./sse-frame-buffer";
15
19
 
16
20
  const nativePassthroughSseResponses = new WeakSet<Response>();
17
21
  const eagerRelaySseResponses = new WeakSet<Response>();
18
22
 
19
- export const MAX_INSPECTION_SSE_FRAME_BYTES = 4 * 1024 * 1024;
23
+ export const MAX_INSPECTION_SSE_FRAME_BYTES = MAX_CLIENT_SSE_FRAME_BYTES;
20
24
  export const MAX_COMPLETED_OUTPUT_ITEMS = 256;
21
25
  export const MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES = 8 * 1024 * 1024;
22
26
  export const MAX_TAIL_ERROR_MESSAGE_CHARS = 512;
@@ -105,30 +109,32 @@ export type SseTerminalOutputBoundary = {
105
109
 
106
110
  /**
107
111
  * Frame-aware client output boundary shared by both native Responses relays.
108
- * It buffers only the current incomplete SSE block, forwards complete blocks
109
- * through the first Responses terminal, and drops every later block/byte.
112
+ * It buffers only the current incomplete SSE block under the same hard byte
113
+ * cap as inspection, forwards complete blocks through the first Responses
114
+ * terminal, and drops every later block/byte.
110
115
  */
111
116
  export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
112
- let decoder: TextDecoder | null = new TextDecoder();
113
- const encoder = new TextEncoder();
114
- let buffer = "";
117
+ const decoder = new TextDecoder();
118
+ const framer = new BoundedSseFrameBuffer(MAX_INSPECTION_SSE_FRAME_BYTES);
115
119
  let terminal = false;
116
120
  let done = false;
117
121
  let disposed = false;
118
122
 
119
- const process = (flush: boolean): Uint8Array => {
120
- if (disposed || terminal) return new Uint8Array(0);
121
- let output = "";
123
+ const processFrames = (
124
+ frames: ReturnType<BoundedSseFrameBuffer["feed"]>,
125
+ ): Uint8Array => {
126
+ if (disposed || terminal || frames.length === 0) return new Uint8Array(0);
127
+ const output: Uint8Array[] = [];
122
128
  let responsesTerminal = false;
123
- for (;;) {
124
- const next = nextSseBlock(buffer);
125
- if (!next) break;
126
- buffer = next.rest;
127
- const payload = sseDataPayload(next.block);
128
- if (!responsesTerminal) output += next.block + next.delimiter;
129
- if (payload === "[DONE]") {
129
+ for (const frame of frames) {
130
+ const payload = sseDataPayload(decoder.decode(frame.block));
131
+ 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);
136
+ if (isDone) {
130
137
  done = true;
131
- if (responsesTerminal) output += next.block + next.delimiter;
132
138
  continue;
133
139
  }
134
140
  if (!responsesTerminal && payload && terminalStatusFromSsePayload(payload)) {
@@ -137,33 +143,26 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
137
143
  }
138
144
  if (responsesTerminal) {
139
145
  terminal = true;
140
- buffer = "";
141
- }
142
- if (flush && !terminal && buffer.length > 0) {
143
- output += buffer;
144
- buffer = "";
146
+ framer.dispose();
145
147
  }
146
- return encoder.encode(output);
148
+ return joinSseFrameBytes(output);
147
149
  };
148
150
 
149
151
  return {
150
152
  feed(chunk) {
151
153
  if (disposed || terminal) return new Uint8Array(0);
152
- buffer += decoder!.decode(chunk, { stream: true });
153
- return process(false);
154
+ return processFrames(framer.feed(chunk));
154
155
  },
155
156
  finish() {
156
157
  if (disposed || terminal) return new Uint8Array(0);
157
- buffer += decoder!.decode();
158
- return process(true);
158
+ return framer.finish();
159
159
  },
160
160
  terminalSeen: () => terminal,
161
161
  doneSeen: () => done,
162
162
  dispose() {
163
163
  if (disposed) return;
164
164
  disposed = true;
165
- decoder = null;
166
- buffer = "";
165
+ framer.dispose();
167
166
  },
168
167
  };
169
168
  }
@@ -228,7 +227,14 @@ export function relaySseWithFailedTail(
228
227
  if (result !== "buffered") return;
229
228
  }
230
229
  } catch (err) {
231
- const partial = terminalBoundary.finish();
230
+ let partial: Uint8Array = new Uint8Array(0);
231
+ try {
232
+ partial = terminalBoundary.finish();
233
+ } catch {
234
+ // A near-cap ambiguous delimiter tail may itself overflow at EOF.
235
+ // Preserve the original read/framing failure and continue emitting
236
+ // the bounded failed tail instead of letting cleanup throw again.
237
+ }
232
238
  terminalBoundary.dispose();
233
239
  if (closed) return;
234
240
  const payload = buildFailedTailPayload(err);
@@ -359,53 +365,42 @@ export function trackSseForRequestLog(
359
365
  onFirstOutput?: () => void,
360
366
  ): ReadableStream<Uint8Array> {
361
367
  const reader = body.getReader();
362
- const decoder = new TextDecoder();
363
- let buffer = "";
364
368
  let terminalReported = false;
365
- const reportFirstOutput = createFirstOutputReporter(onFirstOutput);
366
369
 
367
370
  const reportTerminal = (status: ResponsesTerminalStatus) => {
368
371
  if (terminalReported) return;
369
372
  terminalReported = true;
370
373
  onTerminal(status);
371
374
  };
372
-
373
- const inspectPayload = (payload: string | null) => {
374
- if (!payload) return;
375
- if (logCtx) inspectResponseLogSsePayload(logCtx, payload);
376
- reportFirstOutput.payload(payload);
377
- const status = terminalStatusFromSsePayload(payload);
378
- if (status) reportTerminal(status);
379
- };
380
-
381
- const inspectChunk = (value: Uint8Array) => {
382
- buffer += decoder.decode(value, { stream: true });
383
- let next: { block: string; rest: string } | null;
384
- while ((next = nextSseBlock(buffer))) {
385
- buffer = next.rest;
386
- inspectPayload(sseDataPayload(next.block));
387
- }
388
- };
375
+ // Reuse the byte-bounded inspector so translated responses cannot retain an
376
+ // unterminated upstream frame or parse the same event once per observer.
377
+ const inspector = createSseInspector({
378
+ onTerminal: reportTerminal,
379
+ logCtx,
380
+ onFirstOutput,
381
+ });
389
382
 
390
383
  return new ReadableStream<Uint8Array>({
391
384
  async pull(controller) {
392
385
  try {
393
386
  const { done, value } = await reader.read();
394
387
  if (done) {
395
- buffer += decoder.decode();
396
- if (buffer.trim()) inspectPayload(sseDataPayload(buffer));
388
+ inspector.finish();
397
389
  if (!terminalReported) reportTerminal("incomplete");
390
+ inspector.dispose();
398
391
  controller.close();
399
392
  return;
400
393
  }
401
- inspectChunk(value);
394
+ inspector.feed(value);
402
395
  controller.enqueue(value);
403
396
  } catch (err) {
404
397
  if (!terminalReported) reportTerminal("incomplete");
398
+ inspector.dispose();
405
399
  try { controller.error(err); } catch { /* already torn down */ }
406
400
  }
407
401
  },
408
402
  cancel(reason) {
403
+ inspector.dispose();
409
404
  onCancel();
410
405
  reader.cancel(reason).catch(() => {});
411
406
  },
@@ -516,38 +511,23 @@ export function relaySseWithHeartbeat(
516
511
  ): ReadableStream<Uint8Array> | null {
517
512
  if (!body) return null;
518
513
  const reader = body.getReader();
519
- const decoder = new TextDecoder();
520
514
  const heartbeat = new TextEncoder().encode(": opencodex keepalive\n\n");
521
515
  let timer: ReturnType<typeof setInterval> | undefined;
522
516
  let closed = false;
523
517
  let clientCancelled = false;
524
518
  let terminalReported = false;
525
- let buffer = "";
526
519
 
527
520
  const reportTerminal = (status: ResponsesTerminalStatus) => {
528
521
  if (terminalReported || clientCancelled || closed) return;
529
522
  terminalReported = true;
530
523
  onTerminal?.(status);
531
524
  };
532
-
533
- const inspectPayload = (payload: string | null) => {
534
- if (!payload) return;
535
- const status = terminalStatusFromSsePayload(payload);
536
- if (status) reportTerminal(status);
537
- };
538
-
539
- const inspectChunk = (value: Uint8Array) => {
540
- buffer += decoder.decode(value, { stream: true });
541
- let next: { block: string; rest: string } | null;
542
- while ((next = nextSseBlock(buffer))) {
543
- buffer = next.rest;
544
- inspectPayload(sseDataPayload(next.block));
545
- }
546
- };
525
+ const inspector = createSseInspector({ onTerminal: reportTerminal });
547
526
 
548
527
  const cleanup = () => {
549
528
  if (closed) return;
550
529
  closed = true;
530
+ inspector.dispose();
551
531
  if (timer) clearInterval(timer);
552
532
  timer = undefined;
553
533
  options?.onDone?.();
@@ -569,14 +549,13 @@ export function relaySseWithHeartbeat(
569
549
  try {
570
550
  const { done, value } = await reader.read();
571
551
  if (done) {
572
- buffer += decoder.decode();
573
- if (buffer.trim()) inspectPayload(sseDataPayload(buffer));
552
+ inspector.finish();
574
553
  if (!terminalReported && !clientCancelled) reportTerminal("incomplete");
575
554
  cleanup();
576
555
  controller.close();
577
556
  return;
578
557
  }
579
- inspectChunk(value);
558
+ inspector.feed(value);
580
559
  controller.enqueue(value);
581
560
  } catch (err) {
582
561
  if (!clientCancelled) reportTerminal("incomplete");
@@ -615,6 +594,12 @@ export type SseInspectorHandlers = {
615
594
  logCtx?: RequestLogContext;
616
595
  onCompletedResponse?: (response: { id?: unknown; output?: unknown; status?: unknown }) => void;
617
596
  onFirstOutput?: () => void;
597
+ /**
598
+ * Provider-scoped compatibility: persist the completed snapshot under the
599
+ * first response id exposed to the client when an upstream changes ids
600
+ * between `response.created` and `response.completed`.
601
+ */
602
+ pinCompletedResponseIdToFirstSeen?: boolean;
618
603
  };
619
604
 
620
605
  type CompletedOutputItem = { item: unknown; sourceBytes: number };
@@ -644,17 +629,6 @@ function delimiterLengthAt(
644
629
  return byteAt(index + 3) === 10 ? 4 : 0;
645
630
  }
646
631
 
647
- function joinedBytes(slices: readonly Uint8Array[], byteLength: number): Uint8Array {
648
- if (slices.length === 1 && slices[0]!.byteLength === byteLength) return slices[0]!;
649
- const joined = new Uint8Array(byteLength);
650
- let offset = 0;
651
- for (const slice of slices) {
652
- joined.set(slice, offset);
653
- offset += slice.byteLength;
654
- }
655
- return joined;
656
- }
657
-
658
632
  /**
659
633
  * Per-chunk SSE inspection state machine shared by consumeForInspection,
660
634
  * consumeForResponseLogMetadata, and the eager bounded relay (relay-eager.ts).
@@ -675,8 +649,8 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
675
649
  let reported = false;
676
650
  let sawTerminal = false;
677
651
  let disposed = false;
678
- let delimiterTail = new Uint8Array(0);
679
- let candidateSlices: Uint8Array[] = [];
652
+ let delimiterTail: Uint8Array = new Uint8Array(0);
653
+ let candidate: Uint8Array = new Uint8Array(0);
680
654
  let candidateBytes = 0;
681
655
  let discardingOversizedFrame = false;
682
656
  const reportFirstOutput = createFirstOutputReporter(handlers.onFirstOutput);
@@ -686,10 +660,11 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
686
660
  : null;
687
661
  let aggregateItemBytes = 0;
688
662
  let reconstructionTainted = false;
663
+ let firstResponseId: string | undefined;
689
664
 
690
665
  const clearFrameState = (): void => {
691
666
  delimiterTail = new Uint8Array(0);
692
- candidateSlices = [];
667
+ candidate = new Uint8Array(0);
693
668
  candidateBytes = 0;
694
669
  discardingOversizedFrame = false;
695
670
  };
@@ -706,6 +681,31 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
706
681
  decoder = null;
707
682
  clearFrameState();
708
683
  clearCompletedItems();
684
+ firstResponseId = undefined;
685
+ };
686
+
687
+ const ensureCandidateCapacity = (requiredBytes: number): void => {
688
+ if (candidate.byteLength >= requiredBytes) return;
689
+ let capacity = candidate.byteLength === 0
690
+ ? Math.min(MAX_INSPECTION_SSE_FRAME_BYTES, Math.max(requiredBytes, 4096))
691
+ : candidate.byteLength;
692
+ while (capacity < requiredBytes) {
693
+ capacity = Math.min(
694
+ MAX_INSPECTION_SSE_FRAME_BYTES,
695
+ Math.max(requiredBytes, capacity * 2),
696
+ );
697
+ }
698
+ const grown = new Uint8Array(capacity);
699
+ if (candidateBytes > 0) grown.set(candidate.subarray(0, candidateBytes));
700
+ candidate = grown;
701
+ };
702
+
703
+ const takeCandidate = (): Uint8Array => {
704
+ if (candidateBytes === 0) return new Uint8Array(0);
705
+ const frame = candidate.slice(0, candidateBytes);
706
+ candidate = new Uint8Array(0);
707
+ candidateBytes = 0;
708
+ return frame;
709
709
  };
710
710
 
711
711
  const retainCandidateSlice = (slice: Uint8Array): void => {
@@ -716,7 +716,7 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
716
716
  Math.min(nextBytes, MAX_INSPECTION_SSE_FRAME_BYTES),
717
717
  );
718
718
  if (nextBytes > MAX_INSPECTION_SSE_FRAME_BYTES) {
719
- candidateSlices = [];
719
+ candidate = new Uint8Array(0);
720
720
  candidateBytes = 0;
721
721
  discardingOversizedFrame = true;
722
722
  inspectionCounters.frameCapOverflows += 1;
@@ -726,10 +726,8 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
726
726
  reconstructionTainted = true;
727
727
  return;
728
728
  }
729
- // `subarray()` aliases the upstream chunk's backing buffer. Copy only the
730
- // live candidate bytes so a tiny trailing frame cannot pin a multi-MiB
731
- // chunk whose preceding frames have already been consumed.
732
- candidateSlices.push(slice.slice());
729
+ ensureCandidateCapacity(nextBytes);
730
+ candidate.set(slice, candidateBytes);
733
731
  candidateBytes = nextBytes;
734
732
  };
735
733
 
@@ -800,6 +798,17 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
800
798
  const parsedEvent = parsed && typeof parsed === "object" && !Array.isArray(parsed)
801
799
  ? parsed as ParsedSseEvent
802
800
  : null;
801
+ const responseRecord = parsedEvent
802
+ && typeof parsedEvent.response === "object"
803
+ && parsedEvent.response !== null
804
+ && !Array.isArray(parsedEvent.response)
805
+ ? parsedEvent.response as { id?: unknown }
806
+ : null;
807
+ if (handlers.pinCompletedResponseIdToFirstSeen
808
+ && responseRecord
809
+ && typeof responseRecord.id === "string") {
810
+ firstResponseId ??= responseRecord.id;
811
+ }
803
812
  const doneItem = parsedEvent?.type === "response.output_item.done" ? parsedEvent.item : undefined;
804
813
  if (parsedEvent
805
814
  && doneItem !== undefined
@@ -814,6 +823,11 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
814
823
 
815
824
  let response = completedResponseFromParsedEvent(parsedEvent);
816
825
  if (response) {
826
+ if (handlers.pinCompletedResponseIdToFirstSeen
827
+ && firstResponseId !== undefined
828
+ && response.id !== firstResponseId) {
829
+ response = { ...response, id: firstResponseId };
830
+ }
817
831
  // Authoritative output is a NON-EMPTY ARRAY only. Anything else
818
832
  // (missing, null, scalar, object) keeps the historical backfill
819
833
  // behavior so a malformed terminal cannot reach rememberResponseState
@@ -849,9 +863,7 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
849
863
  return;
850
864
  }
851
865
  const sourceBytes = candidateBytes;
852
- const frame = joinedBytes(candidateSlices, sourceBytes);
853
- candidateSlices = [];
854
- candidateBytes = 0;
866
+ const frame = takeCandidate();
855
867
  if (reported && !handlers.onCompletedResponse) return;
856
868
  const decoded = decoder!.decode(frame);
857
869
  scanPayload(sseDataPayload(decoded), sourceBytes);
@@ -908,7 +920,7 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
908
920
  delimiterTail = new Uint8Array(0);
909
921
  if (!discardingOversizedFrame && candidateBytes > 0 && !reported) {
910
922
  const sourceBytes = candidateBytes;
911
- const decoded = decoder!.decode(joinedBytes(candidateSlices, sourceBytes));
923
+ const decoded = decoder!.decode(takeCandidate());
912
924
  scanPayload(decoded.trim() ? sseDataPayload(decoded) : null, sourceBytes);
913
925
  }
914
926
  } finally {
@@ -929,6 +941,8 @@ export type InspectionConsumerOptions = {
929
941
  drainBounds?: Partial<InspectionDrainBounds>;
930
942
  upstream?: AbortController;
931
943
  now?: () => number;
944
+ /** Forward provider-scoped response-id pinning to the owned inspector. */
945
+ pinCompletedResponseIdToFirstSeen?: boolean;
932
946
  /** Test seam for proving both public consumers dispose their owned inspector. */
933
947
  inspectorFactory?: (handlers: SseInspectorHandlers) => SseInspector;
934
948
  };
@@ -1085,6 +1099,7 @@ export function consumeForInspection(
1085
1099
  logCtx,
1086
1100
  onCompletedResponse,
1087
1101
  onFirstOutput,
1102
+ pinCompletedResponseIdToFirstSeen: options?.pinCompletedResponseIdToFirstSeen,
1088
1103
  });
1089
1104
  startBoundedInspectionPump({
1090
1105
  ...options,
@@ -1130,6 +1145,7 @@ export function consumeForResponseLogMetadata(
1130
1145
  logCtx,
1131
1146
  onCompletedResponse,
1132
1147
  onFirstOutput,
1148
+ pinCompletedResponseIdToFirstSeen: options?.pinCompletedResponseIdToFirstSeen,
1133
1149
  });
1134
1150
  startBoundedInspectionPump({ ...options, reader, inspector, signal, onDone });
1135
1151
  }
@@ -68,6 +68,8 @@ export interface RequestLogContext {
68
68
  modelSupportsServiceTier?: boolean;
69
69
  responseServiceTier?: string;
70
70
  resolvedModel?: string;
71
+ /** Internal: client-facing response metadata must not replace the physical routed model. */
72
+ preserveResolvedModelFromRoute?: boolean;
71
73
  usage?: OcxUsage;
72
74
  usageLogInputTokens?: number;
73
75
  attempts?: PersistedUsageAttempt[];
@@ -252,7 +254,7 @@ export function requestLogEntryFromPersistedUsage(entry: PersistedUsageEntry): R
252
254
  usageStatus: entry.usageStatus,
253
255
  ...(entry.usage ? { usage: entry.usage } : {}),
254
256
  ...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
255
- ...(entry.attempts?.length ? { attempts: entry.attempts } : {}),
257
+ ...(entry.attempts !== undefined ? { attempts: entry.attempts } : {}),
256
258
  ...(routeDecision ? { routeDecision } : {}),
257
259
  };
258
260
  }
@@ -346,7 +348,7 @@ export function addRequestLog(entry: RequestLogEntry) {
346
348
  usageStatus: entry.usageStatus,
347
349
  ...(entry.usage ? { usage: entry.usage } : {}),
348
350
  ...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
349
- ...(entry.attempts?.length ? { attempts: entry.attempts } : {}),
351
+ ...(entry.attempts !== undefined ? { attempts: entry.attempts } : {}),
350
352
  ...failureDiagnostics,
351
353
  ...(entry.routeDecision ? { routeDecision: entry.routeDecision } : {}),
352
354
  });
@@ -512,7 +514,11 @@ export function applyResponseLogMetadata(logCtx: RequestLogContext, payload: unk
512
514
  : payload;
513
515
  if (!source || typeof source !== "object") return;
514
516
  const model = (source as { model?: unknown }).model;
515
- if (typeof model === "string" && model.trim()) logCtx.resolvedModel = model;
517
+ if (
518
+ !logCtx.preserveResolvedModelFromRoute
519
+ && typeof model === "string"
520
+ && model.trim()
521
+ ) logCtx.resolvedModel = model;
516
522
  const serviceTier = (source as { service_tier?: unknown }).service_tier;
517
523
  if (typeof serviceTier === "string" && serviceTier.trim()) logCtx.responseServiceTier = serviceTier;
518
524
  const usage = usageFromResponsesPayload((source as { usage?: unknown }).usage);
@@ -832,7 +838,7 @@ export function addFinalRequestLog(
832
838
  usageStatus,
833
839
  ...(loggedUsage ? { usage: loggedUsage } : {}),
834
840
  ...(totalTokens !== undefined ? { totalTokens } : {}),
835
- ...(attempts?.length ? { attempts } : {}),
841
+ ...(attempts !== undefined ? { attempts } : {}),
836
842
  ...(logCtx.affinity ? { affinity: logCtx.affinity } : {}),
837
843
  ...(logCtx.transportPhase ? { transportPhase: logCtx.transportPhase } : {}),
838
844
  ...(logCtx.terminalSource ? { terminalSource: logCtx.terminalSource } : {}),
@@ -70,7 +70,16 @@ import {
70
70
  type UpstreamSendRecovery,
71
71
  } from "../../lib/upstream-retry";
72
72
  import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upstream-reachability";
73
- import { recordUpstreamHostFailure, resetUpstreamHostHealth, upstreamHostHealthKey } from "../../codex/upstream-host-health";
73
+ import {
74
+ acquireUpstreamHostAdmission,
75
+ disableUpstreamHostCircuitForKey,
76
+ normalizeUpstreamHostCircuitThreshold,
77
+ recordUpstreamHostFailure,
78
+ releaseUpstreamHostAdmission,
79
+ resetUpstreamHostHealth,
80
+ upstreamHostHealthKey,
81
+ type UpstreamHostAdmissionLease,
82
+ } from "../../codex/upstream-host-health";
74
83
  import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "../auth-cors";
75
84
  import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
76
85
  import { isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
@@ -112,7 +121,13 @@ import {
112
121
  import { hasResponsesItemIdRepair, relaySseWithResponsesItemIdRepair } from "../responses-item-id-repair";
113
122
  import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
114
123
 
115
- import { decodeRequestErrorResponse, handleResponses, usesCodexForwardPoolAuth } from "./core";
124
+ import {
125
+ decodeRequestErrorResponse,
126
+ handleResponses,
127
+ preAuthUpstreamHostCircuitKey,
128
+ upstreamHostCircuitOpenResponse,
129
+ usesCodexForwardPoolAuth,
130
+ } from "./core";
116
131
  import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
117
132
 
118
133
  export const COMPACT_RESPONSE_MAX_BYTES = 32 * 1024 * 1024;
@@ -312,12 +327,33 @@ export async function handleResponsesCompact(
312
327
  // official OpenAI API. Any other Responses-shaped gateway must take the routed
313
328
  // summarizer path below, or compaction fails against an endpoint it never had (#422).
314
329
  if (supportsNativeResponsesCompactEndpoint(route.providerName, route.provider)) {
330
+ if (req.signal.aborted) {
331
+ return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
332
+ }
333
+ // The enclosing native-compact guard already restricts this path to
334
+ // supported backends, so compact intentionally does not require the
335
+ // regular Responses adapter check here.
336
+ const preAuthCompactHostKey = preAuthUpstreamHostCircuitKey(route, config, {
337
+ requireResponsesAdapter: false,
338
+ });
339
+ let compactHostAdmissionLease: UpstreamHostAdmissionLease | null = null;
340
+ let authCtx: CodexAuthContext = { kind: "main", accountId: null };
341
+ if (preAuthCompactHostKey) {
342
+ const admission = acquireUpstreamHostAdmission(
343
+ preAuthCompactHostKey,
344
+ config.upstreamHostCircuitThreshold,
345
+ );
346
+ if (admission.kind === "blocked") {
347
+ return upstreamHostCircuitOpenResponse(admission.retryAfterSeconds);
348
+ }
349
+ compactHostAdmissionLease = admission.lease;
350
+ }
351
+ try {
315
352
  // Native ChatGPT/OpenAI model: forward the compact request verbatim to the real backend.
316
353
  // Resolve the SAME pool/thread auth context as /v1/responses — forwarding the caller's raw
317
354
  // headers would run compaction on the wrong account (or 401) whenever a pool account is
318
355
  // active for this thread while normal turns succeed.
319
356
  let compactProvider = route.provider;
320
- let authCtx: CodexAuthContext = { kind: "main", accountId: null };
321
357
  const headers = new Headers({ "content-type": "application/json" });
322
358
  try {
323
359
  if (route.codexAccountMode) {
@@ -364,6 +400,45 @@ export async function handleResponsesCompact(
364
400
  // so routed-model reasoning items (reasoning_text content) don't 400 the ChatGPT backend.
365
401
  const compactBody = sanitizeReasoningInputContent(compactBodyRaw) as typeof compactBodyRaw;
366
402
  const compactUrl = `${base}/responses/compact`;
403
+ const actualCompactHostKey = upstreamHostHealthKey(
404
+ route.providerName,
405
+ safeOriginLabel(compactUrl),
406
+ );
407
+ const compactHostKey = compactProvider.authMode === "forward"
408
+ ? actualCompactHostKey
409
+ : null;
410
+ const compactHostCircuitEnabled = compactHostKey !== null
411
+ && normalizeUpstreamHostCircuitThreshold(config.upstreamHostCircuitThreshold) > 0;
412
+ if (compactHostKey !== null && !compactHostCircuitEnabled) {
413
+ disableUpstreamHostCircuitForKey(actualCompactHostKey);
414
+ }
415
+ if (compactHostAdmissionLease && compactHostAdmissionLease.key !== compactHostKey) {
416
+ releaseCodexAuthContextProbeLease(authCtx);
417
+ return formatErrorResponse(502, "upstream_error", "Provider host changed after circuit admission");
418
+ }
419
+ if (req.signal.aborted) {
420
+ releaseCodexAuthContextProbeLease(authCtx);
421
+ return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
422
+ }
423
+ if (!compactHostAdmissionLease && compactHostCircuitEnabled) {
424
+ const admission = acquireUpstreamHostAdmission(
425
+ compactHostKey!,
426
+ config.upstreamHostCircuitThreshold,
427
+ );
428
+ if (admission.kind === "blocked") {
429
+ releaseCodexAuthContextProbeLease(authCtx);
430
+ return upstreamHostCircuitOpenResponse(admission.retryAfterSeconds);
431
+ }
432
+ compactHostAdmissionLease = admission.lease;
433
+ }
434
+ const settleObservedCompactHostResponse = (): void => {
435
+ if (compactHostCircuitEnabled) {
436
+ resetUpstreamHostHealth(actualCompactHostKey, compactHostAdmissionLease);
437
+ } else {
438
+ resetUpstreamHostHealth(actualCompactHostKey);
439
+ }
440
+ compactHostAdmissionLease = null;
441
+ };
367
442
  const compactThreadId = req.headers.get("x-codex-parent-thread-id");
368
443
  const connectMs = config.connectTimeoutMs ?? 200_000;
369
444
  // Takes its context explicitly: the alternate-account flow below records a rejection
@@ -418,7 +493,7 @@ export async function handleResponsesCompact(
418
493
  ).then(res => {
419
494
  // Every real attempt response — including an intermediate 5xx the retry
420
495
  // wrapper replaces — proves the host was reached (#914 review).
421
- resetUpstreamHostHealth(upstreamHostHealthKey(route.providerName, safeOriginLabel(compactUrl)));
496
+ settleObservedCompactHostResponse();
422
497
  return res;
423
498
  });
424
499
  return recovery === "single"
@@ -442,11 +517,19 @@ export async function handleResponsesCompact(
442
517
  const outcome = classifyTransportFailureKind(err);
443
518
  // Host-level evidence stands regardless of pool membership (#914 review).
444
519
  if (outcome === "connect_neutral") {
445
- recordUpstreamHostFailure(
446
- upstreamHostHealthKey(route.providerName, safeOriginLabel(compactUrl)),
447
- { code: transportErrorCode(err) },
448
- );
520
+ if (compactHostCircuitEnabled) {
521
+ recordUpstreamHostFailure(actualCompactHostKey, {
522
+ code: transportErrorCode(err),
523
+ threshold: config.upstreamHostCircuitThreshold,
524
+ lease: compactHostAdmissionLease,
525
+ });
526
+ } else {
527
+ recordUpstreamHostFailure(actualCompactHostKey, { code: transportErrorCode(err) });
528
+ }
529
+ } else {
530
+ releaseUpstreamHostAdmission(compactHostAdmissionLease);
449
531
  }
532
+ compactHostAdmissionLease = null;
450
533
  recordCompactPoolOutcome(outcomeCtx, outcome);
451
534
  return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
452
535
  }
@@ -517,11 +600,19 @@ export async function handleResponsesCompact(
517
600
  const outcome = classifyTransportFailureKind(err);
518
601
  // Host-level evidence stands regardless of pool membership (#914 review).
519
602
  if (outcome === "connect_neutral") {
520
- recordUpstreamHostFailure(
521
- upstreamHostHealthKey(route.providerName, safeOriginLabel(compactUrl)),
522
- { code: transportErrorCode(err) },
523
- );
603
+ if (compactHostCircuitEnabled) {
604
+ recordUpstreamHostFailure(actualCompactHostKey, {
605
+ code: transportErrorCode(err),
606
+ threshold: config.upstreamHostCircuitThreshold,
607
+ lease: compactHostAdmissionLease,
608
+ });
609
+ } else {
610
+ recordUpstreamHostFailure(actualCompactHostKey, { code: transportErrorCode(err) });
611
+ }
612
+ } else {
613
+ releaseUpstreamHostAdmission(compactHostAdmissionLease);
524
614
  }
615
+ compactHostAdmissionLease = null;
525
616
  recordCompactPoolOutcome(outcomeCtx, outcome);
526
617
  return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
527
618
  }
@@ -548,6 +639,10 @@ export async function handleResponsesCompact(
548
639
  // synthetic buffer errors are not upstream bodies and stay uninspected.
549
640
  if (buffered.ok) inspectResponseLogJson(logCtx, await buffered.clone().text());
550
641
  return buffered;
642
+ } finally {
643
+ releaseUpstreamHostAdmission(compactHostAdmissionLease);
644
+ releaseCodexAuthContextProbeLease(authCtx);
645
+ }
551
646
  }
552
647
 
553
648
  // ROUTED model: run the v2 synthetic-compaction turn internally (appends COMPACT_PROMPT, no