mixdog 0.9.50 → 0.9.52
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 +5 -3
- package/scripts/abort-recovery-test.mjs +17 -1
- package/scripts/agent-model-liveness-test.mjs +79 -2
- package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
- package/scripts/anthropic-transport-policy-test.mjs +466 -0
- package/scripts/atomic-lock-tryonce-test.mjs +60 -1
- package/scripts/build-tui.mjs +13 -1
- package/scripts/channel-daemon-smoke.mjs +630 -10
- package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
- package/scripts/code-graph-disk-hit-test.mjs +11 -0
- package/scripts/code-graph-root-federation-test.mjs +273 -0
- package/scripts/compact-pressure-test.mjs +55 -1
- package/scripts/compact-smoke.mjs +80 -0
- package/scripts/context-mcp-metering-test.mjs +1350 -19
- package/scripts/deferred-tool-loading-test.mjs +17 -0
- package/scripts/gemini-provider-test.mjs +1053 -0
- package/scripts/hook-bus-test.mjs +23 -0
- package/scripts/internal-tools-normalization-test.mjs +10 -0
- package/scripts/interrupted-turn-history-test.mjs +371 -0
- package/scripts/lifecycle-api-test.mjs +76 -0
- package/scripts/max-output-recovery-test.mjs +55 -0
- package/scripts/mcp-grace-deferred-test.mjs +89 -13
- package/scripts/memory-pg-recovery-test.mjs +59 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +391 -4
- package/scripts/process-lifecycle-test.mjs +389 -0
- package/scripts/provider-admission-scheduler-test.mjs +582 -0
- package/scripts/provider-contract-test.mjs +268 -0
- package/scripts/provider-toolcall-test.mjs +520 -3
- package/scripts/reactive-compact-persist-smoke.mjs +59 -0
- package/scripts/resource-admission-test.mjs +789 -0
- package/scripts/session-bench-cache-break-test.mjs +102 -0
- package/scripts/session-bench.mjs +101 -42
- package/scripts/shell-failure-diagnostics-test.mjs +73 -4
- package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
- package/scripts/smoke-loop-failure-summary-test.mjs +38 -0
- package/scripts/smoke-loop-failure-summary.mjs +16 -0
- package/scripts/smoke-loop.mjs +2 -7
- package/scripts/steering-drain-buckets-test.mjs +18 -0
- package/scripts/tool-failures.mjs +15 -1
- package/scripts/toolcall-args-test.mjs +14 -6
- package/scripts/tui-transcript-perf-test.mjs +43 -7
- package/scripts/web-fetch-routing-test.mjs +158 -0
- package/src/cli.mjs +15 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +26 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -0
- package/src/runtime/agent/orchestrator/internal-tools.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +118 -26
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +29 -17
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -38
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +24 -4
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +95 -46
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +89 -17
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +93 -26
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +144 -51
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +74 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +111 -6
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -17
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +224 -104
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +127 -55
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +69 -37
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +6 -7
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +24 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +89 -22
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +380 -37
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -14
- package/src/runtime/memory/index.mjs +22 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
- package/src/runtime/memory/lib/pg/process.mjs +91 -47
- package/src/runtime/memory/lib/pg/supervisor.mjs +50 -13
- package/src/runtime/search/index.mjs +41 -0
- package/src/runtime/search/lib/http-fetch.mjs +154 -0
- package/src/runtime/search/tool-defs.mjs +23 -0
- package/src/runtime/shared/atomic-file.mjs +28 -150
- package/src/runtime/shared/process-lifecycle.mjs +363 -0
- package/src/runtime/shared/process-shutdown.mjs +27 -4
- package/src/runtime/shared/resource-admission.mjs +359 -0
- package/src/runtime/shared/staged-child-result.mjs +19 -0
- package/src/session-runtime/context-status.mjs +38 -27
- package/src/session-runtime/lifecycle-api.mjs +26 -6
- package/src/session-runtime/provider-request-tools.mjs +72 -0
- package/src/session-runtime/runtime-core.mjs +43 -18
- package/src/session-runtime/session-turn-api.mjs +5 -4
- package/src/session-runtime/tool-catalog.mjs +375 -15
- package/src/standalone/agent-tool.mjs +17 -38
- package/src/standalone/channel-daemon-client.mjs +200 -38
- package/src/standalone/channel-daemon-transport.mjs +136 -9
- package/src/standalone/channel-worker.mjs +79 -12
- package/src/standalone/hook-bus/handlers.mjs +4 -0
- package/src/standalone/hook-bus/rules.mjs +7 -1
- package/src/standalone/hook-bus.mjs +10 -2
- package/src/tui/App.jsx +17 -11
- package/src/tui/app/live-spinner-visibility.mjs +20 -0
- package/src/tui/dist/index.mjs +101 -281
- package/src/tui/engine/session-api-ext.mjs +13 -2
- package/src/tui/engine/session-api.mjs +1 -1
- package/src/tui/engine/turn.mjs +10 -6
- package/src/tui/engine.mjs +13 -4
- package/src/tui/index.jsx +4 -1
- package/src/tui/lib/voice-setup.mjs +16 -0
|
@@ -40,9 +40,6 @@ import {
|
|
|
40
40
|
MIDSTREAM_RETRY_POLICY,
|
|
41
41
|
sleepWithAbort,
|
|
42
42
|
} from './retry-classifier.mjs';
|
|
43
|
-
import {
|
|
44
|
-
PROVIDER_RETRY_MAX_ATTEMPTS,
|
|
45
|
-
} from '../stall-policy.mjs';
|
|
46
43
|
import {
|
|
47
44
|
WS_IDLE_MS,
|
|
48
45
|
acquireWebSocket,
|
|
@@ -84,12 +81,14 @@ export {
|
|
|
84
81
|
globalThis.__mixdogOpenaiWsRuntimeLoaded = true;
|
|
85
82
|
|
|
86
83
|
// --- WS_PRE_RESPONSE_CREATED_MS / WS_INTER_CHUNK_MS: extracted to openai-ws-stream.mjs ---
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
// and ensure the numbers exist in exactly ONE place.
|
|
84
|
+
// The official Codex Responses policy has one five-retry stream budget shared
|
|
85
|
+
// by connect/handshake and pre-output stream failures.
|
|
90
86
|
const MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT = MIDSTREAM_RETRY_POLICY.ws.transientCloseRetries;
|
|
91
87
|
const MIDSTREAM_DEFAULT_RETRY_LIMIT = MIDSTREAM_RETRY_POLICY.ws.defaultRetries;
|
|
92
|
-
|
|
88
|
+
// Codex core/src/util.rs uses a 200ms base, factor 2, and symmetric ±10%
|
|
89
|
+
// jitter for each of its five stream retries.
|
|
90
|
+
const MIDSTREAM_BACKOFF_MS = Object.freeze([200, 400, 800, 1600, 3200]);
|
|
91
|
+
const CODEX_RETRY_JITTER_RATIO = 0.1;
|
|
93
92
|
// Policy object passed to the shared classifyMidstreamError for the WS path.
|
|
94
93
|
const WS_MIDSTREAM_POLICY = {
|
|
95
94
|
mode: 'ws',
|
|
@@ -97,20 +96,12 @@ const WS_MIDSTREAM_POLICY = {
|
|
|
97
96
|
defaultRetries: MIDSTREAM_RETRY_POLICY.ws.defaultRetries,
|
|
98
97
|
};
|
|
99
98
|
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// decision and just double the user-visible latency.
|
|
107
|
-
// Aligned to the cross-provider default (retry-classifier DEFAULT_MAX_ATTEMPTS=5,
|
|
108
|
-
// anthropic-oauth MAX_ATTEMPTS=5, withRetry-using providers all default to 5).
|
|
109
|
-
// Bumped from 3 so this provider exhausts the same number of transient-5xx
|
|
110
|
-
// attempts as the others before surfacing failure to the caller.
|
|
111
|
-
const HANDSHAKE_MAX_ATTEMPTS = PROVIDER_RETRY_MAX_ATTEMPTS;
|
|
112
|
-
const HANDSHAKE_BACKOFF_BASE_MS = 500;
|
|
113
|
-
const HANDSHAKE_BACKOFF_CAP_MS = 5000;
|
|
99
|
+
// Kept for the exported _acquireWithRetry test seam and non-Codex callers.
|
|
100
|
+
// sendViaWebSocket deliberately invokes it with maxAttempts:1 so handshake
|
|
101
|
+
// failures consume the same stream budget as pre-output disconnects.
|
|
102
|
+
const HANDSHAKE_MAX_ATTEMPTS = MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT + 1;
|
|
103
|
+
const HANDSHAKE_BACKOFF_BASE_MS = 200;
|
|
104
|
+
const HANDSHAKE_BACKOFF_CAP_MS = 3200;
|
|
114
105
|
|
|
115
106
|
// --- WS pool/handshake/acquire/release: extracted to openai-ws-pool.mjs ---
|
|
116
107
|
|
|
@@ -152,22 +143,21 @@ export function _classifyHandshakeError(err) {
|
|
|
152
143
|
* after the 101 upgrade but before the first response.created event,
|
|
153
144
|
* AND the pre-`response.created` first-byte timeout
|
|
154
145
|
* (state.firstByteTimeout), which is permitted a bounded retry here
|
|
155
|
-
* - HTTP 401 / 403 / 429 surfaced
|
|
146
|
+
* - HTTP 401 / 403 / 429 surfaced after the WS handshake
|
|
156
147
|
* - state.attemptIndex has reached the classifier-specific retry budget
|
|
157
148
|
*/
|
|
158
149
|
// Thin wrapper: the full WS mid-stream decision tree now lives in the shared
|
|
159
150
|
// classifyMidstreamError (retry-classifier.mjs, policy.mode='ws'). Kept as a
|
|
160
151
|
// named export so internal call sites and any external importer keep resolving
|
|
161
|
-
// the same symbol.
|
|
162
|
-
//
|
|
163
|
-
// WS_MIDSTREAM_POLICY (transientCloseRetries=4, defaultRetries=2).
|
|
152
|
+
// the same symbol. The per-classifier gates both use the official five-retry
|
|
153
|
+
// stream budget.
|
|
164
154
|
export function _classifyMidstreamError(err, state) {
|
|
165
155
|
return classifyMidstreamError(err, state, WS_MIDSTREAM_POLICY);
|
|
166
156
|
}
|
|
167
157
|
|
|
168
158
|
// Per-classifier retry budget, used by the sendViaWebSocket loop to bound the
|
|
169
159
|
// attempt count once classifyMidstreamError returns a bucket. Mirrors the
|
|
170
|
-
// shared _midstreamLimitFor(ws) — the
|
|
160
|
+
// shared _midstreamLimitFor(ws) — both values are the same unified budget.
|
|
171
161
|
function _midstreamRetryLimit(classifier) {
|
|
172
162
|
return classifier === 'ws_1006' || classifier === 'ws_1011'
|
|
173
163
|
? MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT
|
|
@@ -176,13 +166,13 @@ function _midstreamRetryLimit(classifier) {
|
|
|
176
166
|
|
|
177
167
|
function _midstreamBackoffFor(retryNumber) {
|
|
178
168
|
const raw = MIDSTREAM_BACKOFF_MS[Math.min(Math.max(retryNumber, 1), MIDSTREAM_BACKOFF_MS.length) - 1];
|
|
179
|
-
return jitterDelayMs(raw);
|
|
169
|
+
return jitterDelayMs(raw, CODEX_RETRY_JITTER_RATIO);
|
|
180
170
|
}
|
|
181
171
|
|
|
182
172
|
function _backoffFor(attempt) {
|
|
183
173
|
// attempt is 1-based. retry 1 → 500, retry 2 → 1000, retry 3 → 2000 … capped.
|
|
184
174
|
const raw = HANDSHAKE_BACKOFF_BASE_MS * (1 << (attempt - 1));
|
|
185
|
-
return jitterDelayMs(Math.min(raw, HANDSHAKE_BACKOFF_CAP_MS));
|
|
175
|
+
return jitterDelayMs(Math.min(raw, HANDSHAKE_BACKOFF_CAP_MS), CODEX_RETRY_JITTER_RATIO);
|
|
186
176
|
}
|
|
187
177
|
|
|
188
178
|
const _defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -648,6 +638,11 @@ export async function sendViaWebSocket({
|
|
|
648
638
|
traceProvider = 'openai-oauth',
|
|
649
639
|
logSuppressedReasoningDeltas = true,
|
|
650
640
|
warmupBody = null,
|
|
641
|
+
// Provider-specific handshake policy seam. OAuth leaves this null and
|
|
642
|
+
// retains the Codex retry budget. Direct OpenAI uses it to surface
|
|
643
|
+
// unsupported-WS handshake statuses immediately so its wrapper can make
|
|
644
|
+
// the single WS→HTTP decision without nested WS retries.
|
|
645
|
+
handshakeErrorPolicy = null,
|
|
651
646
|
// Test seams (undefined in production). Let the unit test drive the
|
|
652
647
|
// retry state machine without opening real sockets or touching the
|
|
653
648
|
// handshake-retry layer.
|
|
@@ -656,18 +651,11 @@ export async function sendViaWebSocket({
|
|
|
656
651
|
_sendFrameFn = _sendFrame,
|
|
657
652
|
_sleepFn = _defaultSleep,
|
|
658
653
|
_sendSpanTraceFn = appendAgentTrace,
|
|
654
|
+
_agentTraceFn = appendAgentTrace,
|
|
659
655
|
}) {
|
|
660
|
-
//
|
|
661
|
-
//
|
|
662
|
-
//
|
|
663
|
-
// error), tear down the socket and reissue the full request from scratch
|
|
664
|
-
// with a classifier-specific budget. ws_1006/ws_1011 get two retries with
|
|
665
|
-
// 250ms/1s backoff; other legacy transient buckets keep the prior one retry.
|
|
666
|
-
// No delta resume — content restarts, which is the accepted tradeoff for
|
|
667
|
-
// reviewer/worker flows that need the complete answer.
|
|
668
|
-
// Retries are layered ABOVE the handshake retry loop (_acquireWithRetry
|
|
669
|
-
// owns connect-level transience); the two never interleave because we
|
|
670
|
-
// force a brand-new acquire for the retry attempt.
|
|
656
|
+
// One bounded Codex stream retry budget covers transient handshake and
|
|
657
|
+
// pre-output stream failures. Every retry acquires a fresh connection.
|
|
658
|
+
// No replay is permitted after live text or an emitted tool call.
|
|
671
659
|
const MAX_MIDSTREAM_RETRIES = MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT;
|
|
672
660
|
let firstAttemptError = null;
|
|
673
661
|
let firstAttemptClassifier = null;
|
|
@@ -783,20 +771,28 @@ export async function sendViaWebSocket({
|
|
|
783
771
|
// one is either torn down or in an unknown state.
|
|
784
772
|
forceFresh: forceFresh || attemptIndex > 0,
|
|
785
773
|
externalSignal,
|
|
786
|
-
|
|
774
|
+
// No nested connect retry budget: the outer stream loop owns
|
|
775
|
+
// all retries for this logical request.
|
|
776
|
+
maxAttempts: 1,
|
|
787
777
|
onRetry: (info) => {
|
|
788
778
|
handshakeRetries += 1;
|
|
789
779
|
sendSpan.handshakeRetries += 1;
|
|
790
780
|
if (info?.classifier) handshakeRetryClassifiers.push(info.classifier);
|
|
791
781
|
const attempt = Number(info?.attempt) || handshakeRetries;
|
|
792
|
-
const max = Number(info?.max) ||
|
|
782
|
+
const max = Number(info?.max) || MAX_MIDSTREAM_RETRIES;
|
|
793
783
|
emitReconnectProgress({ attempt, max, classifier: info?.classifier });
|
|
794
784
|
},
|
|
795
785
|
onBackoffSlept: (ms) => { sendSpan.retryBackoffMs += ms; },
|
|
796
786
|
});
|
|
797
787
|
} catch (err) {
|
|
798
788
|
sendSpan.poolAcquireMs += performance.now() - handshakeStart;
|
|
799
|
-
|
|
789
|
+
// Provenance only; policy remains provider-owned below. This lets
|
|
790
|
+
// the direct wrapper distinguish an upgrade rejection from an
|
|
791
|
+
// application error carrying the same HTTP status.
|
|
792
|
+
try { err.wsFailurePhase = 'handshake'; } catch {}
|
|
793
|
+
const classifier = err?.retryClassifier
|
|
794
|
+
|| _classifyHandshakeError(err)
|
|
795
|
+
|| (err?.code === 'EWSACQUIRETIMEOUT' ? 'acquire_timeout' : null);
|
|
800
796
|
const classifiers = [...handshakeRetryClassifiers];
|
|
801
797
|
if (classifier && !classifiers.includes(classifier)) classifiers.push(classifier);
|
|
802
798
|
if (err?.httpStatus != null || classifier || handshakeRetries > 0 || classifiers.length > 0) {
|
|
@@ -811,12 +807,70 @@ export async function sendViaWebSocket({
|
|
|
811
807
|
handshakeRetryClassifiers: classifiers,
|
|
812
808
|
});
|
|
813
809
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
810
|
+
const handshakeDecision = typeof handshakeErrorPolicy === 'function'
|
|
811
|
+
? handshakeErrorPolicy({
|
|
812
|
+
error: err,
|
|
813
|
+
status: Number(err?.httpStatus || 0),
|
|
814
|
+
classifier,
|
|
815
|
+
attempt: attemptIndex + 1,
|
|
816
|
+
maxAttempts: MAX_MIDSTREAM_RETRIES + 1,
|
|
817
|
+
})
|
|
818
|
+
: null;
|
|
819
|
+
if (handshakeDecision?.retry === false) {
|
|
820
|
+
try {
|
|
821
|
+
err.wsFailurePhase = 'handshake';
|
|
822
|
+
err.wsHttpFallbackEligible = handshakeDecision.httpFallback === true;
|
|
823
|
+
if (classifier) err.retryClassifier = classifier;
|
|
824
|
+
} catch {}
|
|
825
|
+
emitSendSpan('error');
|
|
826
|
+
throw _stampTool(_stampLiveText(err));
|
|
827
|
+
}
|
|
828
|
+
// HTTP 401 is reserved for caller-owned auth refresh. HTTP 426 is
|
|
829
|
+
// caller-owned immediate HTTPS fallback. Every other recognized
|
|
830
|
+
// transport failure spends the shared stream retry budget.
|
|
831
|
+
const retryable = classifier
|
|
832
|
+
&& Number(err?.httpStatus || 0) !== 401
|
|
833
|
+
&& Number(err?.httpStatus || 0) !== 426
|
|
834
|
+
&& !externalSignal?.aborted;
|
|
835
|
+
if (retryable && attemptIndex < MAX_MIDSTREAM_RETRIES) {
|
|
836
|
+
if (!firstAttemptError) {
|
|
837
|
+
firstAttemptError = err;
|
|
838
|
+
firstAttemptClassifier = classifier;
|
|
839
|
+
}
|
|
840
|
+
try { err.midstreamClassifier = classifier; } catch {}
|
|
841
|
+
const retryNumber = attemptIndex + 1;
|
|
842
|
+
emitReconnectProgress({
|
|
843
|
+
attempt: retryNumber,
|
|
844
|
+
max: MAX_MIDSTREAM_RETRIES,
|
|
845
|
+
classifier,
|
|
846
|
+
});
|
|
847
|
+
const sleepStart = performance.now();
|
|
848
|
+
try {
|
|
849
|
+
await _sleepWithAbort(_midstreamBackoffFor(retryNumber), externalSignal, _sleepFn);
|
|
850
|
+
} catch (sleepErr) {
|
|
851
|
+
sendSpan.retryBackoffMs += performance.now() - sleepStart;
|
|
852
|
+
emitSendSpan('error');
|
|
853
|
+
throw sleepErr;
|
|
854
|
+
}
|
|
855
|
+
sendSpan.retryBackoffMs += performance.now() - sleepStart;
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
// A later auth/upgrade decision must win over an earlier transient
|
|
859
|
+
// failure so the caller can refresh or switch transport. Likewise,
|
|
860
|
+
// never replace a current cancellation with stale retry history.
|
|
861
|
+
const currentStatus = Number(err?.httpStatus || 0);
|
|
862
|
+
const surfaceCurrent = currentStatus === 401
|
|
863
|
+
|| currentStatus === 426
|
|
864
|
+
|| externalSignal?.aborted
|
|
865
|
+
|| err?.unsafeToRetry === true;
|
|
866
|
+
if (surfaceCurrent) {
|
|
867
|
+
emitSendSpan('error');
|
|
868
|
+
throw _stampTool(_stampLiveText(err));
|
|
869
|
+
}
|
|
817
870
|
if (attemptIndex > 0 && firstAttemptError) {
|
|
818
871
|
try { firstAttemptError.midstreamRetries = attemptIndex; } catch {}
|
|
819
|
-
|
|
872
|
+
try { firstAttemptError.midstreamClassifier = firstAttemptClassifier; } catch {}
|
|
873
|
+
if (attemptIndex >= MAX_MIDSTREAM_RETRIES) {
|
|
820
874
|
try { firstAttemptError.wsRetriesExhausted = true; } catch {}
|
|
821
875
|
}
|
|
822
876
|
emitSendSpan('error');
|
|
@@ -885,6 +939,7 @@ export async function sendViaWebSocket({
|
|
|
885
939
|
let deltaReason = null;
|
|
886
940
|
let strippedResponseItems = 0;
|
|
887
941
|
let skippedResponseItems = 0;
|
|
942
|
+
let responseOutputMismatch = null;
|
|
888
943
|
let wireFrameHadTurnState = false;
|
|
889
944
|
let wireFrameMetadataTrace = _metadataTrace(null);
|
|
890
945
|
let framePrefixHash = null;
|
|
@@ -976,7 +1031,7 @@ export async function sendViaWebSocket({
|
|
|
976
1031
|
output_tokens: warmupResult.usage?.outputTokens || 0,
|
|
977
1032
|
prompt_tokens: warmupResult.usage?.promptTokens || 0,
|
|
978
1033
|
};
|
|
979
|
-
|
|
1034
|
+
_agentTraceFn({
|
|
980
1035
|
sessionId: poolKey,
|
|
981
1036
|
iteration,
|
|
982
1037
|
kind: 'cache_warmup',
|
|
@@ -1020,6 +1075,7 @@ export async function sendViaWebSocket({
|
|
|
1020
1075
|
deltaReason = delta.reason || null;
|
|
1021
1076
|
strippedResponseItems = delta.strippedResponseItems || 0;
|
|
1022
1077
|
skippedResponseItems = delta.skippedResponseItems || 0;
|
|
1078
|
+
responseOutputMismatch = delta.responseOutputMismatch || null;
|
|
1023
1079
|
const wireFrame = _withCodexWsClientMetadata(frame, entry, useCodexWsClientMetadata, codexMetadataContext);
|
|
1024
1080
|
wireFrameHadTurnState = !!wireFrame?.client_metadata?.['x-codex-turn-state'];
|
|
1025
1081
|
wireFrameMetadataTrace = _metadataTrace(wireFrame?.client_metadata);
|
|
@@ -1082,6 +1138,10 @@ export async function sendViaWebSocket({
|
|
|
1082
1138
|
knownToolNames,
|
|
1083
1139
|
});
|
|
1084
1140
|
} catch (err) {
|
|
1141
|
+
// Preserve failure provenance for the direct provider. This marker
|
|
1142
|
+
// is observational for OAuth and does not change its classifier or
|
|
1143
|
+
// retry budget.
|
|
1144
|
+
try { err.wsFailurePhase = 'stream'; } catch {}
|
|
1085
1145
|
// Snapshot the xAI conversation anchor BEFORE releasing the
|
|
1086
1146
|
// entry. release closes the socket but leaves state fields
|
|
1087
1147
|
// intact; the next forceFresh acquire creates a new entry into
|
|
@@ -1111,9 +1171,24 @@ export async function sendViaWebSocket({
|
|
|
1111
1171
|
if (midState.emittedToolCall) {
|
|
1112
1172
|
_safetyStamps.markTool();
|
|
1113
1173
|
}
|
|
1174
|
+
// Reasoning deltas and an in-progress tool input are already
|
|
1175
|
+
// observable turn progress even though neither is final assistant
|
|
1176
|
+
// text nor a dispatched tool call. Reissuing from the beginning can
|
|
1177
|
+
// duplicate exposed thinking/tool argument streams and can make a
|
|
1178
|
+
// partially generated side-effecting call diverge. Keep the Codex
|
|
1179
|
+
// retry budget only for failures before any such model output.
|
|
1180
|
+
if (midState.emittedReasoning || midState.startedToolCall) {
|
|
1181
|
+
try {
|
|
1182
|
+
err.unsafeToRetry = true;
|
|
1183
|
+
if (midState.emittedReasoning) err.partialReasoningEmitted = true;
|
|
1184
|
+
if (midState.startedToolCall) err.partialToolCallStarted = true;
|
|
1185
|
+
} catch {}
|
|
1186
|
+
}
|
|
1114
1187
|
_stampLiveText(err);
|
|
1115
1188
|
_stampTool(err);
|
|
1116
|
-
const classifier =
|
|
1189
|
+
const classifier = err?.unsafeToRetry === true
|
|
1190
|
+
? null
|
|
1191
|
+
: _classifyMidstreamError(err, midState);
|
|
1117
1192
|
const retryLimit = classifier ? _midstreamRetryLimit(classifier) : 0;
|
|
1118
1193
|
if (classifier && attemptIndex < retryLimit) {
|
|
1119
1194
|
// Retry-eligible: stash the first-attempt error, emit progress,
|
|
@@ -1141,6 +1216,17 @@ export async function sendViaWebSocket({
|
|
|
1141
1216
|
continue;
|
|
1142
1217
|
}
|
|
1143
1218
|
// Not retryable, OR we've already exhausted the retry budget.
|
|
1219
|
+
// Do not let stale retry history mask a current auth/upgrade
|
|
1220
|
+
// decision, cancellation, or newly unsafe-to-replay outcome.
|
|
1221
|
+
const currentStatus = Number(err?.httpStatus || 0);
|
|
1222
|
+
const surfaceCurrent = currentStatus === 401
|
|
1223
|
+
|| currentStatus === 426
|
|
1224
|
+
|| externalSignal?.aborted
|
|
1225
|
+
|| err?.unsafeToRetry === true;
|
|
1226
|
+
if (surfaceCurrent) {
|
|
1227
|
+
emitSendSpan('error');
|
|
1228
|
+
throw _stampTool(_stampLiveText(err));
|
|
1229
|
+
}
|
|
1144
1230
|
if (attemptIndex > 0 && firstAttemptError) {
|
|
1145
1231
|
// Exhausted path: surface the first-attempt error (the one
|
|
1146
1232
|
// the user's turn actually tripped on), tag actual retry count.
|
|
@@ -1292,7 +1378,7 @@ export async function sendViaWebSocket({
|
|
|
1292
1378
|
: null;
|
|
1293
1379
|
if (cacheObservation.actualMiss) {
|
|
1294
1380
|
try {
|
|
1295
|
-
|
|
1381
|
+
_agentTraceFn({
|
|
1296
1382
|
sessionId: poolKey,
|
|
1297
1383
|
iteration,
|
|
1298
1384
|
kind: 'cache_miss',
|
|
@@ -1389,6 +1475,7 @@ export async function sendViaWebSocket({
|
|
|
1389
1475
|
chain_delta_reason: mode === 'delta' ? null : deltaReason,
|
|
1390
1476
|
chain_stripped_response_items: strippedResponseItems,
|
|
1391
1477
|
chain_skipped_response_items: skippedResponseItems,
|
|
1478
|
+
...(responseOutputMismatch || {}),
|
|
1392
1479
|
chain_response_items: Array.isArray(result.responseItems) ? result.responseItems.length : 0,
|
|
1393
1480
|
body_input_items: Array.isArray(requestBody.input) ? requestBody.input.length : null,
|
|
1394
1481
|
frame_input_items: Array.isArray(frame.input) ? frame.input.length : null,
|
|
@@ -1400,7 +1487,7 @@ export async function sendViaWebSocket({
|
|
|
1400
1487
|
keep_socket: keepSocket,
|
|
1401
1488
|
keep_response_chain: keepResponseChain,
|
|
1402
1489
|
};
|
|
1403
|
-
|
|
1490
|
+
_agentTraceFn({
|
|
1404
1491
|
sessionId: poolKey,
|
|
1405
1492
|
iteration,
|
|
1406
1493
|
kind: 'transport',
|
|
@@ -1411,7 +1498,10 @@ export async function sendViaWebSocket({
|
|
|
1411
1498
|
&& deltaReason
|
|
1412
1499
|
&& !['no_anchor', 'full_forced', 'full_default', 'delta_missing_turn_state'].includes(deltaReason);
|
|
1413
1500
|
if (chainFallback || (mode === 'delta' && deltaReason)) {
|
|
1414
|
-
|
|
1501
|
+
const intentionalTransition = typeof sendOpts?.cacheBreakIntent === 'string'
|
|
1502
|
+
? sendOpts.cacheBreakIntent
|
|
1503
|
+
: null;
|
|
1504
|
+
_agentTraceFn({
|
|
1415
1505
|
sessionId: poolKey,
|
|
1416
1506
|
iteration,
|
|
1417
1507
|
kind: 'cache_break',
|
|
@@ -1423,6 +1513,8 @@ export async function sendViaWebSocket({
|
|
|
1423
1513
|
transport: 'websocket',
|
|
1424
1514
|
ws_mode: mode,
|
|
1425
1515
|
reason: mode === 'delta' ? deltaReason : (deltaReason || 'full_frame'),
|
|
1516
|
+
intentional_transition: intentionalTransition,
|
|
1517
|
+
request_tool_choice: requestBody.tool_choice ?? null,
|
|
1426
1518
|
cache_key_hash: transportCacheKeyHash,
|
|
1427
1519
|
cached_tokens: cacheObservation.cachedTokens,
|
|
1428
1520
|
prompt_tokens: cacheObservation.promptTokens,
|
|
@@ -1432,6 +1524,7 @@ export async function sendViaWebSocket({
|
|
|
1432
1524
|
request_has_previous_response_id: transportPayload.request_has_previous_response_id,
|
|
1433
1525
|
chain_stripped_response_items: strippedResponseItems,
|
|
1434
1526
|
chain_skipped_response_items: skippedResponseItems,
|
|
1527
|
+
...(responseOutputMismatch || {}),
|
|
1435
1528
|
chain_response_items: Array.isArray(result.responseItems) ? result.responseItems.length : 0,
|
|
1436
1529
|
body_input_items: Array.isArray(requestBody.input) ? requestBody.input.length : null,
|
|
1437
1530
|
frame_input_items: Array.isArray(frame.input) ? frame.input.length : null,
|
|
@@ -58,7 +58,6 @@ import {
|
|
|
58
58
|
import {
|
|
59
59
|
sendViaHttpSse,
|
|
60
60
|
_envFlag,
|
|
61
|
-
_envPositiveInt,
|
|
62
61
|
_shouldUseOpenAIHttpFallback,
|
|
63
62
|
} from './openai-oauth-http-sse.mjs';
|
|
64
63
|
import { createOpenAIOAuthLogin } from './openai-oauth-login.mjs';
|
|
@@ -830,8 +829,9 @@ export class OpenAIOAuthProvider {
|
|
|
830
829
|
};
|
|
831
830
|
const markStickyHttpFallback = () => {
|
|
832
831
|
if (!poolKey) return;
|
|
833
|
-
|
|
834
|
-
|
|
832
|
+
// Codex disables WebSockets for the remainder of this session after
|
|
833
|
+
// stream retry exhaustion (or an explicit 426 upgrade rejection).
|
|
834
|
+
this._httpFallbackUntilByPoolKey.set(poolKey, Number.POSITIVE_INFINITY);
|
|
835
835
|
};
|
|
836
836
|
const traceWsError = (err, stage = 'primary') => {
|
|
837
837
|
try {
|
|
@@ -854,6 +854,9 @@ export class OpenAIOAuthProvider {
|
|
|
854
854
|
} catch {}
|
|
855
855
|
};
|
|
856
856
|
const dispatchHttp = async (reason, originalErr = null, { sticky = false } = {}) => {
|
|
857
|
+
// Transport switching is a session decision, not an HTTP-success
|
|
858
|
+
// side effect. Persist it before the fallback can fail or abort.
|
|
859
|
+
if (sticky) markStickyHttpFallback();
|
|
857
860
|
appendAgentTrace({
|
|
858
861
|
sessionId: poolKey,
|
|
859
862
|
iteration,
|
|
@@ -892,7 +895,6 @@ export class OpenAIOAuthProvider {
|
|
|
892
895
|
useModel,
|
|
893
896
|
fetchFn: opts._fetchFn,
|
|
894
897
|
});
|
|
895
|
-
if (sticky) markStickyHttpFallback();
|
|
896
898
|
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
897
899
|
process.stderr.write(`[agent-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok transport=http-fallback\n`);
|
|
898
900
|
}
|
|
@@ -933,6 +935,13 @@ export class OpenAIOAuthProvider {
|
|
|
933
935
|
? { ...body, generate: false }
|
|
934
936
|
: null,
|
|
935
937
|
});
|
|
938
|
+
const mustSurfaceFallbackError = (fallbackErr) => externalSignal?.aborted
|
|
939
|
+
|| fallbackErr?.name === 'AbortError'
|
|
940
|
+
|| fallbackErr?.code === 'ABORT_ERR'
|
|
941
|
+
|| fallbackErr?.liveTextEmitted === true
|
|
942
|
+
|| fallbackErr?.emittedToolCall === true
|
|
943
|
+
|| fallbackErr?.toolCallEmitted === true
|
|
944
|
+
|| fallbackErr?.unsafeToRetry === true;
|
|
936
945
|
if (transportPolicy.transport === 'http'
|
|
937
946
|
|| (transportPolicy.allowHttpFallback && (
|
|
938
947
|
opts.forceHttpFallback === true
|
|
@@ -959,7 +968,7 @@ export class OpenAIOAuthProvider {
|
|
|
959
968
|
// the retry (and the HTTP fallback below already refuses) and
|
|
960
969
|
// surface the original error.
|
|
961
970
|
const liveTextEmitted = err?.liveTextEmitted === true || err?.unsafeToRetry === true;
|
|
962
|
-
if (
|
|
971
|
+
if (status === 401 && !liveTextEmitted) {
|
|
963
972
|
process.stderr.write(`[openai-oauth-ws] ${status} — forcing refresh and retrying once over WS\n`);
|
|
964
973
|
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-${status}-retry attempt=1\n`); }
|
|
965
974
|
this._refreshFallbackUntil = 0;
|
|
@@ -971,7 +980,7 @@ export class OpenAIOAuthProvider {
|
|
|
971
980
|
} catch (retryErr) {
|
|
972
981
|
traceWsError(retryErr, 'auth_retry');
|
|
973
982
|
if (httpFallbackEnabled
|
|
974
|
-
&& retryErr?.wsRetriesExhausted === true
|
|
983
|
+
&& (retryErr?.httpStatus === 426 || retryErr?.wsRetriesExhausted === true)
|
|
975
984
|
&& _shouldUseOpenAIHttpFallback(retryErr, externalSignal)) {
|
|
976
985
|
try {
|
|
977
986
|
return await dispatchHttp(
|
|
@@ -980,6 +989,10 @@ export class OpenAIOAuthProvider {
|
|
|
980
989
|
{ sticky: true },
|
|
981
990
|
);
|
|
982
991
|
} catch (fallbackErr) {
|
|
992
|
+
// Cancellation and post-output failures are the
|
|
993
|
+
// current terminal outcome; replacing either with
|
|
994
|
+
// stale WS history could permit upstream replay.
|
|
995
|
+
if (mustSurfaceFallbackError(fallbackErr)) throw fallbackErr;
|
|
983
996
|
try { retryErr.fallbackError = fallbackErr; } catch {}
|
|
984
997
|
throw retryErr;
|
|
985
998
|
}
|
|
@@ -988,7 +1001,7 @@ export class OpenAIOAuthProvider {
|
|
|
988
1001
|
}
|
|
989
1002
|
}
|
|
990
1003
|
// Auth failure after live text already emitted: never reissue.
|
|
991
|
-
if (
|
|
1004
|
+
if (status === 401 && liveTextEmitted) {
|
|
992
1005
|
throw err;
|
|
993
1006
|
}
|
|
994
1007
|
const msg = err?.message || '';
|
|
@@ -1004,7 +1017,7 @@ export class OpenAIOAuthProvider {
|
|
|
1004
1017
|
return this.send(messages, model, tools, { ...opts, _modelRetry: true });
|
|
1005
1018
|
}
|
|
1006
1019
|
if (httpFallbackEnabled
|
|
1007
|
-
&& err?.wsRetriesExhausted === true
|
|
1020
|
+
&& (status === 426 || err?.wsRetriesExhausted === true)
|
|
1008
1021
|
&& _shouldUseOpenAIHttpFallback(err, externalSignal)) {
|
|
1009
1022
|
try {
|
|
1010
1023
|
return await dispatchHttp(
|
|
@@ -1013,6 +1026,7 @@ export class OpenAIOAuthProvider {
|
|
|
1013
1026
|
{ sticky: true },
|
|
1014
1027
|
);
|
|
1015
1028
|
} catch (fallbackErr) {
|
|
1029
|
+
if (mustSurfaceFallbackError(fallbackErr)) throw fallbackErr;
|
|
1016
1030
|
try { err.fallbackError = fallbackErr; } catch {}
|
|
1017
1031
|
throw err;
|
|
1018
1032
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
RESPONSES_TRANSPORT_CAPABILITIES,
|
|
16
16
|
FULL_RESPONSES_TRANSPORT_CAPS,
|
|
17
17
|
} from './openai-transport-policy.mjs';
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
18
19
|
|
|
19
20
|
// If the cached request (sans input) matches the current one and the current
|
|
20
21
|
// input starts with the cached input, return only the tail. Otherwise return
|
|
@@ -157,6 +158,67 @@ function _isReplayLikeHead(item, responseItem) {
|
|
|
157
158
|
return inputType === responseType;
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
// The hash input follows the same per-item normalizations used by
|
|
162
|
+
// _logicalResponseItemMatch, but is never emitted itself. This retains a
|
|
163
|
+
// stable comparison fingerprint after history compaction without putting
|
|
164
|
+
// message content, tool arguments, or custom-tool input into traces.
|
|
165
|
+
function _normalizedResponseItemDiagnosticShape(item) {
|
|
166
|
+
if (!item || typeof item !== 'object') return item || null;
|
|
167
|
+
const type = item.type || (item.role === 'assistant' ? 'message' : '');
|
|
168
|
+
if (type === 'function_call' || type === 'tool_search_call') {
|
|
169
|
+
return {
|
|
170
|
+
type,
|
|
171
|
+
call_id: String(item.call_id || ''),
|
|
172
|
+
name: type === 'function_call' ? String(item.name || '') : undefined,
|
|
173
|
+
arguments: _normalizeArguments(item.arguments),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (type === 'custom_tool_call') {
|
|
177
|
+
return {
|
|
178
|
+
type,
|
|
179
|
+
call_id: String(item.call_id || ''),
|
|
180
|
+
name: String(item.name || ''),
|
|
181
|
+
input: String(item.input || ''),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (type === 'message') {
|
|
185
|
+
return {
|
|
186
|
+
type,
|
|
187
|
+
role: item.role || 'assistant',
|
|
188
|
+
content: Array.isArray(item.content) ? item.content.map(_normalizeContentPart) : [],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (type === 'reasoning') {
|
|
192
|
+
return {
|
|
193
|
+
type,
|
|
194
|
+
id: item.id || null,
|
|
195
|
+
encrypted_content: item.encrypted_content || null,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (type === 'web_search_call') {
|
|
199
|
+
return { type, id: item.id || null, action: item.action || null };
|
|
200
|
+
}
|
|
201
|
+
const { id: _id, status: _status, ...rest } = item;
|
|
202
|
+
return { type, ...rest };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function _normalizedResponseItemDiagnosticHash(item) {
|
|
206
|
+
return createHash('sha256')
|
|
207
|
+
.update(_stableStringify(_normalizedResponseItemDiagnosticShape(item)))
|
|
208
|
+
.digest('hex');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function _responseOutputMismatchDiagnostics(inputItem, responseItem, replayItemCount, responseItemCount) {
|
|
212
|
+
return {
|
|
213
|
+
response_output_mismatch_expected_type: responseItem?.type || 'unknown',
|
|
214
|
+
response_output_mismatch_expected_hash: _normalizedResponseItemDiagnosticHash(responseItem),
|
|
215
|
+
response_output_mismatch_expected_response_item_count: responseItemCount,
|
|
216
|
+
response_output_mismatch_actual_type: inputItem?.type || (inputItem?.role === 'assistant' ? 'message' : 'unknown'),
|
|
217
|
+
response_output_mismatch_actual_hash: _normalizedResponseItemDiagnosticHash(inputItem),
|
|
218
|
+
response_output_mismatch_actual_replayed_input_item_count: replayItemCount,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
160
222
|
export function _stripResponseItemsFromHead(items, responseItems) {
|
|
161
223
|
const tail = Array.isArray(items) ? items : [];
|
|
162
224
|
const outputs = Array.isArray(responseItems) ? responseItems : [];
|
|
@@ -177,6 +239,12 @@ export function _stripResponseItemsFromHead(items, responseItems) {
|
|
|
177
239
|
tail,
|
|
178
240
|
stripped,
|
|
179
241
|
skipped,
|
|
242
|
+
responseOutputMismatch: _responseOutputMismatchDiagnostics(
|
|
243
|
+
tail[cursor],
|
|
244
|
+
output,
|
|
245
|
+
tail.length,
|
|
246
|
+
outputs.length,
|
|
247
|
+
),
|
|
180
248
|
};
|
|
181
249
|
}
|
|
182
250
|
skipped += 1;
|
|
@@ -291,7 +359,12 @@ export function _computeDelta({ entry, body, traceProvider }) {
|
|
|
291
359
|
}
|
|
292
360
|
const stripped = _stripResponseItemsFromHead(afterPreviousInput, entry.lastResponseItems);
|
|
293
361
|
if (!stripped.ok) {
|
|
294
|
-
return {
|
|
362
|
+
return {
|
|
363
|
+
mode: 'full',
|
|
364
|
+
reason: stripped.reason,
|
|
365
|
+
responseOutputMismatch: stripped.responseOutputMismatch,
|
|
366
|
+
frame: buildFrame(body),
|
|
367
|
+
};
|
|
295
368
|
}
|
|
296
369
|
return {
|
|
297
370
|
mode: 'delta',
|