mixdog 0.9.1 → 0.9.2
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 +8 -1
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +306 -66
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +4 -2
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +4 -2
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/rules-builder.cjs +32 -54
- package/src/mixdog-session-runtime.mjs +710 -318
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +12 -2
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +7 -8
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +7 -0
- package/src/rules/shared/01-tool.md +17 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +332 -57
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +59 -32
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +78 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +202 -98
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +183 -20
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +15 -9
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
- package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
- package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +441 -1224
- package/src/runtime/channels/lib/config.mjs +54 -2
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/output-forwarder.mjs +80 -67
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/telegram-format.mjs +283 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +184 -19
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory.mjs +101 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/session-ingest.mjs +107 -0
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +6 -3
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +129 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/standalone/agent-tool.mjs +255 -109
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +8 -291
- package/src/standalone/explore-tool.mjs +2 -2
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/provider-admin.mjs +11 -0
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2096 -732
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +83 -7
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +170 -98
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +159 -21
- package/src/tui/components/tool-output-format.test.mjs +87 -0
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +6965 -2391
- package/src/tui/engine.mjs +287 -126
- package/src/tui/index.jsx +117 -7
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +453 -0
- package/src/tui/markdown/format-token.mjs +129 -76
- package/src/tui/markdown/format-token.test.mjs +61 -19
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +0 -11
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -657
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +81 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +26 -27
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +39 -12
- package/src/workflows/sequential/WORKFLOW.md +46 -0
- package/src/workflows/solo/WORKFLOW.md +7 -0
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +100 -12
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -29,13 +29,17 @@ import {
|
|
|
29
29
|
traceAgentUsage,
|
|
30
30
|
} from '../agent-trace.mjs';
|
|
31
31
|
import {
|
|
32
|
-
PROVIDER_GENERATE_TOTAL_TIMEOUT_MS,
|
|
33
32
|
PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
|
|
33
|
+
PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS,
|
|
34
|
+
PROVIDER_SSE_IDLE_WATCHDOG_ENABLED,
|
|
35
|
+
streamStalledError,
|
|
34
36
|
createTimeoutSignal,
|
|
37
|
+
createPassthroughSignal,
|
|
35
38
|
} from '../stall-policy.mjs';
|
|
36
39
|
import { populateHttpStatusFromMessage, shouldFallbackTransport } from './retry-classifier.mjs';
|
|
37
40
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
38
41
|
import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
|
|
42
|
+
import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
|
|
39
43
|
import {
|
|
40
44
|
normalizeContentForOpenAIResponses,
|
|
41
45
|
splitToolContentForOpenAIResponses,
|
|
@@ -160,6 +164,17 @@ function _displayCodexModel(id) {
|
|
|
160
164
|
return id.replace(/-\d{4}-\d{2}-\d{2}$/, '');
|
|
161
165
|
}
|
|
162
166
|
|
|
167
|
+
function _positiveCodexContextWindow(value) {
|
|
168
|
+
const n = Number(value);
|
|
169
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function _codexContextWindowFromApi(m) {
|
|
173
|
+
return _positiveCodexContextWindow(m?.context_window)
|
|
174
|
+
|| _positiveCodexContextWindow(m?.max_context_window)
|
|
175
|
+
|| null;
|
|
176
|
+
}
|
|
177
|
+
|
|
163
178
|
function _normalizeCodexModel(m) {
|
|
164
179
|
const id = m?.slug || m?.id;
|
|
165
180
|
const family = _codexFamily(id);
|
|
@@ -182,8 +197,8 @@ function _normalizeCodexModel(m) {
|
|
|
182
197
|
display: m?.display_name || id,
|
|
183
198
|
family,
|
|
184
199
|
provider: 'openai-oauth',
|
|
185
|
-
contextWindow: m
|
|
186
|
-
maxContextWindow: m?.max_context_window
|
|
200
|
+
contextWindow: _codexContextWindowFromApi(m),
|
|
201
|
+
maxContextWindow: _positiveCodexContextWindow(m?.max_context_window),
|
|
187
202
|
outputTokens: m?.max_output_tokens || m?.output_tokens || 32768,
|
|
188
203
|
autoCompactTokenLimit: m?.auto_compact_token_limit || null,
|
|
189
204
|
effectiveContextWindowPercent: m?.effective_context_window_percent || null,
|
|
@@ -773,11 +788,20 @@ export async function sendViaHttpSse({
|
|
|
773
788
|
useModel,
|
|
774
789
|
fetchFn = fetch,
|
|
775
790
|
} = {}) {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
)
|
|
791
|
+
// P1 audit fix: no fixed wall-clock total cap on the HTTP/SSE fallback
|
|
792
|
+
// stream. The old createTimeoutSignal(..., PROVIDER_GENERATE_TOTAL_TIMEOUT_MS)
|
|
793
|
+
// killed a healthy, still-streaming turn purely on elapsed time, unlike
|
|
794
|
+
// every other streaming provider path (anthropic-oauth uses the same
|
|
795
|
+
// createPassthroughSignal pattern — see anthropic-oauth.mjs "Option A").
|
|
796
|
+
// The stream is bounded instead by:
|
|
797
|
+
// (a) headerTimeout below (PROVIDER_HTTP_RESPONSE_TIMEOUT_MS) for a
|
|
798
|
+
// socket that never sends the initial response,
|
|
799
|
+
// (b) the SEMANTIC idle watchdog (_armSemanticIdle /
|
|
800
|
+
// PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS), which resets on every
|
|
801
|
+
// meaningful() chunk — a live stream stays alive, a truly silent
|
|
802
|
+
// one still aborts, and
|
|
803
|
+
// (c) externalSignal (client disconnect / replaced-by-newer-request).
|
|
804
|
+
const totalTimeout = createPassthroughSignal(externalSignal);
|
|
781
805
|
const headerTimeout = createTimeoutSignal(
|
|
782
806
|
totalTimeout.signal,
|
|
783
807
|
PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
|
|
@@ -851,6 +875,33 @@ export async function sendViaHttpSse({
|
|
|
851
875
|
if (totalTimeout.signal.aborted) _onTotalAbort();
|
|
852
876
|
else totalTimeout.signal.addEventListener('abort', _onTotalAbort, { once: true });
|
|
853
877
|
}
|
|
878
|
+
// SEMANTIC idle watchdog: reset ONLY on meaningful() (text/reasoning/tool
|
|
879
|
+
// deltas), never on raw bytes/keepalive frames, so a stream that emits some
|
|
880
|
+
// deltas then goes silent trips a short, named terminal failure instead of
|
|
881
|
+
// hanging until the 30-min agent watchdog. Disablable via the shared env.
|
|
882
|
+
let _semanticIdleTimer = null;
|
|
883
|
+
const _clearSemanticIdle = () => {
|
|
884
|
+
if (_semanticIdleTimer) { clearTimeout(_semanticIdleTimer); _semanticIdleTimer = null; }
|
|
885
|
+
};
|
|
886
|
+
const _armSemanticIdle = () => {
|
|
887
|
+
if (!PROVIDER_SSE_IDLE_WATCHDOG_ENABLED || !(PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS > 0)) return;
|
|
888
|
+
_clearSemanticIdle();
|
|
889
|
+
_semanticIdleTimer = setTimeout(() => {
|
|
890
|
+
_streamAbortReason = streamStalledError('OpenAI OAuth HTTP fallback', PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS, { emittedToolCall: emittedToolCallIds.size > 0 });
|
|
891
|
+
// Partial-final recovery parity with anthropic-oauth: attach the
|
|
892
|
+
// streamed partial state so the agent loop can accept a wedged FINAL
|
|
893
|
+
// no-tool summary as a successful partial-final instead of dropping
|
|
894
|
+
// the result. pendingToolUse gates out any mid-flight tool call.
|
|
895
|
+
try {
|
|
896
|
+
_streamAbortReason.partialContent = content;
|
|
897
|
+
_streamAbortReason.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
|
|
898
|
+
_streamAbortReason.pendingToolUse = pendingCalls.size > 0 || emittedToolCallIds.size > 0;
|
|
899
|
+
_streamAbortReason.partialModel = model || undefined;
|
|
900
|
+
} catch { /* best-effort enrichment */ }
|
|
901
|
+
try { reader.cancel(_streamAbortReason).catch(() => {}); } catch {}
|
|
902
|
+
}, PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS);
|
|
903
|
+
try { _semanticIdleTimer.unref?.(); } catch {}
|
|
904
|
+
};
|
|
854
905
|
let buffer = '';
|
|
855
906
|
let content = '';
|
|
856
907
|
let model = '';
|
|
@@ -885,13 +936,75 @@ export async function sendViaHttpSse({
|
|
|
885
936
|
// first complete frame still emits; only redundant re-emits are
|
|
886
937
|
// suppressed.
|
|
887
938
|
const emittedToolCallIds = new Set();
|
|
939
|
+
// Fix 2: cross-path name+args dedupe. A text-leaked synthetic and an
|
|
940
|
+
// identical native function_call must fire onToolCall exactly once.
|
|
941
|
+
const _toolDedupe = createToolCallDedupe();
|
|
888
942
|
const emitToolCall = (call) => {
|
|
889
943
|
if (!call || !call.id) return;
|
|
890
944
|
if (emittedToolCallIds.has(call.id)) return;
|
|
891
945
|
emittedToolCallIds.add(call.id);
|
|
946
|
+
if (!_toolDedupe.shouldDispatch(call.name, call.arguments)) return;
|
|
892
947
|
try { onToolCall?.(call); } catch {}
|
|
893
948
|
};
|
|
894
949
|
|
|
950
|
+
// Leaked tool-call guard. The model sometimes emits a tool call as plain
|
|
951
|
+
// text (XML `<invoke>`/`<function_calls>` or gpt-oss harmony
|
|
952
|
+
// `<|channel|>...to=functions.NAME...<|call|>`) inside
|
|
953
|
+
// `response.output_text.delta` instead of a native function_call. Route
|
|
954
|
+
// text through the guard so leaked calls are suppressed from the visible
|
|
955
|
+
// stream, synthesized (native `call_...` id shape), and dispatched like
|
|
956
|
+
// native ones. Known tool names come from the request body so recovery
|
|
957
|
+
// only fires for tools the model was actually offered. Additive: the
|
|
958
|
+
// native function_call path is untouched.
|
|
959
|
+
const _leakKnownTools = new Set(
|
|
960
|
+
(Array.isArray(body?.tools) ? body.tools : [])
|
|
961
|
+
.map((t) => (typeof t?.name === 'string' ? t.name : null))
|
|
962
|
+
.filter(Boolean),
|
|
963
|
+
);
|
|
964
|
+
const leakGuard = createLeakGuard({ knownToolNames: _leakKnownTools, harmony: true });
|
|
965
|
+
const dispatchLeakedCall = (recovered) => {
|
|
966
|
+
let args = recovered?.arguments;
|
|
967
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
|
|
968
|
+
const call = {
|
|
969
|
+
id: `call_leaked_${randomBytes(8).toString('hex')}`,
|
|
970
|
+
name: recovered.name,
|
|
971
|
+
arguments: args,
|
|
972
|
+
};
|
|
973
|
+
toolCalls.push(call);
|
|
974
|
+
emitToolCall(call);
|
|
975
|
+
};
|
|
976
|
+
const relayLeakText = (delta) => {
|
|
977
|
+
if (!leakGuard.enabled) {
|
|
978
|
+
content += delta || '';
|
|
979
|
+
if (delta && onTextDelta) {
|
|
980
|
+
emittedText = true;
|
|
981
|
+
try { onTextDelta(delta); } catch {}
|
|
982
|
+
}
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
const { text, calls } = leakGuard.push(delta);
|
|
986
|
+
if (text) {
|
|
987
|
+
content += text;
|
|
988
|
+
if (onTextDelta) {
|
|
989
|
+
emittedText = true;
|
|
990
|
+
try { onTextDelta(text); } catch {}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
for (const c of calls) dispatchLeakedCall(c);
|
|
994
|
+
};
|
|
995
|
+
const flushLeak = () => {
|
|
996
|
+
if (!leakGuard.enabled) return;
|
|
997
|
+
const { text, calls } = leakGuard.flush();
|
|
998
|
+
if (text) {
|
|
999
|
+
content += text;
|
|
1000
|
+
if (onTextDelta) {
|
|
1001
|
+
emittedText = true;
|
|
1002
|
+
try { onTextDelta(text); } catch {}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
for (const c of calls) dispatchLeakedCall(c);
|
|
1006
|
+
};
|
|
1007
|
+
|
|
895
1008
|
const pushWebSearchCall = (item) => {
|
|
896
1009
|
if (!item || item.type !== 'web_search_call') return;
|
|
897
1010
|
const key = item.id || JSON.stringify(item.action || item);
|
|
@@ -939,6 +1052,7 @@ export async function sendViaHttpSse({
|
|
|
939
1052
|
};
|
|
940
1053
|
const meaningful = () => {
|
|
941
1054
|
if (ttftMs == null) ttftMs = Date.now() - sseStartedAt;
|
|
1055
|
+
_armSemanticIdle();
|
|
942
1056
|
try { onStreamDelta?.(); } catch {}
|
|
943
1057
|
};
|
|
944
1058
|
const handleEvent = (event) => {
|
|
@@ -949,12 +1063,8 @@ export async function sendViaHttpSse({
|
|
|
949
1063
|
if (event.response?.id) responseId = event.response.id;
|
|
950
1064
|
break;
|
|
951
1065
|
case 'response.output_text.delta':
|
|
952
|
-
content += event.delta || '';
|
|
953
1066
|
meaningful();
|
|
954
|
-
|
|
955
|
-
emittedText = true;
|
|
956
|
-
try { onTextDelta(event.delta); } catch {}
|
|
957
|
-
}
|
|
1067
|
+
relayLeakText(event.delta || '');
|
|
958
1068
|
break;
|
|
959
1069
|
case 'response.reasoning_text.delta':
|
|
960
1070
|
case 'response.reasoning_summary_text.delta':
|
|
@@ -1030,7 +1140,20 @@ export async function sendViaHttpSse({
|
|
|
1030
1140
|
for (const item of resp.output || []) {
|
|
1031
1141
|
if (item.type === 'message') {
|
|
1032
1142
|
for (const part of item.content || []) {
|
|
1033
|
-
if (!content && part.type === 'output_text')
|
|
1143
|
+
if (!content && part.type === 'output_text') {
|
|
1144
|
+
// Completed-output fallback (no streamed text).
|
|
1145
|
+
// Route through the leak guard so a tool call
|
|
1146
|
+
// leaked only in the final bundle is recovered
|
|
1147
|
+
// rather than surfaced as visible content. push
|
|
1148
|
+
// with final=true flushes fully (no held tail).
|
|
1149
|
+
if (leakGuard.enabled) {
|
|
1150
|
+
const { text, calls } = leakGuard.push(part.text || '', true);
|
|
1151
|
+
content += text;
|
|
1152
|
+
for (const c of calls) dispatchLeakedCall(c);
|
|
1153
|
+
} else {
|
|
1154
|
+
content += part.text || '';
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1034
1157
|
if (part.type === 'output_text') _pushOutputTextAnnotations(part, citations, citationKeys);
|
|
1035
1158
|
}
|
|
1036
1159
|
} else if (item.type === 'reasoning') {
|
|
@@ -1116,10 +1239,12 @@ export async function sendViaHttpSse({
|
|
|
1116
1239
|
|
|
1117
1240
|
try {
|
|
1118
1241
|
while (true) {
|
|
1119
|
-
if (totalTimeout.signal
|
|
1242
|
+
if (totalTimeout.signal?.aborted) {
|
|
1243
|
+
_clearSemanticIdle();
|
|
1120
1244
|
const reason = totalTimeout.signal.reason;
|
|
1121
1245
|
throw reason instanceof Error ? reason : new Error('OpenAI OAuth HTTP fallback aborted');
|
|
1122
1246
|
}
|
|
1247
|
+
if (_streamAbortReason) throw _streamAbortReason;
|
|
1123
1248
|
const { value, done } = await reader.read();
|
|
1124
1249
|
if (done) break;
|
|
1125
1250
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -1140,12 +1265,16 @@ export async function sendViaHttpSse({
|
|
|
1140
1265
|
const event = _parseSseFrame(frame);
|
|
1141
1266
|
if (event) handleEvent(event);
|
|
1142
1267
|
}
|
|
1268
|
+
// Flush any partial-sentinel tail held back mid-stream so legitimate
|
|
1269
|
+
// trailing text is never lost (streamed-text path).
|
|
1270
|
+
flushLeak();
|
|
1143
1271
|
} catch (err) {
|
|
1144
1272
|
// Live-text invariant: once a non-empty chunk has been relayed it
|
|
1145
1273
|
// cannot be withdrawn — flag the error so no upstream layer retries.
|
|
1146
1274
|
if (emittedText && err) { try { err.liveTextEmitted = true; err.unsafeToRetry = true; } catch {} }
|
|
1147
1275
|
throw err;
|
|
1148
1276
|
} finally {
|
|
1277
|
+
_clearSemanticIdle();
|
|
1149
1278
|
try { reader.releaseLock?.(); } catch {}
|
|
1150
1279
|
if (_onTotalAbort && totalTimeout.signal) {
|
|
1151
1280
|
try { totalTimeout.signal.removeEventListener('abort', _onTotalAbort); } catch {}
|
|
@@ -1186,15 +1315,27 @@ export async function sendViaHttpSse({
|
|
|
1186
1315
|
serviceTier,
|
|
1187
1316
|
});
|
|
1188
1317
|
}
|
|
1318
|
+
// Dedupe the returned array by name+args (Fix 2, array side): a synthetic
|
|
1319
|
+
// leaked call and an identical native function_call must not both survive,
|
|
1320
|
+
// else the agent loop executes the side-effecting tool twice.
|
|
1321
|
+
const _returnedToolCalls = toolCalls.length
|
|
1322
|
+
? dedupeToolCallList(toolCalls.map(({ _pendingItemId, ...t }) => t))
|
|
1323
|
+
: undefined;
|
|
1189
1324
|
return {
|
|
1190
1325
|
content,
|
|
1191
1326
|
model: liveModel,
|
|
1192
1327
|
reasoningItems: reasoningItems.length ? reasoningItems : undefined,
|
|
1193
|
-
toolCalls:
|
|
1328
|
+
toolCalls: _returnedToolCalls,
|
|
1194
1329
|
citations: citations.length ? citations : undefined,
|
|
1195
1330
|
webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
|
|
1196
1331
|
usage: usage || undefined,
|
|
1197
1332
|
stopReason: stopReason || undefined,
|
|
1333
|
+
// P1 audit fix: text-only max-output cutoff (openai-oauth HTTP/SSE
|
|
1334
|
+
// fallback maps status:'incomplete'/reason=max_output_tokens to
|
|
1335
|
+
// stopReason='length' above and treats it as success). Flag it so
|
|
1336
|
+
// loop.mjs can surface a truncation warning instead of accepting
|
|
1337
|
+
// silently-cut content as a clean final answer.
|
|
1338
|
+
...(stopReason === 'length' && content.length > 0 ? { truncated: true } : {}),
|
|
1198
1339
|
responseId: responseId || undefined,
|
|
1199
1340
|
serviceTier: serviceTier || undefined,
|
|
1200
1341
|
};
|
|
@@ -1328,8 +1469,8 @@ export class OpenAIOAuthProvider {
|
|
|
1328
1469
|
const onTextDelta = typeof opts.onTextDelta === 'function' ? opts.onTextDelta : null;
|
|
1329
1470
|
const externalSignal = opts.signal || null;
|
|
1330
1471
|
const _sendSessionId = opts.sessionId || '(none)';
|
|
1331
|
-
const
|
|
1332
|
-
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] auth-start sessionHash=${createHash('sha256').update(String(_sendSessionId)).digest('hex').slice(0, 8)}
|
|
1472
|
+
const _sendAgent = opts.agent || '(none)';
|
|
1473
|
+
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] auth-start sessionHash=${createHash('sha256').update(String(_sendSessionId)).digest('hex').slice(0, 8)} agent=${_sendAgent} expiringInMs=${this.tokens?.expires_at ? this.tokens.expires_at - Date.now() : 'unknown'}\n`); }
|
|
1333
1474
|
// Build request body in parallel with auth resolution. ensureAuth is
|
|
1334
1475
|
// a no-op fast-path on cached tokens, but a refresh round-trip can
|
|
1335
1476
|
// take 300ms+; the body build (message serialisation) overlaps cleanly.
|
|
@@ -1389,6 +1530,26 @@ export class OpenAIOAuthProvider {
|
|
|
1389
1530
|
this._forceHttpFallback = true;
|
|
1390
1531
|
this._forceHttpFallbackUntil = Date.now() + ttlMs;
|
|
1391
1532
|
};
|
|
1533
|
+
const traceWsError = (err, stage = 'primary') => {
|
|
1534
|
+
try {
|
|
1535
|
+
appendAgentTrace({
|
|
1536
|
+
sessionId: poolKey,
|
|
1537
|
+
iteration,
|
|
1538
|
+
kind: 'transport_error',
|
|
1539
|
+
provider: 'openai-oauth',
|
|
1540
|
+
model: useModel,
|
|
1541
|
+
transport: 'websocket',
|
|
1542
|
+
payload: {
|
|
1543
|
+
stage,
|
|
1544
|
+
error_code: err?.code || null,
|
|
1545
|
+
error_http_status: Number(err?.httpStatus || 0) || null,
|
|
1546
|
+
error_classifier: err?.retryClassifier || err?.midstreamClassifier || null,
|
|
1547
|
+
live_text_emitted: err?.liveTextEmitted === true || err?.unsafeToRetry === true,
|
|
1548
|
+
message: String(err?.message || err || '').slice(0, 500),
|
|
1549
|
+
},
|
|
1550
|
+
});
|
|
1551
|
+
} catch {}
|
|
1552
|
+
};
|
|
1392
1553
|
const dispatchHttp = async (reason, originalErr = null, { sticky = false } = {}) => {
|
|
1393
1554
|
appendAgentTrace({
|
|
1394
1555
|
sessionId: poolKey,
|
|
@@ -1411,7 +1572,7 @@ export class OpenAIOAuthProvider {
|
|
|
1411
1572
|
process.stderr.write('[openai-oauth] WebSocket bypassed (forced); using HTTP/SSE\n');
|
|
1412
1573
|
}
|
|
1413
1574
|
} else {
|
|
1414
|
-
process.stderr.write(`[openai-oauth] WebSocket unhealthy (${reason}); falling back to HTTP/SSE\n`);
|
|
1575
|
+
if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(`[openai-oauth] WebSocket unhealthy (${reason}); falling back to HTTP/SSE\n`);
|
|
1415
1576
|
}
|
|
1416
1577
|
const result = await sendHttp({
|
|
1417
1578
|
auth,
|
|
@@ -1459,11 +1620,12 @@ export class OpenAIOAuthProvider {
|
|
|
1459
1620
|
// Prefer WebSocket for hot cache/delta transport; fall back to HTTP/SSE
|
|
1460
1621
|
// after retry-exhausted handshake/acquire/no-first-event failures.
|
|
1461
1622
|
try {
|
|
1462
|
-
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-start model=${useModel}
|
|
1623
|
+
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-start model=${useModel} agent=${_sendAgent} sessionHash=${createHash('sha256').update(String(_sendSessionId)).digest('hex').slice(0, 8)} iteration=${iteration ?? '(none)'}\n`); }
|
|
1463
1624
|
const result = await dispatchWs(false);
|
|
1464
1625
|
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok\n`); }
|
|
1465
1626
|
return recordLiveModel(result);
|
|
1466
1627
|
} catch (err) {
|
|
1628
|
+
traceWsError(err, 'primary');
|
|
1467
1629
|
const status = err?.httpStatus;
|
|
1468
1630
|
// Live-text invariant: if the WS attempt already relayed a
|
|
1469
1631
|
// non-empty text chunk to the client, NO recovery path may reissue
|
|
@@ -1482,6 +1644,7 @@ export class OpenAIOAuthProvider {
|
|
|
1482
1644
|
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok\n`); }
|
|
1483
1645
|
return recordLiveModel(result);
|
|
1484
1646
|
} catch (retryErr) {
|
|
1647
|
+
traceWsError(retryErr, 'auth_retry');
|
|
1485
1648
|
if (_shouldUseOpenAIHttpFallback(retryErr, externalSignal)) {
|
|
1486
1649
|
try {
|
|
1487
1650
|
return await dispatchHttp(
|
|
@@ -71,6 +71,24 @@ export class OpenAIDirectProvider {
|
|
|
71
71
|
// so gpt-5.4-mini can opt into Priority even when the OAuth catalog does
|
|
72
72
|
// not advertise a Fast tier for its OAuth endpoint.
|
|
73
73
|
applyOpenAIDirectFastTier(body, useModel, opts);
|
|
74
|
+
// P0 audit fix: buildRequestBody (openai-oauth.mjs) defaults
|
|
75
|
+
// store:false (env-gated, MIXDOG_OAI_STORE), which is correct for
|
|
76
|
+
// the openai-oauth ChatGPT-subscription backend — that backend keeps
|
|
77
|
+
// its own conversation state via the WS handshake session_id
|
|
78
|
+
// (see openai-oauth-ws.mjs "conversation slot ... in-memory prefix
|
|
79
|
+
// state"), independent of the public Responses API `store` field.
|
|
80
|
+
// The public OpenAI direct WS path below, however, talks to the real
|
|
81
|
+
// api.openai.com Responses API, where `previous_response_id`
|
|
82
|
+
// continuation is only valid when the anchored response was actually
|
|
83
|
+
// stored — store:false + previous_response_id is a broken
|
|
84
|
+
// combination there (the server has nothing to look up). This
|
|
85
|
+
// provider's WS transport always injects previous_response_id via
|
|
86
|
+
// openai-oauth-ws.mjs's delta path once a response id is cached, so
|
|
87
|
+
// force store:true here — same override xAI's Responses path takes
|
|
88
|
+
// (see openai-compat.mjs _doSendXaiResponses/_doSendXaiResponsesWebSocket:
|
|
89
|
+
// "the public endpoint currently returns previous_response_not_found
|
|
90
|
+
// ... unless the chain is stored").
|
|
91
|
+
body.store = true;
|
|
74
92
|
// Public Responses API supports prompt_cache_retention='24h' at no
|
|
75
93
|
// extra cost (same cached_input_tokens billing as the default 5–10
|
|
76
94
|
// min in-memory cache). openai-oauth rejects the parameter, so it's
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { existsSync, readFileSync
|
|
2
|
-
import * as fsp from 'node:fs/promises';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
2
|
import { join } from 'node:path';
|
|
3
|
+
import { updateJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
4
4
|
import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
|
|
5
5
|
import { getOpenCodeGoAuthCookie } from '../../../shared/config.mjs';
|
|
6
6
|
|
|
@@ -53,12 +53,18 @@ function readJson(file) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// Synchronous atomic+lock write (updateJsonAtomicSync) instead of the prior
|
|
57
|
+
// fire-and-forget fsp.writeFile: this cache is single-entry (one snapshot
|
|
58
|
+
// per file, no cross-process merge), so the lock protects against a torn
|
|
59
|
+
// write racing readers, not a lost-update merge. Only one write happens
|
|
60
|
+
// per successful fetch (TTL-gated, at most once per LIVE_TTL_MS), so the
|
|
61
|
+
// switch off async has no meaningful latency impact on the request path.
|
|
56
62
|
function writeJson(file, value) {
|
|
57
|
-
|
|
63
|
+
let next = null;
|
|
58
64
|
try {
|
|
59
|
-
|
|
60
|
-
void fsp.writeFile(file, JSON.stringify(value, null, 2), 'utf8').catch(() => {});
|
|
65
|
+
next = updateJsonAtomicSync(file, () => value, { lock: true, fsyncDir: true, timeoutMs: 1000 }); // best-effort cache write: short lock timeout, don't block on contention
|
|
61
66
|
} catch {}
|
|
67
|
+
if (next) diskJsonCache = { at: Date.now(), file, value: next }; // only mirror on confirmed write, avoid phantom cache on lock timeout
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
function freshSnapshot(snapshot, ttlMs) {
|
|
@@ -201,7 +201,8 @@ export function getProvider(name) {
|
|
|
201
201
|
// unregistered providers default to false (the openai/gemini majority).
|
|
202
202
|
export function providerInputExcludesCache(name) {
|
|
203
203
|
const p = getProvider(name);
|
|
204
|
-
|
|
204
|
+
if (p?.constructor?.inputExcludesCache === true) return true;
|
|
205
|
+
return String(name || '').toLowerCase().includes('anthropic');
|
|
205
206
|
}
|
|
206
207
|
export function getAllProviders() {
|
|
207
208
|
// Defensive copy — callers must not mutate the live registry or retain
|
|
@@ -126,7 +126,7 @@ export function classifyError(err) {
|
|
|
126
126
|
|| code === 'EAI_AGAIN' || code === 'ENOTFOUND' || code === 'EAI_NODATA'
|
|
127
127
|
|| code === 'ECONNREFUSED' || code === 'ENETUNREACH' || code === 'EHOSTUNREACH'
|
|
128
128
|
|| code === 'EPIPE'
|
|
129
|
-
|| code === 'EPROVIDERTIMEOUT' || code === 'EGEMINITIMEOUT'
|
|
129
|
+
|| code === 'EPROVIDERTIMEOUT' || code === 'EGEMINITIMEOUT' || code === 'ESTREAMSTALL'
|
|
130
130
|
|| code === 'EWSACQUIRETIMEOUT') {
|
|
131
131
|
return 'transient'
|
|
132
132
|
}
|
|
@@ -282,7 +282,7 @@ export function classifyMidstreamError(err, signals, policy = {}) {
|
|
|
282
282
|
|
|
283
283
|
// Verbatim relocation of openai-oauth-ws's _classifyMidstreamError. `signals`
|
|
284
284
|
// carries sawResponseCreated / sawCompleted / emittedText / emittedToolCall /
|
|
285
|
-
// wsCloseCode / firstByteTimeout /
|
|
285
|
+
// wsCloseCode / firstByteTimeout / wsSendFailed /
|
|
286
286
|
// userAbort / watchdogAbort / responseFailedPayload exactly as before.
|
|
287
287
|
function _classifyMidstreamWs(err, state, attemptIndex, policy) {
|
|
288
288
|
if (state.sawCompleted) return null
|
|
@@ -294,9 +294,6 @@ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
|
|
|
294
294
|
if (state.firstByteTimeout || err?.firstByteTimeout) {
|
|
295
295
|
return _allowMidstream('first_byte_timeout', attemptIndex, policy)
|
|
296
296
|
}
|
|
297
|
-
if (state.firstMeaningfulTimeout || err?.firstMeaningfulTimeout) {
|
|
298
|
-
return _allowMidstream('first_meaningful_timeout', attemptIndex, policy)
|
|
299
|
-
}
|
|
300
297
|
if (err?.wsSendFailed || state.wsSendFailed) {
|
|
301
298
|
return _allowMidstream('ws_send_failed', attemptIndex, policy)
|
|
302
299
|
}
|
|
@@ -315,7 +312,12 @@ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
|
|
|
315
312
|
|
|
316
313
|
const name = err?.name || ''
|
|
317
314
|
if (name === 'AgentStallAbortError') return _allowMidstream('agent_stall', attemptIndex, policy)
|
|
318
|
-
if (name === 'StreamStalledAbortError'
|
|
315
|
+
if (name === 'StreamStalledAbortError' || name === 'StreamStalledError' || err?.code === 'ESTREAMSTALL' || err?.streamStalled === true) {
|
|
316
|
+
// A stall that fired AFTER a tool call was emitted is unsafe to replay
|
|
317
|
+
// (double side-effect); surface it as terminal so the turn is not retried.
|
|
318
|
+
if (err?.unsafeToRetry === true) return null
|
|
319
|
+
return _allowMidstream('stream_stalled', attemptIndex, policy)
|
|
320
|
+
}
|
|
319
321
|
|
|
320
322
|
if (state.watchdogAbort === 'AgentStallAbortError') return _allowMidstream('agent_stall', attemptIndex, policy)
|
|
321
323
|
if (state.watchdogAbort === 'StreamStalledAbortError') return _allowMidstream('stream_stalled', attemptIndex, policy)
|
|
@@ -357,7 +359,12 @@ function _classifyMidstreamSse(err, state, attemptIndex, policy) {
|
|
|
357
359
|
|
|
358
360
|
const name = err?.name || ''
|
|
359
361
|
if (name === 'AgentStallAbortError') return 'agent_stall'
|
|
360
|
-
if (name === 'StreamStalledAbortError'
|
|
362
|
+
if (name === 'StreamStalledAbortError' || name === 'StreamStalledError' || err?.code === 'ESTREAMSTALL' || err?.streamStalled === true) {
|
|
363
|
+
// A stall AFTER a tool emit is unsafe to replay (double side-effect) →
|
|
364
|
+
// terminal (null), no mid-stream retry. Otherwise route to stream_stalled.
|
|
365
|
+
if (err?.unsafeToRetry === true) return null
|
|
366
|
+
return 'stream_stalled'
|
|
367
|
+
}
|
|
361
368
|
if (state.watchdogAbort === 'AgentStallAbortError') return 'agent_stall'
|
|
362
369
|
if (state.watchdogAbort === 'StreamStalledAbortError') return 'stream_stalled'
|
|
363
370
|
|
|
@@ -379,7 +386,7 @@ function _classifyMidstreamSse(err, state, attemptIndex, policy) {
|
|
|
379
386
|
// env-flag check (caller computes the flag and passes it).
|
|
380
387
|
const TRANSPORT_FALLBACK_CLASSIFIERS = new Set([
|
|
381
388
|
'timeout', 'reset', 'dns', 'refused', 'network', 'acquire_timeout', 'http_5xx',
|
|
382
|
-
'first_byte_timeout',
|
|
389
|
+
'first_byte_timeout',
|
|
383
390
|
'ws_1006', 'ws_1011', 'ws_1012', 'ws_1000', 'ws_4000', 'agent_stall', 'stream_stalled',
|
|
384
391
|
'response_failed_disconnected', 'response_failed_network', 'response_failed_auth_expired',
|
|
385
392
|
'ws_send_failed',
|
|
@@ -403,7 +410,6 @@ export function shouldFallbackTransport(err, { signal, enabled = true } = {}) {
|
|
|
403
410
|
if (TRANSPORT_FALLBACK_CLASSIFIERS.has(classifier)) return true
|
|
404
411
|
if (/^http_5\d\d$/.test(classifier)) return true
|
|
405
412
|
if (err?.firstByteTimeout) return true
|
|
406
|
-
if (err?.firstMeaningfulTimeout) return true
|
|
407
413
|
const msg = String(err?.message || '')
|
|
408
414
|
return /opening handshake has timed out|socket hang up|acquire timed out|no first server event|no meaningful output/i.test(msg)
|
|
409
415
|
}
|