mixdog 0.9.87 → 0.9.89
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/package.json +13 -7
- package/scripts/tool-failures.mjs +60 -24
- package/src/agents/heavy-worker/AGENT.md +3 -4
- package/src/agents/worker/AGENT.md +1 -2
- package/src/cli.mjs +8 -0
- package/src/defaults/cycle3-review-prompt.md +4 -4
- package/src/rules/agent/00-core.md +3 -0
- package/src/rules/agent/42-cycle3-agent.md +5 -6
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/01-tool.md +29 -35
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
- package/src/runtime/agent/orchestrator/config.mjs +29 -11
- package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +160 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +50 -8
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
- package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
- package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
- package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
- package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
- package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
- package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +86 -3
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -8
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
- package/src/runtime/media/lanes.mjs +2 -2
- package/src/runtime/media/renditions.mjs +101 -17
- package/src/runtime/media/renditions.test.mjs +54 -0
- package/src/runtime/media/store.mjs +111 -43
- package/src/runtime/media/store.test.mjs +53 -11
- package/src/runtime/memory/index.mjs +26 -50
- package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
- package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
- package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
- package/src/runtime/memory/lib/http-router.mjs +4 -0
- package/src/runtime/memory/lib/ko-morph.mjs +49 -6
- package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
- package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
- package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
- package/src/runtime/memory/lib/query-handlers.mjs +26 -28
- package/src/runtime/memory/tool-defs.mjs +3 -3
- package/src/runtime/shared/atomic-file.mjs +5 -1
- package/src/runtime/shared/child-guardian.mjs +73 -2
- package/src/runtime/shared/provider-api-key.mjs +22 -5
- package/src/runtime/shared/tool-execution-contract.mjs +53 -0
- package/src/session-runtime/config-helpers.mjs +6 -5
- package/src/session-runtime/lifecycle-api.mjs +16 -4
- package/src/session-runtime/media-api.mjs +31 -16
- package/src/session-runtime/prewarm.mjs +2 -24
- package/src/session-runtime/runtime-core.mjs +33 -15
- package/src/session-runtime/runtime-tunables.mjs +0 -3
- package/src/session-runtime/session-lifecycle.mjs +0 -6
- package/src/session-runtime/session-title.mjs +228 -0
- package/src/session-runtime/session-turn-api.mjs +11 -7
- package/src/session-runtime/workflow-agents-api.mjs +16 -0
- package/src/session-runtime/workflow.mjs +7 -5
- package/src/standalone/agent-tool/job-views.mjs +90 -36
- package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
- package/src/standalone/agent-tool/worker-index.mjs +8 -0
- package/src/standalone/agent-tool/worker-rows.mjs +2 -1
- package/src/standalone/agent-tool.mjs +17 -17
- package/src/standalone/agent-watchdog-registry.mjs +19 -2
- package/src/standalone/channel-daemon.mjs +8 -0
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/standalone/memory-runtime-proxy.mjs +4 -7
- package/src/standalone/projects.mjs +4 -1
- package/src/standalone/usage-dashboard.mjs +25 -3
- package/src/tui/App.jsx +5 -3
- package/src/tui/app/resume-picker.mjs +7 -2
- package/src/tui/app/route-pickers.mjs +11 -117
- package/src/tui/app/slash-dispatch.mjs +13 -3
- package/src/tui/app/transcript-row-estimate.mjs +1 -1
- package/src/tui/app/transcript-window.mjs +17 -93
- package/src/tui/app/use-transcript-scroll.mjs +32 -2
- package/src/tui/app/use-transcript-window.mjs +64 -114
- package/src/tui/components/Markdown.jsx +19 -2
- package/src/tui/components/Message.jsx +8 -3
- package/src/tui/components/ToolExecution.jsx +7 -3
- package/src/tui/dist/index.mjs +243 -223
- package/src/tui/engine/live-share.mjs +15 -5
- package/src/tui/engine/render-timing.mjs +2 -2
- package/src/tui/engine/session-api-ext.mjs +74 -4
- package/src/tui/engine/session-flow.mjs +3 -1
- package/src/tui/engine.mjs +2 -2
- package/src/tui/hooks/useSharedTick.mjs +0 -2
- package/src/tui/index.jsx +5 -5
- package/src/ui/statusline-agents.mjs +44 -11
- package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
- package/src/workflows/default/WORKFLOW.md +3 -3
- package/src/workflows/solo/WORKFLOW.md +5 -3
- package/src/runtime/media/index.mjs +0 -18
- package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
- package/src/standalone/agent-shard/shard-child.mjs +0 -300
- package/src/standalone/agent-shard/shard-pool.mjs +0 -443
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
createTimeoutSignal,
|
|
7
7
|
providerTimeoutError,
|
|
8
8
|
} from '../stall-policy.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import { typedStatusFrom } from './retry-classifier.mjs';
|
|
10
|
+
import { stampStreamOutcome, STREAM_TRANSPORTS } from './lib/stream-outcome.mjs';
|
|
10
11
|
import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
|
|
11
12
|
import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
|
|
12
13
|
import { randomBytes } from 'crypto';
|
|
@@ -295,6 +296,12 @@ export async function consumeCompatChatCompletionStream(stream, {
|
|
|
295
296
|
// must be treated as permanent — the rendered text cannot be withdrawn and
|
|
296
297
|
// a retry would concatenate a second attempt.
|
|
297
298
|
let emittedText = false;
|
|
299
|
+
// Reasoning exposure invariant: reasoning_content / reasoning / thinking
|
|
300
|
+
// deltas are relayed to the client (onStreamDelta('reasoning')) and kept in
|
|
301
|
+
// the assembled message, so they are OBSERVED, VISIBLE output. A failure
|
|
302
|
+
// afterwards must never be replayed — a retry (or a non-streaming reset
|
|
303
|
+
// recovery) would duplicate the exposed reasoning.
|
|
304
|
+
let emittedReasoning = false;
|
|
298
305
|
let model = '';
|
|
299
306
|
let responseId = '';
|
|
300
307
|
let stopReason = null;
|
|
@@ -327,6 +334,32 @@ export async function consumeCompatChatCompletionStream(stream, {
|
|
|
327
334
|
return call;
|
|
328
335
|
};
|
|
329
336
|
const leakedCalls = [];
|
|
337
|
+
// Canonical stream-outcome stamp for EVERY reject path of this consumer.
|
|
338
|
+
// Without it the failure is "unknown" to the replay gates, and an upstream
|
|
339
|
+
// recovery (withRetry, transport fallback, non-streaming reset) could
|
|
340
|
+
// re-issue a turn whose text/reasoning was already relayed or whose tool
|
|
341
|
+
// call was already dispatched.
|
|
342
|
+
const _stampCompatOutcome = (err, extra = {}) => {
|
|
343
|
+
try {
|
|
344
|
+
stampStreamOutcome(err, {
|
|
345
|
+
transport: STREAM_TRANSPORTS.SSE,
|
|
346
|
+
provider: 'openai-compat',
|
|
347
|
+
terminalObserved: !!stopReason,
|
|
348
|
+
continuation: !stopReason,
|
|
349
|
+
textEmitted: emittedText === true,
|
|
350
|
+
textObservedChars: content.length,
|
|
351
|
+
reasoningEmitted: emittedReasoning === true || reasoningContent.length > 0,
|
|
352
|
+
toolCallsStarted: toolAcc.size > 0 || leakedCalls.length > 0,
|
|
353
|
+
toolCallsComplete: leakedCalls.length,
|
|
354
|
+
toolCallsDispatched: streamEmitState.emittedToolCall === true
|
|
355
|
+
? Math.max(1, leakedCalls.length)
|
|
356
|
+
: 0,
|
|
357
|
+
pendingToolInput: toolAcc.size > 0,
|
|
358
|
+
...extra,
|
|
359
|
+
});
|
|
360
|
+
} catch { /* stamping is best-effort */ }
|
|
361
|
+
return err;
|
|
362
|
+
};
|
|
330
363
|
const relayText = (delta) => {
|
|
331
364
|
const { text, calls } = leakGuard.push(delta);
|
|
332
365
|
if (text) {
|
|
@@ -407,6 +440,7 @@ export async function consumeCompatChatCompletionStream(stream, {
|
|
|
407
440
|
if (reasoningDelta !== null) {
|
|
408
441
|
reasoningContent += reasoningDelta;
|
|
409
442
|
if (reasoningDelta) {
|
|
443
|
+
emittedReasoning = true;
|
|
410
444
|
reportProgress('reasoning');
|
|
411
445
|
}
|
|
412
446
|
}
|
|
@@ -432,7 +466,7 @@ export async function consumeCompatChatCompletionStream(stream, {
|
|
|
432
466
|
err.pendingToolUse = toolAcc.size > 0 || leakedCalls.length > 0;
|
|
433
467
|
err.partialModel = model || undefined;
|
|
434
468
|
} catch { /* best-effort */ }
|
|
435
|
-
throw markUnsafeRetryIfToolEmitted(err, streamEmitState);
|
|
469
|
+
throw _stampCompatOutcome(markUnsafeRetryIfToolEmitted(err, streamEmitState));
|
|
436
470
|
}
|
|
437
471
|
// Partial-final recovery: on a mid-stream stall, attach the
|
|
438
472
|
// streamed partial state so the loop can accept a wedged FINAL no-tool
|
|
@@ -445,13 +479,14 @@ export async function consumeCompatChatCompletionStream(stream, {
|
|
|
445
479
|
err.partialModel = model || undefined;
|
|
446
480
|
} catch { /* best-effort */ }
|
|
447
481
|
}
|
|
448
|
-
throw markUnsafeRetryIfToolEmitted(err, streamEmitState);
|
|
482
|
+
throw _stampCompatOutcome(markUnsafeRetryIfToolEmitted(err, streamEmitState));
|
|
449
483
|
} finally {
|
|
450
484
|
firstByteTimeout.cleanup();
|
|
451
485
|
}
|
|
452
486
|
if (!sawFirstEvent) {
|
|
453
|
-
|
|
454
|
-
throw firstByteCompatStreamError(label);
|
|
487
|
+
// Pre-output: nothing was sampled, so this stays replay-safe.
|
|
488
|
+
if (firstByteTimeout.signal?.aborted) throw _stampCompatOutcome(firstByteCompatStreamError(label));
|
|
489
|
+
throw _stampCompatOutcome(firstByteCompatStreamError(label));
|
|
455
490
|
}
|
|
456
491
|
if (!stopReason) {
|
|
457
492
|
const err = truncatedCompatStreamError(label, 'no finish_reason');
|
|
@@ -468,7 +503,7 @@ export async function consumeCompatChatCompletionStream(stream, {
|
|
|
468
503
|
err.partialModel = model || undefined;
|
|
469
504
|
} catch { /* best-effort */ }
|
|
470
505
|
}
|
|
471
|
-
throw markUnsafeRetryIfToolEmitted(err, streamEmitState);
|
|
506
|
+
throw _stampCompatOutcome(markUnsafeRetryIfToolEmitted(err, streamEmitState));
|
|
472
507
|
}
|
|
473
508
|
const message = {
|
|
474
509
|
content: content || null,
|
|
@@ -496,7 +531,7 @@ export async function consumeCompatChatCompletionStream(stream, {
|
|
|
496
531
|
try { err.message += ` finish_reason=${stopReason}`; } catch {}
|
|
497
532
|
}
|
|
498
533
|
if (emittedText) markErrorLiveTextEmitted(err);
|
|
499
|
-
throw markUnsafeRetryIfToolEmitted(err, streamEmitState);
|
|
534
|
+
throw _stampCompatOutcome(markUnsafeRetryIfToolEmitted(err, streamEmitState));
|
|
500
535
|
}
|
|
501
536
|
if (Array.isArray(toolCalls) && toolCalls.length) {
|
|
502
537
|
for (const call of toolCalls) emitCompatToolCallOnce(streamEmitState, call, onToolCall);
|
|
@@ -572,6 +607,11 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
572
607
|
case 'response.reasoning_text.delta':
|
|
573
608
|
case 'response.reasoning_summary_text.delta':
|
|
574
609
|
if (event.delta) {
|
|
610
|
+
// Reasoning exposure latch (mirrors the Chat path): set at
|
|
611
|
+
// DELTA time, not at completion. Exposed reasoning cannot be
|
|
612
|
+
// withdrawn, so any later failure is non-replayable — a retry
|
|
613
|
+
// or non-streaming reset would duplicate it.
|
|
614
|
+
state.emittedReasoning = true;
|
|
575
615
|
try { onStreamDelta?.('reasoning'); } catch {}
|
|
576
616
|
}
|
|
577
617
|
break;
|
|
@@ -737,7 +777,7 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
737
777
|
else if (event.response.status === 'failed') {
|
|
738
778
|
const msg = event.response?.error?.message || 'response.done failed';
|
|
739
779
|
const err = new Error(`xAI Responses stream response.done failed: ${msg}`);
|
|
740
|
-
|
|
780
|
+
_applyTypedResponsesFailure(err, event);
|
|
741
781
|
throw err;
|
|
742
782
|
} else if (event.response.status === 'incomplete') {
|
|
743
783
|
const reason = incompleteReasonFromResponsesEvent(event);
|
|
@@ -764,11 +804,10 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
764
804
|
case 'response.failed': {
|
|
765
805
|
const msg = event.response?.error?.message || event.error?.message || event.message || 'response.failed';
|
|
766
806
|
const err = new Error(`xAI Responses stream response.failed: ${msg}`);
|
|
767
|
-
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
|
|
771
|
-
if (String(label || '').toLowerCase().startsWith('xai')) err.httpStatus = 500;
|
|
807
|
+
// The wire event's OWN typed status/code is preserved verbatim. A
|
|
808
|
+
// forbidden/unknown failure is never coerced into a synthetic 500:
|
|
809
|
+
// without typed evidence it stays unclassified and is surfaced.
|
|
810
|
+
_applyTypedResponsesFailure(err, event);
|
|
772
811
|
throw err;
|
|
773
812
|
}
|
|
774
813
|
case 'response.incomplete': {
|
|
@@ -791,8 +830,7 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
791
830
|
case 'error': {
|
|
792
831
|
const msg = event.message || event.error?.message || 'unknown';
|
|
793
832
|
const err = new Error(`xAI Responses stream error: ${msg}`);
|
|
794
|
-
|
|
795
|
-
if (String(label || '').toLowerCase().startsWith('xai')) err.httpStatus = 500;
|
|
833
|
+
_applyTypedResponsesFailure(err, event);
|
|
796
834
|
throw err;
|
|
797
835
|
}
|
|
798
836
|
default:
|
|
@@ -800,6 +838,20 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
800
838
|
}
|
|
801
839
|
}
|
|
802
840
|
|
|
841
|
+
// Copy the TYPED failure evidence a Responses `response.failed` / `error`
|
|
842
|
+
// event carries (numeric HTTP status, provider error code/type) onto the
|
|
843
|
+
// thrown error. Message text is never parsed, and nothing is synthesized when
|
|
844
|
+
// the event declares no typed status.
|
|
845
|
+
function _applyTypedResponsesFailure(err, event) {
|
|
846
|
+
const detail = event?.response?.error || event?.error || null;
|
|
847
|
+
const typed = typedStatusFrom(detail, event);
|
|
848
|
+
if (typed) err.httpStatus = typed;
|
|
849
|
+
const code = detail?.code ?? detail?.type ?? event?.code ?? null;
|
|
850
|
+
if (code != null && code !== '') err.providerErrorCode = String(code);
|
|
851
|
+
if (detail) err.providerError = detail;
|
|
852
|
+
return err;
|
|
853
|
+
}
|
|
854
|
+
|
|
803
855
|
export async function consumeCompatResponsesStream(stream, {
|
|
804
856
|
signal,
|
|
805
857
|
label,
|
|
@@ -847,6 +899,10 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
847
899
|
// has been forwarded. A later failure is non-retryable (rendered text
|
|
848
900
|
// cannot be withdrawn; a retry would concatenate attempts).
|
|
849
901
|
emittedText: false,
|
|
902
|
+
// Reasoning-exposure invariant, latched by
|
|
903
|
+
// handleCompatResponsesStreamEvent on the first non-empty reasoning
|
|
904
|
+
// delta (see the Chat path's emittedReasoning).
|
|
905
|
+
emittedReasoning: false,
|
|
850
906
|
semanticIdleDeadlineAt: 0,
|
|
851
907
|
};
|
|
852
908
|
const reportProgress = (kind) => {
|
|
@@ -894,6 +950,34 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
894
950
|
for (const c of calls) dispatchLeakedCall(c);
|
|
895
951
|
};
|
|
896
952
|
const deps = { label, parseResponsesToolCalls, responseOutputText, onStreamDelta: reportProgress, onToolCall, onTextDelta, relayLeakText };
|
|
953
|
+
// Canonical stream-outcome stamp for EVERY reject path of the Responses
|
|
954
|
+
// consumer — identical contract to the Chat consumer. Without it a failure
|
|
955
|
+
// is "unknown" to the replay gates and an upstream retry / transport
|
|
956
|
+
// fallback / non-streaming reset could duplicate exposed output.
|
|
957
|
+
const _toolInFlight = () => (state.pendingCalls?.size > 0)
|
|
958
|
+
|| (state.toolTracker?.items?.size > 0)
|
|
959
|
+
|| state.toolInFlight === true;
|
|
960
|
+
const _stampResponsesOutcome = (err, extra = {}) => {
|
|
961
|
+
try {
|
|
962
|
+
stampStreamOutcome(err, {
|
|
963
|
+
transport: STREAM_TRANSPORTS.SSE,
|
|
964
|
+
provider: 'openai-compat-responses',
|
|
965
|
+
terminalObserved: state.completed === true,
|
|
966
|
+
continuation: state.completed !== true,
|
|
967
|
+
textEmitted: state.emittedText === true,
|
|
968
|
+
textObservedChars: (state.content || '').length,
|
|
969
|
+
reasoningEmitted: state.emittedReasoning === true,
|
|
970
|
+
toolCallsStarted: state.toolCalls.length > 0 || leakedCalls.length > 0 || _toolInFlight(),
|
|
971
|
+
toolCallsComplete: state.toolCalls.length + leakedCalls.length,
|
|
972
|
+
toolCallsDispatched: state.emittedToolCall === true
|
|
973
|
+
? Math.max(1, state.emittedToolCallKeys?.size || 0)
|
|
974
|
+
: 0,
|
|
975
|
+
pendingToolInput: _toolInFlight(),
|
|
976
|
+
...extra,
|
|
977
|
+
});
|
|
978
|
+
} catch { /* stamping is best-effort */ }
|
|
979
|
+
return err;
|
|
980
|
+
};
|
|
897
981
|
try {
|
|
898
982
|
while (true) {
|
|
899
983
|
const { value: event, done } = await nextAsyncWithWatchdog(iterator, {
|
|
@@ -930,13 +1014,14 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
930
1014
|
err.partialModel = state.model || undefined;
|
|
931
1015
|
} catch { /* best-effort */ }
|
|
932
1016
|
}
|
|
933
|
-
throw markUnsafeRetryIfToolEmitted(err, state);
|
|
1017
|
+
throw _stampResponsesOutcome(markUnsafeRetryIfToolEmitted(err, state));
|
|
934
1018
|
} finally {
|
|
935
1019
|
firstByteTimeout.cleanup();
|
|
936
1020
|
}
|
|
937
1021
|
if (!sawFirstEvent) {
|
|
938
|
-
|
|
939
|
-
throw firstByteCompatStreamError(label);
|
|
1022
|
+
// Pre-output: nothing was sampled, so this stays replay-safe.
|
|
1023
|
+
if (firstByteTimeout.signal?.aborted) throw _stampResponsesOutcome(firstByteCompatStreamError(label));
|
|
1024
|
+
throw _stampResponsesOutcome(firstByteCompatStreamError(label));
|
|
940
1025
|
}
|
|
941
1026
|
if (!state.completed) {
|
|
942
1027
|
const err = truncatedCompatStreamError(label, 'no response.completed');
|
|
@@ -957,11 +1042,13 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
957
1042
|
err.partialModel = state.model || undefined;
|
|
958
1043
|
} catch { /* best-effort */ }
|
|
959
1044
|
}
|
|
960
|
-
throw err;
|
|
1045
|
+
throw _stampResponsesOutcome(err);
|
|
961
1046
|
}
|
|
962
1047
|
const unresolved = state.toolCalls.find(t => t._pendingItemId);
|
|
963
1048
|
if (unresolved) {
|
|
964
|
-
throw new Error(
|
|
1049
|
+
throw _stampResponsesOutcome(new Error(
|
|
1050
|
+
`xAI Responses stream function_call salvage failed: missing call_id/name for item_id=${unresolved._pendingItemId || '?'}`,
|
|
1051
|
+
));
|
|
965
1052
|
}
|
|
966
1053
|
const response = state.completedResponse || {
|
|
967
1054
|
id: state.responseId || null,
|
|
@@ -286,9 +286,10 @@ export class OpenAICompatProvider {
|
|
|
286
286
|
const structuredStatus = [err?.status, err?.httpStatus, err?.response?.status]
|
|
287
287
|
.map(value => Number(value))
|
|
288
288
|
.find(value => Number.isFinite(value) && value > 0) || 0;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
289
|
+
// Credential reload + reissue requires a TYPED 401. A message that
|
|
290
|
+
// merely mentions "401" is not evidence, and a typed 403 is a
|
|
291
|
+
// permission decision — reloading the key cannot change it.
|
|
292
|
+
const status = structuredStatus;
|
|
292
293
|
if (status === 401) {
|
|
293
294
|
if (err.liveTextEmitted === true || err.emittedToolCall === true || err.unsafeToRetry === true) {
|
|
294
295
|
throw err;
|
|
@@ -22,11 +22,13 @@ import {
|
|
|
22
22
|
createPassthroughSignal,
|
|
23
23
|
} from '../stall-policy.mjs';
|
|
24
24
|
import {
|
|
25
|
+
classifyError,
|
|
25
26
|
jitterDelayMs,
|
|
26
|
-
populateHttpStatusFromMessage,
|
|
27
27
|
shouldFallbackTransport,
|
|
28
28
|
sleepWithAbort,
|
|
29
|
+
typedStatusFrom,
|
|
29
30
|
} from './retry-classifier.mjs';
|
|
31
|
+
import { stampStreamOutcome, readStreamOutcome, STREAM_TRANSPORTS } from './lib/stream-outcome.mjs';
|
|
30
32
|
import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
|
|
31
33
|
import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
|
|
32
34
|
import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
|
|
@@ -111,6 +113,19 @@ function _isMaxOutputIncompleteReason(reason) {
|
|
|
111
113
|
return /^(?:max_output_tokens|max_tokens|length|output_token_limit)$/i.test(String(reason || '').trim());
|
|
112
114
|
}
|
|
113
115
|
|
|
116
|
+
// Wire-level `end_turn` on a terminal Responses frame (codex-rs
|
|
117
|
+
// codex-api/src/sse/responses.rs ResponseCompleted.end_turn: Option<bool>).
|
|
118
|
+
// Optional by contract: only a real boolean normalizes; a missing/non-boolean
|
|
119
|
+
// field stays undefined so absence is never collapsed into false.
|
|
120
|
+
export function _endTurnFromEvent(event) {
|
|
121
|
+
if (!event || typeof event !== 'object') return undefined;
|
|
122
|
+
const fromResponse = event.response?.end_turn;
|
|
123
|
+
if (typeof fromResponse === 'boolean') return fromResponse;
|
|
124
|
+
const topLevel = event.end_turn;
|
|
125
|
+
if (typeof topLevel === 'boolean') return topLevel;
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
114
129
|
function _pushOutputTextAnnotations(part, citations, citationKeys) {
|
|
115
130
|
const annotations = Array.isArray(part?.annotations) ? part.annotations : [];
|
|
116
131
|
for (const raw of annotations) {
|
|
@@ -166,6 +181,9 @@ function _buildOpenAIHttpFallbackHeaders({ auth, cacheKey }) {
|
|
|
166
181
|
// transport fallback, even if a caller has marked a prior WS attempt exhausted.
|
|
167
182
|
export function _shouldUseOpenAIHttpFallback(err, externalSignal) {
|
|
168
183
|
if (Number(err?.httpStatus || 0) === 429) return false;
|
|
184
|
+
// Codex switches WS→HTTPS on a typed transport failure; the only extra
|
|
185
|
+
// deny is exposure (relayed text / dispatched tool), which the shared
|
|
186
|
+
// predicate reads off the canonical record stamped by the WS transport.
|
|
169
187
|
return shouldFallbackTransport(err, {
|
|
170
188
|
signal: externalSignal,
|
|
171
189
|
enabled: _envFlag('MIXDOG_OPENAI_OAUTH_HTTP_FALLBACK', true),
|
|
@@ -250,10 +268,26 @@ export async function sendViaHttpSse({
|
|
|
250
268
|
}
|
|
251
269
|
|
|
252
270
|
const retryableStatus = response && response.status >= 500 && response.status <= 599;
|
|
271
|
+
// Typed transient transport failures only (errno / SDK connection
|
|
272
|
+
// type). An unknown pre-response failure throws immediately instead of
|
|
273
|
+
// re-issuing the POST.
|
|
253
274
|
const retryableTransport = !response && requestError
|
|
275
|
+
&& classifyError(requestError) === 'transient'
|
|
254
276
|
&& !externalSignal?.aborted
|
|
255
277
|
&& !totalTimeout.signal?.aborted;
|
|
256
278
|
if (attempt < CODEX_REQUEST_MAX_RETRIES && (retryableStatus || retryableTransport)) {
|
|
279
|
+
// Reissuing the POST is a REPLAY: allowed for a typed transient
|
|
280
|
+
// failure of the initial request, denied once the failure carries
|
|
281
|
+
// exposure evidence (relayed output / dispatched tool call).
|
|
282
|
+
const attemptFailure = requestError || Object.assign(
|
|
283
|
+
new Error(`OpenAI OAuth HTTP fallback ${response.status}`),
|
|
284
|
+
{ httpStatus: response.status, headers: response.headers, initialResponseError: true },
|
|
285
|
+
);
|
|
286
|
+
if (readStreamOutcome(attemptFailure).replaySafe !== true) {
|
|
287
|
+
if (response) await response.arrayBuffer().catch(() => {});
|
|
288
|
+
totalTimeout.cleanup();
|
|
289
|
+
throw attemptFailure;
|
|
290
|
+
}
|
|
257
291
|
// A non-success response has not exposed any streamed output. Drain
|
|
258
292
|
// its body before reissuing so the dispatcher can reuse the socket.
|
|
259
293
|
if (response) await response.arrayBuffer().catch(() => {});
|
|
@@ -269,6 +303,8 @@ export async function sendViaHttpSse({
|
|
|
269
303
|
}
|
|
270
304
|
if (requestError) {
|
|
271
305
|
totalTimeout.cleanup();
|
|
306
|
+
// The initial response never arrived: nothing was sampled, so the
|
|
307
|
+
// typed rules upstream decide whether to retry.
|
|
272
308
|
throw requestError;
|
|
273
309
|
}
|
|
274
310
|
break;
|
|
@@ -288,13 +324,13 @@ export async function sendViaHttpSse({
|
|
|
288
324
|
const err = new Error(`OpenAI OAuth HTTP fallback ${response.status}: ${text.slice(0, 200)}`);
|
|
289
325
|
err.httpStatus = response.status;
|
|
290
326
|
err.headers = response.headers;
|
|
291
|
-
|
|
327
|
+
err.initialResponseError = true;
|
|
292
328
|
totalTimeout.cleanup();
|
|
293
329
|
throw err;
|
|
294
330
|
}
|
|
295
331
|
if (!response.body) {
|
|
296
332
|
totalTimeout.cleanup();
|
|
297
|
-
throw new Error('OpenAI OAuth HTTP fallback returned no response body');
|
|
333
|
+
throw Object.assign(new Error('OpenAI OAuth HTTP fallback returned no response body'), { initialResponseError: true });
|
|
298
334
|
}
|
|
299
335
|
|
|
300
336
|
try { onStreamDelta?.('transport'); } catch {}
|
|
@@ -423,12 +459,21 @@ export async function sendViaHttpSse({
|
|
|
423
459
|
const webSearchCallKeys = new Set();
|
|
424
460
|
let completed = false;
|
|
425
461
|
let stopReason = null;
|
|
462
|
+
// Normalized wire `end_turn` from the terminal frame; undefined unless the
|
|
463
|
+
// server actually supplied a boolean.
|
|
464
|
+
let endTurn;
|
|
426
465
|
// Gateway live-text relay invariant: set once a non-empty text chunk has
|
|
427
466
|
// been forwarded to the client. A failure afterwards is non-retryable —
|
|
428
467
|
// the rendered text cannot be withdrawn and a re-request would concatenate
|
|
429
468
|
// a second attempt.
|
|
430
469
|
let emittedText = false;
|
|
431
470
|
|
|
471
|
+
// Reasoning-exposure invariant: set the moment a reasoning/summary delta is
|
|
472
|
+
// seen (NOT at completion, where reasoningItems is assembled). Exposed
|
|
473
|
+
// reasoning is a replay boundary for retry, transport fallback and the
|
|
474
|
+
// reactive compact retry.
|
|
475
|
+
let emittedReasoning = false;
|
|
476
|
+
|
|
432
477
|
// Tool-emit invariant (mirrors emittedText, WS path's emittedToolCall): set
|
|
433
478
|
// once onToolCall has actually dispatched a call. A failure afterwards is
|
|
434
479
|
// non-retryable — the side-effecting tool already ran, and any upstream
|
|
@@ -439,6 +484,32 @@ export async function sendViaHttpSse({
|
|
|
439
484
|
if (emittedToolCall && err) { try { err.emittedToolCall = true; err.unsafeToRetry = true; } catch {} }
|
|
440
485
|
return err;
|
|
441
486
|
};
|
|
487
|
+
// Canonical stream-outcome contract for the HTTP/SSE transport. ONE hint
|
|
488
|
+
// builder is shared by the mid-stream catch and by every post-loop reject
|
|
489
|
+
// so no reject path can escape unstamped (an unstamped outcome is
|
|
490
|
+
// "unknown", which the consumers treat as fail-closed).
|
|
491
|
+
const _outcomeHints = (extra = {}) => ({
|
|
492
|
+
transport: STREAM_TRANSPORTS.HTTP_SSE,
|
|
493
|
+
provider: 'openai-responses',
|
|
494
|
+
terminalObserved: completed === true,
|
|
495
|
+
continuation: completed !== true,
|
|
496
|
+
textEmitted: emittedText === true,
|
|
497
|
+
textObservedChars: content.length,
|
|
498
|
+
reasoningEmitted: emittedReasoning === true || reasoningItems.length > 0,
|
|
499
|
+
toolCallsStarted: pendingCalls.size > 0 || activeToolItems.size > 0
|
|
500
|
+
|| _toolInFlight === true || toolCalls.length > 0,
|
|
501
|
+
toolCallsComplete: toolCalls.length,
|
|
502
|
+
toolCallsDispatched: emittedToolCallIds.size,
|
|
503
|
+
pendingToolInput: pendingCalls.size > 0 || activeToolItems.size > 0 || _toolInFlight === true,
|
|
504
|
+
// Protocol distinction: a terminal frame carrying end_turn=false keeps
|
|
505
|
+
// the SAME user turn open — terminal observed, still a continuation.
|
|
506
|
+
...(endTurn === false ? { continuationDeclared: true } : {}),
|
|
507
|
+
...extra,
|
|
508
|
+
});
|
|
509
|
+
const _stampOutcome = (err, extra = {}) => {
|
|
510
|
+
try { stampStreamOutcome(err, _outcomeHints(extra)); } catch { /* best-effort */ }
|
|
511
|
+
return err;
|
|
512
|
+
};
|
|
442
513
|
|
|
443
514
|
// Single-emit guard for tool calls (matches the WS path's
|
|
444
515
|
// emittedToolCall intent). The HTTP/SSE event stream can surface the
|
|
@@ -593,7 +664,13 @@ export async function sendViaHttpSse({
|
|
|
593
664
|
break;
|
|
594
665
|
case 'response.reasoning_text.delta':
|
|
595
666
|
case 'response.reasoning_summary_text.delta':
|
|
596
|
-
if (event.delta)
|
|
667
|
+
if (event.delta) {
|
|
668
|
+
// Reasoning exposure is a replay boundary the MOMENT a delta
|
|
669
|
+
// arrives — not at response completion. A failure after this
|
|
670
|
+
// point must never be re-issued (duplicate exposed thinking).
|
|
671
|
+
emittedReasoning = true;
|
|
672
|
+
meaningful('reasoning');
|
|
673
|
+
}
|
|
597
674
|
break;
|
|
598
675
|
case 'response.output_item.added':
|
|
599
676
|
if (event.item?.type === 'function_call') {
|
|
@@ -786,14 +863,24 @@ export async function sendViaHttpSse({
|
|
|
786
863
|
}
|
|
787
864
|
if (!reportedBundleProgress) meaningful('semantic');
|
|
788
865
|
completed = true;
|
|
866
|
+
{
|
|
867
|
+
const wireEndTurn = _endTurnFromEvent(event);
|
|
868
|
+
if (typeof wireEndTurn === 'boolean') endTurn = wireEndTurn;
|
|
869
|
+
}
|
|
789
870
|
break;
|
|
790
871
|
}
|
|
791
872
|
case 'response.done':
|
|
792
|
-
if (!event.response || event.response.status === 'completed')
|
|
793
|
-
|
|
873
|
+
if (!event.response || event.response.status === 'completed') {
|
|
874
|
+
completed = true;
|
|
875
|
+
// Terminal success frame for streams that never emit a
|
|
876
|
+
// separate response.completed — same optional end_turn.
|
|
877
|
+
const wireEndTurn = _endTurnFromEvent(event);
|
|
878
|
+
if (typeof wireEndTurn === 'boolean') endTurn = wireEndTurn;
|
|
879
|
+
} else if (event.response.status === 'failed') {
|
|
794
880
|
const msg = event.response?.error?.message || 'response.done failed';
|
|
795
881
|
const err = new Error(`OpenAI OAuth HTTP fallback response.done failed: ${msg}`);
|
|
796
|
-
|
|
882
|
+
const typed = typedStatusFrom(event.response?.error, event.error, event);
|
|
883
|
+
if (typed) err.httpStatus = typed;
|
|
797
884
|
throw err;
|
|
798
885
|
} else if (event.response.status === 'incomplete') {
|
|
799
886
|
const reason = _incompleteReasonFromEvent(event);
|
|
@@ -822,7 +909,9 @@ export async function sendViaHttpSse({
|
|
|
822
909
|
case 'response.failed': {
|
|
823
910
|
const msg = event.response?.error?.message || event.error?.message || event.message || 'response.failed';
|
|
824
911
|
const err = new Error(`OpenAI OAuth HTTP fallback response.failed: ${msg}`);
|
|
825
|
-
|
|
912
|
+
// Typed status only — a text-only failure stays unclassified.
|
|
913
|
+
const typed = typedStatusFrom(event.response?.error, event.error, event);
|
|
914
|
+
if (typed) err.httpStatus = typed;
|
|
826
915
|
throw err;
|
|
827
916
|
}
|
|
828
917
|
case 'response.incomplete': {
|
|
@@ -848,7 +937,8 @@ export async function sendViaHttpSse({
|
|
|
848
937
|
case 'error': {
|
|
849
938
|
const msg = event.message || event.error?.message || 'unknown';
|
|
850
939
|
const err = new Error(`OpenAI OAuth HTTP fallback error: ${msg}`);
|
|
851
|
-
|
|
940
|
+
const typed = typedStatusFrom(event.error, event);
|
|
941
|
+
if (typed) err.httpStatus = typed;
|
|
852
942
|
throw err;
|
|
853
943
|
}
|
|
854
944
|
default:
|
|
@@ -903,6 +993,8 @@ export async function sendViaHttpSse({
|
|
|
903
993
|
// Tool-emit invariant: an error after a dispatched tool call must not
|
|
904
994
|
// reissue the turn (double-execution). Stamp emittedToolCall too.
|
|
905
995
|
_stampToolSafety(err);
|
|
996
|
+
// Canonical record (same shape as the WS path); aliases above preserved.
|
|
997
|
+
_stampOutcome(err);
|
|
906
998
|
throw err;
|
|
907
999
|
} finally {
|
|
908
1000
|
_pendingReadReject = null;
|
|
@@ -917,10 +1009,28 @@ export async function sendViaHttpSse({
|
|
|
917
1009
|
|
|
918
1010
|
const unresolved = toolCalls.find(t => t._pendingItemId);
|
|
919
1011
|
if (unresolved) {
|
|
920
|
-
throw _stampToolSafety(new Error(
|
|
1012
|
+
throw _stampOutcome(_stampToolSafety(new Error(
|
|
1013
|
+
`OpenAI OAuth HTTP fallback function_call salvage failed: missing call_id/name for item_id=${unresolved._pendingItemId || '?'}`,
|
|
1014
|
+
)));
|
|
921
1015
|
}
|
|
922
|
-
|
|
923
|
-
|
|
1016
|
+
// EOF without a terminal frame is ALWAYS a failure, regardless of how much
|
|
1017
|
+
// partial text or how many tool calls were streamed. The turn has no
|
|
1018
|
+
// terminal signal, so it is a continuation: returning it would report an
|
|
1019
|
+
// unfinished sample as a completed assistant turn (and, with tool calls
|
|
1020
|
+
// already dispatched, a half-finished side-effecting turn). The partial
|
|
1021
|
+
// rides on the error for interrupted-turn persistence.
|
|
1022
|
+
if (!completed) {
|
|
1023
|
+
const err = _stampToolSafety(new Error(
|
|
1024
|
+
`OpenAI OAuth HTTP fallback ended before response.completed `
|
|
1025
|
+
+ `(text=${content.length} chars, toolCalls=${toolCalls.length})`,
|
|
1026
|
+
));
|
|
1027
|
+
try {
|
|
1028
|
+
err.partialContent = content;
|
|
1029
|
+
err.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
|
|
1030
|
+
err.partialModel = model || undefined;
|
|
1031
|
+
err.pendingToolUse = pendingCalls.size > 0 || activeToolItems.size > 0 || _toolInFlight === true;
|
|
1032
|
+
} catch { /* best-effort enrichment */ }
|
|
1033
|
+
throw _stampOutcome(err, { terminalObserved: false, continuation: true });
|
|
924
1034
|
}
|
|
925
1035
|
|
|
926
1036
|
const liveModel = model || useModel;
|
|
@@ -963,6 +1073,8 @@ export async function sendViaHttpSse({
|
|
|
963
1073
|
webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
|
|
964
1074
|
usage: usage || undefined,
|
|
965
1075
|
stopReason: stopReason || undefined,
|
|
1076
|
+
// Only present when the terminal frame carried the wire field.
|
|
1077
|
+
...(typeof endTurn === 'boolean' ? { endTurn } : {}),
|
|
966
1078
|
// P1 audit fix: text-only max-output cutoff (openai-oauth HTTP/SSE
|
|
967
1079
|
// fallback maps status:'incomplete'/reason=max_output_tokens to
|
|
968
1080
|
// stopReason='length' above and treats it as success). Flag it so
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
MIDSTREAM_RETRY_POLICY,
|
|
41
41
|
sleepWithAbort,
|
|
42
42
|
} from './retry-classifier.mjs';
|
|
43
|
+
import { stampStreamOutcome, STREAM_TRANSPORTS } from './lib/stream-outcome.mjs';
|
|
43
44
|
import {
|
|
44
45
|
WS_IDLE_MS,
|
|
45
46
|
acquireWebSocket,
|
|
@@ -977,15 +978,31 @@ export async function sendViaWebSocket({
|
|
|
977
978
|
// duplicate exposed thinking/tool argument streams and can make a
|
|
978
979
|
// partially generated side-effecting call diverge. Keep the Codex
|
|
979
980
|
// retry budget only for failures before any such model output.
|
|
981
|
+
// Exposed reasoning is a visibility boundary. A tool call that only
|
|
982
|
+
// STARTED assembling was never dispatched, so it is recorded but is
|
|
983
|
+
// NOT a side effect and must not veto a retry.
|
|
980
984
|
if (midState.emittedReasoning || midState.startedToolCall) {
|
|
981
985
|
try {
|
|
982
|
-
|
|
983
|
-
|
|
986
|
+
if (midState.emittedReasoning) {
|
|
987
|
+
err.unsafeToRetry = true;
|
|
988
|
+
err.partialReasoningEmitted = true;
|
|
989
|
+
}
|
|
984
990
|
if (midState.startedToolCall) err.partialToolCallStarted = true;
|
|
985
991
|
} catch {}
|
|
986
992
|
}
|
|
987
993
|
_stampLiveText(err);
|
|
988
994
|
_stampTool(err);
|
|
995
|
+
// Canonical stream-outcome record for the WS transport, merged
|
|
996
|
+
// with the cross-attempt safety latches above. _streamResponse
|
|
997
|
+
// already stamps its own reject paths; this covers frame-send /
|
|
998
|
+
// handshake-adjacent failures that never reached the stream loop.
|
|
999
|
+
try {
|
|
1000
|
+
stampStreamOutcome(err, midState, {
|
|
1001
|
+
transport: STREAM_TRANSPORTS.WS,
|
|
1002
|
+
provider: 'openai-oauth',
|
|
1003
|
+
continuation: midState.sawCompleted !== true,
|
|
1004
|
+
});
|
|
1005
|
+
} catch { /* stamping is best-effort */ }
|
|
989
1006
|
const classifier = err?.unsafeToRetry === true
|
|
990
1007
|
? null
|
|
991
1008
|
: _classifyMidstreamError(err, midState);
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
createTimeoutSignal,
|
|
40
40
|
createPassthroughSignal,
|
|
41
41
|
} from '../stall-policy.mjs';
|
|
42
|
-
import {
|
|
42
|
+
import { shouldFallbackTransport } from './retry-classifier.mjs';
|
|
43
43
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
44
44
|
import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
|
|
45
45
|
import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
createTimeoutSignal,
|
|
41
41
|
createPassthroughSignal,
|
|
42
42
|
} from '../stall-policy.mjs';
|
|
43
|
-
import {
|
|
43
|
+
import { shouldFallbackTransport } from './retry-classifier.mjs';
|
|
44
44
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
45
45
|
import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
|
|
46
46
|
import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
|