mixdog 0.9.0 → 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 +10 -3
- 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/session-ingest-smoke.mjs +2 -2
- 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/mixdog-debug.cjs +0 -22
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +34 -56
- package/src/mixdog-session-runtime.mjs +710 -319
- 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 -4
- 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 +359 -106
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
- 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 +86 -30
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
- 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 +265 -1
- 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/bash-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
- 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 -1254
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +54 -3
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/session-discovery.mjs +0 -4
- package/src/runtime/channels/lib/telegram-format.mjs +283 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +187 -43
- 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/llm-worker-host.mjs +0 -4
- 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-ops-policy.mjs +0 -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/runtime-fetcher.mjs +43 -18
- package/src/runtime/memory/lib/session-ingest.mjs +116 -7
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +6 -3
- package/src/runtime/search/index.mjs +2 -7
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +0 -1
- 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/child-spawn-gate.mjs +0 -6
- 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/seeds.mjs +1 -11
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2137 -750
- 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 +146 -9
- 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 +177 -100
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +312 -40
- package/src/tui/components/tool-output-format.test.mjs +180 -1
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +7324 -2393
- 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 +354 -142
- package/src/tui/markdown/format-token.test.mjs +155 -17
- 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 -647
- 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-lib.mjs +0 -623
- 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 +154 -20
- 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/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Single dispatch path for the openai-oauth provider (SSE removed in
|
|
5
5
|
* v0.6.117). Uses the `responses_websockets=2026-02-06` beta WebSocket
|
|
6
6
|
* upgrade on chatgpt.com/backend-api/codex/responses. Per-session
|
|
7
|
-
* connections are pooled (
|
|
7
|
+
* connections are pooled (configurable idle TTL, up to 8 parallel sockets per
|
|
8
8
|
* key) so subsequent tool-loop iterations can send only the incremental
|
|
9
9
|
* `input` delta plus `previous_response_id`, skipping the full
|
|
10
10
|
* tools/system/history prefix each turn.
|
|
@@ -33,14 +33,26 @@ import {
|
|
|
33
33
|
traceAgentUsage,
|
|
34
34
|
appendAgentTrace,
|
|
35
35
|
} from '../agent-trace.mjs';
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
classifyHandshakeError,
|
|
38
|
+
classifyMidstreamError,
|
|
39
|
+
createStreamSafetyStamps,
|
|
40
|
+
jitterDelayMs,
|
|
41
|
+
MIDSTREAM_RETRY_POLICY,
|
|
42
|
+
populateHttpStatusFromMessage,
|
|
43
|
+
sleepWithAbort,
|
|
44
|
+
} from './retry-classifier.mjs';
|
|
37
45
|
import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
|
|
46
|
+
import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
|
|
38
47
|
import {
|
|
39
48
|
PROVIDER_RETRY_MAX_ATTEMPTS,
|
|
40
49
|
PROVIDER_WS_ACQUIRE_TIMEOUT_MS,
|
|
41
|
-
PROVIDER_WS_FIRST_MEANINGFUL_TIMEOUT_MS,
|
|
42
50
|
PROVIDER_WS_HANDSHAKE_TIMEOUT_MS,
|
|
43
51
|
PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS,
|
|
52
|
+
PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS,
|
|
53
|
+
PROVIDER_SSE_IDLE_WATCHDOG_ENABLED,
|
|
54
|
+
streamStalledError,
|
|
55
|
+
resolveTimeoutMs,
|
|
44
56
|
} from '../stall-policy.mjs';
|
|
45
57
|
import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
|
|
46
58
|
|
|
@@ -50,17 +62,19 @@ const CODEX_WS_URL = 'wss://chatgpt.com/backend-api/codex/responses';
|
|
|
50
62
|
const CODEX_OAUTH_ORIGINATOR = 'codex_cli_rs';
|
|
51
63
|
const OPENAI_WS_URL = 'wss://api.openai.com/v1/responses';
|
|
52
64
|
const XAI_WS_URL = 'wss://api.x.ai/v1/responses';
|
|
53
|
-
const WS_IDLE_MS =
|
|
65
|
+
const WS_IDLE_MS = resolveTimeoutMs(
|
|
66
|
+
'MIXDOG_PROVIDER_WS_IDLE_MS',
|
|
67
|
+
20 * 60_000,
|
|
68
|
+
{ minMs: 60_000, maxMs: 60 * 60_000 },
|
|
69
|
+
);
|
|
54
70
|
const WS_HANDSHAKE_TIMEOUT_MS = PROVIDER_WS_HANDSHAKE_TIMEOUT_MS;
|
|
55
71
|
const WS_ACQUIRE_TIMEOUT_MS = PROVIDER_WS_ACQUIRE_TIMEOUT_MS;
|
|
56
|
-
// Pre-stream watchdog uses the shared provider deadline so it fails before
|
|
57
|
-
// the 5-minute session slow warning.
|
|
58
|
-
const WS_FIRST_MEANINGFUL_MS = PROVIDER_WS_FIRST_MEANINGFUL_TIMEOUT_MS;
|
|
59
72
|
// Pre-`response.created` deadline. Once the socket is open and the
|
|
60
73
|
// response.create frame is sent, a healthy server emits response.created
|
|
61
74
|
// within seconds. If it stalls past this short bound the socket has wedged
|
|
62
75
|
// post-upgrade with zero server events — treat it as a fast, retryable
|
|
63
|
-
// first-byte timeout
|
|
76
|
+
// first-byte timeout. This is the ONLY pre-stream watchdog; once any server
|
|
77
|
+
// event arrives the single inter-chunk idle timer below takes over.
|
|
64
78
|
// Only this short window is shortened; the post-`response.created`
|
|
65
79
|
// inter-chunk / reasoning span keeps the longer deadlines below.
|
|
66
80
|
const WS_PRE_RESPONSE_CREATED_MS = (() => {
|
|
@@ -69,11 +83,22 @@ const WS_PRE_RESPONSE_CREATED_MS = (() => {
|
|
|
69
83
|
if (Number.isFinite(n) && n > 0) return Math.min(Math.max(n, 1_000), 120_000);
|
|
70
84
|
return 10_000;
|
|
71
85
|
})();
|
|
72
|
-
//
|
|
86
|
+
// Single inter-chunk idle timer. Resets on EVERY received frame (matches codex
|
|
87
|
+
// responses_websocket.rs timeout(idle_timeout, ws_stream.next()) and mixdog
|
|
88
|
+
// anthropic-oauth resetIdleTimer on every chunk).
|
|
73
89
|
const WS_INTER_CHUNK_MS = PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
90
|
+
// Mid-stream retry budgets + backoff now live in the shared MIDSTREAM_RETRY_POLICY
|
|
91
|
+
// table (retry-classifier.mjs). These aliases keep the local call sites readable
|
|
92
|
+
// and ensure the numbers exist in exactly ONE place.
|
|
93
|
+
const MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT = MIDSTREAM_RETRY_POLICY.ws.transientCloseRetries;
|
|
94
|
+
const MIDSTREAM_DEFAULT_RETRY_LIMIT = MIDSTREAM_RETRY_POLICY.ws.defaultRetries;
|
|
95
|
+
const MIDSTREAM_BACKOFF_MS = MIDSTREAM_RETRY_POLICY.ws.backoff;
|
|
96
|
+
// Policy object passed to the shared classifyMidstreamError for the WS path.
|
|
97
|
+
const WS_MIDSTREAM_POLICY = {
|
|
98
|
+
mode: 'ws',
|
|
99
|
+
transientCloseRetries: MIDSTREAM_RETRY_POLICY.ws.transientCloseRetries,
|
|
100
|
+
defaultRetries: MIDSTREAM_RETRY_POLICY.ws.defaultRetries,
|
|
101
|
+
};
|
|
77
102
|
|
|
78
103
|
// Handshake retry policy. The `ws` library surfaces a bare
|
|
79
104
|
// `Opening handshake has timed out` Error after handshakeTimeout; transient
|
|
@@ -1216,11 +1241,11 @@ export async function _streamResponse({
|
|
|
1216
1241
|
logSuppressedReasoningDeltas = true,
|
|
1217
1242
|
traceProvider = 'openai-oauth',
|
|
1218
1243
|
_timeouts = null,
|
|
1244
|
+
knownToolNames = null,
|
|
1219
1245
|
}) {
|
|
1220
1246
|
const errLabel = _wsErrLabel(traceProvider);
|
|
1221
1247
|
const socket = entry.socket;
|
|
1222
1248
|
const preResponseCreatedMs = _positiveInt(_timeouts?.preResponseCreatedMs, WS_PRE_RESPONSE_CREATED_MS);
|
|
1223
|
-
const firstMeaningfulMs = _positiveInt(_timeouts?.firstMeaningfulMs, WS_FIRST_MEANINGFUL_MS);
|
|
1224
1249
|
const interChunkMs = _positiveInt(_timeouts?.interChunkMs, WS_INTER_CHUNK_MS);
|
|
1225
1250
|
const _streamingStart = Date.now();
|
|
1226
1251
|
let _firstDeltaEmitted = false;
|
|
@@ -1236,6 +1261,19 @@ export async function _streamResponse({
|
|
|
1236
1261
|
const citations = [];
|
|
1237
1262
|
const citationKeys = new Set();
|
|
1238
1263
|
const pendingCalls = new Map();
|
|
1264
|
+
// Tool-work-in-flight flag: set the moment a function/custom tool call's
|
|
1265
|
+
// input starts streaming (before it lands in pendingCalls/toolCalls).
|
|
1266
|
+
// Gates partial-final SUCCESS so a stall mid tool-input never looks text-only.
|
|
1267
|
+
let _toolInFlight = false;
|
|
1268
|
+
// Fix 2: cross-path name+args dedupe. A text-leaked synthetic and an
|
|
1269
|
+
// identical native function_call must fire onToolCall exactly once. Every
|
|
1270
|
+
// dispatch site routes through emitToolCallDedupe.
|
|
1271
|
+
const _toolDedupe = createToolCallDedupe();
|
|
1272
|
+
const emitToolCallDedupe = (call) => {
|
|
1273
|
+
if (!_toolDedupe.shouldDispatch(call?.name, call?.arguments)) return;
|
|
1274
|
+
midState.emittedToolCall = true;
|
|
1275
|
+
try { onToolCall?.(call); } catch {}
|
|
1276
|
+
};
|
|
1239
1277
|
// Reasoning items collected from response.output_item.done (or salvaged
|
|
1240
1278
|
// from response.completed.response.output). The request still includes
|
|
1241
1279
|
// `reasoning.encrypted_content` so the server keeps emitting the blobs,
|
|
@@ -1323,8 +1361,7 @@ export async function _streamResponse({
|
|
|
1323
1361
|
const call = customToolCallFromResponseItem(item);
|
|
1324
1362
|
if (!call || toolCalls.some((existing) => existing.id === call.id)) return;
|
|
1325
1363
|
toolCalls.push(call);
|
|
1326
|
-
|
|
1327
|
-
try { onToolCall?.(call); } catch {}
|
|
1364
|
+
emitToolCallDedupe(call);
|
|
1328
1365
|
};
|
|
1329
1366
|
const pushToolSearchCall = (item) => {
|
|
1330
1367
|
if (!item || item.type !== 'tool_search_call') return;
|
|
@@ -1337,9 +1374,60 @@ export async function _streamResponse({
|
|
|
1337
1374
|
nativeType: 'tool_search_call',
|
|
1338
1375
|
};
|
|
1339
1376
|
toolCalls.push(call);
|
|
1377
|
+
emitToolCallDedupe(call);
|
|
1378
|
+
};
|
|
1379
|
+
// Leaked tool-call guard. The model sometimes emits a tool call as plain
|
|
1380
|
+
// text (XML `<invoke>`/`<function_calls>` or gpt-oss harmony
|
|
1381
|
+
// `<|channel|>...to=functions.NAME...<|call|>`) inside
|
|
1382
|
+
// `response.output_text.delta` instead of a native function_call. Route
|
|
1383
|
+
// text through the guard so leaked calls are suppressed from the visible
|
|
1384
|
+
// stream, synthesized (native `call_...` id shape), and dispatched like
|
|
1385
|
+
// native ones. Additive: the native function_call path is untouched.
|
|
1386
|
+
const leakGuard = createLeakGuard({ knownToolNames, harmony: true });
|
|
1387
|
+
const dispatchLeakedCall = (recovered) => {
|
|
1388
|
+
let args = recovered?.arguments;
|
|
1389
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
|
|
1390
|
+
const call = {
|
|
1391
|
+
id: `call_leaked_${randomBytes(8).toString('hex')}`,
|
|
1392
|
+
name: recovered.name,
|
|
1393
|
+
arguments: args,
|
|
1394
|
+
};
|
|
1395
|
+
if (!_toolDedupe.shouldDispatch(call.name, call.arguments)) return;
|
|
1396
|
+
toolCalls.push(call);
|
|
1340
1397
|
midState.emittedToolCall = true;
|
|
1341
1398
|
try { onToolCall?.(call); } catch {}
|
|
1342
1399
|
};
|
|
1400
|
+
const relayLeakText = (delta) => {
|
|
1401
|
+
if (!leakGuard.enabled) {
|
|
1402
|
+
content += delta || '';
|
|
1403
|
+
if (delta && onTextDelta) {
|
|
1404
|
+
if (state) state.emittedText = true;
|
|
1405
|
+
try { onTextDelta(delta); } catch {}
|
|
1406
|
+
}
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const { text, calls } = leakGuard.push(delta);
|
|
1410
|
+
if (text) {
|
|
1411
|
+
content += text;
|
|
1412
|
+
if (onTextDelta) {
|
|
1413
|
+
if (state) state.emittedText = true;
|
|
1414
|
+
try { onTextDelta(text); } catch {}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
for (const c of calls) dispatchLeakedCall(c);
|
|
1418
|
+
};
|
|
1419
|
+
const flushLeak = () => {
|
|
1420
|
+
if (!leakGuard.enabled) return;
|
|
1421
|
+
const { text, calls } = leakGuard.flush();
|
|
1422
|
+
if (text) {
|
|
1423
|
+
content += text;
|
|
1424
|
+
if (onTextDelta) {
|
|
1425
|
+
if (state) state.emittedText = true;
|
|
1426
|
+
try { onTextDelta(text); } catch {}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
for (const c of calls) dispatchLeakedCall(c);
|
|
1430
|
+
};
|
|
1343
1431
|
const logReasoningDeltaSuppression = () => {
|
|
1344
1432
|
if (!logSuppressedReasoningDeltas) return;
|
|
1345
1433
|
const total = reasoningTextDeltaCount + reasoningSummaryTextDeltaCount + reasoningOtherDeltaCount;
|
|
@@ -1368,6 +1456,16 @@ export async function _streamResponse({
|
|
|
1368
1456
|
let messageHandler = null;
|
|
1369
1457
|
let closeHandler = null;
|
|
1370
1458
|
let errorHandler = null;
|
|
1459
|
+
// SEMANTIC idle timer (pi per-event parity): distinct from the inter-chunk
|
|
1460
|
+
// timer, which resets on EVERY frame (rate_limits/metadata/keepalive keep
|
|
1461
|
+
// the socket "alive"). This timer resets ONLY on meaningful output deltas
|
|
1462
|
+
// (text/reasoning/tool args — the same events that call onStreamDelta) so a
|
|
1463
|
+
// stream that emits some deltas then goes silent (server keepalive frames
|
|
1464
|
+
// only) trips a short, named terminal StreamStalledError instead of coasting
|
|
1465
|
+
// to the 300s inter-chunk cap / 30-min agent watchdog.
|
|
1466
|
+
let semanticIdleTimer = null;
|
|
1467
|
+
const semanticIdleMs = PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS;
|
|
1468
|
+
const semanticIdleEnabled = PROVIDER_SSE_IDLE_WATCHDOG_ENABLED && semanticIdleMs > 0;
|
|
1371
1469
|
|
|
1372
1470
|
return new Promise((resolve, reject) => {
|
|
1373
1471
|
// Pre-stream watchdog: the timer fires if the server never sends a
|
|
@@ -1375,10 +1473,10 @@ export async function _streamResponse({
|
|
|
1375
1473
|
// after our last frame. The socket is open and the response.create
|
|
1376
1474
|
// frame was sent, but no server event has come back — a wedged
|
|
1377
1475
|
// post-upgrade socket. Healthy servers ack within seconds, so this
|
|
1378
|
-
// window is intentionally short (~
|
|
1379
|
-
//
|
|
1380
|
-
// the
|
|
1381
|
-
//
|
|
1476
|
+
// window is intentionally short (WS_PRE_RESPONSE_CREATED_MS, ~10s).
|
|
1477
|
+
// Once ANY server event arrives, resetIdle() cancels this watchdog and
|
|
1478
|
+
// the single inter-chunk idle timer takes over — silent gaps
|
|
1479
|
+
// mid-reasoning (openai-oauth spending 50s+ producing reasoning
|
|
1382
1480
|
// tokens) are normal and should not abort the turn.
|
|
1383
1481
|
const armPreStreamWatchdog = () => {
|
|
1384
1482
|
if (idleTimer) clearTimeout(idleTimer);
|
|
@@ -1410,8 +1508,6 @@ export async function _streamResponse({
|
|
|
1410
1508
|
}, preResponseCreatedMs);
|
|
1411
1509
|
};
|
|
1412
1510
|
let interChunkTimer = null;
|
|
1413
|
-
let firstMeaningfulTimer = null;
|
|
1414
|
-
let firstMeaningfulSeen = false;
|
|
1415
1511
|
const traceWsTimeout = (event, timeoutMs) => {
|
|
1416
1512
|
try {
|
|
1417
1513
|
const iteration = Number(midState.iteration);
|
|
@@ -1426,7 +1522,6 @@ export async function _streamResponse({
|
|
|
1426
1522
|
attempt_index: Number.isFinite(attemptIndex) ? attemptIndex : null,
|
|
1427
1523
|
warmup: midState.warmup === true,
|
|
1428
1524
|
saw_response_created: midState.sawResponseCreated === true,
|
|
1429
|
-
first_meaningful_seen: firstMeaningfulSeen === true,
|
|
1430
1525
|
};
|
|
1431
1526
|
appendAgentTrace({
|
|
1432
1527
|
sessionId: midState.sessionId || null,
|
|
@@ -1443,29 +1538,6 @@ export async function _streamResponse({
|
|
|
1443
1538
|
idleTimer = null;
|
|
1444
1539
|
}
|
|
1445
1540
|
};
|
|
1446
|
-
const clearFirstMeaningfulWatchdog = () => {
|
|
1447
|
-
if (firstMeaningfulTimer) {
|
|
1448
|
-
clearTimeout(firstMeaningfulTimer);
|
|
1449
|
-
firstMeaningfulTimer = null;
|
|
1450
|
-
}
|
|
1451
|
-
};
|
|
1452
|
-
const armFirstMeaningfulWatchdog = () => {
|
|
1453
|
-
if (firstMeaningfulSeen || firstMeaningfulMs <= 0) return;
|
|
1454
|
-
clearFirstMeaningfulWatchdog();
|
|
1455
|
-
firstMeaningfulTimer = setTimeout(() => {
|
|
1456
|
-
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
1457
|
-
process.stderr.write(`[agent-trace] ws-timeout kind=first-meaningful afterMs=${firstMeaningfulMs}\n`);
|
|
1458
|
-
}
|
|
1459
|
-
traceWsTimeout('first_meaningful_timeout', firstMeaningfulMs);
|
|
1460
|
-
const err = new Error(`WS stream: no meaningful output within ${firstMeaningfulMs}ms after response.created`);
|
|
1461
|
-
err.wsCloseCode = 4000;
|
|
1462
|
-
err.firstMeaningfulTimeout = true;
|
|
1463
|
-
midState.firstMeaningfulTimeout = true;
|
|
1464
|
-
terminalError = err;
|
|
1465
|
-
try { socket.close(4000, 'first_meaningful_timeout'); } catch {}
|
|
1466
|
-
finish();
|
|
1467
|
-
}, firstMeaningfulMs);
|
|
1468
|
-
};
|
|
1469
1541
|
const resetInterChunk = () => {
|
|
1470
1542
|
if (interChunkTimer) clearTimeout(interChunkTimer);
|
|
1471
1543
|
interChunkTimer = setTimeout(() => {
|
|
@@ -1480,30 +1552,49 @@ export async function _streamResponse({
|
|
|
1480
1552
|
finish();
|
|
1481
1553
|
}, interChunkMs);
|
|
1482
1554
|
};
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1555
|
+
// pi per-event idle: (re)armed only on meaningful output deltas via
|
|
1556
|
+
// bumpSemanticIdle(). Keepalive/metadata frames DON'T touch it, so a
|
|
1557
|
+
// deltas-then-silent wedge trips this short semantic window.
|
|
1558
|
+
const resetSemanticIdle = () => {
|
|
1559
|
+
if (!semanticIdleEnabled) return;
|
|
1560
|
+
if (semanticIdleTimer) clearTimeout(semanticIdleTimer);
|
|
1561
|
+
semanticIdleTimer = setTimeout(() => {
|
|
1562
|
+
traceWsTimeout('semantic_idle_timeout', semanticIdleMs);
|
|
1563
|
+
terminalError = streamStalledError('Responses WS', semanticIdleMs, { emittedToolCall: !!midState?.emittedToolCall });
|
|
1564
|
+
// Partial-final recovery parity: attach streamed partial state so
|
|
1565
|
+
// a wedged FINAL no-tool summary can be accepted as partial-final
|
|
1566
|
+
// success by the loop. pendingToolUse gates out mid-flight tools.
|
|
1567
|
+
try {
|
|
1568
|
+
terminalError.partialContent = content;
|
|
1569
|
+
terminalError.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
|
|
1570
|
+
terminalError.pendingToolUse = pendingCalls.size > 0
|
|
1571
|
+
|| !!midState?.emittedToolCall
|
|
1572
|
+
|| toolCalls.length > 0
|
|
1573
|
+
|| _toolInFlight === true;
|
|
1574
|
+
terminalError.partialModel = model || undefined;
|
|
1575
|
+
} catch { /* best-effort enrichment */ }
|
|
1576
|
+
try { terminalError.wsCloseCode = 4000; } catch {}
|
|
1577
|
+
try { socket.close(4000, 'semantic_idle_timeout'); } catch {}
|
|
1578
|
+
finish();
|
|
1579
|
+
}, semanticIdleMs);
|
|
1580
|
+
try { semanticIdleTimer.unref?.(); } catch {}
|
|
1487
1581
|
};
|
|
1488
|
-
//
|
|
1489
|
-
//
|
|
1490
|
-
//
|
|
1491
|
-
//
|
|
1492
|
-
//
|
|
1493
|
-
const
|
|
1494
|
-
|
|
1495
|
-
firstMeaningfulSeen = true;
|
|
1496
|
-
clearPreStreamWatchdog();
|
|
1497
|
-
clearFirstMeaningfulWatchdog();
|
|
1498
|
-
}
|
|
1582
|
+
// Single idle reset — called on EVERY parsed server event (matches
|
|
1583
|
+
// codex, which resets one idle timer on every received WS frame). Any
|
|
1584
|
+
// frame proves the socket is live; there is no separate "meaningful
|
|
1585
|
+
// output" gate. Also clears the pre-stream watchdog defensively in case
|
|
1586
|
+
// the first event is not response.created.
|
|
1587
|
+
const resetIdle = () => {
|
|
1588
|
+
clearPreStreamWatchdog();
|
|
1499
1589
|
resetInterChunk();
|
|
1500
1590
|
};
|
|
1501
|
-
//
|
|
1502
|
-
|
|
1591
|
+
// Meaningful-output progress bump: called by the same delta cases that
|
|
1592
|
+
// call onStreamDelta (text/reasoning/tool args). Arms the semantic idle.
|
|
1593
|
+
const bumpSemanticIdle = () => { resetSemanticIdle(); };
|
|
1503
1594
|
const cleanup = () => {
|
|
1504
1595
|
if (idleTimer) clearTimeout(idleTimer);
|
|
1505
|
-
clearFirstMeaningfulWatchdog();
|
|
1506
1596
|
if (interChunkTimer) { clearTimeout(interChunkTimer); interChunkTimer = null; }
|
|
1597
|
+
if (semanticIdleTimer) { clearTimeout(semanticIdleTimer); semanticIdleTimer = null; }
|
|
1507
1598
|
if (keepaliveTimer) { clearInterval(keepaliveTimer); keepaliveTimer = null; }
|
|
1508
1599
|
if (messageHandler) socket.off('message', messageHandler);
|
|
1509
1600
|
if (closeHandler) socket.off('close', closeHandler);
|
|
@@ -1512,6 +1603,9 @@ export async function _streamResponse({
|
|
|
1512
1603
|
};
|
|
1513
1604
|
const finish = () => {
|
|
1514
1605
|
logReasoningDeltaSuppression();
|
|
1606
|
+
// Flush any partial-sentinel tail held back mid-stream so
|
|
1607
|
+
// legitimate trailing text is never lost (streamed-text path).
|
|
1608
|
+
flushLeak();
|
|
1515
1609
|
cleanup();
|
|
1516
1610
|
if (terminalError) { reject(terminalError); return; }
|
|
1517
1611
|
resolve({
|
|
@@ -1519,11 +1613,17 @@ export async function _streamResponse({
|
|
|
1519
1613
|
model,
|
|
1520
1614
|
reasoningItems: reasoningItems.length ? reasoningItems : undefined,
|
|
1521
1615
|
responseItems: responseItemsAdded.length ? responseItemsAdded : undefined,
|
|
1522
|
-
|
|
1616
|
+
// Dedupe by name+args (Fix 2, array side) so an identical
|
|
1617
|
+
// synthetic-leaked + native pair can't run the tool twice.
|
|
1618
|
+
toolCalls: toolCalls.length ? dedupeToolCallList(toolCalls) : undefined,
|
|
1523
1619
|
citations: citations.length ? citations : undefined,
|
|
1524
1620
|
webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
|
|
1525
1621
|
usage,
|
|
1526
1622
|
stopReason: stopReason || undefined,
|
|
1623
|
+
// P1 audit fix: mirror the HTTP/SSE fallback's truncated flag
|
|
1624
|
+
// for the WS path (sendViaWebSocket spreads this result
|
|
1625
|
+
// through to the provider caller unchanged).
|
|
1626
|
+
...(stopReason === 'length' && content.length > 0 ? { truncated: true } : {}),
|
|
1527
1627
|
incompleteReason: incompleteReason || undefined,
|
|
1528
1628
|
responseId: responseId || undefined,
|
|
1529
1629
|
serviceTier: responseServiceTier || undefined,
|
|
@@ -1532,7 +1632,10 @@ export async function _streamResponse({
|
|
|
1532
1632
|
|
|
1533
1633
|
messageHandler = (data) => {
|
|
1534
1634
|
resetIdle();
|
|
1535
|
-
//
|
|
1635
|
+
// resetIdle() above resets the SINGLE inter-chunk idle timer on
|
|
1636
|
+
// EVERY received frame (codex parity) — response.created, metadata,
|
|
1637
|
+
// rate_limits, and all deltas keep the socket alive. Separately, do
|
|
1638
|
+
// NOT call onStreamDelta for every frame — metadata/keepalive frames
|
|
1536
1639
|
// must not reset the agent stall watchdog's lastStreamDeltaAt. Only
|
|
1537
1640
|
// meaningful output (text delta / tool call) updates that timestamp.
|
|
1538
1641
|
const text = typeof data === 'string' ? data : data.toString('utf-8');
|
|
@@ -1554,13 +1657,11 @@ export async function _streamResponse({
|
|
|
1554
1657
|
midState.sawResponseCreated = true;
|
|
1555
1658
|
if (event.response?.model) model = event.response.model;
|
|
1556
1659
|
if (event.response?.id) responseId = event.response.id;
|
|
1557
|
-
// Server ack
|
|
1558
|
-
//
|
|
1559
|
-
//
|
|
1560
|
-
onResponseCreated();
|
|
1660
|
+
// Server ack (first event). resetIdle() at the top of
|
|
1661
|
+
// messageHandler already cleared the pre-stream watchdog and
|
|
1662
|
+
// armed the single idle timer — no extra bookkeeping here.
|
|
1561
1663
|
break;
|
|
1562
1664
|
case 'response.output_text.delta':
|
|
1563
|
-
content += event.delta || '';
|
|
1564
1665
|
try {
|
|
1565
1666
|
if (!_firstDeltaEmitted) {
|
|
1566
1667
|
_firstDeltaEmitted = true;
|
|
@@ -1570,6 +1671,7 @@ export async function _streamResponse({
|
|
|
1570
1671
|
}
|
|
1571
1672
|
onStreamDelta?.();
|
|
1572
1673
|
} catch {}
|
|
1674
|
+
bumpSemanticIdle();
|
|
1573
1675
|
// Live text relay (gateway): forward the raw text chunk so
|
|
1574
1676
|
// the client renders first tokens before the final replay.
|
|
1575
1677
|
// Tool-call/argument deltas intentionally stay off this path.
|
|
@@ -1577,11 +1679,10 @@ export async function _streamResponse({
|
|
|
1577
1679
|
// cannot be withdrawn, so flag the attempt so a later
|
|
1578
1680
|
// mid-stream/truncated failure is NOT retried (retry would
|
|
1579
1681
|
// concatenate a second attempt onto rendered text).
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
onMeaningfulOutput();
|
|
1682
|
+
// Routed through the leaked-tool-call guard: appends to
|
|
1683
|
+
// `content`, forwards visible text via onTextDelta, and
|
|
1684
|
+
// recovers/dispatches any leaked known-tool call.
|
|
1685
|
+
relayLeakText(event.delta || '');
|
|
1585
1686
|
break;
|
|
1586
1687
|
case 'response.reasoning_text.delta':
|
|
1587
1688
|
case 'response.reasoning_summary_text.delta':
|
|
@@ -1589,16 +1690,12 @@ export async function _streamResponse({
|
|
|
1589
1690
|
else reasoningSummaryTextDeltaCount += 1;
|
|
1590
1691
|
// Reasoning text is live model progress — refresh
|
|
1591
1692
|
// lastStreamDeltaAt so stream-watchdog does not flag a
|
|
1592
|
-
// long reasoning span as a stall.
|
|
1593
|
-
//
|
|
1594
|
-
//
|
|
1595
|
-
//
|
|
1596
|
-
// first-meaningful timer and abort an otherwise healthy
|
|
1597
|
-
// stream. Reasoning is still suppressed from user
|
|
1598
|
-
// content (no `content +=` here) — only the watchdog
|
|
1599
|
-
// timers are reset.
|
|
1693
|
+
// long reasoning span as a stall. The local WS idle timer
|
|
1694
|
+
// was already reset by resetIdle() at the top of
|
|
1695
|
+
// messageHandler. Reasoning is still suppressed from user
|
|
1696
|
+
// content (no `content +=` here).
|
|
1600
1697
|
try { onStreamDelta?.(); } catch {}
|
|
1601
|
-
|
|
1698
|
+
bumpSemanticIdle();
|
|
1602
1699
|
break;
|
|
1603
1700
|
case 'response.output_item.added':
|
|
1604
1701
|
if (event.item?.type === 'function_call') {
|
|
@@ -1606,16 +1703,20 @@ export async function _streamResponse({
|
|
|
1606
1703
|
name: event.item.name || '',
|
|
1607
1704
|
callId: event.item.call_id || '',
|
|
1608
1705
|
});
|
|
1609
|
-
|
|
1706
|
+
_toolInFlight = true;
|
|
1707
|
+
} else if (event.item?.type === 'custom_tool_call') {
|
|
1708
|
+
_toolInFlight = true;
|
|
1610
1709
|
}
|
|
1611
1710
|
break;
|
|
1612
1711
|
case 'response.function_call_arguments.delta':
|
|
1712
|
+
_toolInFlight = true;
|
|
1613
1713
|
try { onStreamDelta?.(); } catch {}
|
|
1614
|
-
|
|
1714
|
+
bumpSemanticIdle();
|
|
1615
1715
|
break;
|
|
1616
1716
|
case 'response.custom_tool_call_input.delta':
|
|
1717
|
+
_toolInFlight = true;
|
|
1617
1718
|
try { onStreamDelta?.(); } catch {}
|
|
1618
|
-
|
|
1719
|
+
bumpSemanticIdle();
|
|
1619
1720
|
break;
|
|
1620
1721
|
case 'response.function_call_arguments.done': {
|
|
1621
1722
|
const itemId = event.item_id || '';
|
|
@@ -1646,8 +1747,7 @@ export async function _streamResponse({
|
|
|
1646
1747
|
if (pending?.callId && pending?.name) {
|
|
1647
1748
|
const call = { id: pending.callId, name: pending.name, arguments: args };
|
|
1648
1749
|
toolCalls.push(call);
|
|
1649
|
-
|
|
1650
|
-
try { onToolCall?.(call); } catch {}
|
|
1750
|
+
emitToolCallDedupe(call);
|
|
1651
1751
|
} else {
|
|
1652
1752
|
// Synthesizing a `tc_${Date.now()}` callId here would
|
|
1653
1753
|
// make the next turn fail to match the model's
|
|
@@ -1667,7 +1767,7 @@ export async function _streamResponse({
|
|
|
1667
1767
|
});
|
|
1668
1768
|
}
|
|
1669
1769
|
try { onStreamDelta?.(); } catch {}
|
|
1670
|
-
|
|
1770
|
+
bumpSemanticIdle();
|
|
1671
1771
|
break;
|
|
1672
1772
|
}
|
|
1673
1773
|
case 'response.output_item.done':
|
|
@@ -1681,11 +1781,9 @@ export async function _streamResponse({
|
|
|
1681
1781
|
if (event.item?.type === 'web_search_call') pushWebSearchCall(event.item);
|
|
1682
1782
|
if (event.item?.type === 'tool_search_call') {
|
|
1683
1783
|
pushToolSearchCall(event.item);
|
|
1684
|
-
onMeaningfulOutput();
|
|
1685
1784
|
}
|
|
1686
1785
|
if (event.item?.type === 'custom_tool_call') {
|
|
1687
1786
|
pushCustomToolCall(event.item);
|
|
1688
|
-
onMeaningfulOutput();
|
|
1689
1787
|
}
|
|
1690
1788
|
break;
|
|
1691
1789
|
case 'response.completed': {
|
|
@@ -1717,7 +1815,18 @@ export async function _streamResponse({
|
|
|
1717
1815
|
if (!content && item.type === 'message') {
|
|
1718
1816
|
for (const c of item.content || []) {
|
|
1719
1817
|
if (c.type === 'output_text') {
|
|
1720
|
-
|
|
1818
|
+
// Completed-output fallback (no streamed
|
|
1819
|
+
// text). Route through the leak guard so
|
|
1820
|
+
// a tool call leaked only in the final
|
|
1821
|
+
// bundle is recovered, not surfaced as
|
|
1822
|
+
// visible content. final=true → full flush.
|
|
1823
|
+
if (leakGuard.enabled) {
|
|
1824
|
+
const { text, calls } = leakGuard.push(c.text || '', true);
|
|
1825
|
+
content += text;
|
|
1826
|
+
for (const lc of calls) dispatchLeakedCall(lc);
|
|
1827
|
+
} else {
|
|
1828
|
+
content += c.text || '';
|
|
1829
|
+
}
|
|
1721
1830
|
pushOutputTextAnnotations(c);
|
|
1722
1831
|
}
|
|
1723
1832
|
}
|
|
@@ -1753,8 +1862,7 @@ export async function _streamResponse({
|
|
|
1753
1862
|
if (tc.id && tc.name) {
|
|
1754
1863
|
delete tc._deferred;
|
|
1755
1864
|
delete tc._pendingItemId;
|
|
1756
|
-
|
|
1757
|
-
try { onToolCall?.(tc); } catch {}
|
|
1865
|
+
emitToolCallDedupe(tc);
|
|
1758
1866
|
}
|
|
1759
1867
|
}
|
|
1760
1868
|
}
|
|
@@ -1891,6 +1999,13 @@ export async function _streamResponse({
|
|
|
1891
1999
|
&& event.type.startsWith('response.reasoning')
|
|
1892
2000
|
&& event.type.endsWith('.delta')) {
|
|
1893
2001
|
reasoningOtherDeltaCount += 1;
|
|
2002
|
+
// These ARE live model progress (reviewer Medium): a
|
|
2003
|
+
// provider that emits only these reasoning variants for a
|
|
2004
|
+
// long span would otherwise trip the SEMANTIC idle abort.
|
|
2005
|
+
// Refresh both the watchdog and the semantic idle timer,
|
|
2006
|
+
// matching the named reasoning_text.delta case above.
|
|
2007
|
+
try { onStreamDelta?.(); } catch {}
|
|
2008
|
+
bumpSemanticIdle();
|
|
1894
2009
|
}
|
|
1895
2010
|
// Trace-only events (response.in_progress, etc.)
|
|
1896
2011
|
break;
|
|
@@ -1997,34 +2112,12 @@ export async function _streamResponse({
|
|
|
1997
2112
|
* 'http_5xx' (with specific status e.g. 'http_503') — server overload
|
|
1998
2113
|
* null — not retryable
|
|
1999
2114
|
*/
|
|
2115
|
+
// Thin re-export wrapper: handshake classification now lives in the shared
|
|
2116
|
+
// retry-classifier (classifyHandshakeError). Kept here as a named export so
|
|
2117
|
+
// internal call sites (_acquireWithRetry) and any external importer keep
|
|
2118
|
+
// resolving the same symbol.
|
|
2000
2119
|
export function _classifyHandshakeError(err) {
|
|
2001
|
-
|
|
2002
|
-
const code = err.code || '';
|
|
2003
|
-
const msg = String(err.message || '');
|
|
2004
|
-
const status = Number(err.httpStatus || 0);
|
|
2005
|
-
|
|
2006
|
-
// Permanent HTTP (auth / quota / not-found) short-circuits.
|
|
2007
|
-
if (status === 401 || status === 403 || status === 404 || status === 429) {
|
|
2008
|
-
return null;
|
|
2009
|
-
}
|
|
2010
|
-
// 5xx transient.
|
|
2011
|
-
if (status >= 500 && status < 600) {
|
|
2012
|
-
return `http_${status}`;
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
// Node errno codes.
|
|
2016
|
-
if (code === 'ECONNRESET') return 'reset';
|
|
2017
|
-
if (code === 'EAI_AGAIN' || code === 'ENOTFOUND' || code === 'EAI_NODATA') return 'dns';
|
|
2018
|
-
if (code === 'ECONNREFUSED') return 'refused';
|
|
2019
|
-
if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT') return 'timeout';
|
|
2020
|
-
if (code === 'EWSACQUIRETIMEOUT') return 'acquire_timeout';
|
|
2021
|
-
if (code === 'ENETUNREACH' || code === 'EHOSTUNREACH' || code === 'EPIPE') return 'network';
|
|
2022
|
-
|
|
2023
|
-
// `ws` library's handshake-timeout path: thrown as a bare Error.
|
|
2024
|
-
if (/opening handshake has timed out/i.test(msg)) return 'timeout';
|
|
2025
|
-
if (/socket hang up/i.test(msg)) return 'reset';
|
|
2026
|
-
|
|
2027
|
-
return null;
|
|
2120
|
+
return classifyHandshakeError(err);
|
|
2028
2121
|
}
|
|
2029
2122
|
|
|
2030
2123
|
/**
|
|
@@ -2048,9 +2141,6 @@ export function _classifyHandshakeError(err) {
|
|
|
2048
2141
|
* response.create frame sent, but the server never
|
|
2049
2142
|
* emitted response.created within the short
|
|
2050
2143
|
* pre-stream deadline. Fast-fail retryable.
|
|
2051
|
-
* 'first_meaningful_timeout' — server ACKed response.created, then emitted
|
|
2052
|
-
* no real text/reasoning/tool progress before the
|
|
2053
|
-
* first-meaningful deadline.
|
|
2054
2144
|
* 'response_failed_network' — response.failed with network_error
|
|
2055
2145
|
* 'response_failed_disconnected' — response.failed with stream_disconnected
|
|
2056
2146
|
*
|
|
@@ -2065,128 +2155,25 @@ export function _classifyHandshakeError(err) {
|
|
|
2065
2155
|
* - HTTP 401 / 403 / 429 surfaced on the error
|
|
2066
2156
|
* - state.attemptIndex has reached the classifier-specific retry budget
|
|
2067
2157
|
*/
|
|
2158
|
+
// Thin wrapper: the full WS mid-stream decision tree now lives in the shared
|
|
2159
|
+
// classifyMidstreamError (retry-classifier.mjs, policy.mode='ws'). Kept as a
|
|
2160
|
+
// named export so internal call sites and any external importer keep resolving
|
|
2161
|
+
// the same symbol. Behavior is byte-identical — the shared function is the
|
|
2162
|
+
// relocated original, with the per-classifier budget gating supplied by
|
|
2163
|
+
// WS_MIDSTREAM_POLICY (transientCloseRetries=4, defaultRetries=2).
|
|
2068
2164
|
export function _classifyMidstreamError(err, state) {
|
|
2069
|
-
|
|
2070
|
-
const attemptIndex = state.attemptIndex | 0;
|
|
2071
|
-
// Already completed (shouldn't throw, but defensive).
|
|
2072
|
-
if (state.sawCompleted) return null;
|
|
2073
|
-
// Any tool call already surfaced to the caller — retrying would
|
|
2074
|
-
// normally duplicate the side effect. EXCEPTION: ws_1000 truncation
|
|
2075
|
-
// (server-side normal close after response.created, before completion)
|
|
2076
|
-
// leaves the caller with an orphaned tool_use that the next turn cannot
|
|
2077
|
-
// pair to a tool_result, which the provider rejects with a hard 400.
|
|
2078
|
-
// The duplicate-side-effect risk is preferable to deterministic worker
|
|
2079
|
-
// death, especially for detached agents that re-dispatch idempotently.
|
|
2080
|
-
if (state.emittedToolCall) {
|
|
2081
|
-
const _cc = Number(err?.wsCloseCode || state.wsCloseCode || 0);
|
|
2082
|
-
if (!(_cc === 1000 && state.sawResponseCreated && !state.sawCompleted)) return null;
|
|
2083
|
-
}
|
|
2084
|
-
// Live-text invariant: once a non-empty text chunk has been relayed to the
|
|
2085
|
-
// client (gateway live mode), the rendered output cannot be withdrawn and a
|
|
2086
|
-
// retry would concatenate a second attempt. Treat every subsequent
|
|
2087
|
-
// mid-stream/truncated failure as final — never retry.
|
|
2088
|
-
if (state.emittedText || err?.liveTextEmitted) return null;
|
|
2089
|
-
// Post-upgrade-no-first-event: the socket opened, our response.create
|
|
2090
|
-
// frame was sent, but the server never emitted a single event before
|
|
2091
|
-
// the short pre-`response.created` watchdog fired. The handshake retry
|
|
2092
|
-
// layer only sees pre-upgrade failures and the legacy pre-stream gate
|
|
2093
|
-
// below would deny this case (sawResponseCreated === false). Tag it
|
|
2094
|
-
// here as a fast retryable bucket so the worker reconnects within
|
|
2095
|
-
// seconds instead of stalling for the full first-meaningful window.
|
|
2096
|
-
if (state.firstByteTimeout || err?.firstByteTimeout) {
|
|
2097
|
-
return _allowMidstreamRetry('first_byte_timeout', attemptIndex);
|
|
2098
|
-
}
|
|
2099
|
-
if (state.firstMeaningfulTimeout || err?.firstMeaningfulTimeout) {
|
|
2100
|
-
return _allowMidstreamRetry('first_meaningful_timeout', attemptIndex);
|
|
2101
|
-
}
|
|
2102
|
-
// _sendFrame failure (socket not OPEN, send callback errored, JSON
|
|
2103
|
-
// serialize threw). Always retryable: caller will forceFresh next
|
|
2104
|
-
// attempt so the wedged socket is dropped.
|
|
2105
|
-
if (err?.wsSendFailed || state.wsSendFailed) {
|
|
2106
|
-
return _allowMidstreamRetry('ws_send_failed', attemptIndex);
|
|
2107
|
-
}
|
|
2108
|
-
// Pre-stream failures normally belong to the handshake retry layer. BUT
|
|
2109
|
-
// WS close 1011 / 1012 can fire after the 101 upgrade but BEFORE the
|
|
2110
|
-
// first response.created event when the server's keepalive times out or
|
|
2111
|
-
// the service restarts. Neither the handshake retry layer (it only sees
|
|
2112
|
-
// pre-upgrade failures) nor the existing mid-stream gate covers this
|
|
2113
|
-
// window, so permit bounded retry here for those two codes only.
|
|
2114
|
-
if (!state.sawResponseCreated) {
|
|
2115
|
-
const closeCode = Number(err?.wsCloseCode || state.wsCloseCode || 0);
|
|
2116
|
-
if (closeCode !== 1011 && closeCode !== 1012) return null;
|
|
2117
|
-
}
|
|
2118
|
-
// User/caller abort — never retry.
|
|
2119
|
-
if (state.userAbort) return null;
|
|
2120
|
-
|
|
2121
|
-
if (!err) return null;
|
|
2122
|
-
const status = Number(err?.httpStatus || 0);
|
|
2123
|
-
if (status === 401 || status === 403 || status === 429) return null;
|
|
2124
|
-
// Transient 5xx surfaced via populateHttpStatusFromMessage (case 'error'
|
|
2125
|
-
// and case 'response.failed' branches sniff server-supplied text like
|
|
2126
|
-
// "Our servers are currently overloaded" and assign httpStatus=503).
|
|
2127
|
-
// Allow one bounded mid-stream retry on the same budget as the WS close-
|
|
2128
|
-
// code buckets above so server-side overload no longer leaks straight
|
|
2129
|
-
// to the caller without a single retry attempt.
|
|
2130
|
-
if (status >= 500 && status < 600) {
|
|
2131
|
-
return _allowMidstreamRetry(`http_${status}`, attemptIndex);
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
const name = err?.name || '';
|
|
2135
|
-
if (name === 'AgentStallAbortError') return _allowMidstreamRetry('agent_stall', attemptIndex);
|
|
2136
|
-
if (name === 'StreamStalledAbortError') return _allowMidstreamRetry('stream_stalled', attemptIndex);
|
|
2137
|
-
|
|
2138
|
-
// Watchdog abort surfaced via externalSignal handler → err is the reason
|
|
2139
|
-
// itself. state.watchdogAbort captures the class name when the error
|
|
2140
|
-
// shape was preserved but the name was stripped by some wrapper.
|
|
2141
|
-
if (state.watchdogAbort === 'AgentStallAbortError') return _allowMidstreamRetry('agent_stall', attemptIndex);
|
|
2142
|
-
if (state.watchdogAbort === 'StreamStalledAbortError') return _allowMidstreamRetry('stream_stalled', attemptIndex);
|
|
2143
|
-
|
|
2144
|
-
// WS close codes: prefer the decorated property, fall back to state.
|
|
2145
|
-
const closeCode = Number(err?.wsCloseCode || state.wsCloseCode || 0);
|
|
2146
|
-
if (closeCode === 1006) return _allowMidstreamRetry('ws_1006', attemptIndex);
|
|
2147
|
-
if (closeCode === 1011) return _allowMidstreamRetry('ws_1011', attemptIndex);
|
|
2148
|
-
if (closeCode === 1012) return _allowMidstreamRetry('ws_1012', attemptIndex);
|
|
2149
|
-
// Private 4xxx codes from a server/proxy are auth/policy/application closes;
|
|
2150
|
-
// never treat them as transient. 4000 is our local pre-stream watchdog code.
|
|
2151
|
-
if (closeCode >= 4000 && closeCode < 5000 && closeCode !== 4000) return null;
|
|
2152
|
-
if (closeCode === 4000) return _allowMidstreamRetry('ws_4000', attemptIndex);
|
|
2153
|
-
// Server-side normal close (1000) AFTER response.created but BEFORE
|
|
2154
|
-
// response.completed = truncated stream; legitimate transient. The
|
|
2155
|
-
// pre-stream gate above already rejects 1000 before sawResponseCreated
|
|
2156
|
-
// (handshake retry layer owns that window).
|
|
2157
|
-
if (closeCode === 1000 && state.sawResponseCreated && !state.sawCompleted) return _allowMidstreamRetry('ws_1000', attemptIndex);
|
|
2158
|
-
|
|
2159
|
-
// response.failed payload mentioning network_error / stream_disconnected.
|
|
2160
|
-
// xAI's gRPC backend periodically rotates auth context (server-side TTL)
|
|
2161
|
-
// and surfaces "Auth context expired" as a response.failed event. The
|
|
2162
|
-
// attemptIndex > 0 path in sendViaWebSocket forces a fresh WS handshake,
|
|
2163
|
-
// which re-authenticates — so a single bounded retry recovers the turn
|
|
2164
|
-
// instead of letting the worker die mid-session.
|
|
2165
|
-
const failed = err?.responseFailed || state.responseFailedPayload;
|
|
2166
|
-
if (failed) {
|
|
2167
|
-
try {
|
|
2168
|
-
const blob = JSON.stringify(failed).toLowerCase();
|
|
2169
|
-
if (blob.includes('stream_disconnected')) return _allowMidstreamRetry('response_failed_disconnected', attemptIndex);
|
|
2170
|
-
if (blob.includes('network_error')) return _allowMidstreamRetry('response_failed_network', attemptIndex);
|
|
2171
|
-
if (blob.includes('auth context expired')) return _allowMidstreamRetry('response_failed_auth_expired', attemptIndex);
|
|
2172
|
-
} catch {}
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
// Unknown → default-deny (don't risk a second full-cost turn for an error
|
|
2176
|
-
// class we haven't proven is transient).
|
|
2177
|
-
return null;
|
|
2165
|
+
return classifyMidstreamError(err, state, WS_MIDSTREAM_POLICY);
|
|
2178
2166
|
}
|
|
2179
2167
|
|
|
2168
|
+
// Per-classifier retry budget, used by the sendViaWebSocket loop to bound the
|
|
2169
|
+
// attempt count once classifyMidstreamError returns a bucket. Mirrors the
|
|
2170
|
+
// shared _midstreamLimitFor(ws) — the numbers come from MIDSTREAM_RETRY_POLICY.
|
|
2180
2171
|
function _midstreamRetryLimit(classifier) {
|
|
2181
2172
|
return classifier === 'ws_1006' || classifier === 'ws_1011'
|
|
2182
2173
|
? MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT
|
|
2183
2174
|
: MIDSTREAM_DEFAULT_RETRY_LIMIT;
|
|
2184
2175
|
}
|
|
2185
2176
|
|
|
2186
|
-
function _allowMidstreamRetry(classifier, attemptIndex) {
|
|
2187
|
-
return attemptIndex < _midstreamRetryLimit(classifier) ? classifier : null;
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
2177
|
function _midstreamBackoffFor(retryNumber) {
|
|
2191
2178
|
const raw = MIDSTREAM_BACKOFF_MS[Math.min(Math.max(retryNumber, 1), MIDSTREAM_BACKOFF_MS.length) - 1];
|
|
2192
2179
|
return jitterDelayMs(raw);
|
|
@@ -2200,25 +2187,11 @@ function _backoffFor(attempt) {
|
|
|
2200
2187
|
|
|
2201
2188
|
const _defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2202
2189
|
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
}
|
|
2209
|
-
await new Promise((resolve, reject) => {
|
|
2210
|
-
const t = setTimeout(() => {
|
|
2211
|
-
externalSignal.removeEventListener('abort', onAbort);
|
|
2212
|
-
resolve();
|
|
2213
|
-
}, ms);
|
|
2214
|
-
const onAbort = () => {
|
|
2215
|
-
clearTimeout(t);
|
|
2216
|
-
const reason = externalSignal.reason;
|
|
2217
|
-
reject(reason instanceof Error ? reason : new Error('OpenAI OAuth WS retry backoff aborted'));
|
|
2218
|
-
};
|
|
2219
|
-
if (externalSignal.aborted) { onAbort(); return; }
|
|
2220
|
-
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
2221
|
-
});
|
|
2190
|
+
// Abort-aware backoff sleep → shared sleepWithAbort (retry-classifier.mjs). The
|
|
2191
|
+
// abortMessage preserves the prior fallback text when the abort reason is not an
|
|
2192
|
+
// Error; _sleepFn (test seam) is threaded through as the no-signal sleep impl.
|
|
2193
|
+
function _sleepWithAbort(ms, externalSignal, sleepFn = _defaultSleep) {
|
|
2194
|
+
return sleepWithAbort(ms, externalSignal, sleepFn, 'OpenAI OAuth WS retry backoff aborted');
|
|
2222
2195
|
}
|
|
2223
2196
|
|
|
2224
2197
|
/**
|
|
@@ -2274,7 +2247,7 @@ export async function _acquireWithRetry({
|
|
|
2274
2247
|
try { err.retryClassifier = classifier; } catch {}
|
|
2275
2248
|
}
|
|
2276
2249
|
try {
|
|
2277
|
-
process.stderr.write(
|
|
2250
|
+
if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(
|
|
2278
2251
|
`[openai-oauth-ws] handshake failed after ${attempt}/${HANDSHAKE_MAX_ATTEMPTS} attempts: ${err?.message || err}\n`,
|
|
2279
2252
|
);
|
|
2280
2253
|
} catch {}
|
|
@@ -2283,7 +2256,7 @@ export async function _acquireWithRetry({
|
|
|
2283
2256
|
// Schedule backoff and emit progress.
|
|
2284
2257
|
const backoff = _backoffFor(attempt);
|
|
2285
2258
|
try {
|
|
2286
|
-
process.stderr.write(
|
|
2259
|
+
if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(
|
|
2287
2260
|
`[openai-oauth-ws] worker retry ${attempt}/${HANDSHAKE_MAX_ATTEMPTS} (transient: ${classifier}, backoff ${backoff}ms)\n`,
|
|
2288
2261
|
);
|
|
2289
2262
|
} catch {}
|
|
@@ -2371,6 +2344,14 @@ export async function sendViaWebSocket({
|
|
|
2371
2344
|
const MAX_MIDSTREAM_RETRIES = MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT;
|
|
2372
2345
|
let firstAttemptError = null;
|
|
2373
2346
|
let firstAttemptClassifier = null;
|
|
2347
|
+
// Known tool names for the leaked-tool-call guard in _streamResponse.
|
|
2348
|
+
// Derived from the exact request body so a recovered leaked call only
|
|
2349
|
+
// synthesizes when it names a tool actually offered to this request.
|
|
2350
|
+
const knownToolNames = new Set(
|
|
2351
|
+
(Array.isArray(body?.tools) ? body.tools : [])
|
|
2352
|
+
.map((t) => (typeof t?.name === 'string' ? t.name : null))
|
|
2353
|
+
.filter(Boolean),
|
|
2354
|
+
);
|
|
2374
2355
|
// Live-text invariant across attempts: once ANY attempt has relayed a
|
|
2375
2356
|
// non-empty text chunk to the client, no error thrown out of this function
|
|
2376
2357
|
// may omit the liveTextEmitted/unsafeToRetry markers — otherwise an
|
|
@@ -2379,23 +2360,15 @@ export async function sendViaWebSocket({
|
|
|
2379
2360
|
// already-rendered output. A text-emitting attempt is never retry-eligible
|
|
2380
2361
|
// (_classifyMidstreamError returns null on emittedText), so the surfaced
|
|
2381
2362
|
// error is frequently an EARLIER attempt's firstAttemptError that never saw
|
|
2382
|
-
// the marker;
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
// call was forwarded, surfaced errors must block HTTP fallback / reissue.
|
|
2392
|
-
let toolEmittedAcrossAttempts = false;
|
|
2393
|
-
const _stampTool = (e) => {
|
|
2394
|
-
if (toolEmittedAcrossAttempts && e) {
|
|
2395
|
-
try { e.emittedToolCall = true; e.unsafeToRetry = true; } catch {}
|
|
2396
|
-
}
|
|
2397
|
-
return e;
|
|
2398
|
-
};
|
|
2363
|
+
// the marker; stampText re-applies it on every throw path. The latch state
|
|
2364
|
+
// + stamp semantics now come from the shared createStreamSafetyStamps()
|
|
2365
|
+
// factory (retry-classifier.mjs) — identical to the former _stampLiveText /
|
|
2366
|
+
// _stampTool closures. markText()/markTool() set the latch (replacing the
|
|
2367
|
+
// liveTextEmittedAcrossAttempts / toolEmittedAcrossAttempts booleans);
|
|
2368
|
+
// stampText/stampTool re-apply the markers on every throw.
|
|
2369
|
+
const _safetyStamps = createStreamSafetyStamps();
|
|
2370
|
+
const _stampLiveText = _safetyStamps.stampText;
|
|
2371
|
+
const _stampTool = _safetyStamps.stampTool;
|
|
2399
2372
|
// Server-side xAI conversation anchor preserved across mid-stream
|
|
2400
2373
|
// retries. xAI keys its conversation by previous_response_id alone
|
|
2401
2374
|
// (sessionToken is null for xAI in _mintSessionToken); a forceFresh
|
|
@@ -2639,6 +2612,7 @@ export async function sendViaWebSocket({
|
|
|
2639
2612
|
logSuppressedReasoningDeltas,
|
|
2640
2613
|
traceProvider,
|
|
2641
2614
|
_timeouts: streamTimeouts,
|
|
2615
|
+
knownToolNames,
|
|
2642
2616
|
});
|
|
2643
2617
|
} catch (err) {
|
|
2644
2618
|
// Snapshot the xAI conversation anchor BEFORE releasing the
|
|
@@ -2665,10 +2639,10 @@ export async function sendViaWebSocket({
|
|
|
2665
2639
|
// Latch across attempts: even though THIS error is never
|
|
2666
2640
|
// retry-eligible once text is out, a later/earlier surfaced
|
|
2667
2641
|
// error (firstAttemptError) must still carry the marker.
|
|
2668
|
-
|
|
2642
|
+
_safetyStamps.markText();
|
|
2669
2643
|
}
|
|
2670
2644
|
if (midState.emittedToolCall) {
|
|
2671
|
-
|
|
2645
|
+
_safetyStamps.markTool();
|
|
2672
2646
|
}
|
|
2673
2647
|
_stampLiveText(err);
|
|
2674
2648
|
_stampTool(err);
|
|
@@ -2685,7 +2659,7 @@ export async function sendViaWebSocket({
|
|
|
2685
2659
|
const backoff = _midstreamBackoffFor(retryNumber);
|
|
2686
2660
|
try {
|
|
2687
2661
|
const line = `[openai-oauth-ws] mid-stream recovered: retry ${retryNumber}/${retryLimit} (cause: ${classifier}, backoff ${backoff}ms)\n`;
|
|
2688
|
-
process.stderr.write(line);
|
|
2662
|
+
if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(line);
|
|
2689
2663
|
emittedProgress.push(line);
|
|
2690
2664
|
} catch {}
|
|
2691
2665
|
await _sleepWithAbort(backoff, externalSignal, _sleepFn);
|
|
@@ -2794,8 +2768,8 @@ export async function sendViaWebSocket({
|
|
|
2794
2768
|
transport: 'websocket',
|
|
2795
2769
|
ws_mode: mode,
|
|
2796
2770
|
ws_pre_response_created_timeout_ms: WS_PRE_RESPONSE_CREATED_MS,
|
|
2797
|
-
ws_first_meaningful_timeout_ms: WS_FIRST_MEANINGFUL_MS,
|
|
2798
2771
|
ws_inter_chunk_timeout_ms: WS_INTER_CHUNK_MS,
|
|
2772
|
+
ws_idle_ms: WS_IDLE_MS,
|
|
2799
2773
|
iteration_delta_tokens: deltaTokens,
|
|
2800
2774
|
reused_connection: reused,
|
|
2801
2775
|
requested_service_tier: requestedServiceTier,
|