@bitkyc08/opencodex 2.8.0 → 2.9.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 (175) hide show
  1. package/README.md +24 -0
  2. package/bin/ocx.mjs +32 -4
  3. package/gui/dist/assets/index-CHwf3tTD.css +1 -0
  4. package/gui/dist/assets/index-u5eFOv2y.js +67 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/adapters/anthropic-image-normalize.ts +114 -20
  8. package/src/adapters/anthropic.ts +126 -10
  9. package/src/adapters/azure.ts +3 -3
  10. package/src/adapters/base.ts +7 -3
  11. package/src/adapters/cursor/discovery.ts +14 -4
  12. package/src/adapters/cursor/effort-map.ts +18 -6
  13. package/src/adapters/cursor/framing.ts +102 -27
  14. package/src/adapters/cursor/kv-store.ts +30 -3
  15. package/src/adapters/cursor/live-models.ts +22 -2
  16. package/src/adapters/cursor/live-transport.ts +245 -49
  17. package/src/adapters/cursor/mcp-manager.ts +105 -8
  18. package/src/adapters/cursor/native-exec-mcp.ts +5 -3
  19. package/src/adapters/cursor/native-exec-shell.ts +296 -14
  20. package/src/adapters/cursor/native-exec.ts +381 -33
  21. package/src/adapters/cursor/protobuf-events.ts +28 -1
  22. package/src/adapters/cursor/protobuf-request.ts +71 -39
  23. package/src/adapters/cursor/request-builder.ts +2 -2
  24. package/src/adapters/cursor/transport.ts +2 -0
  25. package/src/adapters/cursor.ts +13 -2
  26. package/src/adapters/google-antigravity-replay.ts +184 -17
  27. package/src/adapters/google.ts +58 -8
  28. package/src/adapters/kiro-thinking.ts +23 -9
  29. package/src/adapters/kiro-tools.ts +49 -18
  30. package/src/adapters/kiro.ts +377 -133
  31. package/src/adapters/mimo-free.ts +36 -4
  32. package/src/adapters/openai-chat.ts +143 -17
  33. package/src/adapters/openai-responses.ts +130 -14
  34. package/src/adapters/run-turn-queue.ts +7 -1
  35. package/src/bridge.ts +466 -69
  36. package/src/chat/outbound.ts +144 -38
  37. package/src/claude/inbound-debug.ts +53 -8
  38. package/src/claude/outbound.ts +224 -38
  39. package/src/cli/agent-driven.ts +34 -1
  40. package/src/cli/catalog-prewarm.ts +5 -2
  41. package/src/cli/claude-desktop.ts +2 -2
  42. package/src/cli/doctor.ts +12 -0
  43. package/src/cli/export-command.ts +187 -0
  44. package/src/cli/help.ts +11 -0
  45. package/src/cli/index.ts +13 -3
  46. package/src/cli/init.ts +129 -102
  47. package/src/cli/opencode.ts +36 -151
  48. package/src/cli/star-prompt.ts +13 -4
  49. package/src/cli/status-oauth.ts +12 -2
  50. package/src/clients/config-export.ts +377 -0
  51. package/src/codex/account-runtime-state.ts +19 -1
  52. package/src/codex/account-store.ts +162 -82
  53. package/src/codex/auth-api.ts +467 -159
  54. package/src/codex/auth-context.ts +15 -2
  55. package/src/codex/catalog/aggregation.ts +15 -0
  56. package/src/codex/catalog/effort.ts +16 -6
  57. package/src/codex/catalog/metadata.ts +6 -0
  58. package/src/codex/catalog/parsing.ts +3 -1
  59. package/src/codex/catalog/provider-fetch.ts +29 -0
  60. package/src/codex/catalog/sync.ts +64 -7
  61. package/src/codex/catalog.ts +2 -2
  62. package/src/codex/inject.ts +5 -5
  63. package/src/codex/main-account-cache.ts +8 -1
  64. package/src/codex/model-cache.ts +81 -2
  65. package/src/codex/pool-rotation.ts +39 -0
  66. package/src/codex/project-config-warnings.ts +12 -1
  67. package/src/codex/quota.ts +35 -3
  68. package/src/codex/routing.ts +46 -1
  69. package/src/codex/shim.ts +10 -4
  70. package/src/codex/subagent-model-fallback.ts +12 -0
  71. package/src/codex/websocket-registry.ts +27 -0
  72. package/src/combos/failover.ts +31 -1
  73. package/src/combos/request.ts +9 -0
  74. package/src/combos/resolve.ts +60 -4
  75. package/src/combos/types.ts +12 -0
  76. package/src/config.ts +510 -55
  77. package/src/github/star-state.ts +13 -1
  78. package/src/images/fulfill.ts +39 -1
  79. package/src/images/loop.ts +52 -12
  80. package/src/lib/admission.ts +83 -0
  81. package/src/lib/app-owned-memory-stores.ts +173 -0
  82. package/src/lib/app-owned-memory.ts +265 -0
  83. package/src/lib/bun-stream-caps.ts +31 -7
  84. package/src/lib/config-ownership.ts +33 -0
  85. package/src/lib/crash-guard.ts +65 -5
  86. package/src/lib/debug-log-buffer.ts +47 -6
  87. package/src/lib/destination-policy.ts +12 -1
  88. package/src/lib/errors.ts +3 -0
  89. package/src/lib/gcp-adc.ts +40 -2
  90. package/src/lib/injection-debug-log.ts +26 -2
  91. package/src/lib/provider-outbound.ts +3 -0
  92. package/src/lib/sidecar-tracker.ts +5 -2
  93. package/src/lib/sse-decoder.ts +257 -37
  94. package/src/lib/state-store-registrations.ts +109 -0
  95. package/src/lib/state-store-sweeper.ts +184 -0
  96. package/src/lib/translator-budget.ts +356 -0
  97. package/src/lib/windows-secret-acl.ts +33 -12
  98. package/src/lib/winsw.ts +14 -1
  99. package/src/oauth/anthropic-routing.ts +31 -7
  100. package/src/oauth/google-antigravity.ts +2 -1
  101. package/src/oauth/health.ts +30 -12
  102. package/src/oauth/index.ts +127 -23
  103. package/src/oauth/kiro-credentials.ts +72 -1
  104. package/src/oauth/kiro.ts +23 -4
  105. package/src/oauth/store.ts +165 -18
  106. package/src/oauth/token-guardian.ts +43 -4
  107. package/src/oauth/types.ts +2 -1
  108. package/src/providers/base-url-choices.ts +10 -0
  109. package/src/providers/derive.ts +12 -0
  110. package/src/providers/free-directory.ts +4 -1
  111. package/src/providers/key-failover.ts +12 -0
  112. package/src/providers/openai-sidecar.ts +4 -1
  113. package/src/providers/quota.ts +68 -7
  114. package/src/providers/registry.ts +279 -3
  115. package/src/responses/parser.ts +5 -1
  116. package/src/responses/spill-store.ts +394 -0
  117. package/src/responses/state.ts +520 -102
  118. package/src/router.ts +18 -1
  119. package/src/server/adapter-resolve.ts +20 -3
  120. package/src/server/auth-cors.ts +121 -28
  121. package/src/server/chat-completions.ts +57 -12
  122. package/src/server/claude-messages.ts +85 -13
  123. package/src/server/index.ts +242 -100
  124. package/src/server/lifecycle.ts +155 -25
  125. package/src/server/management/agent-settings-routes.ts +79 -36
  126. package/src/server/management/api-key-usage.ts +167 -0
  127. package/src/server/management/body.ts +35 -0
  128. package/src/server/management/combo-routes.ts +5 -1
  129. package/src/server/management/config-routes.ts +42 -12
  130. package/src/server/management/logs-usage-routes.ts +41 -21
  131. package/src/server/management/model-routes.ts +188 -54
  132. package/src/server/management/oauth-account-routes.ts +115 -26
  133. package/src/server/management/provider-routes.ts +56 -6
  134. package/src/server/management/shared.ts +16 -3
  135. package/src/server/management/sidebar-routes.ts +50 -1
  136. package/src/server/management/system-restart.ts +13 -6
  137. package/src/server/management/system-routes.ts +15 -3
  138. package/src/server/management/usage-summary-cache.ts +86 -0
  139. package/src/server/management-api.ts +39 -5
  140. package/src/server/management-auth.ts +65 -14
  141. package/src/server/port-reclaim.ts +58 -12
  142. package/src/server/ports.ts +2 -0
  143. package/src/server/proxy-liveness.ts +60 -14
  144. package/src/server/relay-eager.ts +20 -4
  145. package/src/server/relay.ts +548 -154
  146. package/src/server/request-decompress.ts +51 -4
  147. package/src/server/request-log.ts +134 -15
  148. package/src/server/responses/collaboration.ts +15 -4
  149. package/src/server/responses/compact.ts +3 -0
  150. package/src/server/responses/core.ts +241 -66
  151. package/src/server/responses-image-gen-repair.ts +19 -5
  152. package/src/server/responses-item-id-repair.ts +23 -5
  153. package/src/server/sse-payload-rewrite.ts +71 -12
  154. package/src/server/startup-health-cache.ts +14 -1
  155. package/src/server/system-env.ts +8 -1
  156. package/src/server/windows-tcp-drop.ts +15 -5
  157. package/src/server/ws-bridge.ts +25 -0
  158. package/src/service.ts +179 -13
  159. package/src/storage/policy-job.ts +93 -23
  160. package/src/storage/policy-worker.ts +6 -0
  161. package/src/storage/restore-job.ts +62 -16
  162. package/src/storage/restore-worker.ts +6 -0
  163. package/src/storage/storage-mutation-coordinator.ts +36 -6
  164. package/src/storage/worker-lifecycle.ts +181 -47
  165. package/src/tray/windows.ts +97 -25
  166. package/src/types.ts +39 -6
  167. package/src/update/index.ts +24 -5
  168. package/src/update/job.ts +598 -73
  169. package/src/usage/log.ts +115 -17
  170. package/src/usage/summary.ts +67 -2
  171. package/src/vision/index.ts +112 -22
  172. package/src/web-search/loop.ts +38 -6
  173. package/src/web-search/progress-stream.ts +14 -3
  174. package/gui/dist/assets/index-BDjpkcRN.js +0 -67
  175. package/gui/dist/assets/index-BHsKRFh9.css +0 -1
@@ -17,6 +17,10 @@ import { KiroThinkingParser } from "./kiro-thinking";
17
17
  import { isCompleteKiroToolInput, kiroTruncationErrorMessage } from "./kiro-truncation";
18
18
  import { createKiroToolNameRegistry, fallbackToolUseId, fingerprint, invocationId, isValidKiroConversationId, mapModelId, normalizeToolId, osTag, stableConversationId } from "./kiro-wire";
19
19
  import { namespacedToolName } from "../types";
20
+ import {
21
+ isTranslatorBudgetExceededError,
22
+ type TranslatorBudget,
23
+ } from "../lib/translator-budget";
20
24
  import type {
21
25
  AdapterEvent,
22
26
  OcxAssistantMessage,
@@ -52,6 +56,7 @@ const AMZ_TARGET = "AmazonCodeWhispererStreamingService.GenerateAssistantRespons
52
56
  const SDK_VERSION = "1.0.27";
53
57
  const NODE_VERSION = "22.21.1";
54
58
  const KIRO_IDE_VERSION = "1.0.0";
59
+ const KIRO_FALLBACK_SERIALIZATION_ENVELOPE_BYTES = 64 * 1024;
55
60
  type KiroWireClient = "ide" | "cli";
56
61
 
57
62
  function kiroCliPlatform(): "linux" | "macos" | "windows" {
@@ -429,6 +434,10 @@ export function buildKiroPayload(
429
434
  // Neutralize Codex's GPT-5 identity line so a routed Kiro model never misreports as GPT-5/OpenAI
430
435
  // and the proxy identity never leaks upstream.
431
436
  if (parsed.context.systemPrompt?.length) systemParts.push(neutralizeIdentity(parsed.context.systemPrompt.join("\n\n")));
437
+ for (const addition of toolContext.systemAdditions) {
438
+ const boundedAddition = boundedInjectedInstruction(addition, injectedChars);
439
+ if (boundedAddition) systemParts.push(boundedAddition);
440
+ }
432
441
  const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeFromNames(kiroToolWireNames(kiroTools));
433
442
  const boundedNudge = toolCatalogNudge ? boundedInjectedInstruction(toolCatalogNudge, injectedChars) : undefined;
434
443
  if (boundedNudge) systemParts.push(boundedNudge);
@@ -600,7 +609,7 @@ export function buildKiroPayload(
600
609
  // CodeWhisperer GenerateAssistantResponse ALWAYS returns an AWS eventstream body (there is no
601
610
  // non-streaming mode), so both the streaming bridge and the non-streaming web-search sidecar loop
602
611
  // decode the same way — parseResponse just collects what parseStream yields.
603
- interface KiroAttemptResult {
612
+ interface KiroAttemptParseResult {
604
613
  terminal?: AdapterEvent;
605
614
  needsFallback?: boolean;
606
615
  usage?: OcxUsage;
@@ -609,12 +618,93 @@ interface KiroAttemptResult {
609
618
  sawReasoning: boolean;
610
619
  }
611
620
 
621
+ interface KiroAttemptResult extends KiroAttemptParseResult {
622
+ releaseRetained(): void;
623
+ }
624
+
625
+ interface KiroAttemptRetention {
626
+ trackReplacement(previousBytes: number, nextBytes: number): void;
627
+ retainEvent(event: AdapterEvent, bytes: number): void;
628
+ releaseEvent(event: AdapterEvent): void;
629
+ releaseAll(): void;
630
+ }
631
+
632
+ function createKiroAttemptRetention(budget: TranslatorBudget): KiroAttemptRetention {
633
+ let retainedBytes = 0;
634
+ const eventBytes = new Map<AdapterEvent, number>();
635
+ return {
636
+ trackReplacement(previousBytes, nextBytes) {
637
+ retainedBytes = Math.max(0, retainedBytes - previousBytes) + nextBytes;
638
+ },
639
+ retainEvent(event, bytes) {
640
+ retainedBytes += bytes;
641
+ eventBytes.set(event, bytes);
642
+ },
643
+ releaseEvent(event) {
644
+ const bytes = eventBytes.get(event);
645
+ if (bytes === undefined) return;
646
+ eventBytes.delete(event);
647
+ retainedBytes = Math.max(0, retainedBytes - bytes);
648
+ budget.releaseRetained(bytes, { kind: "retained_collectors" });
649
+ },
650
+ releaseAll() {
651
+ if (retainedBytes > 0) budget.releaseRetained(retainedBytes, { kind: "retained_collectors" });
652
+ retainedBytes = 0;
653
+ eventBytes.clear();
654
+ },
655
+ };
656
+ }
657
+
612
658
  interface KiroFallbackAttempt {
613
659
  response: Response;
614
660
  inputTokens: number;
615
661
  contextInputEstimate: number;
616
662
  nameMap: Map<string, string>;
617
663
  conversationId: string;
664
+ releaseRequestBody?: () => void;
665
+ }
666
+
667
+ function appendedUtf8Bytes(previous: string, previousBytes: number, fragment: string): number {
668
+ let nextBytes = previousBytes + Buffer.byteLength(fragment);
669
+ const previousLast = previous.charCodeAt(previous.length - 1);
670
+ const fragmentFirst = fragment.charCodeAt(0);
671
+ if (previousLast >= 0xd800 && previousLast <= 0xdbff
672
+ && fragmentFirst >= 0xdc00 && fragmentFirst <= 0xdfff) {
673
+ nextBytes -= 2;
674
+ }
675
+ return nextBytes;
676
+ }
677
+
678
+ /** Exact UTF-8 size JSON.stringify() will use for a string, without materializing that copy. */
679
+ function jsonStringSerializedUtf8Bytes(value: string): number {
680
+ let bytes = 2; // Opening and closing quotes.
681
+ for (let index = 0; index < value.length; index++) {
682
+ const code = value.charCodeAt(index);
683
+ if (code === 0x22 || code === 0x5c) {
684
+ bytes += 2;
685
+ } else if (code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d) {
686
+ bytes += 2;
687
+ } else if (code < 0x20) {
688
+ bytes += 6;
689
+ } else if (code <= 0x7f) {
690
+ bytes += 1;
691
+ } else if (code <= 0x7ff) {
692
+ bytes += 2;
693
+ } else if (code >= 0xd800 && code <= 0xdbff) {
694
+ const next = value.charCodeAt(index + 1);
695
+ if (next >= 0xdc00 && next <= 0xdfff) {
696
+ bytes += 4;
697
+ index++;
698
+ } else {
699
+ bytes += 6;
700
+ }
701
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
702
+ bytes += 6;
703
+ } else {
704
+ bytes += 3;
705
+ }
706
+ }
707
+ return bytes;
618
708
  }
619
709
 
620
710
  interface KiroContextWindowState {
@@ -625,6 +715,7 @@ type KiroFallbackFactory = (
625
715
  conversationId: string | undefined,
626
716
  assistantText: string,
627
717
  sawReasoning: boolean,
718
+ budget: TranslatorBudget,
628
719
  ) => Promise<KiroFallbackAttempt>;
629
720
 
630
721
  function mergeKiroUsage(
@@ -708,6 +799,7 @@ const KIRO_END_TURN_STOP_REASON = "END_TURN";
708
799
 
709
800
  async function* parseKiroAttempt(
710
801
  response: Response,
802
+ budget: TranslatorBudget,
711
803
  mode: KiroCompletionMode,
712
804
  modelId: string | undefined,
713
805
  inputTokens: number,
@@ -721,8 +813,10 @@ async function* parseKiroAttempt(
721
813
  // `required` mode holds staged commentary until a real tool call or terminal metadata identifies
722
814
  // the attempt boundary. Anything the inner parser leaves behind is flushed before the terminal.
723
815
  const deferred: AdapterEvent[] = [];
816
+ const retention = createKiroAttemptRetention(budget);
724
817
  const attempt = parseKiroAttemptEvents(
725
818
  response,
819
+ budget,
726
820
  mode,
727
821
  modelId,
728
822
  inputTokens,
@@ -730,20 +824,30 @@ async function* parseKiroAttempt(
730
824
  nameMap,
731
825
  conversationId,
732
826
  deferred,
827
+ retention,
733
828
  contextInputEstimate,
734
829
  priorEmittedOutput,
735
830
  );
736
- let next = await attempt.next();
737
- while (!next.done) {
738
- yield next.value;
739
- next = await attempt.next();
831
+ let handedOff = false;
832
+ try {
833
+ let next = await attempt.next();
834
+ while (!next.done) {
835
+ yield next.value;
836
+ next = await attempt.next();
837
+ }
838
+ for (const event of deferred.splice(0)) {
839
+ try { yield event; } finally { retention.releaseEvent(event); }
840
+ }
841
+ handedOff = true;
842
+ return { ...next.value, releaseRetained: () => retention.releaseAll() };
843
+ } finally {
844
+ if (!handedOff) retention.releaseAll();
740
845
  }
741
- for (const event of deferred.splice(0)) yield event;
742
- return next.value;
743
846
  }
744
847
 
745
848
  async function* parseKiroAttemptEvents(
746
849
  response: Response,
850
+ budget: TranslatorBudget,
747
851
  mode: KiroCompletionMode,
748
852
  modelId: string | undefined,
749
853
  inputTokens: number,
@@ -751,10 +855,11 @@ async function* parseKiroAttemptEvents(
751
855
  nameMap: Map<string, string> | undefined,
752
856
  conversationId: string | undefined,
753
857
  deferred: AdapterEvent[],
858
+ retention: KiroAttemptRetention,
754
859
  contextInputEstimate?: number,
755
860
  priorEmittedOutput = false,
756
- ): AsyncGenerator<AdapterEvent, KiroAttemptResult> {
757
- const emptyResult = (): KiroAttemptResult => ({ assistantText: "", sawReasoning: false });
861
+ ): AsyncGenerator<AdapterEvent, KiroAttemptParseResult> {
862
+ const emptyResult = (): KiroAttemptParseResult => ({ assistantText: "", sawReasoning: false });
758
863
  if (!response.body) {
759
864
  return {
760
865
  ...emptyResult(),
@@ -764,9 +869,11 @@ async function* parseKiroAttemptEvents(
764
869
 
765
870
  let open: { id: string; name: string; chunks: string[]; completion: boolean } | null = null;
766
871
  let outputChars = "";
872
+ let outputCharsBytes = 0;
767
873
  let contextUsagePercentage: number | undefined;
768
874
  let returnedConversationId = conversationId;
769
875
  let assistantText = "";
876
+ let assistantTextBytes = 0;
770
877
  let sawText = false;
771
878
  let sawReasoning = false;
772
879
  let sawRealTool = false;
@@ -775,7 +882,19 @@ async function* parseKiroAttemptEvents(
775
882
  let authoritativeUsage: OcxUsage | undefined;
776
883
  let stopReason: string | undefined;
777
884
  const fallbackEvents: AdapterEvent[] = [];
778
- const thinking = new KiroThinkingParser();
885
+ const thinking = new KiroThinkingParser(budget);
886
+
887
+ const retainedEventBytes = (event: AdapterEvent): number => Buffer.byteLength(JSON.stringify(event));
888
+ const retainEvent = (event: AdapterEvent): void => {
889
+ const bytes = retainedEventBytes(event);
890
+ budget.chargeRetained(bytes, { kind: "retained_collectors" });
891
+ retention.retainEvent(event, bytes);
892
+ };
893
+ const emitRetained = async function* (events: Iterable<AdapterEvent>): AsyncGenerator<AdapterEvent> {
894
+ for (const event of events) {
895
+ try { yield event; } finally { retention.releaseEvent(event); }
896
+ }
897
+ };
779
898
 
780
899
  const providerState = (): { kiro: { conversationId: string } } | undefined =>
781
900
  returnedConversationId ? { kiro: { conversationId: returnedConversationId } } : undefined;
@@ -889,19 +1008,33 @@ async function* parseKiroAttemptEvents(
889
1008
  if (sawRealTool) return [...deferred.splice(0), event];
890
1009
  if (event.type !== "text_delta" && deferred.length === 0) return [event];
891
1010
  deferred.push(event);
1011
+ retainEvent(event);
892
1012
  return [{ type: "heartbeat" }];
893
1013
  };
894
1014
 
895
1015
  const stage = (event: AdapterEvent): AdapterEvent[] => {
896
1016
  if (event.type === "text_delta") {
1017
+ const nextAssistantTextBytes = appendedUtf8Bytes(assistantText, assistantTextBytes, event.text);
1018
+ const assistantReservation = budget.reserveTransient(nextAssistantTextBytes, { kind: "retained_collectors" });
897
1019
  assistantText += event.text;
1020
+ assistantReservation.commitRetained();
1021
+ budget.releaseRetained(assistantTextBytes, { kind: "retained_collectors" });
1022
+ retention.trackReplacement(assistantTextBytes, nextAssistantTextBytes);
1023
+ assistantTextBytes = nextAssistantTextBytes;
898
1024
  if (event.text.trim()) sawText = true;
1025
+ const nextOutputCharsBytes = appendedUtf8Bytes(outputChars, outputCharsBytes, event.text);
1026
+ const outputReservation = budget.reserveTransient(nextOutputCharsBytes, { kind: "retained_collectors" });
899
1027
  outputChars += event.text;
1028
+ outputReservation.commitRetained();
1029
+ budget.releaseRetained(outputCharsBytes, { kind: "retained_collectors" });
1030
+ retention.trackReplacement(outputCharsBytes, nextOutputCharsBytes);
1031
+ outputCharsBytes = nextOutputCharsBytes;
900
1032
  const phased = mode === "disabled"
901
1033
  ? event
902
1034
  : { ...event, phase: "commentary" as const };
903
1035
  if (mode === "text_fallback") {
904
1036
  fallbackEvents.push(phased);
1037
+ retainEvent(phased);
905
1038
  return [];
906
1039
  }
907
1040
  return mode === "required" ? defer(phased) : [phased];
@@ -909,10 +1042,17 @@ async function* parseKiroAttemptEvents(
909
1042
  if (event.type === "reasoning_raw_delta" || event.type === "thinking_delta") {
910
1043
  const text = event.type === "reasoning_raw_delta" ? event.text : event.thinking;
911
1044
  if (text.trim()) sawReasoning = true;
1045
+ const nextOutputCharsBytes = appendedUtf8Bytes(outputChars, outputCharsBytes, text);
1046
+ const reasoningReservation = budget.reserveTransient(nextOutputCharsBytes, { kind: "retained_collectors" });
912
1047
  outputChars += text;
1048
+ reasoningReservation.commitRetained();
1049
+ budget.releaseRetained(outputCharsBytes, { kind: "retained_collectors" });
1050
+ retention.trackReplacement(outputCharsBytes, nextOutputCharsBytes);
1051
+ outputCharsBytes = nextOutputCharsBytes;
913
1052
  }
914
1053
  if (mode === "text_fallback" && event.type !== "heartbeat") {
915
1054
  fallbackEvents.push(event);
1055
+ retainEvent(event);
916
1056
  return [];
917
1057
  }
918
1058
  return mode === "required" ? defer(event) : [event];
@@ -940,6 +1080,7 @@ async function* parseKiroAttemptEvents(
940
1080
  if (!open) return { events: [] };
941
1081
  const tool = open;
942
1082
  open = null;
1083
+ budget.closeCall(tool.id);
943
1084
  const input = tool.chunks.join("");
944
1085
  if (!isCompleteKiroToolInput(input)) {
945
1086
  return { events: [], terminal: protocolTerminal(kiroTruncationErrorMessage("incomplete tool input JSON"), tool.completion) };
@@ -1018,21 +1159,21 @@ async function* parseKiroAttemptEvents(
1018
1159
  }
1019
1160
  if (ev.data) {
1020
1161
  for (const contentEvent of thinking.feed(ev.data)) {
1021
- for (const staged of stage(contentEvent)) yield staged;
1162
+ yield* emitRetained(stage(contentEvent));
1022
1163
  }
1023
1164
  }
1024
1165
  break;
1025
1166
  case "reasoning":
1026
1167
  for (const contentEvent of thinking.flush()) {
1027
- for (const staged of stage(contentEvent)) yield staged;
1168
+ yield* emitRetained(stage(contentEvent));
1028
1169
  }
1029
1170
  if (ev.data) {
1030
- for (const staged of stage({ type: "reasoning_raw_delta", text: ev.data })) yield staged;
1171
+ yield* emitRetained(stage({ type: "reasoning_raw_delta", text: ev.data }));
1031
1172
  }
1032
1173
  break;
1033
1174
  case "tool": {
1034
1175
  for (const contentEvent of thinking.flush()) {
1035
- for (const staged of stage(contentEvent)) yield staged;
1176
+ yield* emitRetained(stage(contentEvent));
1036
1177
  }
1037
1178
  if (!open) {
1038
1179
  if (ev.stop === true) {
@@ -1044,10 +1185,12 @@ async function* parseKiroAttemptEvents(
1044
1185
  const started = beginTool(ev.toolUseId, ev.name);
1045
1186
  if (started.terminal) return { assistantText, sawReasoning, terminal: started.terminal };
1046
1187
  open = started.tool!;
1188
+ budget.openCall(open.id);
1047
1189
  } else if (
1048
1190
  (ev.toolUseId && ev.toolUseId !== open.id)
1049
1191
  || (ev.name && open.name !== "unknown" && ev.name !== open.name)
1050
1192
  ) {
1193
+ budget.closeCall(open.id);
1051
1194
  open = null;
1052
1195
  return { assistantText, sawReasoning, terminal: protocolTerminal(kiroTruncationErrorMessage("tool input changed identity before stop")) };
1053
1196
  }
@@ -1060,14 +1203,25 @@ async function* parseKiroAttemptEvents(
1060
1203
  }
1061
1204
  }
1062
1205
  if (open && ev.input !== undefined) {
1206
+ const previousCallBytes = open.chunks.reduce((total, chunk) => total + Buffer.byteLength(chunk), 0);
1207
+ const nextCallBytes = previousCallBytes + Buffer.byteLength(ev.input);
1208
+ const callReservation = budget.reserveTransient(nextCallBytes, { kind: "tool_args", callId: open.id });
1063
1209
  open.chunks.push(ev.input);
1210
+ callReservation.commitRetained();
1211
+ budget.releaseRetained(previousCallBytes, { kind: "tool_args", callId: open.id });
1212
+ const nextOutputCharsBytes = appendedUtf8Bytes(outputChars, outputCharsBytes, ev.input);
1213
+ const toolOutputReservation = budget.reserveTransient(nextOutputCharsBytes, { kind: "retained_collectors" });
1064
1214
  outputChars += ev.input;
1215
+ toolOutputReservation.commitRetained();
1216
+ budget.releaseRetained(outputCharsBytes, { kind: "retained_collectors" });
1217
+ retention.trackReplacement(outputCharsBytes, nextOutputCharsBytes);
1218
+ outputCharsBytes = nextOutputCharsBytes;
1065
1219
  }
1066
1220
  if (ev.stop === true) {
1067
1221
  const flushed = flushOpen();
1068
1222
  if (flushed.terminal) return { assistantText, sawReasoning, terminal: flushed.terminal };
1069
1223
  for (const event of flushed.events) {
1070
- for (const staged of stage(event)) yield staged;
1224
+ yield* emitRetained(stage(event));
1071
1225
  }
1072
1226
  } else {
1073
1227
  yield { type: "heartbeat" };
@@ -1087,7 +1241,7 @@ async function* parseKiroAttemptEvents(
1087
1241
  }
1088
1242
 
1089
1243
  for (const contentEvent of thinking.flush()) {
1090
- for (const staged of stage(contentEvent)) yield staged;
1244
+ yield* emitRetained(stage(contentEvent));
1091
1245
  }
1092
1246
  if (open) {
1093
1247
  const input = open.chunks.join("");
@@ -1103,7 +1257,7 @@ async function* parseKiroAttemptEvents(
1103
1257
  const flushed = flushOpen();
1104
1258
  if (flushed.terminal) return { assistantText, sawReasoning, terminal: flushed.terminal };
1105
1259
  for (const event of flushed.events) {
1106
- for (const staged of stage(event)) yield staged;
1260
+ yield* emitRetained(stage(event));
1107
1261
  }
1108
1262
  }
1109
1263
 
@@ -1138,12 +1292,12 @@ async function* parseKiroAttemptEvents(
1138
1292
  });
1139
1293
 
1140
1294
  if (mode === "required") {
1141
- for (const event of deferred.splice(0)) yield event;
1295
+ yield* emitRetained(deferred.splice(0));
1142
1296
  }
1143
1297
 
1144
1298
  if (mode === "text_fallback") {
1145
1299
  if (completionAnswer !== undefined) {
1146
- for (const event of fallbackEvents) yield event;
1300
+ yield* emitRetained(fallbackEvents);
1147
1301
  yield { type: "text_delta", text: completionAnswer, phase: "final_answer" };
1148
1302
  return {
1149
1303
  assistantText,
@@ -1152,7 +1306,7 @@ async function* parseKiroAttemptEvents(
1152
1306
  };
1153
1307
  }
1154
1308
  if (sawRealTool) {
1155
- for (const event of fallbackEvents) yield event;
1309
+ yield* emitRetained(fallbackEvents);
1156
1310
  return {
1157
1311
  assistantText,
1158
1312
  sawReasoning,
@@ -1161,8 +1315,12 @@ async function* parseKiroAttemptEvents(
1161
1315
  }
1162
1316
  if (sawText) {
1163
1317
  for (const event of fallbackEvents) {
1164
- if (event.type !== "text_delta") yield event;
1165
- else yield { ...event, phase: "final_answer" };
1318
+ try {
1319
+ if (event.type !== "text_delta") yield event;
1320
+ else yield { ...event, phase: "final_answer" };
1321
+ } finally {
1322
+ retention.releaseEvent(event);
1323
+ }
1166
1324
  }
1167
1325
  return {
1168
1326
  assistantText,
@@ -1170,7 +1328,7 @@ async function* parseKiroAttemptEvents(
1170
1328
  terminal: { type: "done", usage: finalUsage, endTurn: true, ...(finalProviderState ? { providerState: finalProviderState } : {}) },
1171
1329
  };
1172
1330
  }
1173
- for (const event of fallbackEvents) yield event;
1331
+ yield* emitRetained(fallbackEvents);
1174
1332
  return {
1175
1333
  assistantText,
1176
1334
  sawReasoning,
@@ -1295,6 +1453,20 @@ async function* parseKiroAttemptEvents(
1295
1453
  },
1296
1454
  };
1297
1455
  } catch (err) {
1456
+ if (isTranslatorBudgetExceededError(err)) {
1457
+ if (open) budget.closeCall(open.id);
1458
+ return {
1459
+ assistantText,
1460
+ sawReasoning,
1461
+ terminal: {
1462
+ type: "error",
1463
+ status: 502,
1464
+ errorType: "upstream_error",
1465
+ code: "translation_buffer_limit",
1466
+ message: "upstream translation buffer exceeded the safe limit",
1467
+ },
1468
+ };
1469
+ }
1298
1470
  // Mid-stream socket closes after response.created / heartbeats only must stay retryable:
1299
1471
  // nothing was relayed to the client, so a string-body replay is safe (see #519 / cursor's
1300
1472
  // emittedOutput gate). Once any assistant text, reasoning, tool, or deferred content exists
@@ -1329,6 +1501,7 @@ async function* parseKiroAttemptEvents(
1329
1501
 
1330
1502
  export async function* parseKiroStream(
1331
1503
  response: Response,
1504
+ budget: TranslatorBudget,
1332
1505
  modelId?: string,
1333
1506
  inputTokens = 0,
1334
1507
  contextWindow?: number,
@@ -1341,6 +1514,7 @@ export async function* parseKiroStream(
1341
1514
  const contextWindowState: KiroContextWindowState = { value: contextWindow };
1342
1515
  const first = parseKiroAttempt(
1343
1516
  response,
1517
+ budget,
1344
1518
  completionMode,
1345
1519
  modelId,
1346
1520
  inputTokens,
@@ -1348,6 +1522,7 @@ export async function* parseKiroStream(
1348
1522
  nameMap,
1349
1523
  conversationId,
1350
1524
  contextInputEstimate,
1525
+ false,
1351
1526
  );
1352
1527
  let firstNext = await first.next();
1353
1528
  while (!firstNext.done) {
@@ -1355,106 +1530,139 @@ export async function* parseKiroStream(
1355
1530
  firstNext = await first.next();
1356
1531
  }
1357
1532
  const firstResult = firstNext.value;
1358
- if (!firstResult.needsFallback) {
1359
- if (firstResult.terminal) yield firstResult.terminal;
1360
- return;
1361
- }
1362
- if (!fallbackFactory) {
1363
- yield retryableKiroIncomplete(
1364
- "uncompleted_kiro_response",
1365
- "Kiro produced progress without an explicit final answer and no bounded retry transport was available",
1366
- firstResult.usage ?? { inputTokens, outputTokens: 0, estimated: true },
1367
- firstResult.providerState,
1368
- );
1369
- return;
1370
- }
1371
-
1372
- yield { type: "heartbeat" };
1373
- // First attempt already flushed deferred progress before this point. Gate fallback
1374
- // setup/HTTP failures the same way as the second-stream catch so a replay cannot
1375
- // duplicate visible commentary (#520).
1376
- const priorEmittedOutput = Boolean(firstResult.assistantText.trim()) || firstResult.sawReasoning;
1377
- let fallback: KiroFallbackAttempt;
1378
1533
  try {
1379
- fallback = await fallbackFactory(
1380
- firstResult.providerState?.kiro.conversationId ?? conversationId,
1381
- firstResult.assistantText,
1382
- firstResult.sawReasoning,
1383
- );
1384
- } catch (err) {
1385
- yield {
1386
- type: "error",
1387
- message: safeKiroErrorMessage({}, err instanceof Error ? err.message : String(err)),
1388
- status: err instanceof Error && err.name === "TimeoutError" ? 504 : 502,
1389
- errorType: "upstream_error",
1390
- retryable: !priorEmittedOutput,
1391
- usage: firstResult.usage,
1392
- };
1393
- return;
1394
- }
1395
- if (!fallback.response.ok) {
1396
- const payload = await fallback.response.text().catch(() => "");
1397
- const failure = classifyKiroHttpError(fallback.response.status, fallback.response.headers, payload);
1398
- yield {
1399
- type: "error",
1400
- message: failure.message,
1401
- status: failure.status,
1402
- errorType: failure.errorType,
1403
- code: failure.code,
1404
- retryable: priorEmittedOutput ? false : failure.retryable,
1405
- usage: firstResult.usage,
1406
- };
1407
- return;
1408
- }
1534
+ if (!firstResult.needsFallback) {
1535
+ if (firstResult.terminal) yield firstResult.terminal;
1536
+ return;
1537
+ }
1538
+ if (!fallbackFactory) {
1539
+ yield retryableKiroIncomplete(
1540
+ "uncompleted_kiro_response",
1541
+ "Kiro produced progress without an explicit final answer and no bounded retry transport was available",
1542
+ firstResult.usage ?? { inputTokens, outputTokens: 0, estimated: true },
1543
+ firstResult.providerState,
1544
+ );
1545
+ return;
1546
+ }
1409
1547
 
1410
- const second = parseKiroAttempt(
1411
- fallback.response,
1412
- "text_fallback",
1413
- modelId,
1414
- fallback.inputTokens,
1415
- contextWindowState,
1416
- fallback.nameMap,
1417
- fallback.conversationId,
1418
- fallback.contextInputEstimate,
1419
- // First attempt already flushed deferred progress to the client before this fallback.
1420
- // A zero-output transport failure here must stay non-retryable to avoid duplicating that text.
1421
- priorEmittedOutput,
1422
- );
1423
- let secondNext = await second.next();
1424
- while (!secondNext.done) {
1425
- yield secondNext.value;
1426
- secondNext = await second.next();
1427
- }
1428
- const secondResult = secondNext.value;
1429
- if (!secondResult.terminal) {
1430
- yield retryableKiroIncomplete(
1431
- "empty_kiro_fallback",
1432
- "Kiro's bounded completion retry ended without a terminal result",
1433
- mergeKiroUsage(firstResult.usage, secondResult.usage, Boolean(firstResult.assistantText))
1434
- ?? { inputTokens, outputTokens: 0, estimated: true },
1435
- secondResult.providerState ?? firstResult.providerState,
1436
- !priorEmittedOutput,
1548
+ yield { type: "heartbeat" };
1549
+ // First attempt already flushed deferred progress before this point. Gate fallback
1550
+ // setup/HTTP failures the same way as the second-stream catch so a replay cannot
1551
+ // duplicate visible commentary (#520).
1552
+ const priorEmittedOutput = Boolean(firstResult.assistantText.trim()) || firstResult.sawReasoning;
1553
+ let firstAssistantText = firstResult.assistantText;
1554
+ const firstHadAssistantText = firstAssistantText.length > 0;
1555
+ let fallback: KiroFallbackAttempt;
1556
+ try {
1557
+ fallback = await fallbackFactory(
1558
+ firstResult.providerState?.kiro.conversationId ?? conversationId,
1559
+ firstAssistantText,
1560
+ firstResult.sawReasoning,
1561
+ budget,
1562
+ );
1563
+ } catch (err) {
1564
+ firstAssistantText = "";
1565
+ firstResult.assistantText = "";
1566
+ firstResult.releaseRetained();
1567
+ if (isTranslatorBudgetExceededError(err)) {
1568
+ yield {
1569
+ type: "error",
1570
+ message: "upstream translation buffer exceeded the safe limit",
1571
+ status: 502,
1572
+ errorType: "upstream_error",
1573
+ code: "translation_buffer_limit",
1574
+ usage: firstResult.usage,
1575
+ };
1576
+ return;
1577
+ }
1578
+ yield {
1579
+ type: "error",
1580
+ message: safeKiroErrorMessage({}, err instanceof Error ? err.message : String(err)),
1581
+ status: err instanceof Error && err.name === "TimeoutError" ? 504 : 502,
1582
+ errorType: "upstream_error",
1583
+ retryable: !priorEmittedOutput,
1584
+ usage: firstResult.usage,
1585
+ };
1586
+ return;
1587
+ }
1588
+ // The factory has finished using the live first-attempt alias and has retained its own retry
1589
+ // serialization through the fetch boundary. The discarded parser collectors can now release
1590
+ // before the second attempt begins on the same turn budget.
1591
+ firstAssistantText = "";
1592
+ firstResult.assistantText = "";
1593
+ firstResult.releaseRetained();
1594
+ fallback.releaseRequestBody?.();
1595
+ if (!fallback.response.ok) {
1596
+ const payload = await fallback.response.text().catch(() => "");
1597
+ const failure = classifyKiroHttpError(fallback.response.status, fallback.response.headers, payload);
1598
+ yield {
1599
+ type: "error",
1600
+ message: failure.message,
1601
+ status: failure.status,
1602
+ errorType: failure.errorType,
1603
+ code: failure.code,
1604
+ retryable: priorEmittedOutput ? false : failure.retryable,
1605
+ usage: firstResult.usage,
1606
+ };
1607
+ return;
1608
+ }
1609
+
1610
+ const second = parseKiroAttempt(
1611
+ fallback.response,
1612
+ budget,
1613
+ "text_fallback",
1614
+ modelId,
1615
+ fallback.inputTokens,
1616
+ contextWindowState,
1617
+ fallback.nameMap,
1618
+ fallback.conversationId,
1619
+ fallback.contextInputEstimate,
1620
+ // First attempt already flushed deferred progress to the client before this fallback.
1621
+ // A zero-output transport failure here must stay non-retryable to avoid duplicating that text.
1622
+ priorEmittedOutput,
1437
1623
  );
1438
- return;
1439
- }
1440
- if (secondResult.terminal.type === "done" || secondResult.terminal.type === "incomplete") {
1441
- yield {
1442
- ...secondResult.terminal,
1443
- // Belt-and-suspenders: never advertise a replay-safe incomplete after flushed progress.
1444
- ...(secondResult.terminal.type === "incomplete" && priorEmittedOutput
1445
- ? { retryable: false as const }
1446
- : {}),
1447
- usage: mergeKiroUsage(firstResult.usage, secondResult.terminal.usage, Boolean(firstResult.assistantText)),
1448
- providerState: secondResult.terminal.providerState ?? firstResult.providerState,
1449
- };
1450
- return;
1624
+ let secondNext = await second.next();
1625
+ while (!secondNext.done) {
1626
+ yield secondNext.value;
1627
+ secondNext = await second.next();
1628
+ }
1629
+ const secondResult = secondNext.value;
1630
+ try {
1631
+ if (!secondResult.terminal) {
1632
+ yield retryableKiroIncomplete(
1633
+ "empty_kiro_fallback",
1634
+ "Kiro's bounded completion retry ended without a terminal result",
1635
+ mergeKiroUsage(firstResult.usage, secondResult.usage, firstHadAssistantText)
1636
+ ?? { inputTokens, outputTokens: 0, estimated: true },
1637
+ secondResult.providerState ?? firstResult.providerState,
1638
+ !priorEmittedOutput,
1639
+ );
1640
+ return;
1641
+ }
1642
+ if (secondResult.terminal.type === "done" || secondResult.terminal.type === "incomplete") {
1643
+ yield {
1644
+ ...secondResult.terminal,
1645
+ // Belt-and-suspenders: never advertise a replay-safe incomplete after flushed progress.
1646
+ ...(secondResult.terminal.type === "incomplete" && priorEmittedOutput
1647
+ ? { retryable: false as const }
1648
+ : {}),
1649
+ usage: mergeKiroUsage(firstResult.usage, secondResult.terminal.usage, firstHadAssistantText),
1650
+ providerState: secondResult.terminal.providerState ?? firstResult.providerState,
1651
+ };
1652
+ return;
1653
+ }
1654
+ yield {
1655
+ ...secondResult.terminal,
1656
+ ...(secondResult.terminal.type === "error"
1657
+ ? { usage: mergeKiroUsage(firstResult.usage, secondResult.terminal.usage, firstHadAssistantText) }
1658
+ : {}),
1659
+ };
1660
+ } finally {
1661
+ secondResult.releaseRetained();
1662
+ }
1663
+ } finally {
1664
+ firstResult.releaseRetained();
1451
1665
  }
1452
- yield {
1453
- ...secondResult.terminal,
1454
- ...(secondResult.terminal.type === "error"
1455
- ? { usage: mergeKiroUsage(firstResult.usage, secondResult.terminal.usage, Boolean(firstResult.assistantText)) }
1456
- : {}),
1457
- };
1458
1666
  }
1459
1667
 
1460
1668
  // Adapter
@@ -1469,6 +1677,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1469
1677
  let conversationId: string | undefined;
1470
1678
  let completionMode: KiroCompletionMode = "disabled";
1471
1679
  let requestSnapshot: OcxParsedRequest | undefined;
1680
+ let firstRequestBodyBytes = 0;
1472
1681
  let requestAbortSignal: AbortSignal | undefined;
1473
1682
 
1474
1683
  const build = async (
@@ -1551,6 +1760,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1551
1760
  returnedConversationId,
1552
1761
  assistantText,
1553
1762
  _sawReasoning,
1763
+ budget,
1554
1764
  ) => {
1555
1765
  if (!requestSnapshot) throw new Error("Kiro completion retry lost its request state");
1556
1766
  if (requestAbortSignal?.aborted) {
@@ -1574,19 +1784,50 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1574
1784
  timestamp: Date.now(),
1575
1785
  });
1576
1786
  }
1577
- const retry = await build(retryParsed, "text_fallback");
1578
- const response = await fetchKiroWithRetry(retry.request, {
1579
- abortSignal: requestAbortSignal,
1580
- returnRawErrors: true,
1581
- stream: true,
1582
- });
1583
- return {
1584
- response,
1585
- inputTokens: retry.inputTokens,
1586
- contextInputEstimate: retry.contextInputEstimate,
1587
- nameMap: retry.nameMap,
1588
- conversationId: retry.conversationId,
1787
+ // The retry starts from the already measured first wire body, adds one JSON-escaped replay
1788
+ // string, and only changes bounded Kiro-owned fields (completion prompt/tool, history wrapper,
1789
+ // and <=256-byte conversation id). 64 KiB is a conservative envelope for those fixed fields.
1790
+ // Reserve that complete upper bound while the first-attempt collectors are still charged so a
1791
+ // near-cap turn fails before build() can materialize the retry payload or serialized body.
1792
+ const retryBodyUpperBound = firstRequestBodyBytes
1793
+ + jsonStringSerializedUtf8Bytes(assistantText)
1794
+ + KIRO_FALLBACK_SERIALIZATION_ENVELOPE_BYTES;
1795
+ const retryBodyReservation = budget.reserveTransient(retryBodyUpperBound, { kind: "request_copies" });
1796
+ let retryBodyBytes = 0;
1797
+ let retryBodyRetained = false;
1798
+ let requestBodyReleased = false;
1799
+ const releaseRequestBody = () => {
1800
+ if (requestBodyReleased) return;
1801
+ requestBodyReleased = true;
1802
+ if (retryBodyRetained) budget.releaseRetained(retryBodyBytes, { kind: "request_copies" });
1803
+ else retryBodyReservation.release();
1589
1804
  };
1805
+ try {
1806
+ const retry = await build(retryParsed, "text_fallback");
1807
+ retryBodyBytes = Buffer.byteLength(retry.request.body);
1808
+ if (retryBodyBytes > retryBodyUpperBound) {
1809
+ throw new Error("Kiro retry serialization exceeded its pre-admitted upper bound");
1810
+ }
1811
+ retryBodyReservation.commitRetained();
1812
+ retryBodyRetained = true;
1813
+ budget.releaseRetained(retryBodyUpperBound - retryBodyBytes, { kind: "request_copies" });
1814
+ const response = await fetchKiroWithRetry(retry.request, {
1815
+ abortSignal: requestAbortSignal,
1816
+ returnRawErrors: true,
1817
+ stream: true,
1818
+ });
1819
+ return {
1820
+ response,
1821
+ inputTokens: retry.inputTokens,
1822
+ contextInputEstimate: retry.contextInputEstimate,
1823
+ nameMap: retry.nameMap,
1824
+ conversationId: retry.conversationId,
1825
+ releaseRequestBody,
1826
+ };
1827
+ } catch (error) {
1828
+ releaseRequestBody();
1829
+ throw error;
1830
+ }
1590
1831
  };
1591
1832
 
1592
1833
  return {
@@ -1601,13 +1842,15 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1601
1842
  conversationId = built.conversationId;
1602
1843
  completionMode = built.completionMode;
1603
1844
  requestSnapshot = structuredClone(parsed);
1845
+ firstRequestBodyBytes = Buffer.byteLength(built.request.body);
1604
1846
  requestAbortSignal = incoming?.abortSignal;
1605
1847
  return built.request;
1606
1848
  },
1607
1849
 
1608
- parseStream(response: Response): AsyncGenerator<AdapterEvent> {
1850
+ parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent> {
1609
1851
  return parseKiroStream(
1610
1852
  response,
1853
+ budget,
1611
1854
  modelId,
1612
1855
  inputTokens,
1613
1856
  contextWindow,
@@ -1635,10 +1878,11 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1635
1878
  // non-streamed so it can inspect tool calls). CW only ever event-streams, so we drain the
1636
1879
  // same decoder into an array. Without this, any Codex request that includes the web_search
1637
1880
  // tool failed with "web-search sidecar requires a non-streaming adapter" (kiro-only).
1638
- async parseResponse(response: Response): Promise<AdapterEvent[]> {
1881
+ async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
1639
1882
  const events: AdapterEvent[] = [];
1640
1883
  for await (const e of parseKiroStream(
1641
1884
  response,
1885
+ budget,
1642
1886
  modelId,
1643
1887
  inputTokens,
1644
1888
  contextWindow,