@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
@@ -1,4 +1,5 @@
1
1
  import type { ResponsesTerminalStatus } from "../bridge";
2
+ import { isTranslatorBudgetExceededError } from "../lib/translator-budget";
2
3
  import { isUsageDebugEnabled } from "../usage/debug";
3
4
  import {
4
5
  addRequestLog,
@@ -6,16 +7,52 @@ import {
6
7
  httpStatusForRequestLogTerminal,
7
8
  inspectResponseLogJson,
8
9
  inspectResponseLogSsePayload,
10
+ inspectResponseLogSsePayloadParsed,
9
11
  recordFirstOutput,
10
12
  type RequestLogContext,
11
13
  type RequestLogEntry,
12
14
  } from "./request-log";
13
15
 
14
16
  const nativePassthroughSseResponses = new WeakSet<Response>();
17
+ const eagerRelaySseResponses = new WeakSet<Response>();
18
+
19
+ export const MAX_INSPECTION_SSE_FRAME_BYTES = 4 * 1024 * 1024;
20
+ export const MAX_COMPLETED_OUTPUT_ITEMS = 256;
21
+ export const MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES = 8 * 1024 * 1024;
22
+ export const MAX_TAIL_ERROR_MESSAGE_CHARS = 512;
23
+
24
+ export type InspectionCounters = {
25
+ frameBufferHighWaterBytes: number;
26
+ completedItemsMaxCount: number;
27
+ frameCapOverflows: number;
28
+ itemCapEvictions: number;
29
+ postCancelDrainStops: number;
30
+ };
31
+
32
+ const inspectionCounters: InspectionCounters = {
33
+ frameBufferHighWaterBytes: 0,
34
+ completedItemsMaxCount: 0,
35
+ frameCapOverflows: 0,
36
+ itemCapEvictions: 0,
37
+ postCancelDrainStops: 0,
38
+ };
39
+
40
+ export function getInspectionCounters(): InspectionCounters {
41
+ return { ...inspectionCounters };
42
+ }
43
+
44
+ export function resetInspectionCountersForTest(): void {
45
+ inspectionCounters.frameBufferHighWaterBytes = 0;
46
+ inspectionCounters.completedItemsMaxCount = 0;
47
+ inspectionCounters.frameCapOverflows = 0;
48
+ inspectionCounters.itemCapEvictions = 0;
49
+ inspectionCounters.postCancelDrainStops = 0;
50
+ }
15
51
 
16
52
  export function relayWithAbort(
17
53
  body: ReadableStream<Uint8Array> | null,
18
54
  upstream: AbortController,
55
+ onClientGone?: (reason?: unknown) => void,
19
56
  ): ReadableStream<Uint8Array> | null {
20
57
  if (!body) return null;
21
58
  const reader = body.getReader();
@@ -33,13 +70,31 @@ export function relayWithAbort(
33
70
  }
34
71
  },
35
72
  cancel(reason) {
36
- // Client disconnected: abort the upstream fetch and release the reader so we do not leak it.
37
- upstream.abort(reason);
73
+ // A tee caller may transfer abort ownership to its bounded inspection pump.
74
+ if (onClientGone) onClientGone(reason);
75
+ else upstream.abort(reason);
38
76
  reader.cancel(reason).catch(() => {});
39
77
  },
40
78
  });
41
79
  }
42
80
 
81
+ export function buildFailedTailPayload(err: unknown): string {
82
+ const translatorOverflow = isTranslatorBudgetExceededError(err);
83
+ const message = (translatorOverflow
84
+ ? "upstream translation buffer exceeded the safe limit"
85
+ : `Upstream stream terminated unexpectedly: ${err instanceof Error ? err.message : String(err)}`)
86
+ .slice(0, MAX_TAIL_ERROR_MESSAGE_CHARS);
87
+ const failure = {
88
+ type: "upstream_error",
89
+ code: translatorOverflow ? "translation_buffer_limit" : "upstream_reset",
90
+ message,
91
+ };
92
+ return JSON.stringify({
93
+ type: "response.failed",
94
+ response: { status: "failed", error: failure, last_error: failure },
95
+ });
96
+ }
97
+
43
98
  /**
44
99
  * Relay a passthrough SSE body like relayWithAbort, but convert a MID-STREAM failure (upstream
45
100
  * reset after headers) into a clean terminal: any partial block is closed off, then a synthetic
@@ -51,6 +106,7 @@ export function relayWithAbort(
51
106
  export function relaySseWithFailedTail(
52
107
  body: ReadableStream<Uint8Array>,
53
108
  upstream: AbortController,
109
+ onClientGone?: (reason?: unknown) => void,
54
110
  ): ReadableStream<Uint8Array> {
55
111
  const reader = body.getReader();
56
112
  const encoder = new TextEncoder();
@@ -64,15 +120,7 @@ export function relaySseWithFailedTail(
64
120
  }
65
121
  controller.enqueue(value);
66
122
  } catch (err) {
67
- const failure = {
68
- type: "upstream_error",
69
- code: "upstream_reset",
70
- message: `Upstream stream terminated unexpectedly: ${err instanceof Error ? err.message : String(err)}`,
71
- };
72
- const payload = JSON.stringify({
73
- type: "response.failed",
74
- response: { status: "failed", error: failure, last_error: failure },
75
- });
123
+ const payload = buildFailedTailPayload(err);
76
124
  try {
77
125
  // Leading blank line terminates a partial SSE block so the failed frame parses cleanly.
78
126
  controller.enqueue(encoder.encode(`\n\nevent: response.failed\ndata: ${payload}\n\ndata: [DONE]\n\n`));
@@ -82,7 +130,8 @@ export function relaySseWithFailedTail(
82
130
  }
83
131
  },
84
132
  cancel(reason) {
85
- upstream.abort(reason);
133
+ if (onClientGone) onClientGone(reason);
134
+ else upstream.abort(reason);
86
135
  reader.cancel(reason).catch(() => {});
87
136
  },
88
137
  });
@@ -108,49 +157,61 @@ export function sseDataPayload(block: string): string | null {
108
157
  }
109
158
 
110
159
  export function terminalStatusFromSsePayload(payload: string): ResponsesTerminalStatus | null {
111
- return terminalStatusFromSsePayloadInner(payload);
160
+ if (payload === "[DONE]") return null;
161
+ try {
162
+ return terminalStatusFromParsed(JSON.parse(payload));
163
+ } catch {
164
+ return null;
165
+ }
112
166
  }
113
167
 
114
168
  /** True when a native Responses SSE payload carries the FIRST kind of non-empty model output. */
115
169
  export function isFirstOutputSsePayload(payload: string | null): boolean {
116
170
  if (!payload || payload === "[DONE]") return false;
117
171
  try {
118
- const event = JSON.parse(payload) as { type?: unknown; delta?: unknown };
119
- return (event.type === "response.output_text.delta"
120
- || event.type === "response.reasoning_summary_text.delta"
121
- || event.type === "response.reasoning_text.delta")
122
- && typeof event.delta === "string"
123
- && event.delta.length > 0;
172
+ return firstOutputFromParsed(JSON.parse(payload));
124
173
  } catch {
125
174
  return false;
126
175
  }
127
176
  }
128
177
 
129
- function createFirstOutputReporter(onFirstOutput?: () => void): (payload: string | null) => void {
178
+ export function firstOutputFromParsed(parsed: unknown): boolean {
179
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
180
+ const event = parsed as { type?: unknown; delta?: unknown };
181
+ return (event.type === "response.output_text.delta"
182
+ || event.type === "response.reasoning_summary_text.delta"
183
+ || event.type === "response.reasoning_text.delta")
184
+ && typeof event.delta === "string"
185
+ && event.delta.length > 0;
186
+ }
187
+
188
+ function createFirstOutputReporter(onFirstOutput?: () => void): {
189
+ payload: (payload: string | null) => void;
190
+ parsed: (parsed: unknown) => void;
191
+ } {
130
192
  let reported = false;
131
- return payload => {
132
- if (reported || !isFirstOutputSsePayload(payload)) return;
193
+ const report = (isFirst: boolean) => {
194
+ if (reported || !isFirst) return;
133
195
  reported = true;
134
196
  try { onFirstOutput?.(); } catch { /* metrics must not break the stream */ }
135
197
  };
198
+ return {
199
+ payload: payload => report(isFirstOutputSsePayload(payload)),
200
+ parsed: parsed => report(firstOutputFromParsed(parsed)),
201
+ };
136
202
  }
137
203
 
138
- function terminalStatusFromSsePayloadInner(payload: string): ResponsesTerminalStatus | null {
139
- if (payload === "[DONE]") return null;
140
- try {
141
- const json = JSON.parse(payload) as { type?: unknown };
142
- switch (json.type) {
143
- case "response.completed":
144
- return "completed";
145
- case "response.failed":
146
- return "failed";
147
- case "response.incomplete":
148
- return "incomplete";
149
- default:
150
- return null;
151
- }
152
- } catch {
153
- return null;
204
+ export function terminalStatusFromParsed(parsed: unknown): ResponsesTerminalStatus | null {
205
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
206
+ switch ((parsed as { type?: unknown }).type) {
207
+ case "response.completed":
208
+ return "completed";
209
+ case "response.failed":
210
+ return "failed";
211
+ case "response.incomplete":
212
+ return "incomplete";
213
+ default:
214
+ return null;
154
215
  }
155
216
  }
156
217
 
@@ -166,11 +227,12 @@ export function completedResponseFromSsePayload(payload: string): { id?: unknown
166
227
  }
167
228
 
168
229
  /** Extract the response object from an already-parsed `response.completed` event, or null. */
169
- function completedResponseFromParsedEvent(
170
- json: { type?: unknown; response?: unknown } | null,
230
+ export function completedResponseFromParsedEvent(
231
+ json: unknown,
171
232
  ): { id?: unknown; output?: unknown; status?: unknown } | null {
172
- if (!json || json.type !== "response.completed") return null;
173
- const response = json.response;
233
+ if (!json || typeof json !== "object" || Array.isArray(json)
234
+ || (json as { type?: unknown }).type !== "response.completed") return null;
235
+ const response = (json as { response?: unknown }).response;
174
236
  if (!response || typeof response !== "object" || Array.isArray(response)) return null;
175
237
  return response as { id?: unknown; output?: unknown; status?: unknown };
176
238
  }
@@ -197,7 +259,7 @@ export function trackSseForRequestLog(
197
259
  const inspectPayload = (payload: string | null) => {
198
260
  if (!payload) return;
199
261
  if (logCtx) inspectResponseLogSsePayload(logCtx, payload);
200
- reportFirstOutput(payload);
262
+ reportFirstOutput.payload(payload);
201
263
  const status = terminalStatusFromSsePayload(payload);
202
264
  if (status) reportTerminal(status);
203
265
  };
@@ -321,6 +383,16 @@ export function isNativePassthroughSseResponse(response: Response): boolean {
321
383
  return nativePassthroughSseResponses.has(response);
322
384
  }
323
385
 
386
+ export function markEagerRelaySseResponse(response: Response): Response {
387
+ eagerRelaySseResponses.add(response);
388
+ return response;
389
+ }
390
+
391
+ /** Test-only path identity seam; runtime behavior must not branch on this marker. */
392
+ export function isEagerRelaySseResponse(response: Response): boolean {
393
+ return eagerRelaySseResponses.has(response);
394
+ }
395
+
324
396
  export function relaySseWithHeartbeat(
325
397
  body: ReadableStream<Uint8Array> | null,
326
398
  upstream: AbortController,
@@ -416,10 +488,59 @@ export type SseInspector = {
416
488
  feed(chunk: Uint8Array): void;
417
489
  /** Flush the decoder + trailing unterminated buffer (upstream cleanly done). */
418
490
  finish(): void;
491
+ /** Drop every retained frame/item reference without parsing. Idempotent. */
492
+ dispose(): void;
419
493
  /** True once a protocol terminal was detected and reported. */
420
494
  reported(): boolean;
495
+ /** True once any protocol terminal was parsed, including metadata-only inspectors. */
496
+ terminalSeen(): boolean;
421
497
  };
422
498
 
499
+ export type SseInspectorHandlers = {
500
+ onTerminal?: (status: ResponsesTerminalStatus, httpStatusOverride?: number) => void;
501
+ logCtx?: RequestLogContext;
502
+ onCompletedResponse?: (response: { id?: unknown; output?: unknown; status?: unknown }) => void;
503
+ onFirstOutput?: () => void;
504
+ };
505
+
506
+ type CompletedOutputItem = { item: unknown; sourceBytes: number };
507
+
508
+ function delimiterLengthAt(
509
+ index: number,
510
+ length: number,
511
+ byteAt: (index: number) => number,
512
+ ): number | 0 | undefined {
513
+ const first = byteAt(index);
514
+ if (first === 10) {
515
+ if (index + 1 >= length) return undefined;
516
+ const second = byteAt(index + 1);
517
+ if (second === 10) return 2;
518
+ if (second !== 13) return 0;
519
+ if (index + 2 >= length) return undefined;
520
+ return byteAt(index + 2) === 10 ? 3 : 0;
521
+ }
522
+ if (first !== 13) return 0;
523
+ if (index + 1 >= length) return undefined;
524
+ if (byteAt(index + 1) !== 10) return 0;
525
+ if (index + 2 >= length) return undefined;
526
+ const third = byteAt(index + 2);
527
+ if (third === 10) return 3;
528
+ if (third !== 13) return 0;
529
+ if (index + 3 >= length) return undefined;
530
+ return byteAt(index + 3) === 10 ? 4 : 0;
531
+ }
532
+
533
+ function joinedBytes(slices: readonly Uint8Array[], byteLength: number): Uint8Array {
534
+ if (slices.length === 1 && slices[0]!.byteLength === byteLength) return slices[0]!;
535
+ const joined = new Uint8Array(byteLength);
536
+ let offset = 0;
537
+ for (const slice of slices) {
538
+ joined.set(slice, offset);
539
+ offset += slice.byteLength;
540
+ }
541
+ return joined;
542
+ }
543
+
423
544
  /**
424
545
  * Per-chunk SSE inspection state machine shared by consumeForInspection,
425
546
  * consumeForResponseLogMetadata, and the eager bounded relay (relay-eager.ts).
@@ -435,44 +556,136 @@ export type SseInspector = {
435
556
  * - Synthetic terminals (incomplete / failed-502) are the CALLER's decision:
436
557
  * the caller owns `cancelled` state and reads `reported()` to decide.
437
558
  */
438
- export function createSseInspector(handlers: {
439
- onTerminal?: (status: ResponsesTerminalStatus, httpStatusOverride?: number) => void;
440
- logCtx?: RequestLogContext;
441
- onCompletedResponse?: (response: { id?: unknown; output?: unknown; status?: unknown }) => void;
442
- onFirstOutput?: () => void;
443
- }): SseInspector {
444
- const decoder = new TextDecoder();
445
- let buffer = "";
559
+ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector {
560
+ let decoder: TextDecoder | null = new TextDecoder();
446
561
  let reported = false;
562
+ let sawTerminal = false;
563
+ let disposed = false;
564
+ let delimiterTail = new Uint8Array(0);
565
+ let candidateSlices: Uint8Array[] = [];
566
+ let candidateBytes = 0;
567
+ let discardingOversizedFrame = false;
447
568
  const reportFirstOutput = createFirstOutputReporter(handlers.onFirstOutput);
448
569
  // Allocate reconstruction state only for persistence-capable inspectors.
449
570
  const completedItemsByOutputIndex = handlers.onCompletedResponse
450
- ? new Map<number, unknown>()
571
+ ? new Map<number, CompletedOutputItem>()
451
572
  : null;
573
+ let aggregateItemBytes = 0;
574
+ let reconstructionTainted = false;
575
+
576
+ const clearFrameState = (): void => {
577
+ delimiterTail = new Uint8Array(0);
578
+ candidateSlices = [];
579
+ candidateBytes = 0;
580
+ discardingOversizedFrame = false;
581
+ };
452
582
 
453
- const scanPayload = (payload: string | null): void => {
454
- if (!reported && handlers.logCtx) inspectResponseLogSsePayload(handlers.logCtx, payload);
455
- reportFirstOutput(payload);
583
+ const clearCompletedItems = (): void => {
584
+ completedItemsByOutputIndex?.clear();
585
+ aggregateItemBytes = 0;
586
+ reconstructionTainted = false;
587
+ };
588
+
589
+ const dispose = (): void => {
590
+ if (disposed) return;
591
+ disposed = true;
592
+ decoder = null;
593
+ clearFrameState();
594
+ clearCompletedItems();
595
+ };
596
+
597
+ const retainCandidateSlice = (slice: Uint8Array): void => {
598
+ if (slice.byteLength === 0 || discardingOversizedFrame) return;
599
+ const nextBytes = candidateBytes + slice.byteLength;
600
+ inspectionCounters.frameBufferHighWaterBytes = Math.max(
601
+ inspectionCounters.frameBufferHighWaterBytes,
602
+ Math.min(nextBytes, MAX_INSPECTION_SSE_FRAME_BYTES),
603
+ );
604
+ if (nextBytes > MAX_INSPECTION_SSE_FRAME_BYTES) {
605
+ candidateSlices = [];
606
+ candidateBytes = 0;
607
+ discardingOversizedFrame = true;
608
+ inspectionCounters.frameCapOverflows += 1;
609
+ // The rejected frame may have carried an output item we will never see;
610
+ // any later empty-output terminal must not synthesize a partial replay
611
+ // from the surviving map entries (same taint rule as item eviction).
612
+ reconstructionTainted = true;
613
+ return;
614
+ }
615
+ // `subarray()` aliases the upstream chunk's backing buffer. Copy only the
616
+ // live candidate bytes so a tiny trailing frame cannot pin a multi-MiB
617
+ // chunk whose preceding frames have already been consumed.
618
+ candidateSlices.push(slice.slice());
619
+ candidateBytes = nextBytes;
620
+ };
621
+
622
+ const retainCompletedItem = (index: number, item: unknown, sourceBytes: number): void => {
623
+ const previous = completedItemsByOutputIndex!.get(index);
624
+ if (previous) {
625
+ aggregateItemBytes -= previous.sourceBytes;
626
+ completedItemsByOutputIndex!.delete(index);
627
+ }
628
+ if (sourceBytes > MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES) {
629
+ reconstructionTainted = true;
630
+ inspectionCounters.itemCapEvictions += 1;
631
+ return;
632
+ }
633
+ completedItemsByOutputIndex!.set(index, { item, sourceBytes });
634
+ aggregateItemBytes += sourceBytes;
635
+ while (completedItemsByOutputIndex!.size > MAX_COMPLETED_OUTPUT_ITEMS
636
+ || aggregateItemBytes > MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES) {
637
+ let highestIndex = -1;
638
+ for (const retainedIndex of completedItemsByOutputIndex!.keys()) {
639
+ if (retainedIndex > highestIndex) highestIndex = retainedIndex;
640
+ }
641
+ const evicted = completedItemsByOutputIndex!.get(highestIndex);
642
+ if (!evicted) break;
643
+ completedItemsByOutputIndex!.delete(highestIndex);
644
+ aggregateItemBytes -= evicted.sourceBytes;
645
+ reconstructionTainted = true;
646
+ inspectionCounters.itemCapEvictions += 1;
647
+ }
648
+ inspectionCounters.completedItemsMaxCount = Math.max(
649
+ inspectionCounters.completedItemsMaxCount,
650
+ completedItemsByOutputIndex!.size,
651
+ );
652
+ };
653
+
654
+ const scanPayload = (payload: string | null, sourceBytes: number): void => {
456
655
  if (!payload) return;
457
- if (!reported && handlers.onTerminal) {
458
- const status = terminalStatusFromSsePayload(payload);
459
- if (status) {
656
+ let parsed: unknown | undefined;
657
+ if (payload !== "[DONE]") {
658
+ try {
659
+ parsed = JSON.parse(payload);
660
+ } catch {
661
+ /* malformed SSE payloads remain best-effort/no-throw */
662
+ }
663
+ }
664
+ if (!reported && handlers.logCtx) {
665
+ inspectResponseLogSsePayloadParsed(handlers.logCtx, payload, parsed);
666
+ }
667
+ reportFirstOutput.parsed(parsed);
668
+ const status = terminalStatusFromParsed(parsed);
669
+ if (status) sawTerminal = true;
670
+ if (!reported && handlers.onTerminal && status) {
671
+ try {
460
672
  reported = true;
461
673
  if (handlers.logCtx) {
462
674
  handlers.logCtx.transportPhase = "terminal_sse";
463
675
  handlers.logCtx.terminalSource = "upstream";
464
676
  }
465
677
  handlers.onTerminal(status);
678
+ } finally {
679
+ if (status === "failed" || status === "incomplete") clearCompletedItems();
466
680
  }
681
+ } else if (status === "failed" || status === "incomplete") {
682
+ clearCompletedItems();
467
683
  }
468
684
  if (handlers.onCompletedResponse) {
469
685
  type ParsedSseEvent = { type?: unknown; output_index?: unknown; item?: unknown; response?: unknown };
470
- let parsedEvent: ParsedSseEvent | null = null;
471
- try {
472
- if (payload !== "[DONE]") parsedEvent = JSON.parse(payload) as ParsedSseEvent;
473
- } catch {
474
- /* malformed SSE payloads remain best-effort/no-throw */
475
- }
686
+ const parsedEvent = parsed && typeof parsed === "object" && !Array.isArray(parsed)
687
+ ? parsed as ParsedSseEvent
688
+ : null;
476
689
  const doneItem = parsedEvent?.type === "response.output_item.done" ? parsedEvent.item : undefined;
477
690
  if (parsedEvent
478
691
  && doneItem !== undefined
@@ -482,107 +695,309 @@ export function createSseInspector(handlers: {
482
695
  && doneItem !== null
483
696
  && !Array.isArray(doneItem)
484
697
  && typeof (doneItem as { type?: unknown }).type === "string") {
485
- completedItemsByOutputIndex!.set(parsedEvent.output_index as number, doneItem);
698
+ retainCompletedItem(parsedEvent.output_index as number, doneItem, sourceBytes);
486
699
  }
487
700
 
488
701
  let response = completedResponseFromParsedEvent(parsedEvent);
489
- if (response
490
- && (!Array.isArray(response.output) || response.output.length === 0)
491
- && completedItemsByOutputIndex!.size > 0) {
492
- response = {
493
- ...response,
494
- output: [...completedItemsByOutputIndex!.entries()]
495
- .sort(([left], [right]) => left - right)
496
- .map(([, item]) => item),
497
- };
702
+ if (response) {
703
+ // Authoritative output is a NON-EMPTY ARRAY only. Anything else
704
+ // (missing, null, scalar, object) keeps the historical backfill
705
+ // behavior so a malformed terminal cannot reach rememberResponseState
706
+ // and destroy continuation state (review C1-2).
707
+ const hasAuthoritativeOutput = Array.isArray(response.output)
708
+ && response.output.length > 0;
709
+ if (!hasAuthoritativeOutput && reconstructionTainted) {
710
+ clearCompletedItems();
711
+ return;
712
+ }
713
+ if (!hasAuthoritativeOutput && completedItemsByOutputIndex!.size > 0) {
714
+ response = {
715
+ ...response,
716
+ output: [...completedItemsByOutputIndex!.entries()]
717
+ .sort(([left], [right]) => left - right)
718
+ .map(([, retained]) => retained.item),
719
+ };
720
+ }
721
+ try {
722
+ handlers.onCompletedResponse(response);
723
+ } finally {
724
+ clearCompletedItems();
725
+ }
726
+ } else if (parsedEvent?.type === "response.completed") {
727
+ clearCompletedItems();
728
+ }
729
+ }
730
+ };
731
+
732
+ const completeCandidate = (): void => {
733
+ if (discardingOversizedFrame) {
734
+ discardingOversizedFrame = false;
735
+ return;
736
+ }
737
+ const sourceBytes = candidateBytes;
738
+ const frame = joinedBytes(candidateSlices, sourceBytes);
739
+ candidateSlices = [];
740
+ candidateBytes = 0;
741
+ if (reported && !handlers.onCompletedResponse) return;
742
+ const decoded = decoder!.decode(frame);
743
+ scanPayload(sseDataPayload(decoded), sourceBytes);
744
+ };
745
+
746
+ const scanChunk = (chunk: Uint8Array): void => {
747
+ const previousTail = delimiterTail;
748
+ delimiterTail = new Uint8Array(0);
749
+ const tailLength = previousTail.byteLength;
750
+ const totalLength = tailLength + chunk.byteLength;
751
+ const byteAt = (index: number): number => index < tailLength
752
+ ? previousTail[index]!
753
+ : chunk[index - tailLength]!;
754
+ const retainRange = (start: number, end: number): void => {
755
+ if (end <= start || discardingOversizedFrame) return;
756
+ if (start < tailLength) {
757
+ retainCandidateSlice(previousTail.subarray(start, Math.min(end, tailLength)));
758
+ }
759
+ if (end > tailLength) {
760
+ retainCandidateSlice(chunk.subarray(Math.max(0, start - tailLength), end - tailLength));
761
+ }
762
+ };
763
+ let index = 0;
764
+ let retainedThrough = 0;
765
+ while (index < totalLength) {
766
+ const delimiterLength = delimiterLengthAt(index, totalLength, byteAt);
767
+ if (delimiterLength === undefined) break;
768
+ if (delimiterLength > 0) {
769
+ retainRange(retainedThrough, index);
770
+ completeCandidate();
771
+ index += delimiterLength;
772
+ retainedThrough = index;
773
+ continue;
774
+ }
775
+ index += 1;
776
+ }
777
+ retainRange(retainedThrough, index);
778
+ if (index < totalLength) {
779
+ delimiterTail = new Uint8Array(totalLength - index);
780
+ for (let offset = 0; offset < delimiterTail.byteLength; offset += 1) {
781
+ delimiterTail[offset] = byteAt(index + offset);
498
782
  }
499
- if (response) handlers.onCompletedResponse(response);
500
783
  }
501
784
  };
502
785
 
503
786
  return {
504
787
  feed(chunk) {
505
- buffer += decoder.decode(chunk, { stream: true });
506
- let next: { block: string; rest: string } | null;
507
- while ((next = nextSseBlock(buffer))) {
508
- buffer = next.rest;
509
- if (reported && !handlers.onCompletedResponse) continue;
510
- scanPayload(sseDataPayload(next.block));
511
- }
788
+ if (!disposed) scanChunk(chunk);
512
789
  },
513
790
  finish() {
514
- buffer += decoder.decode();
515
- if (buffer.trim() && !reported) {
516
- scanPayload(sseDataPayload(buffer));
791
+ if (disposed) return;
792
+ try {
793
+ retainCandidateSlice(delimiterTail);
794
+ delimiterTail = new Uint8Array(0);
795
+ if (!discardingOversizedFrame && candidateBytes > 0 && !reported) {
796
+ const sourceBytes = candidateBytes;
797
+ const decoded = decoder!.decode(joinedBytes(candidateSlices, sourceBytes));
798
+ scanPayload(decoded.trim() ? sseDataPayload(decoded) : null, sourceBytes);
799
+ }
800
+ } finally {
801
+ clearFrameState();
802
+ clearCompletedItems();
517
803
  }
518
- buffer = "";
519
804
  },
805
+ dispose,
520
806
  reported: () => reported,
807
+ terminalSeen: () => sawTerminal,
521
808
  };
522
809
  }
523
810
 
524
- export function consumeForInspection(
525
- body: ReadableStream<Uint8Array>,
526
- onTerminal: (status: ResponsesTerminalStatus, httpStatusOverride?: number) => void,
527
- signal?: AbortSignal,
528
- onDone?: () => void,
529
- logCtx?: RequestLogContext,
530
- onCancel?: () => void,
531
- onCompletedResponse?: (response: { id?: unknown; output?: unknown; status?: unknown }) => void,
532
- onFirstOutput?: () => void,
533
- ): void {
534
- const reader = body.getReader();
535
- const inspector = createSseInspector({ onTerminal, logCtx, onCompletedResponse, onFirstOutput });
811
+ export type InspectionDrainBounds = { ms: number; bytes: number };
812
+
813
+ export type InspectionConsumerOptions = {
814
+ clientGoneSignal?: AbortSignal;
815
+ drainBounds?: Partial<InspectionDrainBounds>;
816
+ upstream?: AbortController;
817
+ now?: () => number;
818
+ /** Test seam for proving both public consumers dispose their owned inspector. */
819
+ inspectorFactory?: (handlers: SseInspectorHandlers) => SseInspector;
820
+ };
821
+
822
+ const DEFAULT_INSPECTION_DRAIN_MS = 15_000;
823
+ const DEFAULT_INSPECTION_DRAIN_BYTES = 32 * 1024 * 1024;
824
+ type InspectionPumpOptions = InspectionConsumerOptions & {
825
+ reader: ReadableStreamDefaultReader<Uint8Array>;
826
+ inspector: SseInspector;
827
+ signal?: AbortSignal;
828
+ onDone?: () => void;
829
+ onCancel?: () => void;
830
+ onCleanEof?: () => void;
831
+ onReadError?: () => void;
832
+ };
833
+
834
+ function startBoundedInspectionPump(options: InspectionPumpOptions): void {
835
+ const { reader, inspector, signal, clientGoneSignal } = options;
536
836
  let cancelled = false;
537
- if (signal) {
538
- if (signal.aborted) {
539
- // Aborted before we could read anything (Codex disconnects the instant it finishes reading).
540
- // Finalize as a client-cancel and release the turn — the early return skips pump()'s finally,
541
- // so onDone/onCancel must run here or the entry is silently dropped (#44).
542
- cancelled = true;
543
- reader.cancel(signal.reason).catch(() => {});
544
- onCancel?.();
545
- onDone?.();
837
+ let clientGone = false;
838
+ let clientGoneReason: unknown;
839
+ let drainedBytes = 0;
840
+ let drainDeadline = Number.POSITIVE_INFINITY;
841
+ let drainTimer: ReturnType<typeof setTimeout> | undefined;
842
+ let drainStopped = false;
843
+ const drainMs = options.drainBounds?.ms ?? DEFAULT_INSPECTION_DRAIN_MS;
844
+ const drainBytes = options.drainBounds?.bytes ?? DEFAULT_INSPECTION_DRAIN_BYTES;
845
+ const now = options.now ?? Date.now;
846
+ let cancelFired = false;
847
+ const fireCancel = () => {
848
+ if (cancelFired) return;
849
+ cancelFired = true;
850
+ options.onCancel?.();
851
+ };
852
+ const markClientGone = () => {
853
+ if (clientGone || cancelled) return;
854
+ clientGone = true;
855
+ clientGoneReason = clientGoneSignal?.reason;
856
+ drainDeadline = now() + drainMs;
857
+ if (inspector.terminalSeen() || drainMs <= 0 || drainBytes <= 0) {
858
+ stopDrain();
546
859
  return;
547
860
  }
548
- signal.addEventListener("abort", () => {
549
- // Mid-drain disconnect: record a client-cancel entry (idempotent downstream) instead of the
550
- // suppressed onTerminal path. onDone still fires via pump()'s finally after the read rejects.
551
- cancelled = true;
552
- reader.cancel(signal.reason).catch(() => {});
553
- onCancel?.();
554
- }, { once: true });
861
+ // Do not unref: on Bun/Windows a pending `reader.read()` can be the only
862
+ // wake source; an unref'd timer may never run, so a silent post-cancel
863
+ // drain (time bound, no bytes) hangs the suite until the job timeout.
864
+ drainTimer = setTimeout(stopDrain, drainMs);
865
+ };
866
+ // Ends the bounded drain by cancelling the reader: the pending read settles
867
+ // and the pump loop observes `drainStopped`. Deliberately NOT a shared
868
+ // Promise.race companion — racing every read against one pending promise
869
+ // retains O(chunk-count) reactions on long streams (review C1-1), the exact
870
+ // retention class this phase removes.
871
+ const stopDrain = () => {
872
+ if (drainStopped || cancelled) return;
873
+ drainStopped = true;
874
+ reader.cancel(clientGoneReason).catch(() => {});
875
+ };
876
+ const abortImmediately = () => {
877
+ if (cancelled) return;
878
+ cancelled = true;
879
+ reader.cancel(signal?.reason).catch(() => {});
880
+ fireCancel();
881
+ };
882
+
883
+ if (signal?.aborted) {
884
+ cancelled = true;
885
+ reader.cancel(signal.reason).catch(() => {});
886
+ inspector.dispose();
887
+ fireCancel();
888
+ options.onDone?.();
889
+ return;
555
890
  }
891
+ signal?.addEventListener("abort", abortImmediately, { once: true });
892
+ clientGoneSignal?.addEventListener("abort", markClientGone, { once: true });
893
+ if (clientGoneSignal?.aborted) markClientGone();
894
+
556
895
  const pump = async () => {
896
+ let clientGoneWithoutTerminal = false;
897
+ let boundEndedDrain = false;
557
898
  try {
558
899
  for (;;) {
559
900
  const { done, value } = await reader.read();
901
+ if (drainStopped) {
902
+ // stopDrain() cancelled the reader; the settled read is the wake-up.
903
+ clientGoneWithoutTerminal = !inspector.terminalSeen();
904
+ boundEndedDrain = clientGoneWithoutTerminal;
905
+ break;
906
+ }
560
907
  if (done) {
561
908
  inspector.finish();
562
- if (!inspector.reported() && !cancelled) {
563
- if (logCtx) logCtx.terminalSource = "synthetic";
564
- onTerminal("incomplete");
565
- }
566
- return;
909
+ if (clientGone) clientGoneWithoutTerminal = !inspector.terminalSeen();
910
+ else if (!cancelled) options.onCleanEof?.();
911
+ break;
912
+ }
913
+ if (!clientGone) {
914
+ inspector.feed(value);
915
+ continue;
916
+ }
917
+ if (now() >= drainDeadline) {
918
+ clientGoneWithoutTerminal = true;
919
+ boundEndedDrain = true;
920
+ break;
921
+ }
922
+ const remainingBytes = Math.max(0, drainBytes - drainedBytes);
923
+ const inspectedValue = value.byteLength > remainingBytes
924
+ ? value.subarray(0, remainingBytes)
925
+ : value;
926
+ if (inspectedValue.byteLength > 0) inspector.feed(inspectedValue);
927
+ drainedBytes += inspectedValue.byteLength;
928
+ if (inspector.terminalSeen()) break;
929
+ if (value.byteLength > remainingBytes
930
+ || drainedBytes >= drainBytes
931
+ || now() >= drainDeadline) {
932
+ clientGoneWithoutTerminal = true;
933
+ boundEndedDrain = true;
934
+ break;
567
935
  }
568
- inspector.feed(value);
569
936
  }
570
937
  } catch {
938
+ if (clientGone) clientGoneWithoutTerminal = !inspector.terminalSeen();
939
+ else if (!cancelled) options.onReadError?.();
940
+ } finally {
941
+ if (drainTimer) clearTimeout(drainTimer);
942
+ signal?.removeEventListener("abort", abortImmediately);
943
+ clientGoneSignal?.removeEventListener("abort", markClientGone);
944
+ if (clientGone) {
945
+ if (boundEndedDrain) inspectionCounters.postCancelDrainStops += 1;
946
+ if (clientGoneWithoutTerminal) fireCancel();
947
+ options.upstream?.abort(clientGoneReason);
948
+ reader.cancel(clientGoneReason).catch(() => {});
949
+ }
950
+ inspector.dispose();
951
+ options.onDone?.();
952
+ }
953
+ };
954
+ void pump();
955
+ }
956
+
957
+ export function consumeForInspection(
958
+ body: ReadableStream<Uint8Array>,
959
+ onTerminal: (status: ResponsesTerminalStatus, httpStatusOverride?: number) => void,
960
+ signal?: AbortSignal,
961
+ onDone?: () => void,
962
+ logCtx?: RequestLogContext,
963
+ onCancel?: () => void,
964
+ onCompletedResponse?: (response: { id?: unknown; output?: unknown; status?: unknown }) => void,
965
+ onFirstOutput?: () => void,
966
+ options?: InspectionConsumerOptions,
967
+ ): void {
968
+ const reader = body.getReader();
969
+ const inspector = (options?.inspectorFactory ?? createSseInspector)({
970
+ onTerminal,
971
+ logCtx,
972
+ onCompletedResponse,
973
+ onFirstOutput,
974
+ });
975
+ startBoundedInspectionPump({
976
+ ...options,
977
+ reader,
978
+ inspector,
979
+ signal,
980
+ onDone,
981
+ onCancel,
982
+ onCleanEof: () => {
983
+ if (!inspector.reported()) {
984
+ if (logCtx) logCtx.terminalSource = "synthetic";
985
+ onTerminal("incomplete");
986
+ }
987
+ },
988
+ onReadError: () => {
571
989
  // Upstream read failure after HTTP 200 (mid-stream socket reset) is not a
572
990
  // protocol `response.incomplete` terminal. Report a synthetic 502 so account
573
991
  // health treats it as transient; abort-driven client cancellation still wins.
574
- if (!inspector.reported() && !cancelled) {
992
+ if (!inspector.reported()) {
575
993
  if (logCtx) {
576
994
  logCtx.transportPhase = "mid_stream";
577
995
  logCtx.terminalSource = "synthetic";
578
996
  }
579
997
  onTerminal("failed", 502);
580
998
  }
581
- } finally {
582
- onDone?.();
583
- }
584
- };
585
- pump();
999
+ },
1000
+ });
586
1001
  }
587
1002
 
588
1003
  export function consumeForResponseLogMetadata(
@@ -592,38 +1007,17 @@ export function consumeForResponseLogMetadata(
592
1007
  onDone?: () => void,
593
1008
  onCompletedResponse?: (response: { id?: unknown; output?: unknown; status?: unknown }) => void,
594
1009
  onFirstOutput?: () => void,
1010
+ options?: InspectionConsumerOptions,
595
1011
  ): void {
596
1012
  const reader = body.getReader();
597
1013
  // No onTerminal → the inspector's `reported` gate stays permanently false,
598
1014
  // reproducing this consumer's unconditional logCtx inspection.
599
- const inspector = createSseInspector({ logCtx, onCompletedResponse, onFirstOutput });
600
- if (signal) {
601
- if (signal.aborted) {
602
- reader.cancel(signal.reason).catch(() => {});
603
- onDone?.();
604
- return;
605
- }
606
- signal.addEventListener("abort", () => {
607
- reader.cancel(signal.reason).catch(() => {});
608
- }, { once: true });
609
- }
610
- const pump = async () => {
611
- try {
612
- for (;;) {
613
- const { done, value } = await reader.read();
614
- if (done) {
615
- inspector.finish();
616
- return;
617
- }
618
- inspector.feed(value);
619
- }
620
- } catch {
621
- /* metadata inspection must not affect the client-facing stream */
622
- } finally {
623
- onDone?.();
624
- }
625
- };
626
- pump();
1015
+ const inspector = (options?.inspectorFactory ?? createSseInspector)({
1016
+ logCtx,
1017
+ onCompletedResponse,
1018
+ onFirstOutput,
1019
+ });
1020
+ startBoundedInspectionPump({ ...options, reader, inspector, signal, onDone });
627
1021
  }
628
1022
 
629
1023
  /**