@bitkyc08/opencodex 2.65.0-preview.20260925 → 2.66.0-preview.20260925

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 (213) hide show
  1. package/README.md +14 -0
  2. package/gui/dist/assets/App-BVjA0T6O.js +51 -0
  3. package/gui/dist/assets/App-gkoJIEOV.css +1 -0
  4. package/gui/dist/assets/{Tray-DUvc_Wul.js → Tray-BCFtiWwD.js} +1 -1
  5. package/gui/dist/assets/index-ComVsdSk.css +1 -0
  6. package/gui/dist/assets/index-vO4dgfiU.js +86 -0
  7. package/gui/dist/assets/{tray-data-f0lTZ4sF.js → tray-data-C4FiNaxk.js} +1 -1
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -2
  10. package/src/adapters/anthropic/beta-allowlist.ts +80 -0
  11. package/src/adapters/anthropic/passthrough.ts +221 -0
  12. package/src/adapters/anthropic-image-codec.ts +56 -19
  13. package/src/adapters/anthropic-image-normalize.ts +20 -10
  14. package/src/adapters/anthropic.ts +55 -24
  15. package/src/adapters/coding-agent/protocol.ts +29 -19
  16. package/src/adapters/cursor/live-transport.ts +18 -3
  17. package/src/adapters/cursor/native-exec-shell.ts +18 -63
  18. package/src/adapters/cursor/native-exec.ts +5 -2
  19. package/src/adapters/cursor/native-foreground-shell.ts +44 -0
  20. package/src/adapters/devin/cloud-direct/chat.ts +7 -1
  21. package/src/adapters/exec-tool-result-normalize.ts +4 -1
  22. package/src/adapters/google.ts +12 -0
  23. package/src/adapters/ollama-native.ts +32 -1
  24. package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
  25. package/src/adapters/openai-chat.ts +6 -4
  26. package/src/adapters/openai-responses/passthrough.ts +1 -0
  27. package/src/adapters/openai-responses/reasoning.ts +45 -1
  28. package/src/bridge/response-json.ts +10 -2
  29. package/src/chat/outbound.ts +104 -52
  30. package/src/claude/claude-code-block.ts +16 -0
  31. package/src/claude/context-windows.ts +52 -4
  32. package/src/claude/desktop-first-party.ts +25 -8
  33. package/src/claude/inbound.ts +22 -0
  34. package/src/claude/intercept/connect-proxy.ts +37 -4
  35. package/src/claude/intercept/proxy-auth.ts +166 -0
  36. package/src/claude/intercept/runtime.ts +21 -0
  37. package/src/claude/intercept/settings.ts +33 -12
  38. package/src/claude/outbound.ts +45 -27
  39. package/src/cli/account-extended.ts +47 -15
  40. package/src/cli/account-target.ts +115 -0
  41. package/src/cli/account.ts +20 -28
  42. package/src/cli/agent.ts +10 -1
  43. package/src/cli/api-protocols.ts +206 -0
  44. package/src/cli/capabilities.ts +86 -0
  45. package/src/cli/combo.ts +2 -2
  46. package/src/cli/connect.ts +127 -3
  47. package/src/cli/dispatch.ts +8 -0
  48. package/src/cli/doctor.ts +33 -0
  49. package/src/cli/help.ts +2 -0
  50. package/src/cli/link.ts +288 -0
  51. package/src/cli/registry.ts +23 -0
  52. package/src/cli/runtime-api.ts +79 -0
  53. package/src/cli/system-command.ts +21 -2
  54. package/src/client/connect.ts +125 -23
  55. package/src/client/hub-relay.ts +12 -8
  56. package/src/client/link-join.ts +323 -0
  57. package/src/client/link-relay.ts +241 -0
  58. package/src/client/link-state.ts +110 -0
  59. package/src/client/link-teardown.ts +63 -0
  60. package/src/client/link-tunnel.ts +465 -0
  61. package/src/client/machine-listener.ts +14 -5
  62. package/src/client/runtime.ts +91 -42
  63. package/src/client/state.ts +4 -0
  64. package/src/codex/catalog/build-entries.ts +4 -0
  65. package/src/codex/catalog/derive-entry.ts +5 -0
  66. package/src/codex/catalog/parsing.ts +4 -0
  67. package/src/codex/catalog/retained-sync.ts +4 -1
  68. package/src/codex/desktop-switches.ts +88 -8
  69. package/src/codex/inject/plan.ts +16 -3
  70. package/src/codex/inject.ts +3 -0
  71. package/src/codex/internal/catalog-writer.ts +13 -2
  72. package/src/combos/failover.ts +3 -2
  73. package/src/combos/index.ts +12 -0
  74. package/src/combos/jev.ts +646 -0
  75. package/src/combos/request.ts +5 -0
  76. package/src/combos/resolve.ts +17 -15
  77. package/src/combos/types.ts +47 -3
  78. package/src/config/atomic-write.ts +3 -0
  79. package/src/config/live-reconcile.ts +20 -4
  80. package/src/config/persist-unlocked.ts +20 -7
  81. package/src/config/schema/config-schema.ts +16 -0
  82. package/src/config/schema/leaf-validators.ts +38 -1
  83. package/src/generated/compatibility-version.json +416 -128
  84. package/src/integrations/omp-yaml-source.ts +109 -4
  85. package/src/lab/automation/orchestrator.ts +60 -4
  86. package/src/lab/events/limits.ts +1 -1
  87. package/src/lib/bounded-body.ts +55 -38
  88. package/src/lib/config-ownership.ts +11 -22
  89. package/src/lib/local-desktop-snapshot-capability.ts +56 -0
  90. package/src/lib/local-management-capability.ts +25 -3
  91. package/src/lib/optional-shutdown-hooks.ts +17 -0
  92. package/src/lib/state-store-registrations.ts +2 -2
  93. package/src/lib/windows-elevation.ts +62 -25
  94. package/src/lib/windows-secret-acl.ts +114 -23
  95. package/src/link/admission-wait.ts +73 -0
  96. package/src/link/compensation.ts +100 -0
  97. package/src/link/fingerprint.ts +21 -0
  98. package/src/link/paths.ts +19 -0
  99. package/src/link/ports.ts +12 -0
  100. package/src/link/routes.ts +32 -0
  101. package/src/link/ssh-argv.ts +170 -0
  102. package/src/link/ssh-config.ts +152 -0
  103. package/src/link/ssh-runner.ts +161 -0
  104. package/src/link/status-projection.ts +96 -0
  105. package/src/link/store.ts +139 -0
  106. package/src/link/supervisor.ts +404 -0
  107. package/src/link/tunnel-state.ts +91 -0
  108. package/src/protocols/baseline.ts +88 -0
  109. package/src/protocols/codecs/chat.ts +17 -0
  110. package/src/protocols/codecs/messages.ts +18 -0
  111. package/src/protocols/codecs/responses.ts +17 -0
  112. package/src/protocols/contract.ts +185 -0
  113. package/src/protocols/dto.ts +267 -0
  114. package/src/protocols/encoders/adapter-events.ts +876 -0
  115. package/src/protocols/encoders/chat.ts +243 -0
  116. package/src/protocols/encoders/messages.ts +462 -0
  117. package/src/protocols/envelope.ts +43 -0
  118. package/src/protocols/features.ts +295 -0
  119. package/src/protocols/guard.ts +33 -0
  120. package/src/protocols/opaque-state.ts +132 -0
  121. package/src/protocols/path.ts +39 -0
  122. package/src/protocols/plan-snapshot.ts +226 -0
  123. package/src/protocols/plan.ts +192 -0
  124. package/src/protocols/provider-summary.ts +101 -0
  125. package/src/protocols/settings.ts +118 -0
  126. package/src/protocols/shadow-plan.ts +36 -0
  127. package/src/protocols/shadow.ts +62 -0
  128. package/src/protocols/trace.ts +335 -0
  129. package/src/providers/model-rename-migration.ts +3 -3
  130. package/src/providers/registry/entries-core.ts +6 -1
  131. package/src/providers/registry/entries-extended.ts +15 -0
  132. package/src/providers/registry/model-ids.ts +1 -0
  133. package/src/providers/registry/types.ts +6 -0
  134. package/src/providers/registry.ts +1 -1
  135. package/src/remote-control/workspace-hub.ts +5 -4
  136. package/src/responses/apply-patch-envelope.ts +6 -1
  137. package/src/responses/code-mode-shell-input.ts +4 -2
  138. package/src/responses/freeform-wrapper-scan.ts +51 -24
  139. package/src/responses/progressive-freeform-input.ts +14 -20
  140. package/src/responses/spill-store.ts +187 -33
  141. package/src/responses/state/snapshot-select.ts +49 -0
  142. package/src/responses/state/spill-inspect.ts +46 -0
  143. package/src/responses/state/spill-queue.ts +16 -0
  144. package/src/responses/state.ts +51 -27
  145. package/src/server/audio-client.ts +8 -3
  146. package/src/server/audio-upstream.ts +9 -4
  147. package/src/server/auth-cors.ts +25 -8
  148. package/src/server/chat-completions.ts +71 -11
  149. package/src/server/chat-native-eligibility.ts +74 -0
  150. package/src/server/chat-native.ts +139 -74
  151. package/src/server/claude-messages.ts +140 -22
  152. package/src/server/hub-usage.ts +4 -3
  153. package/src/server/index/link-listener.ts +195 -0
  154. package/src/server/index/optional-listeners.ts +115 -0
  155. package/src/server/index/serve-options.ts +44 -10
  156. package/src/server/index.ts +10 -9
  157. package/src/server/inference/attempt.ts +51 -0
  158. package/src/server/inference/client-encoder-delivery.ts +235 -0
  159. package/src/server/inference/client-wire-log.ts +49 -0
  160. package/src/server/inference/client-wire.ts +69 -0
  161. package/src/server/inference/context.ts +15 -0
  162. package/src/server/inference/final-log.ts +37 -0
  163. package/src/server/local-desktop-snapshot-auth.ts +57 -0
  164. package/src/server/management/agent-settings-routes.ts +15 -13
  165. package/src/server/management/api-access.ts +11 -1
  166. package/src/server/management/config-routes.ts +3 -6
  167. package/src/server/management/context.ts +25 -0
  168. package/src/server/management/link-routes.ts +535 -0
  169. package/src/server/management/logs-usage-routes.ts +31 -1
  170. package/src/server/management/native-integration-routes.ts +6 -11
  171. package/src/server/management/oauth-account-routes.ts +42 -16
  172. package/src/server/management/protocol-routes.ts +159 -0
  173. package/src/server/management/protocol-settings-patch.ts +145 -0
  174. package/src/server/management/provider-patch-transaction.ts +61 -0
  175. package/src/server/management/provider-routes.ts +39 -18
  176. package/src/server/management/route-registry.ts +12 -0
  177. package/src/server/management/sidebar-routes.ts +8 -3
  178. package/src/server/management/system-routes.ts +18 -1
  179. package/src/server/management/usage-aggregate-cache.ts +206 -6
  180. package/src/server/management-api.ts +26 -1
  181. package/src/server/management-auth.ts +19 -5
  182. package/src/server/messages-native-eligibility.ts +161 -0
  183. package/src/server/messages-native-oauth.ts +149 -0
  184. package/src/server/messages-native.ts +790 -0
  185. package/src/server/relay.ts +9 -0
  186. package/src/server/request-log-filter.ts +92 -0
  187. package/src/server/request-log.ts +31 -70
  188. package/src/server/responses/adapter-delivery.ts +43 -16
  189. package/src/server/responses/agent-task-recovery.ts +90 -22
  190. package/src/server/responses/codex-ws-exchange.ts +4 -0
  191. package/src/server/responses/codex-ws-wire.ts +10 -1
  192. package/src/server/responses/core-combo-native.ts +323 -0
  193. package/src/server/responses/core-combo.ts +224 -13
  194. package/src/server/responses/core-options.ts +22 -0
  195. package/src/server/responses/core.ts +14 -26
  196. package/src/server/responses/request-sidecar-auth.ts +8 -3
  197. package/src/server/responses/run-turn-execution.ts +257 -63
  198. package/src/server/responses/sidecar-execution.ts +7 -4
  199. package/src/server/system-env.ts +7 -0
  200. package/src/service/diagnostics.ts +7 -1
  201. package/src/service/launchd.ts +26 -37
  202. package/src/service/windows-ops.ts +24 -20
  203. package/src/types/config.ts +58 -1
  204. package/src/types.ts +2 -0
  205. package/src/update/transactional-install.d.mts +0 -1
  206. package/src/update/transactional-install.mjs +17 -17
  207. package/src/usage/jev-stats.ts +495 -0
  208. package/src/usage/log.ts +24 -3
  209. package/src/web-search/run-turn-loop.ts +568 -0
  210. package/gui/dist/assets/App-8NMiZxT0.css +0 -1
  211. package/gui/dist/assets/App-CsYvvpr3.js +0 -50
  212. package/gui/dist/assets/index--EWgGQvZ.css +0 -1
  213. package/gui/dist/assets/index-BB0iHG8-.js +0 -86
@@ -0,0 +1,876 @@
1
+ /**
2
+ * AdapterEvent -> client wire, without the internal Responses SSE (PF-09).
3
+ *
4
+ * The routed Chat and Messages paths used to re-encode adapter events as Responses SSE
5
+ * (`bridgeToResponsesSSE`) and then parse that SSE again into the client's wire
6
+ * (`responsesSseToChatCompletionsSse`, `responsesSseToAnthropicSse`). This driver walks the
7
+ * adapter events with the bridge's own item state machine and, where the bridge would emit a
8
+ * Responses frame a client converter reacts to, calls the matching `ClientWireWriter` method
9
+ * instead. The writers in `chat.ts` and `messages.ts` then apply the converters' mapping, so the
10
+ * client-visible frames stay what they were.
11
+ *
12
+ * What is ported from `src/bridge/sse.ts`, and must stay in step with it:
13
+ * - item boundaries: which event closes the open message, reasoning item, tool call or search,
14
+ * the signature-update grouping, hidden-thinking and redacted envelopes, the Kiro blob;
15
+ * - tool naming (declared-name normalization, namespace mapping, freeform and tool-search
16
+ * classification), argument streaming gated on `toolCallArgumentsCouldBeJson`, the integral
17
+ * float repair on the completed arguments and the malformed-arguments failure;
18
+ * - terminals: completed, truncated `done`, incomplete, error, adapter EOF, the stall watchdog,
19
+ * translator-budget overflow and the proxy-error catch, each with the bridge's usage and
20
+ * durability rules;
21
+ * - the wire-silence heartbeat and stall ticks, pull-based stepping (one event's frames per
22
+ * pull) and cancellation, which stops the upstream exactly once.
23
+ *
24
+ * Not ported, because no Chat or Messages client observes it: Responses item ids and sequence
25
+ * numbers, message phases, url_citation annotations, freeform input rewriting and remote
26
+ * compaction (the server never encodes a compaction turn directly). Declared-tool enforcement is
27
+ * off on both wires (#4735), so it is not applied here either. Replay-cache side effects and the
28
+ * completed-response effects belong to the caller, which folds the same events with
29
+ * `buildResponseJSON` at the terminal through `beforeTerminal`.
30
+ */
31
+ import type { AdapterEvent, OcxMessagePhase, OcxProviderContinuationState, OcxUsage } from "../../types";
32
+ import { normalizeDeclaredToolName } from "../../types";
33
+ import { coerceIntegerToolArguments } from "../../lib/tool-argument-integers";
34
+ import { classifyError, isCyberPolicyCode, type OcxErrorPayload } from "../../lib/errors";
35
+ import { redactSecretString } from "../../lib/redact";
36
+ import { isTranslatorBudgetExceededError, type TranslatorBudget } from "../../lib/translator-budget";
37
+ import { createCitationMarkerFilter, type CitationMarkerFilter } from "../../responses/citation-markers";
38
+ import { isTruncatedStopReason, truncationReasonFor } from "../../responses/truncated-stop-reason";
39
+ import { safeWebSearchSources } from "../../web-search/sources";
40
+ import { resolveStallTimeoutSec } from "../../stall-timeout";
41
+ import type { RelayedEventObservation } from "../../usage/attempt-delivery";
42
+ import {
43
+ adapterFailureFromEvent,
44
+ toolCallArgumentsCouldBeJson,
45
+ toolCallArgumentsUsable,
46
+ uuid,
47
+ } from "../../bridge/internal";
48
+
49
+ export type ClientToolKind = "function" | "custom" | "tool_search";
50
+
51
+ export interface ClientToolCall {
52
+ itemId: string;
53
+ callId: string;
54
+ /** Request-visible name: declared-name normalized, namespace mapping applied. */
55
+ name: string;
56
+ kind: ClientToolKind;
57
+ }
58
+
59
+ export interface ClientWebSearch {
60
+ itemId: string;
61
+ status: "completed" | "failed";
62
+ queries: string[];
63
+ sources: { url: string; title?: string }[];
64
+ }
65
+
66
+ /**
67
+ * How the turn ended, in the shape the bridge's terminal Responses frame had. The caller builds
68
+ * its request-log view from it; the writers map it to the client's finish/stop reason.
69
+ */
70
+ export interface EncodedTerminal {
71
+ status: "completed" | "incomplete" | "failed";
72
+ usage?: OcxUsage;
73
+ /** Whether the bridged terminal carried a usage object (`usage: null` otherwise). */
74
+ usageOnWire: boolean;
75
+ endTurn?: boolean;
76
+ incomplete?: { reason: string; message?: string; retryable?: boolean };
77
+ error?: OcxErrorPayload;
78
+ retryable?: boolean;
79
+ /** Translator-budget overflow: one bounded error frame and nothing else. */
80
+ overflow?: true;
81
+ /** A `done` event: the bridge handed this response to `onCompletedResponse`. */
82
+ completedResponse: boolean;
83
+ providerState?: OcxProviderContinuationState;
84
+ /** Whether the bridge reported `usage` through `onUsage` for this terminal. */
85
+ reportUsage: boolean;
86
+ /** Whether the bridge awaited thought-signature durability before this terminal. */
87
+ durable: boolean;
88
+ }
89
+
90
+ /** Frame output owned by the driver: budget reservation, delivery release and relay counts. */
91
+ export interface ClientFrameSink {
92
+ readonly budget: TranslatorBudget;
93
+ emit(text: string, observation?: RelayedEventObservation): void;
94
+ /** Admit every frame before exposing any of them. */
95
+ emitBatch(frames: { text: string; observation?: RelayedEventObservation }[]): void;
96
+ /** A fixed, bounded frame that must reach the client even when the budget is exhausted. */
97
+ emitBounded(text: string): void;
98
+ /** A transport keepalive; it counts as neither wire activity nor a relayed event. */
99
+ emitKeepalive(text: string): void;
100
+ desiredSize(): number;
101
+ }
102
+
103
+ /** One method per Responses frame a Chat or Messages converter reacts to. */
104
+ export interface ClientWireWriter {
105
+ start(): void;
106
+ /** Wire silence while awaiting upstream (the bridge's heartbeat frame). */
107
+ heartbeat(): void;
108
+ text(delta: string): void;
109
+ messageDone(): void;
110
+ reasoning(delta: string, itemId: string, channel: "summary" | "raw"): void;
111
+ reasoningDone(item: { itemId: string; signature?: string; redacted?: string[] }): void;
112
+ toolStart(call: ClientToolCall): void;
113
+ toolArgsDelta(itemId: string, delta: string): void;
114
+ toolArgsDone(itemId: string, args: string): void;
115
+ toolDone(call: ClientToolCall & { arguments: string; status: "completed" | "incomplete" }): void;
116
+ webSearchDone(search: ClientWebSearch): void;
117
+ terminal(terminal: EncodedTerminal): void;
118
+ overflow(): void;
119
+ /** Client cancellation: release anything the writer still holds. */
120
+ dispose(): void;
121
+ }
122
+
123
+ export interface ClientEncodeHooks {
124
+ /** First non-empty text or reasoning delta (TTFT), as the bridge reports it. */
125
+ onFirstOutput?(): void;
126
+ /** Before the terminal frames: the caller's fold, durability barrier and effects. */
127
+ beforeTerminal?(terminal: EncodedTerminal): void | Promise<void>;
128
+ /** After the terminal frames were admitted; called at most once. */
129
+ afterTerminal?(terminal: EncodedTerminal): void;
130
+ /** The bridge's `onCancel`: at the terminal and on client cancel, at most once. */
131
+ stopUpstream?(): void;
132
+ onClientCancel?(): void;
133
+ onRelayed?(observation: RelayedEventObservation): void;
134
+ }
135
+
136
+ export interface AdapterEventEncodeOptions {
137
+ translatorBudget: TranslatorBudget;
138
+ hideThinkingSummary?: boolean;
139
+ toolNsMap?: ReadonlyMap<string, { namespace: string; name: string; freeform?: true }>;
140
+ declaredToolNames?: ReadonlySet<string>;
141
+ toolParameterSchemas?: ReadonlyMap<string, Record<string, unknown>>;
142
+ freeformToolNames?: ReadonlySet<string>;
143
+ toolSearchToolNames?: ReadonlySet<string>;
144
+ stallTimeoutSec?: number;
145
+ /** Wire-silence heartbeat and stall tick; the bridge's 2 s default. */
146
+ heartbeatMs?: number;
147
+ hooks?: ClientEncodeHooks;
148
+ /** Test seam for the beat and keepalive timers. */
149
+ timers?: {
150
+ setInterval: (handler: () => void, ms: number) => unknown;
151
+ clearInterval: (id: unknown) => void;
152
+ };
153
+ }
154
+
155
+ type OpenToolCall = ClientToolCall & { args: string; argsBytes: number; namespace?: string };
156
+
157
+ /** Drive `events` through `writer` as a pull-based byte stream. */
158
+ export function encodeAdapterEventStream(
159
+ events: AsyncIterable<AdapterEvent>,
160
+ createWriter: (sink: ClientFrameSink) => ClientWireWriter,
161
+ options: AdapterEventEncodeOptions & { keepaliveMs?: number },
162
+ ): ReadableStream<Uint8Array> {
163
+ const budget = options.translatorBudget;
164
+ const hooks = options.hooks ?? {};
165
+ const heartbeatMs = options.heartbeatMs ?? 2_000;
166
+ const setTimer = options.timers?.setInterval ?? ((handler: () => void, ms: number) => setInterval(handler, ms));
167
+ const clearTimer = options.timers?.clearInterval ?? ((id: unknown) => clearInterval(id as ReturnType<typeof setInterval>));
168
+ const maxStallTicks = Math.ceil((resolveStallTimeoutSec(options.stallTimeoutSec) * 1000) / heartbeatMs);
169
+ const textEncoder = new TextEncoder();
170
+
171
+ let controller!: ReadableStreamDefaultController<Uint8Array>;
172
+ let closed = false;
173
+ let clientCancelled = false;
174
+ let terminated = false;
175
+ let terminalSettled = false;
176
+ let stepping = false;
177
+ let gated = false;
178
+ let emittedFrames = 0;
179
+ let upstreamActivity = false;
180
+ let wireActivity = false;
181
+ let stallTicks = 0;
182
+ let beat: unknown;
183
+ let keepalive: unknown;
184
+ const queuedFrameBytes: number[] = [];
185
+
186
+ const relayed = (observation?: RelayedEventObservation) => {
187
+ try { hooks.onRelayed?.(observation ?? {}); } catch { /* counters never break the stream */ }
188
+ };
189
+ const enqueueCharged = (text: string, observation?: RelayedEventObservation, activity = true): void => {
190
+ if (closed) return;
191
+ const frame = textEncoder.encode(text);
192
+ const reservation = budget.reserveTransient(frame.byteLength, { kind: "live_transient" });
193
+ try {
194
+ controller.enqueue(frame);
195
+ } catch {
196
+ reservation.release();
197
+ closed = true;
198
+ return;
199
+ }
200
+ reservation.commitRetained();
201
+ queuedFrameBytes.push(frame.byteLength);
202
+ emittedFrames++;
203
+ if (activity) wireActivity = true;
204
+ // A keepalive is transport liveness, not a relayed event: the bridge enqueues its heartbeat
205
+ // outside the relay counter, and the converters' keepalives never reach it either.
206
+ if (activity) relayed(observation);
207
+ };
208
+ const sink: ClientFrameSink = {
209
+ budget,
210
+ emit: (text, observation) => enqueueCharged(text, observation),
211
+ emitBatch: frames => {
212
+ if (closed) return;
213
+ const staged: { frame: Uint8Array; reservation: ReturnType<TranslatorBudget["reserveTransient"]>; observation?: RelayedEventObservation }[] = [];
214
+ try {
215
+ for (const { text, observation } of frames) {
216
+ const frame = textEncoder.encode(text);
217
+ staged.push({ frame, reservation: budget.reserveTransient(frame.byteLength, { kind: "live_transient" }), ...(observation ? { observation } : {}) });
218
+ }
219
+ } catch (error) {
220
+ for (const entry of staged) entry.reservation.release();
221
+ throw error;
222
+ }
223
+ for (const entry of staged) {
224
+ if (closed) { entry.reservation.release(); continue; }
225
+ try {
226
+ controller.enqueue(entry.frame);
227
+ } catch {
228
+ entry.reservation.release();
229
+ closed = true;
230
+ continue;
231
+ }
232
+ entry.reservation.commitRetained();
233
+ queuedFrameBytes.push(entry.frame.byteLength);
234
+ emittedFrames++;
235
+ relayed(entry.observation);
236
+ }
237
+ wireActivity = true;
238
+ },
239
+ emitBounded: text => {
240
+ if (closed) return;
241
+ try {
242
+ controller.enqueue(textEncoder.encode(text));
243
+ emittedFrames++;
244
+ relayed({ terminal: true });
245
+ } catch {
246
+ closed = true;
247
+ }
248
+ },
249
+ emitKeepalive: text => enqueueCharged(text, undefined, false),
250
+ desiredSize: () => controller.desiredSize ?? 0,
251
+ };
252
+ const writer = createWriter(sink);
253
+
254
+ const releaseDeliveredFrame = () => {
255
+ const bytes = queuedFrameBytes.shift();
256
+ if (bytes !== undefined) budget.releaseRetained(bytes, { kind: "live_transient" });
257
+ };
258
+ const stopTimers = () => {
259
+ if (beat !== undefined) clearTimer(beat);
260
+ if (keepalive !== undefined) clearTimer(keepalive);
261
+ beat = undefined;
262
+ keepalive = undefined;
263
+ };
264
+ const closeController = () => {
265
+ if (closed) return;
266
+ closed = true;
267
+ try { controller.close(); } catch { /* already closed */ }
268
+ };
269
+
270
+ // Iterator ownership, as in the bridge: a return() before the first next() never enters the
271
+ // generator, so its finally blocks could not cancel a prepared upstream body.
272
+ const it = events[Symbol.asyncIterator]();
273
+ let iteratorStarted = false;
274
+ let iteratorReturned = false;
275
+ let upstreamDone = false;
276
+ const returnIterator = () => {
277
+ if (iteratorReturned) return;
278
+ iteratorReturned = true;
279
+ const finishReturn = () => {
280
+ try { void it.return?.()?.catch(() => {}); } catch { /* best-effort cleanup */ }
281
+ };
282
+ if (!iteratorStarted) {
283
+ iteratorStarted = true;
284
+ try { void it.next().then(finishReturn, () => {}).catch(() => {}); } catch { /* best-effort */ }
285
+ return;
286
+ }
287
+ finishReturn();
288
+ };
289
+ let upstreamStopped = false;
290
+ const stopUpstreamOnce = () => {
291
+ if (upstreamStopped) return;
292
+ upstreamStopped = true;
293
+ try { hooks.stopUpstream?.(); } catch { /* cancellation must not strand the client stream */ }
294
+ returnIterator();
295
+ };
296
+ const settleTerminal = (terminal: EncodedTerminal) => {
297
+ if (terminalSettled || clientCancelled) return;
298
+ terminalSettled = true;
299
+ try { hooks.afterTerminal?.(terminal); } catch { /* terminal bookkeeping never breaks the stream */ }
300
+ };
301
+ /** Terminal paths the bridge runs synchronously (stall, overflow): never awaited. */
302
+ const runBeforeTerminalSync = (terminal: EncodedTerminal) => {
303
+ try { void Promise.resolve(hooks.beforeTerminal?.(terminal)).catch(() => {}); } catch { /* best-effort */ }
304
+ };
305
+
306
+ let firstOutputReported = false;
307
+ const reportFirstOutput = (event: AdapterEvent) => {
308
+ if (firstOutputReported) return;
309
+ const nonEmpty = event.type === "text_delta" ? event.text.length > 0
310
+ : event.type === "thinking_delta" ? event.thinking.length > 0
311
+ : event.type === "reasoning_raw_delta" ? event.text.length > 0
312
+ : false;
313
+ if (!nonEmpty) return;
314
+ firstOutputReported = true;
315
+ try { hooks.onFirstOutput?.(); } catch { /* metrics must not break the stream */ }
316
+ };
317
+
318
+ // Item state, ported from bridgeToResponsesSSE.
319
+ let currentMsg: { phase?: OcxMessagePhase; citationFilter: CitationMarkerFilter } | null = null;
320
+ let currentReasoning: { itemId: string } | null = null;
321
+ let currentRawReasoning: { itemId: string } | null = null;
322
+ let pendingSignature: string | undefined;
323
+ let pendingRedacted: string[] = [];
324
+ let hiddenRawBytes = 0;
325
+ let pendingKiroRedacted: string | undefined;
326
+ let currentToolCall: OpenToolCall | null = null;
327
+ let currentWebSearch: { itemId: string; eventId: string } | null = null;
328
+
329
+ const takeReasoningEnvelope = (): { signature?: string; redacted?: string[] } | undefined => {
330
+ if (!pendingSignature && pendingRedacted.length === 0) return undefined;
331
+ const envelope = {
332
+ ...(pendingSignature ? { signature: pendingSignature } : {}),
333
+ ...(pendingRedacted.length > 0 ? { redacted: pendingRedacted } : {}),
334
+ };
335
+ pendingSignature = undefined;
336
+ pendingRedacted = [];
337
+ return envelope;
338
+ };
339
+ const closeCurrentMessage = () => {
340
+ if (!currentMsg) return;
341
+ // A citation span can straddle a delta boundary; the filter releases its held tail here.
342
+ const trailing = currentMsg.citationFilter.flush();
343
+ if (trailing) writer.text(trailing);
344
+ writer.messageDone();
345
+ currentMsg = null;
346
+ };
347
+ const closeCurrentReasoning = () => {
348
+ if (!currentReasoning) return;
349
+ const envelope = takeReasoningEnvelope();
350
+ writer.reasoningDone({ itemId: currentReasoning.itemId, ...envelope });
351
+ currentReasoning = null;
352
+ };
353
+ const closeCurrentRawReasoning = () => {
354
+ if (!currentRawReasoning) return;
355
+ writer.reasoningDone({ itemId: currentRawReasoning.itemId });
356
+ currentRawReasoning = null;
357
+ };
358
+ // hideThinkingSummary: a signed or redacted block still round-trips as an envelope-only item.
359
+ const flushHiddenReasoningEnvelope = () => {
360
+ const envelope = takeReasoningEnvelope();
361
+ if (!envelope) return;
362
+ writer.reasoningDone({ itemId: `rs_${uuid()}`, ...envelope });
363
+ };
364
+ const flushHiddenRawReasoning = () => {
365
+ if (hiddenRawBytes === 0) return;
366
+ hiddenRawBytes = 0;
367
+ writer.reasoningDone({ itemId: `rs_${uuid()}` });
368
+ };
369
+ // Kiro's blob lands after every item `done` closed, never on arrival (see the bridge).
370
+ const flushKiroRedactedReasoning = () => {
371
+ if (!pendingKiroRedacted) return;
372
+ pendingKiroRedacted = undefined;
373
+ writer.reasoningDone({ itemId: `rs_${uuid()}` });
374
+ };
375
+ const closeCurrentToolCall = () => {
376
+ if (!currentToolCall) return;
377
+ const call = currentToolCall;
378
+ // Empty input serializes as "{}"; integral floats are repaired against the schema (#1611).
379
+ const argsStr = coerceIntegerToolArguments(
380
+ call.args || "{}",
381
+ options.toolParameterSchemas?.get(call.name),
382
+ call.namespace === undefined ? call.name : undefined,
383
+ );
384
+ if (call.kind === "function") writer.toolArgsDone(call.itemId, argsStr);
385
+ writer.toolDone({
386
+ itemId: call.itemId, callId: call.callId, name: call.name, kind: call.kind,
387
+ arguments: call.kind === "function" ? argsStr : call.args,
388
+ status: "completed",
389
+ });
390
+ budget.closeCall(call.callId);
391
+ currentToolCall = null;
392
+ };
393
+ // Failed/incomplete terminal with an open call: no arguments-done frame, status incomplete.
394
+ const failCurrentToolCall = () => {
395
+ if (!currentToolCall) return;
396
+ const call = currentToolCall;
397
+ writer.toolDone({
398
+ itemId: call.itemId, callId: call.callId, name: call.name, kind: call.kind,
399
+ arguments: call.args || "{}",
400
+ status: "incomplete",
401
+ });
402
+ budget.closeCall(call.callId);
403
+ currentToolCall = null;
404
+ };
405
+ const closeCurrentWebSearch = (status: "completed" | "failed", queries: string[], sources?: { url: string; title?: string }[]) => {
406
+ if (!currentWebSearch) return;
407
+ writer.webSearchDone({ itemId: currentWebSearch.itemId, status, queries, sources: sources ?? [] });
408
+ currentWebSearch = null;
409
+ };
410
+ const appendToolArgs = (call: OpenToolCall, fragment: string) => {
411
+ const nextBytes = call.argsBytes + Buffer.byteLength(fragment);
412
+ const scope = { kind: "tool_args" as const, ...(call.callId ? { callId: call.callId } : {}) };
413
+ const reservation = budget.reserveTransient(nextBytes, scope);
414
+ try {
415
+ const value = call.args + fragment;
416
+ reservation.commitRetained();
417
+ budget.releaseRetained(call.argsBytes, scope);
418
+ call.args = value;
419
+ call.argsBytes = nextBytes;
420
+ } catch (error) {
421
+ reservation.release();
422
+ throw error;
423
+ }
424
+ };
425
+ const closeOpenItemsForTermination = (closeMessage: boolean) => {
426
+ if (closeMessage) {
427
+ if (currentMsg) closeCurrentMessage();
428
+ if (currentReasoning) closeCurrentReasoning();
429
+ if (currentRawReasoning) closeCurrentRawReasoning();
430
+ }
431
+ flushHiddenRawReasoning();
432
+ if (currentToolCall) failCurrentToolCall();
433
+ if (currentWebSearch) closeCurrentWebSearch("failed", []);
434
+ };
435
+
436
+ let handlingOverflow = false;
437
+ const terminateForOverflow = () => {
438
+ if (handlingOverflow || terminated || clientCancelled || closed) return;
439
+ handlingOverflow = true;
440
+ if (currentToolCall) {
441
+ budget.closeCall(currentToolCall.callId);
442
+ currentToolCall = null;
443
+ }
444
+ currentWebSearch = null;
445
+ const terminal: EncodedTerminal = {
446
+ status: "failed",
447
+ error: adapterFailureFromEvent({
448
+ type: "error",
449
+ status: 502,
450
+ errorType: "upstream_error",
451
+ code: "translation_buffer_limit",
452
+ message: "upstream translation buffer exceeded the safe limit",
453
+ }).error,
454
+ usageOnWire: false,
455
+ overflow: true,
456
+ completedResponse: false,
457
+ reportUsage: false,
458
+ durable: false,
459
+ };
460
+ runBeforeTerminalSync(terminal);
461
+ try { writer.overflow(); } catch { /* the bounded frame is best-effort once the client is gone */ }
462
+ settleTerminal(terminal);
463
+ terminated = true;
464
+ stopUpstreamOnce();
465
+ stopTimers();
466
+ closeController();
467
+ gated = true;
468
+ stepping = false;
469
+ };
470
+ /** The bridge's attemptTerminationCleanup: false when an overflow already ended the stream. */
471
+ const cleanupForTermination = (action: () => void): boolean => {
472
+ try {
473
+ action();
474
+ return !terminated && !closed;
475
+ } catch (error) {
476
+ if (!isTranslatorBudgetExceededError(error)) throw error;
477
+ terminateForOverflow();
478
+ return false;
479
+ }
480
+ };
481
+ const deliverTerminal = async (terminal: EncodedTerminal): Promise<void> => {
482
+ await hooks.beforeTerminal?.(terminal);
483
+ writer.terminal(terminal);
484
+ settleTerminal(terminal);
485
+ };
486
+ /** Terminal delivery outside the step try: an overflow while writing it ends the stream. */
487
+ const deliverTerminalGuarded = async (terminal: EncodedTerminal): Promise<boolean> => {
488
+ try {
489
+ await deliverTerminal(terminal);
490
+ return true;
491
+ } catch (error) {
492
+ if (isTranslatorBudgetExceededError(error)) terminateForOverflow();
493
+ return false;
494
+ }
495
+ };
496
+
497
+ const stall = () => {
498
+ if (!cleanupForTermination(() => closeOpenItemsForTermination(true))) return;
499
+ // Synchronous, like the bridge's beat callback: no durability barrier on this kill path.
500
+ const terminal: EncodedTerminal = {
501
+ status: "incomplete",
502
+ incomplete: { reason: "upstream_stall_timeout" },
503
+ usageOnWire: false,
504
+ completedResponse: false,
505
+ reportUsage: false,
506
+ durable: false,
507
+ };
508
+ runBeforeTerminalSync(terminal);
509
+ try {
510
+ writer.terminal(terminal);
511
+ } catch (error) {
512
+ if (isTranslatorBudgetExceededError(error)) terminateForOverflow();
513
+ return;
514
+ }
515
+ settleTerminal(terminal);
516
+ stopUpstreamOnce();
517
+ terminated = true;
518
+ stopTimers();
519
+ closeController();
520
+ };
521
+
522
+ const step = async (): Promise<void> => {
523
+ if (stepping || closed) return;
524
+ stepping = true;
525
+ gated = false;
526
+ const emittedAtStart = emittedFrames;
527
+ try {
528
+ while (!terminated && !closed && emittedFrames === emittedAtStart) {
529
+ iteratorStarted = true;
530
+ const next = await it.next();
531
+ // A cancel during the await must not process or charge a late event.
532
+ if (closed || clientCancelled) {
533
+ gated = true;
534
+ stepping = false;
535
+ return;
536
+ }
537
+ if (next.done) { upstreamDone = true; break; }
538
+ const event = next.value;
539
+ let terminal: EncodedTerminal | undefined;
540
+ upstreamActivity = true;
541
+ stallTicks = 0;
542
+ if (event.type !== "heartbeat" && event.type !== "thinking_signature" && event.type !== "kiro_redacted_reasoning") {
543
+ wireActivity = true;
544
+ }
545
+ reportFirstOutput(event);
546
+ // A signature update belongs to the current thinking block; the next semantic event
547
+ // closes that block (or flushes the hidden envelope).
548
+ if (pendingSignature !== undefined && event.type !== "thinking_signature" && event.type !== "heartbeat") {
549
+ if (currentReasoning) closeCurrentReasoning();
550
+ else flushHiddenReasoningEnvelope();
551
+ }
552
+ switch (event.type) {
553
+ case "assistant_boundary": {
554
+ if (currentMsg) closeCurrentMessage();
555
+ if (currentReasoning) closeCurrentReasoning();
556
+ if (currentRawReasoning) closeCurrentRawReasoning();
557
+ flushHiddenRawReasoning();
558
+ if (currentToolCall) closeCurrentToolCall();
559
+ flushHiddenReasoningEnvelope();
560
+ break;
561
+ }
562
+ case "text_delta": {
563
+ if (currentReasoning) closeCurrentReasoning();
564
+ if (currentRawReasoning) closeCurrentRawReasoning();
565
+ flushHiddenRawReasoning();
566
+ if (currentToolCall) closeCurrentToolCall();
567
+ // Only an explicit phase change starts a new message.
568
+ if (currentMsg && event.phase !== undefined && currentMsg.phase !== event.phase) closeCurrentMessage();
569
+ if (!currentMsg) {
570
+ currentMsg = { citationFilter: createCitationMarkerFilter(), ...(event.phase ? { phase: event.phase } : {}) };
571
+ }
572
+ const visible = currentMsg.citationFilter.push(event.text);
573
+ if (visible) writer.text(visible);
574
+ break;
575
+ }
576
+ case "thinking_delta": {
577
+ if (options.hideThinkingSummary) {
578
+ flushHiddenRawReasoning();
579
+ break;
580
+ }
581
+ if (currentMsg) closeCurrentMessage();
582
+ if (currentRawReasoning) closeCurrentRawReasoning();
583
+ flushHiddenRawReasoning();
584
+ if (currentToolCall) closeCurrentToolCall();
585
+ if (!currentReasoning) currentReasoning = { itemId: `rs_${uuid()}` };
586
+ writer.reasoning(event.thinking, currentReasoning.itemId, "summary");
587
+ break;
588
+ }
589
+ case "thinking_signature": {
590
+ pendingSignature = event.signature;
591
+ break;
592
+ }
593
+ case "redacted_thinking": {
594
+ if (currentMsg) closeCurrentMessage();
595
+ if (currentReasoning) closeCurrentReasoning();
596
+ if (currentRawReasoning) closeCurrentRawReasoning();
597
+ flushHiddenRawReasoning();
598
+ if (currentToolCall) closeCurrentToolCall();
599
+ pendingRedacted.push(event.data);
600
+ // A redacted block is complete on arrival.
601
+ flushHiddenReasoningEnvelope();
602
+ break;
603
+ }
604
+ case "kiro_redacted_reasoning": {
605
+ pendingKiroRedacted = event.data;
606
+ break;
607
+ }
608
+ case "reasoning_raw_delta": {
609
+ if (options.hideThinkingSummary) {
610
+ hiddenRawBytes += Buffer.byteLength(event.text);
611
+ break;
612
+ }
613
+ if (currentMsg) closeCurrentMessage();
614
+ if (currentReasoning) closeCurrentReasoning();
615
+ if (currentToolCall) closeCurrentToolCall();
616
+ if (!currentRawReasoning) currentRawReasoning = { itemId: `rs_${uuid()}` };
617
+ writer.reasoning(event.text, currentRawReasoning.itemId, "raw");
618
+ break;
619
+ }
620
+ case "tool_call_start": {
621
+ if (currentMsg) closeCurrentMessage();
622
+ if (currentReasoning) closeCurrentReasoning();
623
+ if (currentRawReasoning) closeCurrentRawReasoning();
624
+ flushHiddenRawReasoning();
625
+ if (currentToolCall) closeCurrentToolCall();
626
+ const effectiveName = normalizeDeclaredToolName(event.name, options.declaredToolNames);
627
+ const mapped = options.toolNsMap?.get(effectiveName);
628
+ const name = mapped?.name ?? effectiveName;
629
+ const toolSearch = options.toolSearchToolNames?.has(name) ?? false;
630
+ const freeform = !toolSearch && (mapped
631
+ ? mapped.freeform === true
632
+ : (options.freeformToolNames?.has(name) ?? false));
633
+ const kind: ClientToolKind = toolSearch ? "tool_search" : freeform ? "custom" : "function";
634
+ const itemId = `${toolSearch ? "tsc" : freeform ? "ctc" : "fc"}_${uuid()}`;
635
+ const call: ClientToolCall = { itemId, callId: event.id, name, kind };
636
+ writer.toolStart(call);
637
+ currentToolCall = { ...call, args: "", argsBytes: 0, ...(mapped ? { namespace: mapped.namespace } : {}) };
638
+ budget.openCall(event.id);
639
+ break;
640
+ }
641
+ case "tool_call_delta": {
642
+ if (!currentToolCall) break;
643
+ appendToolArgs(currentToolCall, event.arguments);
644
+ // Hold fragments whose buffer can never parse as JSON (#765).
645
+ if (currentToolCall.kind === "function" && toolCallArgumentsCouldBeJson(currentToolCall.args)) {
646
+ writer.toolArgsDelta(currentToolCall.itemId, event.arguments);
647
+ }
648
+ break;
649
+ }
650
+ case "tool_call_end": {
651
+ // Streamed fragments cannot be repaired: unusable arguments fail the turn.
652
+ if (currentToolCall && currentToolCall.kind === "function" && !toolCallArgumentsUsable(currentToolCall.args)) {
653
+ failCurrentToolCall();
654
+ terminal = {
655
+ status: "failed",
656
+ error: classifyError(502, "upstream_error", "upstream stream produced malformed tool call arguments"),
657
+ usageOnWire: false,
658
+ completedResponse: false,
659
+ reportUsage: false,
660
+ durable: false,
661
+ };
662
+ break;
663
+ }
664
+ closeCurrentToolCall();
665
+ break;
666
+ }
667
+ case "web_search_call_begin": {
668
+ if (currentMsg) closeCurrentMessage();
669
+ if (currentReasoning) closeCurrentReasoning();
670
+ if (currentRawReasoning) closeCurrentRawReasoning();
671
+ flushHiddenRawReasoning();
672
+ if (currentToolCall) closeCurrentToolCall();
673
+ if (currentWebSearch) closeCurrentWebSearch("completed", []);
674
+ currentWebSearch = { itemId: `ws_${uuid()}`, eventId: event.id };
675
+ break;
676
+ }
677
+ case "web_search_call_end": {
678
+ if (!currentWebSearch || currentWebSearch.eventId !== event.id) {
679
+ if (currentWebSearch) closeCurrentWebSearch("completed", []);
680
+ currentWebSearch = { itemId: `ws_${uuid()}`, eventId: event.id };
681
+ }
682
+ closeCurrentWebSearch(event.status ?? "completed", event.queries, safeWebSearchSources(event.sources));
683
+ break;
684
+ }
685
+ case "done": {
686
+ const truncated = isTruncatedStopReason(event.stopReason);
687
+ if (currentMsg) closeCurrentMessage();
688
+ if (currentReasoning) closeCurrentReasoning();
689
+ if (currentRawReasoning) closeCurrentRawReasoning();
690
+ flushHiddenRawReasoning();
691
+ if (currentToolCall) {
692
+ if (truncated) failCurrentToolCall();
693
+ else closeCurrentToolCall();
694
+ }
695
+ if (currentWebSearch) closeCurrentWebSearch(truncated ? "failed" : "completed", []);
696
+ flushHiddenReasoningEnvelope();
697
+ flushKiroRedactedReasoning();
698
+ const truncation = truncationReasonFor(event.stopReason);
699
+ terminal = {
700
+ status: truncation ? "incomplete" : "completed",
701
+ ...(truncation ? { incomplete: { reason: truncation } } : {}),
702
+ ...(event.usage ? { usage: event.usage } : {}),
703
+ usageOnWire: true,
704
+ ...(event.endTurn !== undefined ? { endTurn: event.endTurn } : {}),
705
+ completedResponse: true,
706
+ ...(event.providerState ? { providerState: event.providerState } : {}),
707
+ reportUsage: true,
708
+ durable: true,
709
+ };
710
+ break;
711
+ }
712
+ case "incomplete": {
713
+ if (currentMsg) closeCurrentMessage();
714
+ if (currentReasoning) closeCurrentReasoning();
715
+ if (currentRawReasoning) closeCurrentRawReasoning();
716
+ flushHiddenRawReasoning();
717
+ if (currentToolCall) failCurrentToolCall();
718
+ if (currentWebSearch) closeCurrentWebSearch("failed", []);
719
+ flushHiddenReasoningEnvelope();
720
+ terminal = {
721
+ status: "incomplete",
722
+ incomplete: {
723
+ reason: event.reason,
724
+ ...(event.message ? { message: event.message } : {}),
725
+ ...(event.retryable !== undefined ? { retryable: event.retryable } : {}),
726
+ },
727
+ ...(event.usage ? { usage: event.usage } : {}),
728
+ usageOnWire: true,
729
+ ...(event.endTurn !== undefined ? { endTurn: event.endTurn } : {}),
730
+ completedResponse: false,
731
+ reportUsage: true,
732
+ durable: true,
733
+ };
734
+ break;
735
+ }
736
+ case "error": {
737
+ if (event.code === "translation_buffer_limit") {
738
+ terminateForOverflow();
739
+ return;
740
+ }
741
+ if (currentMsg) closeCurrentMessage();
742
+ if (currentReasoning) closeCurrentReasoning();
743
+ if (currentRawReasoning) closeCurrentRawReasoning();
744
+ flushHiddenRawReasoning();
745
+ if (currentToolCall) failCurrentToolCall();
746
+ if (currentWebSearch) closeCurrentWebSearch("failed", []);
747
+ const failure = adapterFailureFromEvent(event);
748
+ const retryable = isCyberPolicyCode(failure.error.code) ? false : event.retryable;
749
+ terminal = {
750
+ status: "failed",
751
+ error: failure.error,
752
+ ...(event.usage ? { usage: event.usage } : {}),
753
+ usageOnWire: event.usage !== undefined,
754
+ ...(retryable !== undefined ? { retryable } : {}),
755
+ completedResponse: false,
756
+ reportUsage: event.usage !== undefined,
757
+ durable: true,
758
+ };
759
+ break;
760
+ }
761
+ default:
762
+ break;
763
+ }
764
+ if (terminal) {
765
+ await deliverTerminal(terminal);
766
+ stopUpstreamOnce();
767
+ terminated = true;
768
+ break;
769
+ }
770
+ }
771
+ } catch (err) {
772
+ if (isTranslatorBudgetExceededError(err)) {
773
+ terminateForOverflow();
774
+ return;
775
+ }
776
+ if (!terminated && !closed) {
777
+ if (!cleanupForTermination(() => closeOpenItemsForTermination(false))) return;
778
+ const failure = classifyError(500, "proxy_error", redactSecretString(err instanceof Error ? err.message : String(err)));
779
+ const terminal: EncodedTerminal = {
780
+ status: "failed",
781
+ error: failure,
782
+ usageOnWire: false,
783
+ ...(isCyberPolicyCode(failure.code) ? { retryable: false } : {}),
784
+ completedResponse: false,
785
+ reportUsage: false,
786
+ durable: false,
787
+ };
788
+ try {
789
+ if (!await deliverTerminalGuarded(terminal)) return;
790
+ } catch { /* a failing hook on the failure path must not throw into the pull */ }
791
+ stopUpstreamOnce();
792
+ terminated = true;
793
+ }
794
+ }
795
+
796
+ if (!terminated && !upstreamDone) {
797
+ gated = true;
798
+ stepping = false;
799
+ return;
800
+ }
801
+ stopTimers();
802
+ if (!terminated && !closed) {
803
+ // The adapter generator ended without a terminal: a truncated stream, never a success.
804
+ if (!cleanupForTermination(() => closeOpenItemsForTermination(true))) return;
805
+ const terminal: EncodedTerminal = {
806
+ status: "incomplete",
807
+ incomplete: { reason: "adapter_eof" },
808
+ usageOnWire: true,
809
+ completedResponse: false,
810
+ reportUsage: true,
811
+ durable: true,
812
+ };
813
+ try {
814
+ if (!await deliverTerminalGuarded(terminal)) return;
815
+ } catch { /* see the catch path above */ }
816
+ terminated = true;
817
+ }
818
+ closeController();
819
+ gated = true;
820
+ stepping = false;
821
+ };
822
+
823
+ return new ReadableStream<Uint8Array>({
824
+ start(streamController) {
825
+ controller = streamController;
826
+ try {
827
+ writer.start();
828
+ } catch (error) {
829
+ if (isTranslatorBudgetExceededError(error)) terminateForOverflow();
830
+ else throw error;
831
+ }
832
+ // Default HWM=1: one event's frames fill the queue, then stepping pauses until demand.
833
+ gated = true;
834
+ beat = setTimer(() => {
835
+ if (closed || gated) return;
836
+ if (upstreamActivity) {
837
+ upstreamActivity = false;
838
+ stallTicks = 0;
839
+ } else if (++stallTicks >= maxStallTicks) {
840
+ stall();
841
+ return;
842
+ }
843
+ // Wire silence is independent of invisible upstream heartbeats.
844
+ if (wireActivity) {
845
+ wireActivity = false;
846
+ return;
847
+ }
848
+ try { writer.heartbeat(); } catch { /* a keepalive never fails the stream */ }
849
+ }, heartbeatMs);
850
+ if (options.keepaliveMs !== undefined && options.keepaliveMs > 0) {
851
+ keepalive = setTimer(() => {
852
+ if (terminated || closed) return;
853
+ try { writer.heartbeat(); } catch { /* the read loop is ending anyway */ }
854
+ }, options.keepaliveMs);
855
+ }
856
+ },
857
+ pull() {
858
+ releaseDeliveredFrame();
859
+ return step();
860
+ },
861
+ cancel() {
862
+ // The client disconnected: stop emitting and stop the upstream turn (RC2).
863
+ clientCancelled = true;
864
+ closed = true;
865
+ stopTimers();
866
+ stopUpstreamOnce();
867
+ while (queuedFrameBytes.length > 0) releaseDeliveredFrame();
868
+ if (currentToolCall) {
869
+ budget.closeCall(currentToolCall.callId);
870
+ currentToolCall = null;
871
+ }
872
+ try { writer.dispose(); } catch { /* best-effort release */ }
873
+ try { hooks.onClientCancel?.(); } catch { /* bookkeeping never throws into cancel */ }
874
+ },
875
+ });
876
+ }