@bitkyc08/opencodex 2.54.0-preview.20260914 → 2.55.0-preview.20260914

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 (86) hide show
  1. package/gui/dist/assets/{index-B4VYfZcY.js → index-DH2PUHqr.js} +10 -10
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +1 -1
  4. package/src/adapters/anthropic-image-codec.ts +57 -0
  5. package/src/adapters/anthropic-image-normalize.ts +28 -1
  6. package/src/adapters/anthropic.ts +68 -6
  7. package/src/adapters/base.ts +8 -0
  8. package/src/adapters/coding-agent/protocol.ts +41 -16
  9. package/src/adapters/cursor/cursor-errors.ts +1 -1
  10. package/src/adapters/cursor/live-transport.ts +5 -1
  11. package/src/adapters/cursor/native-exec-fs.ts +10 -10
  12. package/src/adapters/cursor/native-exec-network.ts +2 -2
  13. package/src/adapters/cursor/native-exec-shell.ts +13 -12
  14. package/src/adapters/cursor/native-exec.ts +51 -10
  15. package/src/adapters/cursor/policy-error.ts +75 -0
  16. package/src/adapters/cursor/protobuf-request.ts +105 -1
  17. package/src/adapters/devin/cloud-direct/catalog.ts +34 -2
  18. package/src/adapters/devin/live-models.ts +33 -2
  19. package/src/adapters/google-wire-compiler.ts +8 -0
  20. package/src/adapters/google.ts +46 -0
  21. package/src/adapters/input-media-guard.ts +45 -0
  22. package/src/adapters/kiro/adapter.ts +8 -0
  23. package/src/adapters/kiro/payload.ts +28 -6
  24. package/src/adapters/kiro-events.ts +25 -6
  25. package/src/adapters/kiro-images.ts +30 -0
  26. package/src/adapters/kiro-retry.ts +8 -0
  27. package/src/adapters/openai-chat.ts +33 -4
  28. package/src/adapters/openai-responses.ts +26 -0
  29. package/src/adapters/registry.ts +4 -0
  30. package/src/bridge.ts +163 -116
  31. package/src/chat/image-parts.ts +151 -0
  32. package/src/chat/inbound.ts +70 -33
  33. package/src/cli/connect.ts +30 -9
  34. package/src/cli/dispatch.ts +7 -3
  35. package/src/cli/index.ts +3 -0
  36. package/src/cli/runtime-api.ts +25 -0
  37. package/src/cli/status.ts +21 -19
  38. package/src/cli/system-restart-client.ts +25 -0
  39. package/src/clients/config-export.ts +14 -4
  40. package/src/codex/app-server-processes.ts +25 -0
  41. package/src/codex/auth-context.ts +8 -0
  42. package/src/codex/autostart-health.ts +36 -2
  43. package/src/codex/catalog/provider-fetch.ts +41 -0
  44. package/src/codex/catalog-auto-refresh.ts +182 -0
  45. package/src/codex/catalog-refresh-status.ts +93 -0
  46. package/src/codex/history-provider.ts +55 -0
  47. package/src/codex/model-entitlements.ts +78 -0
  48. package/src/codex/native-profile-processes.ts +114 -15
  49. package/src/codex/prompt-text-probe.ts +274 -41
  50. package/src/codex/routing-adoption.ts +189 -0
  51. package/src/codex/routing.ts +520 -48
  52. package/src/codex/runtime.ts +249 -7
  53. package/src/combos/failover.ts +45 -0
  54. package/src/config.ts +124 -4
  55. package/src/generated/compatibility-version.json +110 -74
  56. package/src/generated/model-metadata.ts +1 -0
  57. package/src/lib/request-execution-budget.ts +202 -0
  58. package/src/lib/upstream-retry.ts +95 -8
  59. package/src/lib/workflow-budget.ts +172 -0
  60. package/src/oauth/devin.ts +57 -12
  61. package/src/providers/quota.ts +37 -6
  62. package/src/providers/registry.ts +53 -6
  63. package/src/responses/input-media.ts +65 -0
  64. package/src/responses/parser-content.ts +42 -0
  65. package/src/responses/schema.ts +12 -2
  66. package/src/server/audio-live.ts +1 -2
  67. package/src/server/audio-transcriptions.ts +1 -2
  68. package/src/server/auth-cors.ts +1 -1
  69. package/src/server/background-lifecycle.ts +18 -0
  70. package/src/server/chat-completions.ts +23 -8
  71. package/src/server/chat-native.ts +17 -17
  72. package/src/server/index.ts +24 -0
  73. package/src/server/management/request-history-routes.ts +5 -0
  74. package/src/server/request-log.ts +8 -2
  75. package/src/server/responses/compact.ts +51 -3
  76. package/src/server/responses/core.ts +238 -28
  77. package/src/server/search.ts +7 -9
  78. package/src/types/config.ts +51 -6
  79. package/src/usage/log.ts +37 -0
  80. package/src/vision/eligibility.ts +37 -4
  81. package/src/vision/index.ts +1 -0
  82. package/src/vision/plan.ts +45 -10
  83. package/src/web-search/alpha-search.ts +324 -0
  84. package/src/web-search/index.ts +13 -22
  85. package/src/web-search/passthrough-bridge.ts +195 -22
  86. package/src/web-search/sidecar-providers.ts +22 -0
package/src/bridge.ts CHANGED
@@ -204,6 +204,14 @@ interface OutputItem {
204
204
 
205
205
  export type ResponsesTerminalStatus = "completed" | "failed" | "incomplete";
206
206
 
207
+ /** Accumulates string fragments and their total byte length without concatenating. */
208
+ interface StringChunks {
209
+ chunks: string[];
210
+ bytes: number;
211
+ }
212
+ const emptyChunks = (): StringChunks => ({ chunks: [], bytes: 0 });
213
+ const joinChunks = (sc: StringChunks): string => sc.chunks.join("");
214
+
207
215
  export function bridgeToResponsesSSE(
208
216
  events: AsyncIterable<AdapterEvent>,
209
217
  modelId: string,
@@ -337,6 +345,31 @@ export function bridgeToResponsesSSE(
337
345
  };
338
346
  const bytesOf = (value: string): number => Buffer.byteLength(value);
339
347
  const appendString = (
348
+ previous: StringChunks,
349
+ fragment: string,
350
+ kind: TranslatorBufferKind,
351
+ callId?: string,
352
+ ): StringChunks => {
353
+ const fragmentBytes = bytesOf(fragment);
354
+ if (fragmentBytes === 0) return previous;
355
+ const nextBytes = previous.bytes + fragmentBytes;
356
+ const scope = { kind, ...(callId ? { callId } : {}) };
357
+ const reservation = budget.reserveTransient(nextBytes, scope);
358
+ try {
359
+ previous.chunks.push(fragment);
360
+ const result: StringChunks = { chunks: previous.chunks, bytes: nextBytes };
361
+ reservation.commitRetained();
362
+ budget.releaseRetained(previous.bytes, scope);
363
+ return result;
364
+ } catch (error) {
365
+ reservation.release();
366
+ throw error;
367
+ }
368
+ };
369
+ // Tool-call arguments deliberately use plain string concatenation because
370
+ // downstream parsers and intermediate inspectors perform incremental JSON reads mid-stream.
371
+ // Converting tool args to StringChunks would require frequent join operations.
372
+ const appendStringDirect = (
340
373
  previous: string,
341
374
  previousBytes: number,
342
375
  fragment: string,
@@ -468,13 +501,12 @@ export function bridgeToResponsesSSE(
468
501
  let currentMsg: {
469
502
  itemId: string;
470
503
  outputIndex: number;
471
- text: string;
472
- textBytes: number;
504
+ text: StringChunks;
473
505
  citationFilter: CitationMarkerFilter;
474
506
  phase?: OcxMessagePhase;
475
507
  } | null = null;
476
- let currentReasoning: { itemId: string; outputIndex: number; text: string; textBytes: number } | null = null;
477
- let currentRawReasoning: { itemId: string; outputIndex: number; text: string; textBytes: number } | null = null;
508
+ let currentReasoning: { itemId: string; outputIndex: number; text: StringChunks } | null = null;
509
+ let currentRawReasoning: { itemId: string; outputIndex: number; text: StringChunks } | null = null;
478
510
  // Anthropic extended-thinking round-trip state: the signature signs the CURRENT thinking
479
511
  // block; redacted blocks are opaque payloads replayed verbatim. Attached to the reasoning
480
512
  // item as an ocxr1 encrypted_content envelope on close. hiddenThinkingText collects the
@@ -482,8 +514,7 @@ export function bridgeToResponsesSSE(
482
514
  let pendingSignature: string | undefined;
483
515
  let pendingSignatureBytes = 0;
484
516
  let pendingRedacted: string[] = [];
485
- let hiddenThinkingText = "";
486
- let hiddenThinkingBytes = 0;
517
+ let hiddenThinking = emptyChunks();
487
518
  const takeReasoningEnvelope = (hiddenText?: string): string | undefined => {
488
519
  if (!pendingSignature && pendingRedacted.length === 0) return undefined;
489
520
  const envelope: ReasoningEnvelope = {};
@@ -492,7 +523,7 @@ export function bridgeToResponsesSSE(
492
523
  if (hiddenText) envelope.txt = hiddenText;
493
524
  const previousBytes = pendingSignatureBytes
494
525
  + pendingRedacted.reduce((sum, value) => sum + bytesOf(value), 0)
495
- + (hiddenText ? hiddenThinkingBytes : 0);
526
+ + (hiddenText ? hiddenThinking.bytes : 0);
496
527
  const encoded = encodeReasoningEnvelope(envelope, budget);
497
528
  const reservation = budget?.reserveTransient(bytesOf(encoded), { kind: "reasoning" });
498
529
  pendingSignature = undefined;
@@ -505,9 +536,9 @@ export function bridgeToResponsesSSE(
505
536
  // hideThinkingSummary path: no visible reasoning item exists, but a signed thinking block
506
537
  // must still round-trip — emit an envelope-only reasoning item (empty summary, no text leak).
507
538
  const flushHiddenReasoningEnvelope = () => {
508
- const encrypted = takeReasoningEnvelope(hiddenThinkingText || undefined);
509
- hiddenThinkingText = "";
510
- hiddenThinkingBytes = 0;
539
+ const hiddenText = joinChunks(hiddenThinking);
540
+ const encrypted = takeReasoningEnvelope(hiddenText || undefined);
541
+ hiddenThinking = emptyChunks();
511
542
  if (!encrypted) return;
512
543
  const itemId = `rs_${uuid()}`;
513
544
  const item = { type: "reasoning", id: itemId, summary: [] as never[], encrypted_content: encrypted };
@@ -521,8 +552,7 @@ export function bridgeToResponsesSSE(
521
552
  // like native models — but the text still round-trips in a txt-only ocxr1 envelope so
522
553
  // preserveReasoningContentModels replay (GLM interleaved thinking) keeps working. Direct
523
554
  // encodeReasoningEnvelope: takeReasoningEnvelope's sig/red guard would drop txt-only.
524
- let hiddenRawReasoningText = "";
525
- let hiddenRawReasoningBytes = 0;
555
+ let hiddenRawReasoning = emptyChunks();
526
556
  // Raw reasoning text flushed most recently, waiting for the tool call it
527
557
  // preceded. Recorded into the replay cache on tool_call_start so a later
528
558
  // continuation can re-attach it when history lost the reasoning item
@@ -530,13 +560,13 @@ export function bridgeToResponsesSSE(
530
560
  // calls share the same preceding reasoning block.
531
561
  let rawReasoningForNextToolCall = "";
532
562
  const flushHiddenRawReasoning = () => {
533
- if (!hiddenRawReasoningText) return;
534
- rawReasoningForNextToolCall = hiddenRawReasoningText;
535
- const previousBytes = hiddenRawReasoningBytes;
536
- const encrypted = encodeReasoningEnvelope({ txt: hiddenRawReasoningText }, budget);
563
+ const hiddenRawText = joinChunks(hiddenRawReasoning);
564
+ if (!hiddenRawText) return;
565
+ rawReasoningForNextToolCall = hiddenRawText;
566
+ const previousBytes = hiddenRawReasoning.bytes;
567
+ const encrypted = encodeReasoningEnvelope({ txt: hiddenRawText }, budget);
537
568
  const reservation = budget?.reserveTransient(bytesOf(encrypted), { kind: "reasoning" });
538
- hiddenRawReasoningText = "";
539
- hiddenRawReasoningBytes = 0;
569
+ hiddenRawReasoning = emptyChunks();
540
570
  reservation?.commitRetained();
541
571
  budget?.releaseRetained(previousBytes, { kind: "reasoning" });
542
572
  const itemId = `rs_${uuid()}`;
@@ -571,8 +601,7 @@ export function bridgeToResponsesSSE(
571
601
  };
572
602
  // Full assistant text of a compaction turn (across message boundaries) — becomes the
573
603
  // synthetic compaction item's payload on done.
574
- let compactionText = "";
575
- let compactionTextBytes = 0;
604
+ let compaction = emptyChunks();
576
605
  let currentToolCall: { itemId: string; outputIndex: number; callId: string; name: string; args: string; argsBytes: number; namespace?: string; freeform?: boolean; toolSearch?: boolean; inputEmitted?: string; codeModeHelperName?: string; providerMetadata?: OcxProviderOpaqueToolCallMetadata } | null = null;
577
606
  // Open native web-search cell (between begin and end). Holds the output index allocated on
578
607
  // begin so the matching done reuses it; closed as `failed` if the stream terminates early.
@@ -612,7 +641,7 @@ export function bridgeToResponsesSSE(
612
641
  content_index: 0, delta: trailing,
613
642
  });
614
643
  }
615
- const messageText = stripCitationMarkers(currentMsg.text);
644
+ const messageText = stripCitationMarkers(joinChunks(currentMsg.text));
616
645
  // Chat Completions has no message-phase field. Keep its live item provisional, then
617
646
  // classify it only when the next adapter event proves whether this text led into more
618
647
  // work or completed the turn. Explicit adapter phases always outrank this inference.
@@ -634,45 +663,47 @@ export function bridgeToResponsesSSE(
634
663
  ...(phase ? { phase } : {}),
635
664
  };
636
665
  emit("response.output_item.done", { output_index: currentMsg.outputIndex, item });
637
- retainFinishedItem(item as OutputItem, currentMsg.textBytes + bytesOf(JSON.stringify(annotations)));
666
+ retainFinishedItem(item as OutputItem, currentMsg.text.bytes + bytesOf(JSON.stringify(annotations)));
638
667
  outputIndex++;
639
668
  currentMsg = null;
640
669
  };
641
670
 
642
671
  const closeCurrentReasoning = () => {
643
672
  if (!currentReasoning) return;
673
+ const reasoningText = joinChunks(currentReasoning.text);
644
674
  emit("response.reasoning_summary_text.done", {
645
- item_id: currentReasoning.itemId, output_index: currentReasoning.outputIndex, summary_index: 0, text: currentReasoning.text,
675
+ item_id: currentReasoning.itemId, output_index: currentReasoning.outputIndex, summary_index: 0, text: reasoningText,
646
676
  });
647
677
  emit("response.reasoning_summary_part.done", {
648
678
  item_id: currentReasoning.itemId, output_index: currentReasoning.outputIndex, summary_index: 0,
649
- part: { type: "summary_text", text: currentReasoning.text },
679
+ part: { type: "summary_text", text: reasoningText },
650
680
  });
651
681
  const encrypted = takeReasoningEnvelope();
652
682
  const item = {
653
683
  type: "reasoning", id: currentReasoning.itemId,
654
- summary: [{ type: "summary_text", text: currentReasoning.text }],
684
+ summary: [{ type: "summary_text", text: reasoningText }],
655
685
  ...(encrypted ? { encrypted_content: encrypted } : {}),
656
686
  };
657
687
  emit("response.output_item.done", { output_index: currentReasoning.outputIndex, item });
658
- retainFinishedItem(item as OutputItem, currentReasoning.textBytes + bytesOf(encrypted ?? ""), "reasoning");
688
+ retainFinishedItem(item as OutputItem, currentReasoning.text.bytes + bytesOf(encrypted ?? ""), "reasoning");
659
689
  outputIndex++;
660
690
  currentReasoning = null;
661
691
  };
662
692
 
663
693
  const closeCurrentRawReasoning = () => {
664
694
  if (!currentRawReasoning) return;
665
- rawReasoningForNextToolCall = currentRawReasoning.text;
695
+ const rawText = joinChunks(currentRawReasoning.text);
696
+ rawReasoningForNextToolCall = rawText;
666
697
  emit("response.reasoning_text.done", {
667
- item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, content_index: 0, text: currentRawReasoning.text,
698
+ item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, content_index: 0, text: rawText,
668
699
  });
669
700
  const item = {
670
701
  type: "reasoning", id: currentRawReasoning.itemId,
671
702
  summary: [] as never[],
672
- content: [{ type: "reasoning_text", text: currentRawReasoning.text }],
703
+ content: [{ type: "reasoning_text", text: rawText }],
673
704
  };
674
705
  emit("response.output_item.done", { output_index: currentRawReasoning.outputIndex, item });
675
- retainFinishedItem(item as OutputItem, currentRawReasoning.textBytes, "reasoning");
706
+ retainFinishedItem(item as OutputItem, currentRawReasoning.text.bytes, "reasoning");
676
707
  outputIndex++;
677
708
  currentRawReasoning = null;
678
709
  };
@@ -942,12 +973,11 @@ export function bridgeToResponsesSSE(
942
973
  // its compaction UI renders nothing mid-turn, so nothing is lost visually.
943
974
  if (options?.compaction) {
944
975
  if (event.type === "text_delta") {
945
- ({ value: compactionText, bytes: compactionTextBytes } = appendString(
946
- compactionText,
947
- compactionTextBytes,
976
+ compaction = appendString(
977
+ compaction,
948
978
  event.text,
949
979
  "retained_collectors",
950
- ));
980
+ );
951
981
  continue;
952
982
  }
953
983
  if (event.type !== "done" && event.type !== "incomplete" && event.type !== "error") continue;
@@ -999,17 +1029,16 @@ export function bridgeToResponsesSSE(
999
1029
  part: { type: "output_text", text: "", annotations: [] },
1000
1030
  });
1001
1031
  currentMsg = {
1002
- itemId, outputIndex, text: "", textBytes: 0,
1032
+ itemId, outputIndex, text: emptyChunks(),
1003
1033
  citationFilter: createCitationMarkerFilter(),
1004
1034
  ...(event.phase ? { phase: event.phase } : {}),
1005
1035
  };
1006
1036
  }
1007
- ({ value: currentMsg.text, bytes: currentMsg.textBytes } = appendString(
1037
+ currentMsg.text = appendString(
1008
1038
  currentMsg.text,
1009
- currentMsg.textBytes,
1010
1039
  event.text,
1011
1040
  "retained_collectors",
1012
- ));
1041
+ );
1013
1042
  // A citation span can straddle a delta boundary, so the filter withholds an
1014
1043
  // unterminated tail and releases it at close (#3150). The accumulator above
1015
1044
  // keeps the raw text; it is stripped once in closeCurrentMessage.
@@ -1030,12 +1059,11 @@ export function bridgeToResponsesSSE(
1030
1059
  // recorded for a LATER tool call (CodeRabbit on #971).
1031
1060
  flushHiddenRawReasoning();
1032
1061
  rawReasoningForNextToolCall = "";
1033
- ({ value: hiddenThinkingText, bytes: hiddenThinkingBytes } = appendString(
1034
- hiddenThinkingText,
1035
- hiddenThinkingBytes,
1062
+ hiddenThinking = appendString(
1063
+ hiddenThinking,
1036
1064
  event.thinking,
1037
1065
  "reasoning",
1038
- ));
1066
+ );
1039
1067
  break;
1040
1068
  }
1041
1069
  if (currentMsg) closeCurrentMessage("commentary");
@@ -1051,14 +1079,13 @@ export function bridgeToResponsesSSE(
1051
1079
  item_id: itemId, output_index: outputIndex, summary_index: 0,
1052
1080
  part: { type: "summary_text", text: "" },
1053
1081
  });
1054
- currentReasoning = { itemId, outputIndex, text: "", textBytes: 0 };
1082
+ currentReasoning = { itemId, outputIndex, text: emptyChunks() };
1055
1083
  }
1056
- ({ value: currentReasoning.text, bytes: currentReasoning.textBytes } = appendString(
1084
+ currentReasoning.text = appendString(
1057
1085
  currentReasoning.text,
1058
- currentReasoning.textBytes,
1059
1086
  event.thinking,
1060
1087
  "reasoning",
1061
- ));
1088
+ );
1062
1089
  emit("response.reasoning_summary_text.delta", {
1063
1090
  item_id: currentReasoning.itemId, output_index: currentReasoning.outputIndex,
1064
1091
  summary_index: 0, delta: event.thinking,
@@ -1093,12 +1120,11 @@ export function bridgeToResponsesSSE(
1093
1120
  }
1094
1121
  case "reasoning_raw_delta": {
1095
1122
  if (options?.hideThinkingSummary) {
1096
- ({ value: hiddenRawReasoningText, bytes: hiddenRawReasoningBytes } = appendString(
1097
- hiddenRawReasoningText,
1098
- hiddenRawReasoningBytes,
1123
+ hiddenRawReasoning = appendString(
1124
+ hiddenRawReasoning,
1099
1125
  event.text,
1100
1126
  "reasoning",
1101
- ));
1127
+ );
1102
1128
  break;
1103
1129
  }
1104
1130
  if (currentMsg) closeCurrentMessage("commentary");
@@ -1108,14 +1134,13 @@ export function bridgeToResponsesSSE(
1108
1134
  const itemId = `rs_${uuid()}`;
1109
1135
  const item = { type: "reasoning", id: itemId, summary: [] as { type: string; text: string }[] };
1110
1136
  emit("response.output_item.added", { output_index: outputIndex, item });
1111
- currentRawReasoning = { itemId, outputIndex, text: "", textBytes: 0 };
1137
+ currentRawReasoning = { itemId, outputIndex, text: emptyChunks() };
1112
1138
  }
1113
- ({ value: currentRawReasoning.text, bytes: currentRawReasoning.textBytes } = appendString(
1139
+ currentRawReasoning.text = appendString(
1114
1140
  currentRawReasoning.text,
1115
- currentRawReasoning.textBytes,
1116
1141
  event.text,
1117
1142
  "reasoning",
1118
- ));
1143
+ );
1119
1144
  // Raw reasoning (openai-chat reasoning_content, kiro tags) rides the CONTENT
1120
1145
  // channel. Clients control raw-reasoning display; this text is not a
1121
1146
  // provider-authored summary.
@@ -1175,7 +1200,7 @@ export function bridgeToResponsesSSE(
1175
1200
  }
1176
1201
  case "tool_call_delta": {
1177
1202
  if (currentToolCall) {
1178
- ({ value: currentToolCall.args, bytes: currentToolCall.argsBytes } = appendString(
1203
+ ({ value: currentToolCall.args, bytes: currentToolCall.argsBytes } = appendStringDirect(
1179
1204
  currentToolCall.args,
1180
1205
  currentToolCall.argsBytes,
1181
1206
  event.arguments,
@@ -1311,12 +1336,12 @@ export function bridgeToResponsesSSE(
1311
1336
  // Exactly one compaction item per turn; codex-rs takes the first and fatals on 0.
1312
1337
  const item = {
1313
1338
  type: "compaction", id: `cmp_${uuid()}`,
1314
- encrypted_content: event.compactionEncryptedContent ?? encodeCompactionSummary(compactionText),
1339
+ encrypted_content: event.compactionEncryptedContent ?? encodeCompactionSummary(joinChunks(compaction)),
1315
1340
  };
1316
1341
  emit("response.output_item.done", { output_index: outputIndex, item });
1317
1342
  retainFinishedItem(item as OutputItem, event.compactionEncryptedContent
1318
1343
  ? bytesOf(event.compactionEncryptedContent)
1319
- : compactionTextBytes);
1344
+ : compaction.bytes);
1320
1345
  outputIndex++;
1321
1346
  }
1322
1347
  // Recognize every adapter's truncation vocabulary, not just the canonical pair.
@@ -1620,6 +1645,34 @@ function buildResponseJSONWithBudget(
1620
1645
  const encoder = new TextEncoder();
1621
1646
  const bytesOf = (value: string): number => Buffer.byteLength(value);
1622
1647
  const appendBatchString = (
1648
+ previous: StringChunks,
1649
+ fragment: string,
1650
+ kind: TranslatorBufferKind,
1651
+ callId?: string,
1652
+ ): StringChunks => {
1653
+ const fragmentBytes = bytesOf(fragment);
1654
+ if (fragmentBytes === 0) return previous;
1655
+ const nextBytes = previous.bytes + fragmentBytes;
1656
+ if (!budget) {
1657
+ previous.chunks.push(fragment);
1658
+ return { chunks: previous.chunks, bytes: nextBytes };
1659
+ }
1660
+ const scope = { kind, ...(callId ? { callId } : {}) };
1661
+ const reservation = budget.reserveTransient(nextBytes, scope);
1662
+ try {
1663
+ previous.chunks.push(fragment);
1664
+ const result: StringChunks = { chunks: previous.chunks, bytes: nextBytes };
1665
+ reservation.commitRetained();
1666
+ budget.releaseRetained(previous.bytes, scope);
1667
+ return result;
1668
+ } catch (error) {
1669
+ reservation.release();
1670
+ throw error;
1671
+ }
1672
+ };
1673
+ // Batch counterpart: tool-call arguments require direct string representation for immediate
1674
+ // JSON serialization compatibility.
1675
+ const appendBatchStringDirect = (
1623
1676
  previous: string,
1624
1677
  previousBytes: number,
1625
1678
  fragment: string,
@@ -1668,17 +1721,13 @@ function buildResponseJSONWithBudget(
1668
1721
  // which is only true for a `done` without a stop reason. A buffered turn whose adapter simply
1669
1722
  // stopped emitting has no terminal at all, and must not be reported as a success.
1670
1723
  let sawTerminal = false;
1671
- let compactionText = "";
1672
- let compactionTextBytes = 0;
1724
+ let batchCompaction = emptyChunks();
1673
1725
  let compactionEncryptedContent: string | undefined;
1674
1726
 
1675
- let currentText = "";
1676
- let currentTextBytes = 0;
1727
+ let currentText = emptyChunks();
1677
1728
  let currentTextPhase: OcxMessagePhase | undefined;
1678
- let currentSummaryReasoning = "";
1679
- let currentSummaryReasoningBytes = 0;
1680
- let currentRawReasoning = "";
1681
- let currentRawReasoningBytes = 0;
1729
+ let currentSummaryReasoning = emptyChunks();
1730
+ let currentRawReasoning = emptyChunks();
1682
1731
  // Same replay-cache handoff as the streaming path (issue #950): the most
1683
1732
  // recently flushed raw reasoning waits for the tool call it preceded.
1684
1733
  let rawReasoningForNextToolCall = "";
@@ -1716,12 +1765,13 @@ function buildResponseJSONWithBudget(
1716
1765
  };
1717
1766
 
1718
1767
  const flushText = (inferredPhase?: OcxMessagePhase) => {
1719
- if (!currentText) return;
1768
+ const currentTextStr = joinChunks(currentText);
1769
+ if (!currentTextStr) return;
1720
1770
  const phase = currentTextPhase ?? inferredPhase;
1721
1771
  // ChatGPT-backend citation markers arrive as literal private-use characters that the
1722
1772
  // Codex TUI prints verbatim (#3150). Strip them here rather than at the accumulator so
1723
1773
  // the retained byte accounting above still describes what the upstream actually sent.
1724
- const text = stripCitationMarkers(currentText);
1774
+ const text = stripCitationMarkers(currentTextStr);
1725
1775
  const sourceBytes = pendingWebSources.reduce((sum, source) => sum + bytesOf(JSON.stringify(source)), 0);
1726
1776
  const annotations = pendingWebSources.map(s => ({
1727
1777
  type: "url_citation", url: s.url, ...(s.title ? { title: s.title } : {}), start_index: 0, end_index: 0,
@@ -1732,60 +1782,57 @@ function buildResponseJSONWithBudget(
1732
1782
  content: [{ type: "output_text", text, annotations }],
1733
1783
  ...(phase ? { phase } : {}),
1734
1784
  } as OutputItem;
1735
- pushOutput(item, currentTextBytes);
1785
+ pushOutput(item, currentText.bytes);
1736
1786
  budget?.releaseRetained(sourceBytes, { kind: "tool_search_sources" });
1737
- currentText = "";
1738
- currentTextBytes = 0;
1787
+ currentText = emptyChunks();
1739
1788
  currentTextPhase = undefined;
1740
1789
  };
1741
1790
  const flushSummaryReasoning = () => {
1742
- if (!currentSummaryReasoning && !batchSignature && batchRedacted.length === 0) return;
1791
+ const summaryText = joinChunks(currentSummaryReasoning);
1792
+ if (!summaryText && !batchSignature && batchRedacted.length === 0) return;
1743
1793
  const envelope: ReasoningEnvelope = {};
1744
1794
  if (batchSignature) envelope.sig = batchSignature;
1745
1795
  if (batchRedacted.length > 0) envelope.red = batchRedacted;
1746
1796
  const hidden = options?.hideThinkingSummary === true;
1747
- if (hidden && currentSummaryReasoning && (envelope.sig || envelope.red)) envelope.txt = currentSummaryReasoning;
1797
+ if (hidden && summaryText && (envelope.sig || envelope.red)) envelope.txt = summaryText;
1748
1798
  const encrypted = envelope.sig || envelope.red || envelope.txt ? encodeReasoningEnvelope(envelope, budget) : undefined;
1749
- const sourceBytes = currentSummaryReasoningBytes + batchSignatureBytes + batchRedactedBytes;
1799
+ const sourceBytes = currentSummaryReasoning.bytes + batchSignatureBytes + batchRedactedBytes;
1750
1800
  batchSignature = undefined;
1751
1801
  batchSignatureBytes = 0;
1752
1802
  batchRedacted = [];
1753
1803
  batchRedactedBytes = 0;
1754
1804
  if (hidden && !encrypted) {
1755
1805
  budget?.releaseRetained(sourceBytes, { kind: "reasoning" });
1756
- currentSummaryReasoning = "";
1757
- currentSummaryReasoningBytes = 0;
1806
+ currentSummaryReasoning = emptyChunks();
1758
1807
  return;
1759
1808
  }
1760
1809
  const item = {
1761
1810
  type: "reasoning", id: `rs_${uuid()}`,
1762
- summary: !hidden && currentSummaryReasoning ? [{ type: "summary_text", text: currentSummaryReasoning }] : [],
1811
+ summary: !hidden && summaryText ? [{ type: "summary_text", text: summaryText }] : [],
1763
1812
  ...(encrypted ? { encrypted_content: encrypted } : {}),
1764
1813
  } as OutputItem;
1765
1814
  pushOutput(item, sourceBytes, "reasoning");
1766
- currentSummaryReasoning = "";
1767
- currentSummaryReasoningBytes = 0;
1815
+ currentSummaryReasoning = emptyChunks();
1768
1816
  };
1769
1817
  const flushRawReasoning = () => {
1770
- if (!currentRawReasoning) return;
1771
- rawReasoningForNextToolCall = currentRawReasoning;
1818
+ const rawText = joinChunks(currentRawReasoning);
1819
+ if (!rawText) return;
1820
+ rawReasoningForNextToolCall = rawText;
1772
1821
  if (options?.hideThinkingSummary === true) {
1773
1822
  // Same contract as the streaming path: no visible reasoning, txt-only envelope round-trip.
1774
1823
  pushOutput({
1775
1824
  type: "reasoning", id: `rs_${uuid()}`, summary: [],
1776
- encrypted_content: encodeReasoningEnvelope({ txt: currentRawReasoning }, budget),
1777
- }, currentRawReasoningBytes, "reasoning");
1778
- currentRawReasoning = "";
1779
- currentRawReasoningBytes = 0;
1825
+ encrypted_content: encodeReasoningEnvelope({ txt: rawText }, budget),
1826
+ }, currentRawReasoning.bytes, "reasoning");
1827
+ currentRawReasoning = emptyChunks();
1780
1828
  return;
1781
1829
  }
1782
1830
  pushOutput({
1783
1831
  type: "reasoning", id: `rs_${uuid()}`,
1784
1832
  summary: [],
1785
- content: [{ type: "reasoning_text", text: currentRawReasoning }],
1786
- }, currentRawReasoningBytes, "reasoning");
1787
- currentRawReasoning = "";
1788
- currentRawReasoningBytes = 0;
1833
+ content: [{ type: "reasoning_text", text: rawText }],
1834
+ }, currentRawReasoning.bytes, "reasoning");
1835
+ currentRawReasoning = emptyChunks();
1789
1836
  };
1790
1837
  const flushToolCall = (status: "completed" | "incomplete" = "completed") => {
1791
1838
  if (!currentToolCallId) return;
@@ -1861,9 +1908,9 @@ function buildResponseJSONWithBudget(
1861
1908
  case "text_delta":
1862
1909
  // Only flush on an explicit phase change. A later delta that omits `phase` must keep
1863
1910
  // appending under the previously established phase.
1864
- if (currentText && e.phase !== undefined && currentTextPhase !== e.phase) flushText("commentary");
1865
- if (currentSummaryReasoning) flushSummaryReasoning();
1866
- if (currentRawReasoning) flushRawReasoning();
1911
+ if (currentText.bytes > 0 && e.phase !== undefined && currentTextPhase !== e.phase) flushText("commentary");
1912
+ if (currentSummaryReasoning.bytes > 0) flushSummaryReasoning();
1913
+ if (currentRawReasoning.bytes > 0) flushRawReasoning();
1867
1914
  // Empty text deltas (batch chat responses always carry content, often "") must
1868
1915
  // not wipe reasoning that precedes a tool call (#950 non-streaming path).
1869
1916
  if (e.text.length > 0) rawReasoningForNextToolCall = "";
@@ -1871,26 +1918,26 @@ function buildResponseJSONWithBudget(
1871
1918
  // Compaction turns keep the summary out of normal message output (replay dedup — see
1872
1919
  // bridgeToResponsesSSE); it ships only inside the synthetic compaction item below.
1873
1920
  if (options?.compaction) {
1874
- ({ value: compactionText, bytes: compactionTextBytes } = appendBatchString(
1875
- compactionText, compactionTextBytes, e.text, "retained_collectors",
1876
- ));
1921
+ batchCompaction = appendBatchString(
1922
+ batchCompaction, e.text, "retained_collectors",
1923
+ );
1877
1924
  }
1878
1925
  else {
1879
1926
  if (e.phase !== undefined) currentTextPhase = e.phase;
1880
- ({ value: currentText, bytes: currentTextBytes } = appendBatchString(
1881
- currentText, currentTextBytes, e.text, "retained_collectors",
1882
- ));
1927
+ currentText = appendBatchString(
1928
+ currentText, e.text, "retained_collectors",
1929
+ );
1883
1930
  }
1884
1931
  break;
1885
1932
  case "thinking_delta":
1886
- if (currentText) flushText("commentary");
1887
- if (currentRawReasoning) flushRawReasoning();
1933
+ if (currentText.bytes > 0) flushText("commentary");
1934
+ if (currentRawReasoning.bytes > 0) flushRawReasoning();
1888
1935
  if (e.thinking.length > 0) rawReasoningForNextToolCall = "";
1889
1936
  if (currentToolCallId) flushToolCall();
1890
1937
  {
1891
- ({ value: currentSummaryReasoning, bytes: currentSummaryReasoningBytes } = appendBatchString(
1892
- currentSummaryReasoning, currentSummaryReasoningBytes, e.thinking, "reasoning",
1893
- ));
1938
+ currentSummaryReasoning = appendBatchString(
1939
+ currentSummaryReasoning, e.thinking, "reasoning",
1940
+ );
1894
1941
  }
1895
1942
  break;
1896
1943
  case "thinking_signature":
@@ -1923,19 +1970,19 @@ function buildResponseJSONWithBudget(
1923
1970
  batchKiroRedacted = e.data;
1924
1971
  break;
1925
1972
  case "reasoning_raw_delta":
1926
- if (currentText) flushText("commentary");
1927
- if (currentSummaryReasoning) flushSummaryReasoning();
1973
+ if (currentText.bytes > 0) flushText("commentary");
1974
+ if (currentSummaryReasoning.bytes > 0) flushSummaryReasoning();
1928
1975
  if (currentToolCallId) flushToolCall();
1929
1976
  {
1930
- ({ value: currentRawReasoning, bytes: currentRawReasoningBytes } = appendBatchString(
1931
- currentRawReasoning, currentRawReasoningBytes, e.text, "reasoning",
1932
- ));
1977
+ currentRawReasoning = appendBatchString(
1978
+ currentRawReasoning, e.text, "reasoning",
1979
+ );
1933
1980
  }
1934
1981
  break;
1935
1982
  case "tool_call_start": {
1936
- if (currentText) flushText("commentary");
1937
- if (currentSummaryReasoning) flushSummaryReasoning();
1938
- if (currentRawReasoning) flushRawReasoning();
1983
+ if (currentText.bytes > 0) flushText("commentary");
1984
+ if (currentSummaryReasoning.bytes > 0) flushSummaryReasoning();
1985
+ if (currentRawReasoning.bytes > 0) flushRawReasoning();
1939
1986
  if (rawReasoningForNextToolCall) {
1940
1987
  rememberReasoningForCall(e.id, rawReasoningForNextToolCall, replayCacheScope);
1941
1988
  }
@@ -1963,7 +2010,7 @@ function buildResponseJSONWithBudget(
1963
2010
  }
1964
2011
  case "tool_call_delta":
1965
2012
  {
1966
- ({ value: currentToolCallArgs, bytes: currentToolCallArgsBytes } = appendBatchString(
2013
+ ({ value: currentToolCallArgs, bytes: currentToolCallArgsBytes } = appendBatchStringDirect(
1967
2014
  currentToolCallArgs, currentToolCallArgsBytes, e.arguments, "tool_args", currentToolCallId,
1968
2015
  ));
1969
2016
  }
@@ -1995,9 +2042,9 @@ function buildResponseJSONWithBudget(
1995
2042
  // single finalized item, emitted on `end`. Begin is a no-op here.
1996
2043
  break;
1997
2044
  case "web_search_call_end": {
1998
- if (currentText) flushText("commentary");
1999
- if (currentSummaryReasoning) flushSummaryReasoning();
2000
- if (currentRawReasoning) flushRawReasoning();
2045
+ if (currentText.bytes > 0) flushText("commentary");
2046
+ if (currentSummaryReasoning.bytes > 0) flushSummaryReasoning();
2047
+ if (currentRawReasoning.bytes > 0) flushRawReasoning();
2001
2048
  flushToolCall();
2002
2049
  const safeSources = safeWebSearchSources(e.sources);
2003
2050
  pushOutput({
@@ -2085,9 +2132,9 @@ function buildResponseJSONWithBudget(
2085
2132
  ) {
2086
2133
  const item = {
2087
2134
  type: "compaction", id: `cmp_${uuid()}`,
2088
- encrypted_content: compactionEncryptedContent ?? encodeCompactionSummary(compactionText),
2135
+ encrypted_content: compactionEncryptedContent ?? encodeCompactionSummary(joinChunks(batchCompaction)),
2089
2136
  };
2090
- pushOutput(item, compactionEncryptedContent ? bytesOf(compactionEncryptedContent) : compactionTextBytes);
2137
+ pushOutput(item, compactionEncryptedContent ? bytesOf(compactionEncryptedContent) : batchCompaction.bytes);
2091
2138
  }
2092
2139
 
2093
2140
  const failure = errorEvent ? adapterFailureFromEvent(errorEvent) : undefined;