@yansigit/opencodex 2.33.1 → 2.35.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.
- package/README.md +3 -3
- package/gui/dist/assets/index-BjCaHxdz.js +112 -0
- package/gui/dist/assets/index-DLkXOXLC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -3
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +40 -3
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/discovery.ts +60 -2
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +9 -9
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-shell.ts +103 -6
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +74 -39
- package/src/adapters/cursor/request-builder.ts +32 -10
- package/src/adapters/cursor/tool-definitions.ts +13 -10
- package/src/adapters/cursor/tool-result-normalize.ts +35 -3
- package/src/adapters/cursor/types.ts +16 -4
- package/src/adapters/cursor.ts +97 -70
- package/src/adapters/google-antigravity-replay.ts +105 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +41 -12
- package/src/adapters/google-http.ts +12 -11
- package/src/adapters/google.ts +156 -65
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +15 -0
- package/src/adapters/kiro-tools.ts +43 -15
- package/src/adapters/kiro.ts +54 -9
- package/src/adapters/openai-chat.ts +286 -242
- package/src/adapters/openai-responses.ts +334 -24
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -26
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/outbound.ts +10 -8
- package/src/cli/account-api.ts +27 -7
- package/src/cli/account-extended.ts +10 -3
- package/src/cli/account.ts +29 -5
- package/src/cli/alias.ts +66 -0
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +13 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +5 -0
- package/src/cli/init.ts +1 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +13 -7
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/registry.ts +6 -1
- package/src/cli/telemetry-commands.ts +25 -0
- package/src/cli/v2.ts +34 -10
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +80 -6
- package/src/codex/auth-api.ts +48 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- package/src/codex/catalog/model-metadata.ts +13 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +16 -0
- package/src/codex/catalog/provider-fetch.ts +20 -37
- package/src/codex/catalog/sync.ts +127 -2
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +22 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +18 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +14 -1
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +123 -18
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +198 -27
- package/src/codex/transition-state.ts +107 -8
- package/src/combos/types.ts +10 -0
- package/src/compatibility/openai-responses.ts +33 -1
- package/src/config/autonomous-remediation.ts +21 -0
- package/src/config/provider-validation.ts +14 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +191 -17
- package/src/generated/compatibility-version.json +229 -153
- package/src/images/loop.ts +5 -4
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +11 -2
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/oauth/aistudio-credentials.ts +65 -0
- package/src/oauth/aistudio-native-daemon.ts +54 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/login-cli.ts +19 -20
- package/src/oauth/types.ts +15 -0
- package/src/providers/codex-capacity.ts +5 -2
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +73 -14
- package/src/providers/registry.ts +160 -68
- package/src/providers/slug-codec.ts +52 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +34 -10
- package/src/responses/parser.ts +4 -0
- package/src/responses/schema.ts +5 -1
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +23 -0
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/effort-policy.ts +16 -0
- package/src/server/index.ts +152 -80
- package/src/server/lifecycle.ts +52 -1
- package/src/server/management/agent-settings-routes.ts +31 -15
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +27 -9
- package/src/server/management/context.ts +9 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/provider-routes.ts +137 -3
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +21 -0
- package/src/server/request-log.ts +46 -20
- package/src/server/responses/collaboration.ts +42 -5
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +571 -137
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +14 -6
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +35 -5
- package/src/server/responses-custom-tool-repair.ts +52 -7
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +15 -4
- package/src/telemetry/dispatcher.ts +44 -0
- package/src/telemetry/fingerprint.ts +24 -0
- package/src/telemetry/hook.ts +43 -0
- package/src/telemetry/ledger.ts +54 -0
- package/src/telemetry/types.ts +23 -0
- package/src/types/config.ts +66 -14
- package/src/types/provider.ts +77 -0
- package/src/types/request.ts +16 -10
- package/src/types/tools.ts +20 -25
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +17 -7
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-CIDo4y4k.js +0 -102
- package/gui/dist/assets/index-DrSQdTRd.css +0 -1
- package/src/server/aistudio-ws-hub.ts +0 -295
|
@@ -70,6 +70,7 @@ import { handleOauthAccountRoutes } from "./management/oauth-account-routes";
|
|
|
70
70
|
import { handleComboRoutes } from "./management/combo-routes";
|
|
71
71
|
import { handleSystemRoutes } from "./management/system-routes";
|
|
72
72
|
import { handleSidebarRoutes } from "./management/sidebar-routes";
|
|
73
|
+
import { handleCodexPromptRoutes } from "./management/codex-prompt-routes";
|
|
73
74
|
import { handleIntegrationRoutes } from "./management/integration-routes";
|
|
74
75
|
import { handleNativeIntegrationRoutes } from "./management/native-integration-routes";
|
|
75
76
|
import type { ManagementContext } from "./management/context";
|
|
@@ -229,6 +230,7 @@ export async function handleManagementAPI(
|
|
|
229
230
|
?? (await handleIntegrationRoutes(ctx))
|
|
230
231
|
?? (await handleNativeIntegrationRoutes(ctx))
|
|
231
232
|
?? (await handleAgentSettingsRoutes(ctx))
|
|
233
|
+
?? (await handleCodexPromptRoutes(ctx))
|
|
232
234
|
?? (await handleOauthAccountRoutes(ctx))
|
|
233
235
|
?? (await handleComboRoutes(ctx))
|
|
234
236
|
?? (await handleSystemRoutes(ctx))
|
|
@@ -24,7 +24,12 @@
|
|
|
24
24
|
* up to the drain window.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
adapterEofIncompleteFrame,
|
|
29
|
+
createSseTerminalOutputBoundary,
|
|
30
|
+
doneFrame,
|
|
31
|
+
failedTailFrame,
|
|
32
|
+
} from "./relay";
|
|
28
33
|
import {
|
|
29
34
|
nextSseBlock,
|
|
30
35
|
payloadRewriteAsBlockRewrite,
|
|
@@ -100,9 +105,19 @@ export function relaySseEagerBounded(
|
|
|
100
105
|
const now = opts?.now ?? Date.now;
|
|
101
106
|
|
|
102
107
|
const reader = body.getReader();
|
|
108
|
+
const terminalEncoder = new TextEncoder();
|
|
109
|
+
const adapterEofFrame = adapterEofIncompleteFrame(terminalEncoder);
|
|
110
|
+
const terminalSentinel = doneFrame(terminalEncoder);
|
|
103
111
|
const terminalBoundary = createSseTerminalOutputBoundary();
|
|
104
112
|
const activeRewrite: SseBlockRewrite | undefined = hooks.rewriteBlocks
|
|
105
113
|
?? (hooks.rewritePayload ? payloadRewriteAsBlockRewrite(hooks.rewritePayload) : undefined);
|
|
114
|
+
const encodeFailedTail = (error: unknown): Uint8Array | null => {
|
|
115
|
+
try {
|
|
116
|
+
return failedTailFrame(terminalEncoder, error);
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
106
121
|
const rewriteDecoder = activeRewrite ? new TextDecoder() : null;
|
|
107
122
|
const rewriteEncoder = activeRewrite ? new TextEncoder() : null;
|
|
108
123
|
const rewriteBudget = opts?.rewriteBudget;
|
|
@@ -164,13 +179,21 @@ export function relaySseEagerBounded(
|
|
|
164
179
|
let queuedBytes = 0;
|
|
165
180
|
let cancelled = false;
|
|
166
181
|
let done = false;
|
|
167
|
-
const terminalSentinel = new TextEncoder().encode("data: [DONE]\n\n");
|
|
168
182
|
// Pause gate: resolved by client pull, client cancel, or upstream abort so a
|
|
169
183
|
// paused producer ALWAYS resumes (audit blocker 2 — no deadlock; onDone and
|
|
170
184
|
// turn unregistration stay reachable, drainAndShutdown never hangs).
|
|
171
185
|
let wake: (() => void) | null = null;
|
|
172
186
|
const wakeUp = () => { const w = wake; wake = null; w?.(); };
|
|
173
|
-
const paused = () => new Promise<void>(resolve => {
|
|
187
|
+
const paused = () => new Promise<void>(resolve => {
|
|
188
|
+
wake = resolve;
|
|
189
|
+
// A pull, cancel, or abort can win the tiny window between the loop's
|
|
190
|
+
// predicate check and installing this resolver. Re-check every wake
|
|
191
|
+
// predicate after installation so that an already-fired wake cannot leave
|
|
192
|
+
// the producer parked forever.
|
|
193
|
+
if (queuedBytes <= maxQueueBytes || cancelled || upstream.signal.aborted) {
|
|
194
|
+
wakeUp();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
174
197
|
upstream.signal.addEventListener("abort", wakeUp, { once: true });
|
|
175
198
|
|
|
176
199
|
let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null;
|
|
@@ -196,6 +219,9 @@ export function relaySseEagerBounded(
|
|
|
196
219
|
|
|
197
220
|
const producer = async () => {
|
|
198
221
|
let syntheticKind: "incomplete" | "failed" | null = null;
|
|
222
|
+
let deliveryFallbackSent = false;
|
|
223
|
+
let priorRewriteFailure = false;
|
|
224
|
+
let priorRewriteError: unknown;
|
|
199
225
|
// reader.read() is not intrinsically tied to the upstream AbortController
|
|
200
226
|
// (a fetch body usually rejects on abort, but that coupling is the fetch
|
|
201
227
|
// implementation's, not the stream's), so abort must break a parked read on
|
|
@@ -221,18 +247,46 @@ export function relaySseEagerBounded(
|
|
|
221
247
|
if (upstreamDone) {
|
|
222
248
|
hooks.finishInspection();
|
|
223
249
|
const boundedTail = terminalBoundary.finish();
|
|
250
|
+
let clientTail = boundedTail;
|
|
251
|
+
let rewriteFailed = false;
|
|
252
|
+
let rewriteError: unknown;
|
|
224
253
|
if (activeRewrite) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
254
|
+
try {
|
|
255
|
+
const rewritten = rewriteOutbound(boundedTail);
|
|
256
|
+
clientTail = joinUint8Arrays(rewritten, flushRewriteTail());
|
|
257
|
+
} catch (error) {
|
|
258
|
+
rewriteFailed = true;
|
|
259
|
+
rewriteError = error;
|
|
260
|
+
clientTail = new Uint8Array(0);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (rewriteFailed) {
|
|
264
|
+
const safeTail = encodeFailedTail(rewriteError);
|
|
265
|
+
if (safeTail && !cancelled && !upstream.signal.aborted) {
|
|
266
|
+
if (!hooks.sawTerminal()) syntheticKind = "failed";
|
|
267
|
+
queuedBytes += safeTail.byteLength;
|
|
268
|
+
try { controllerRef?.enqueue(safeTail); } catch { /* client already torn down */ }
|
|
269
|
+
try { controllerRef?.close(); } catch { /* client already gone */ }
|
|
230
270
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
if (clientTail.byteLength > 0 && !cancelled) {
|
|
274
|
+
queuedBytes += clientTail.byteLength;
|
|
275
|
+
try { controllerRef?.enqueue(clientTail); } catch { /* client already gone */ }
|
|
234
276
|
}
|
|
235
|
-
if (
|
|
277
|
+
if (terminalBoundary.terminalSeen()) {
|
|
278
|
+
if (!terminalBoundary.doneSeen() && !cancelled) {
|
|
279
|
+
queuedBytes += terminalSentinel.byteLength;
|
|
280
|
+
try { controllerRef?.enqueue(terminalSentinel); } catch { /* client already gone */ }
|
|
281
|
+
}
|
|
282
|
+
} else if (!hooks.sawTerminal() && !cancelled && !upstream.signal.aborted) {
|
|
283
|
+
// A clean 200 EOF without a Responses terminal must be visible to
|
|
284
|
+
// Codex as one incomplete turn, followed by the normal sentinel.
|
|
285
|
+
queuedBytes += adapterEofFrame.byteLength + terminalSentinel.byteLength;
|
|
286
|
+
try {
|
|
287
|
+
controllerRef?.enqueue(adapterEofFrame);
|
|
288
|
+
controllerRef?.enqueue(terminalSentinel);
|
|
289
|
+
} catch { /* client already gone */ }
|
|
236
290
|
syntheticKind = "incomplete";
|
|
237
291
|
}
|
|
238
292
|
break;
|
|
@@ -247,7 +301,21 @@ export function relaySseEagerBounded(
|
|
|
247
301
|
continue;
|
|
248
302
|
}
|
|
249
303
|
const terminalBounded = terminalBoundary.feed(value);
|
|
250
|
-
|
|
304
|
+
let outbound: Uint8Array;
|
|
305
|
+
if (activeRewrite) {
|
|
306
|
+
try {
|
|
307
|
+
outbound = rewriteOutbound(terminalBounded);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
// Preserve the first rewrite failure across the teardown flush. A
|
|
310
|
+
// second empty flush may succeed, but the terminal bytes still
|
|
311
|
+
// must not bypass the failed rewrite or become DONE-only output.
|
|
312
|
+
priorRewriteFailure = true;
|
|
313
|
+
priorRewriteError = error;
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
outbound = terminalBounded;
|
|
318
|
+
}
|
|
251
319
|
if (outbound.byteLength > 0) {
|
|
252
320
|
queuedBytes += outbound.byteLength;
|
|
253
321
|
try {
|
|
@@ -278,16 +346,75 @@ export function relaySseEagerBounded(
|
|
|
278
346
|
} catch (err) {
|
|
279
347
|
// Upstream read failure. Distinguish genuine mid-stream reset from
|
|
280
348
|
// abort-driven teardown (shutdown/cancel-expiry) — audit M3.
|
|
281
|
-
|
|
349
|
+
// A read can fail after delivering an unterminated terminal block. Flush
|
|
350
|
+
// both observers before deciding whether this is a synthetic reset so
|
|
351
|
+
// eager mode matches the tee/pull boundary semantics at EOF.
|
|
352
|
+
let boundedTail: Uint8Array<ArrayBufferLike> = new Uint8Array(0);
|
|
353
|
+
let tailTerminal = false;
|
|
354
|
+
let tailDone = false;
|
|
355
|
+
try { hooks.finishInspection(); } catch { /* preserve the original read failure */ }
|
|
356
|
+
try {
|
|
357
|
+
boundedTail = terminalBoundary.finish();
|
|
358
|
+
tailTerminal = terminalBoundary.terminalSeen();
|
|
359
|
+
tailDone = terminalBoundary.doneSeen();
|
|
360
|
+
} catch {
|
|
361
|
+
// A near-cap ambiguous delimiter tail may itself overflow at EOF.
|
|
362
|
+
// Preserve the original read/framing failure and continue emitting
|
|
363
|
+
// the bounded failed tail instead of letting cleanup throw again.
|
|
364
|
+
}
|
|
365
|
+
let clientTail: Uint8Array<ArrayBufferLike> = boundedTail;
|
|
366
|
+
let rewriteFailed = false;
|
|
367
|
+
let rewriteError: unknown;
|
|
368
|
+
if (priorRewriteFailure) {
|
|
369
|
+
rewriteFailed = true;
|
|
370
|
+
rewriteError = priorRewriteError;
|
|
371
|
+
clientTail = new Uint8Array(0);
|
|
372
|
+
} else if (activeRewrite) {
|
|
373
|
+
try {
|
|
374
|
+
const rewritten = rewriteOutbound(boundedTail);
|
|
375
|
+
clientTail = joinUint8Arrays(rewritten, flushRewriteTail());
|
|
376
|
+
} catch (error) {
|
|
377
|
+
rewriteFailed = true;
|
|
378
|
+
rewriteError = error;
|
|
379
|
+
clientTail = new Uint8Array(0);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (clientTail.byteLength > 0 && !cancelled && !upstream.signal.aborted) {
|
|
383
|
+
queuedBytes += clientTail.byteLength;
|
|
384
|
+
try { controllerRef?.enqueue(clientTail); } catch { /* client already torn down */ }
|
|
385
|
+
}
|
|
386
|
+
if (rewriteFailed && !cancelled && !upstream.signal.aborted) {
|
|
387
|
+
// Never bypass a client rewrite after it fails: boundedTail can contain
|
|
388
|
+
// provider metadata or content that the active rewrite was required to
|
|
389
|
+
// remove. Emit one safe failed envelope instead. When inspection has
|
|
390
|
+
// already reported the real upstream terminal, this is a delivery
|
|
391
|
+
// fallback only and must not create a second accounting outcome.
|
|
392
|
+
const safeTail = encodeFailedTail(rewriteError ?? err);
|
|
393
|
+
if (safeTail && !cancelled && !upstream.signal.aborted) {
|
|
394
|
+
if (!hooks.sawTerminal()) syntheticKind = "failed";
|
|
395
|
+
deliveryFallbackSent = true;
|
|
396
|
+
queuedBytes += safeTail.byteLength;
|
|
397
|
+
try { controllerRef?.enqueue(safeTail); } catch { /* client already torn down */ }
|
|
398
|
+
try { controllerRef?.close(); } catch { /* client already gone */ }
|
|
399
|
+
}
|
|
400
|
+
} else if (tailTerminal && !cancelled && !upstream.signal.aborted) {
|
|
401
|
+
if (!tailDone) {
|
|
402
|
+
queuedBytes += terminalSentinel.byteLength;
|
|
403
|
+
try { controllerRef?.enqueue(terminalSentinel); } catch { /* client already gone */ }
|
|
404
|
+
}
|
|
405
|
+
} else if (!tailTerminal && !cancelled && !upstream.signal.aborted) {
|
|
282
406
|
// Serializing `err` can run user-defined accessors (Error.message
|
|
283
407
|
// getters, toString) that re-entrantly cancel the client or abort the
|
|
284
408
|
// upstream. Build the tail FIRST, then re-check eligibility before
|
|
285
409
|
// committing to the synthetic terminal (adversarial review blocker).
|
|
286
|
-
const tail =
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
410
|
+
const tail = encodeFailedTail(err);
|
|
411
|
+
if (tail && !cancelled && !upstream.signal.aborted) {
|
|
412
|
+
// Inspection and client framing have separate bounded parsers. If
|
|
413
|
+
// inspection resynchronized after an oversized frame and observed a
|
|
414
|
+
// later real terminal, it still must not suppress a terminal delivery
|
|
415
|
+
// to the client. Only accounting remains tied to the inspected result.
|
|
416
|
+
if (!hooks.sawTerminal()) syntheticKind = "failed";
|
|
417
|
+
deliveryFallbackSent = true;
|
|
291
418
|
queuedBytes += tail.byteLength;
|
|
292
419
|
try { controllerRef?.enqueue(tail); } catch { /* client already torn down */ }
|
|
293
420
|
try { controllerRef?.close(); } catch { /* client already torn down */ }
|
|
@@ -306,7 +433,7 @@ export function relaySseEagerBounded(
|
|
|
306
433
|
if (cancelled && !hooks.sawTerminal()) {
|
|
307
434
|
hooks.onClientCancel();
|
|
308
435
|
}
|
|
309
|
-
if (cancelled || upstream.signal.aborted || syntheticKind === "failed") {
|
|
436
|
+
if (cancelled || upstream.signal.aborted || syntheticKind === "failed" || deliveryFallbackSent) {
|
|
310
437
|
upstream.abort();
|
|
311
438
|
reader.cancel().catch(() => {});
|
|
312
439
|
}
|
package/src/server/relay.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { ResponsesTerminalStatus } from "../bridge";
|
|
2
|
+
import {
|
|
3
|
+
cyberPolicyErrorType,
|
|
4
|
+
CYBER_POLICY_ERROR_CODE,
|
|
5
|
+
CYBER_POLICY_FALLBACK_MESSAGE,
|
|
6
|
+
isCyberPolicyCode,
|
|
7
|
+
isCyberPolicyMessage,
|
|
8
|
+
} from "../lib/errors";
|
|
9
|
+
import { redactSecretString } from "../lib/redact";
|
|
2
10
|
import { isTranslatorBudgetExceededError } from "../lib/translator-budget";
|
|
3
11
|
import { isUsageDebugEnabled } from "../usage/debug";
|
|
4
12
|
import {
|
|
@@ -16,6 +24,7 @@ import {
|
|
|
16
24
|
joinSseFrameBytes,
|
|
17
25
|
MAX_CLIENT_SSE_FRAME_BYTES,
|
|
18
26
|
} from "./sse-frame-buffer";
|
|
27
|
+
import { replaceSseDataPayload } from "./sse-payload-rewrite";
|
|
19
28
|
|
|
20
29
|
const nativePassthroughSseResponses = new WeakSet<Response>();
|
|
21
30
|
const eagerRelaySseResponses = new WeakSet<Response>();
|
|
@@ -24,6 +33,30 @@ export const MAX_INSPECTION_SSE_FRAME_BYTES = MAX_CLIENT_SSE_FRAME_BYTES;
|
|
|
24
33
|
export const MAX_COMPLETED_OUTPUT_ITEMS = 256;
|
|
25
34
|
export const MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES = 8 * 1024 * 1024;
|
|
26
35
|
export const MAX_TAIL_ERROR_MESSAGE_CHARS = 512;
|
|
36
|
+
const ADAPTER_EOF_INCOMPLETE_PAYLOAD = JSON.stringify({
|
|
37
|
+
type: "response.incomplete",
|
|
38
|
+
response: {
|
|
39
|
+
status: "incomplete",
|
|
40
|
+
incomplete_details: { reason: "adapter_eof" },
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const DONE_SSE_FRAME_TEXT = "data: [DONE]\n\n";
|
|
44
|
+
const FAILED_TAIL_FALLBACK_PAYLOAD = JSON.stringify({
|
|
45
|
+
type: "response.failed",
|
|
46
|
+
response: {
|
|
47
|
+
status: "failed",
|
|
48
|
+
error: {
|
|
49
|
+
type: "upstream_error",
|
|
50
|
+
code: "upstream_reset",
|
|
51
|
+
message: "Upstream stream terminated unexpectedly",
|
|
52
|
+
},
|
|
53
|
+
last_error: {
|
|
54
|
+
type: "upstream_error",
|
|
55
|
+
code: "upstream_reset",
|
|
56
|
+
message: "Upstream stream terminated unexpectedly",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
27
60
|
|
|
28
61
|
export type InspectionCounters = {
|
|
29
62
|
frameBufferHighWaterBytes: number;
|
|
@@ -99,6 +132,21 @@ export function buildFailedTailPayload(err: unknown): string {
|
|
|
99
132
|
});
|
|
100
133
|
}
|
|
101
134
|
|
|
135
|
+
function buildFailedTailPayloadOrFallback(err: unknown): string {
|
|
136
|
+
try {
|
|
137
|
+
return buildFailedTailPayload(err);
|
|
138
|
+
} catch {
|
|
139
|
+
// Error.message and String(error) may execute hostile accessors. Preserve a
|
|
140
|
+
// bounded protocol terminal even when diagnostic serialization is unsafe.
|
|
141
|
+
return FAILED_TAIL_FALLBACK_PAYLOAD;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function failedTailFrame(encoder: TextEncoder, err: unknown): Uint8Array {
|
|
146
|
+
const payload = buildFailedTailPayloadOrFallback(err);
|
|
147
|
+
return encoder.encode(`\n\nevent: response.failed\ndata: ${payload}\n\n${DONE_SSE_FRAME_TEXT}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
102
150
|
export type SseTerminalOutputBoundary = {
|
|
103
151
|
feed(chunk: Uint8Array): Uint8Array;
|
|
104
152
|
finish(): Uint8Array;
|
|
@@ -111,13 +159,16 @@ export type SseTerminalOutputBoundary = {
|
|
|
111
159
|
* Frame-aware client output boundary shared by both native Responses relays.
|
|
112
160
|
* It buffers only the current incomplete SSE block under the same hard byte
|
|
113
161
|
* cap as inspection, forwards complete blocks through the first Responses
|
|
114
|
-
* terminal, and drops every later block/byte.
|
|
162
|
+
* terminal, and drops every later block/byte. A premature [DONE] is held until
|
|
163
|
+
* a terminal arrives so clean EOF can synthesize one terminal and one sentinel.
|
|
115
164
|
*/
|
|
116
165
|
export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
|
|
117
166
|
const decoder = new TextDecoder();
|
|
167
|
+
const encoder = new TextEncoder();
|
|
118
168
|
const framer = new BoundedSseFrameBuffer(MAX_INSPECTION_SSE_FRAME_BYTES);
|
|
119
169
|
let terminal = false;
|
|
120
170
|
let done = false;
|
|
171
|
+
let pendingDone: { block: Uint8Array; delimiter: Uint8Array } | null = null;
|
|
121
172
|
let disposed = false;
|
|
122
173
|
|
|
123
174
|
const processFrames = (
|
|
@@ -129,16 +180,37 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
|
|
|
129
180
|
for (const frame of frames) {
|
|
130
181
|
const payload = sseDataPayload(decoder.decode(frame.block));
|
|
131
182
|
const isDone = payload === "[DONE]";
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
183
|
+
const parsed = payload === null ? undefined : parseSsePayload(payload);
|
|
184
|
+
const policyError = parsed !== undefined && isPolicyRewriteType(parsed)
|
|
185
|
+
? cyberPolicyTerminalError(parsed)
|
|
186
|
+
: undefined;
|
|
187
|
+
const outboundBlock = policyError
|
|
188
|
+
? encoder.encode(rewritePolicyTerminalBlock(
|
|
189
|
+
decoder.decode(frame.block),
|
|
190
|
+
policyFailurePayload(policyError, parsed),
|
|
191
|
+
))
|
|
192
|
+
: frame.block;
|
|
136
193
|
if (isDone) {
|
|
137
194
|
done = true;
|
|
195
|
+
if (responsesTerminal) {
|
|
196
|
+
output.push(outboundBlock, frame.delimiter);
|
|
197
|
+
} else if (!pendingDone) {
|
|
198
|
+
// Do not expose a sentinel before a Responses terminal. If EOF
|
|
199
|
+
// follows, the synthetic incomplete path owns the one sentinel;
|
|
200
|
+
// if a terminal arrives later, this pending frame is emitted then.
|
|
201
|
+
pendingDone = { block: outboundBlock, delimiter: frame.delimiter };
|
|
202
|
+
}
|
|
138
203
|
continue;
|
|
139
204
|
}
|
|
140
|
-
|
|
205
|
+
// Preserve every frame through the first Responses terminal. Every
|
|
206
|
+
// later non-DONE frame is dropped.
|
|
207
|
+
if (!responsesTerminal) output.push(outboundBlock, frame.delimiter);
|
|
208
|
+
if (!responsesTerminal && payload && terminalStatusFromParsed(parsed)) {
|
|
141
209
|
responsesTerminal = true;
|
|
210
|
+
if (pendingDone) {
|
|
211
|
+
output.push(pendingDone.block, pendingDone.delimiter);
|
|
212
|
+
pendingDone = null;
|
|
213
|
+
}
|
|
142
214
|
}
|
|
143
215
|
}
|
|
144
216
|
if (responsesTerminal) {
|
|
@@ -155,13 +227,22 @@ export function createSseTerminalOutputBoundary(): SseTerminalOutputBoundary {
|
|
|
155
227
|
},
|
|
156
228
|
finish() {
|
|
157
229
|
if (disposed || terminal) return new Uint8Array(0);
|
|
158
|
-
|
|
230
|
+
const tail = framer.finish();
|
|
231
|
+
if (tail.byteLength === 0) return new Uint8Array(0);
|
|
232
|
+
// EOF may cut off the final SSE block before its blank-line delimiter.
|
|
233
|
+
// Feed it through the exact same parser/rewrite/terminal path as a
|
|
234
|
+
// complete frame, using a synthetic delimiter so the client receives a
|
|
235
|
+
// dispatchable event rather than an unterminated tail.
|
|
236
|
+
const tailText = decoder.decode(tail);
|
|
237
|
+
const delimiter = encoder.encode(tailText.includes("\r\n") ? "\r\n\r\n" : "\n\n");
|
|
238
|
+
return processFrames([{ block: tail, delimiter }]);
|
|
159
239
|
},
|
|
160
240
|
terminalSeen: () => terminal,
|
|
161
241
|
doneSeen: () => done,
|
|
162
242
|
dispose() {
|
|
163
243
|
if (disposed) return;
|
|
164
244
|
disposed = true;
|
|
245
|
+
pendingDone = null;
|
|
165
246
|
framer.dispose();
|
|
166
247
|
},
|
|
167
248
|
};
|
|
@@ -198,7 +279,7 @@ export function relaySseWithFailedTail(
|
|
|
198
279
|
// Preserve through the terminal block only, add the conventional sentinel
|
|
199
280
|
// when there was no real [DONE] data event, then stop reading upstream.
|
|
200
281
|
if (!terminalBoundary.doneSeen()) {
|
|
201
|
-
controller.enqueue(encoder
|
|
282
|
+
controller.enqueue(doneFrame(encoder));
|
|
202
283
|
}
|
|
203
284
|
closed = true;
|
|
204
285
|
controller.close();
|
|
@@ -219,6 +300,16 @@ export function relaySseWithFailedTail(
|
|
|
219
300
|
if (done) {
|
|
220
301
|
const tail = terminalBoundary.finish();
|
|
221
302
|
if (tail.byteLength > 0) controller.enqueue(tail);
|
|
303
|
+
if (terminalBoundary.terminalSeen()) {
|
|
304
|
+
if (!terminalBoundary.doneSeen()) controller.enqueue(doneFrame(encoder));
|
|
305
|
+
} else {
|
|
306
|
+
// A clean upstream EOF is still a failed Responses turn when no
|
|
307
|
+
// protocol terminal arrived. Make that state explicit so Codex
|
|
308
|
+
// does not treat HTTP 200 + bare EOF as a retryable disconnect.
|
|
309
|
+
const incomplete = adapterEofIncompleteFrame(encoder);
|
|
310
|
+
controller.enqueue(incomplete);
|
|
311
|
+
controller.enqueue(doneFrame(encoder));
|
|
312
|
+
}
|
|
222
313
|
terminalBoundary.dispose();
|
|
223
314
|
controller.close();
|
|
224
315
|
return;
|
|
@@ -228,8 +319,10 @@ export function relaySseWithFailedTail(
|
|
|
228
319
|
}
|
|
229
320
|
} catch (err) {
|
|
230
321
|
let partial: Uint8Array = new Uint8Array(0);
|
|
322
|
+
let tailTerminal = false;
|
|
231
323
|
try {
|
|
232
324
|
partial = terminalBoundary.finish();
|
|
325
|
+
tailTerminal = terminalBoundary.terminalSeen();
|
|
233
326
|
} catch {
|
|
234
327
|
// A near-cap ambiguous delimiter tail may itself overflow at EOF.
|
|
235
328
|
// Preserve the original read/framing failure and continue emitting
|
|
@@ -237,11 +330,14 @@ export function relaySseWithFailedTail(
|
|
|
237
330
|
}
|
|
238
331
|
terminalBoundary.dispose();
|
|
239
332
|
if (closed) return;
|
|
240
|
-
const payload = buildFailedTailPayload(err);
|
|
241
333
|
try {
|
|
242
334
|
if (partial.byteLength > 0) controller.enqueue(partial);
|
|
243
|
-
|
|
244
|
-
|
|
335
|
+
if (tailTerminal) {
|
|
336
|
+
if (!terminalBoundary.doneSeen()) controller.enqueue(doneFrame(encoder));
|
|
337
|
+
} else {
|
|
338
|
+
// Leading blank line terminates a partial SSE block so the failed frame parses cleanly.
|
|
339
|
+
controller.enqueue(failedTailFrame(encoder, err));
|
|
340
|
+
}
|
|
245
341
|
controller.close();
|
|
246
342
|
} catch { /* client already torn down */ }
|
|
247
343
|
upstream.abort();
|
|
@@ -276,15 +372,139 @@ export function sseDataPayload(block: string): string | null {
|
|
|
276
372
|
return data.length > 0 ? data.join("\n") : null;
|
|
277
373
|
}
|
|
278
374
|
|
|
279
|
-
|
|
280
|
-
|
|
375
|
+
type JsonRecord = Record<string, unknown>;
|
|
376
|
+
|
|
377
|
+
function asJsonRecord(value: unknown): JsonRecord | null {
|
|
378
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
379
|
+
? value as JsonRecord
|
|
380
|
+
: null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function stringField(record: JsonRecord | null, key: string): string | undefined {
|
|
384
|
+
const value = record?.[key];
|
|
385
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Return a high-confidence policy error carried by an upstream terminal shape.
|
|
390
|
+
* Deliberately inspect structured error fields and known refusal copy only — a
|
|
391
|
+
* bare `cyber_policy` token in an unrelated payload is not sufficient.
|
|
392
|
+
*/
|
|
393
|
+
function cyberPolicyTerminalError(parsed: unknown): { message: string; type?: string } | undefined {
|
|
394
|
+
const root = asJsonRecord(parsed);
|
|
395
|
+
if (!root) return undefined;
|
|
396
|
+
const response = asJsonRecord(root.response);
|
|
397
|
+
const candidates = [
|
|
398
|
+
asJsonRecord(root.error),
|
|
399
|
+
asJsonRecord(root.last_error),
|
|
400
|
+
asJsonRecord(response?.error),
|
|
401
|
+
asJsonRecord(response?.incomplete_details),
|
|
402
|
+
root,
|
|
403
|
+
response,
|
|
404
|
+
];
|
|
405
|
+
for (const candidate of candidates) {
|
|
406
|
+
if (!candidate) continue;
|
|
407
|
+
if (isCyberPolicyCode(stringField(candidate, "code"))) {
|
|
408
|
+
return {
|
|
409
|
+
message: stringField(candidate, "message")
|
|
410
|
+
?? CYBER_POLICY_FALLBACK_MESSAGE,
|
|
411
|
+
...(stringField(candidate, "type") ? { type: stringField(candidate, "type") } : {}),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
for (const candidate of candidates) {
|
|
416
|
+
const message = stringField(candidate, "message");
|
|
417
|
+
if (message && isCyberPolicyMessage(message)) {
|
|
418
|
+
return {
|
|
419
|
+
message,
|
|
420
|
+
...(stringField(candidate, "type") ? { type: stringField(candidate, "type") } : {}),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function parseSsePayload(payload: string): unknown | undefined {
|
|
428
|
+
if (payload === "[DONE]") return undefined;
|
|
281
429
|
try {
|
|
282
|
-
return
|
|
430
|
+
return JSON.parse(payload);
|
|
283
431
|
} catch {
|
|
284
|
-
return
|
|
432
|
+
return undefined;
|
|
285
433
|
}
|
|
286
434
|
}
|
|
287
435
|
|
|
436
|
+
function isPolicyRewriteType(parsed: unknown): boolean {
|
|
437
|
+
const type = asJsonRecord(parsed)?.type;
|
|
438
|
+
return type === "response.failed" || type === "response.incomplete" || type === "error";
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function rewritePolicyTerminalBlock(block: string, payload: string): string {
|
|
442
|
+
const newline = block.includes("\r\n") ? "\r\n" : "\n";
|
|
443
|
+
const rewritten = replaceSseDataPayload(block, payload);
|
|
444
|
+
const lines = rewritten.split(/\r?\n/);
|
|
445
|
+
let eventRewritten = false;
|
|
446
|
+
const withEvent = lines.map(line => {
|
|
447
|
+
if (!eventRewritten && line.startsWith("event:")) {
|
|
448
|
+
eventRewritten = true;
|
|
449
|
+
return "event: response.failed";
|
|
450
|
+
}
|
|
451
|
+
return line;
|
|
452
|
+
});
|
|
453
|
+
if (!eventRewritten) withEvent.unshift("event: response.failed");
|
|
454
|
+
return withEvent.join(newline);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function policyFailurePayload(policyError: { message: string; type?: string }, parsed: unknown): string {
|
|
458
|
+
const error = {
|
|
459
|
+
type: cyberPolicyErrorType(policyError.type),
|
|
460
|
+
code: CYBER_POLICY_ERROR_CODE,
|
|
461
|
+
message: redactSecretString(policyError.message).slice(0, MAX_TAIL_ERROR_MESSAGE_CHARS),
|
|
462
|
+
};
|
|
463
|
+
const root = asJsonRecord(parsed);
|
|
464
|
+
const originalResponse = asJsonRecord(root?.response);
|
|
465
|
+
const preservedResponse = Object.fromEntries(
|
|
466
|
+
Object.entries(originalResponse ?? {}).filter(([key]) => key !== "incomplete_details"),
|
|
467
|
+
);
|
|
468
|
+
const response = {
|
|
469
|
+
...preservedResponse,
|
|
470
|
+
status: "failed",
|
|
471
|
+
error,
|
|
472
|
+
last_error: error,
|
|
473
|
+
retryable: false,
|
|
474
|
+
};
|
|
475
|
+
// Responses event metadata such as sequence_number is normally top-level.
|
|
476
|
+
// Keep it (and any other non-error, non-response fields) while replacing only
|
|
477
|
+
// the protocol type and error envelope.
|
|
478
|
+
const preservedRoot = root
|
|
479
|
+
? Object.fromEntries(Object.entries(root).filter(([key]) => (
|
|
480
|
+
key !== "type"
|
|
481
|
+
&& key !== "response"
|
|
482
|
+
&& key !== "error"
|
|
483
|
+
&& key !== "last_error"
|
|
484
|
+
&& key !== "retryable"
|
|
485
|
+
)))
|
|
486
|
+
: {};
|
|
487
|
+
return JSON.stringify({
|
|
488
|
+
...preservedRoot,
|
|
489
|
+
type: "response.failed",
|
|
490
|
+
retryable: false,
|
|
491
|
+
response,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function adapterEofIncompleteFrame(encoder: TextEncoder): Uint8Array {
|
|
496
|
+
return encoder.encode(`event: response.incomplete\ndata: ${ADAPTER_EOF_INCOMPLETE_PAYLOAD}\n\n`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export function doneFrame(encoder: TextEncoder): Uint8Array {
|
|
500
|
+
return encoder.encode(DONE_SSE_FRAME_TEXT);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function terminalStatusFromSsePayload(payload: string): ResponsesTerminalStatus | null {
|
|
504
|
+
if (payload === "[DONE]") return null;
|
|
505
|
+
return terminalStatusFromParsed(parseSsePayload(payload));
|
|
506
|
+
}
|
|
507
|
+
|
|
288
508
|
/** True when a native Responses SSE payload carries the FIRST kind of non-empty model output. */
|
|
289
509
|
export function isFirstOutputSsePayload(payload: string | null): boolean {
|
|
290
510
|
if (!payload || payload === "[DONE]") return false;
|
|
@@ -322,14 +542,16 @@ function createFirstOutputReporter(onFirstOutput?: () => void): {
|
|
|
322
542
|
}
|
|
323
543
|
|
|
324
544
|
export function terminalStatusFromParsed(parsed: unknown): ResponsesTerminalStatus | null {
|
|
325
|
-
|
|
326
|
-
switch (
|
|
545
|
+
const type = asJsonRecord(parsed)?.type;
|
|
546
|
+
switch (type) {
|
|
327
547
|
case "response.completed":
|
|
328
548
|
return "completed";
|
|
329
549
|
case "response.failed":
|
|
330
550
|
return "failed";
|
|
331
551
|
case "response.incomplete":
|
|
332
|
-
return "incomplete";
|
|
552
|
+
return cyberPolicyTerminalError(parsed) ? "failed" : "incomplete";
|
|
553
|
+
case "error":
|
|
554
|
+
return cyberPolicyTerminalError(parsed) ? "failed" : null;
|
|
333
555
|
default:
|
|
334
556
|
return null;
|
|
335
557
|
}
|
|
@@ -800,6 +1022,9 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
800
1022
|
}
|
|
801
1023
|
reportFirstOutput.parsed(parsed);
|
|
802
1024
|
const status = terminalStatusFromParsed(parsed);
|
|
1025
|
+
const policyTerminal = status === "failed"
|
|
1026
|
+
&& isPolicyRewriteType(parsed)
|
|
1027
|
+
&& cyberPolicyTerminalError(parsed) !== undefined;
|
|
803
1028
|
if (status) sawTerminal = true;
|
|
804
1029
|
if (!reported && handlers.onTerminal && status) {
|
|
805
1030
|
try {
|
|
@@ -808,7 +1033,7 @@ export function createSseInspector(handlers: SseInspectorHandlers): SseInspector
|
|
|
808
1033
|
handlers.logCtx.transportPhase = "terminal_sse";
|
|
809
1034
|
handlers.logCtx.terminalSource = "upstream";
|
|
810
1035
|
}
|
|
811
|
-
handlers.onTerminal(status);
|
|
1036
|
+
handlers.onTerminal(status, policyTerminal ? 400 : undefined);
|
|
812
1037
|
} finally {
|
|
813
1038
|
if (status === "failed" || status === "incomplete") clearCompletedItems();
|
|
814
1039
|
}
|
|
@@ -1087,6 +1312,13 @@ function startBoundedInspectionPump(options: InspectionPumpOptions): void {
|
|
|
1087
1312
|
}
|
|
1088
1313
|
}
|
|
1089
1314
|
} catch {
|
|
1315
|
+
// A read error can follow a final SSE block without its blank-line
|
|
1316
|
+
// delimiter. Flush that candidate before classifying the transport as a
|
|
1317
|
+
// synthetic reset; otherwise a real completed/failed/policy terminal is
|
|
1318
|
+
// downgraded to 502 on the inspection branch.
|
|
1319
|
+
if (!cancelled) {
|
|
1320
|
+
try { inspector.finish(); } catch { /* preserve the original read error */ }
|
|
1321
|
+
}
|
|
1090
1322
|
if (clientGone) clientGoneWithoutTerminal = !inspector.terminalSeen();
|
|
1091
1323
|
else if (!cancelled) options.onReadError?.();
|
|
1092
1324
|
} finally {
|