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.
Files changed (214) hide show
  1. package/package.json +8 -1
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/background-task-meta-smoke.mjs +1 -1
  6. package/scripts/bench-run.mjs +262 -0
  7. package/scripts/compact-smoke.mjs +12 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  9. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  10. package/scripts/internal-comms-bench.mjs +727 -0
  11. package/scripts/internal-comms-smoke.mjs +75 -0
  12. package/scripts/lead-workflow-smoke.mjs +4 -4
  13. package/scripts/live-worker-smoke.mjs +9 -9
  14. package/scripts/output-style-bench.mjs +285 -0
  15. package/scripts/output-style-smoke.mjs +13 -10
  16. package/scripts/patch-replay.mjs +90 -0
  17. package/scripts/provider-stream-stall-test.mjs +276 -0
  18. package/scripts/provider-toolcall-test.mjs +599 -1
  19. package/scripts/routing-corpus.mjs +281 -0
  20. package/scripts/session-bench.mjs +1526 -0
  21. package/scripts/session-diag.mjs +595 -0
  22. package/scripts/task-bench.mjs +207 -0
  23. package/scripts/tool-failures.mjs +6 -6
  24. package/scripts/tool-smoke.mjs +306 -66
  25. package/scripts/toolcall-args-test.mjs +81 -0
  26. package/src/agents/debugger/AGENT.md +4 -4
  27. package/src/agents/heavy-worker/AGENT.md +4 -2
  28. package/src/agents/reviewer/AGENT.md +4 -4
  29. package/src/agents/worker/AGENT.md +4 -2
  30. package/src/app.mjs +10 -6
  31. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  32. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  33. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  34. package/src/headless-role.mjs +14 -14
  35. package/src/help.mjs +1 -0
  36. package/src/lib/rules-builder.cjs +32 -54
  37. package/src/mixdog-session-runtime.mjs +710 -318
  38. package/src/output-styles/default.md +12 -7
  39. package/src/output-styles/minimal.md +25 -0
  40. package/src/output-styles/oneline.md +21 -0
  41. package/src/output-styles/simple.md +10 -9
  42. package/src/repl.mjs +12 -2
  43. package/src/rules/agent/00-common.md +7 -5
  44. package/src/rules/agent/30-explorer.md +7 -8
  45. package/src/rules/lead/01-general.md +3 -1
  46. package/src/rules/lead/lead-tool.md +7 -0
  47. package/src/rules/shared/01-tool.md +17 -12
  48. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  49. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  50. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  51. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  52. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  53. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  54. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  55. package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
  56. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  57. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  58. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
  59. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  60. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  61. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  62. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +332 -57
  63. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +59 -32
  64. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
  65. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  66. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  67. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  68. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
  69. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +78 -3
  70. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +202 -98
  71. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +183 -20
  72. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  73. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
  74. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  75. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +15 -9
  76. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  77. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  78. package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
  79. package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
  80. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  81. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  82. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  83. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  84. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
  85. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  86. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  87. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
  88. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  89. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  90. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  91. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  92. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  93. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  95. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
  96. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  97. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  98. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
  99. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  100. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
  101. package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
  102. package/src/runtime/channels/backends/discord.mjs +99 -9
  103. package/src/runtime/channels/backends/telegram.mjs +501 -0
  104. package/src/runtime/channels/index.mjs +441 -1224
  105. package/src/runtime/channels/lib/config.mjs +54 -2
  106. package/src/runtime/channels/lib/format.mjs +4 -2
  107. package/src/runtime/channels/lib/output-forwarder.mjs +80 -67
  108. package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
  109. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  110. package/src/runtime/channels/lib/telegram-format.mjs +283 -0
  111. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  112. package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
  113. package/src/runtime/channels/lib/webhook.mjs +59 -31
  114. package/src/runtime/channels/tool-defs.mjs +1 -1
  115. package/src/runtime/memory/index.mjs +184 -19
  116. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  117. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  118. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  119. package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
  120. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  121. package/src/runtime/memory/lib/memory.mjs +101 -4
  122. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  123. package/src/runtime/memory/lib/session-ingest.mjs +107 -0
  124. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  125. package/src/runtime/memory/tool-defs.mjs +6 -3
  126. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  127. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  128. package/src/runtime/shared/config.mjs +9 -0
  129. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  130. package/src/runtime/shared/schedules-store.mjs +21 -19
  131. package/src/runtime/shared/tool-surface.mjs +98 -13
  132. package/src/runtime/shared/transcript-writer.mjs +129 -0
  133. package/src/runtime/shared/update-checker.mjs +214 -0
  134. package/src/standalone/agent-tool.mjs +255 -109
  135. package/src/standalone/channel-admin.mjs +133 -40
  136. package/src/standalone/channel-worker.mjs +8 -291
  137. package/src/standalone/explore-tool.mjs +2 -2
  138. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  139. package/src/standalone/provider-admin.mjs +11 -0
  140. package/src/standalone/usage-dashboard.mjs +1 -1
  141. package/src/tui/App.jsx +2096 -732
  142. package/src/tui/components/ConfirmBar.jsx +47 -0
  143. package/src/tui/components/ContextPanel.jsx +5 -3
  144. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  145. package/src/tui/components/Markdown.jsx +22 -98
  146. package/src/tui/components/Message.jsx +14 -35
  147. package/src/tui/components/Picker.jsx +87 -12
  148. package/src/tui/components/PromptInput.jsx +83 -7
  149. package/src/tui/components/QueuedCommands.jsx +1 -1
  150. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  151. package/src/tui/components/Spinner.jsx +7 -7
  152. package/src/tui/components/StatusLine.jsx +40 -21
  153. package/src/tui/components/TextEntryPanel.jsx +51 -7
  154. package/src/tui/components/ToolExecution.jsx +170 -98
  155. package/src/tui/components/TurnDone.jsx +4 -4
  156. package/src/tui/components/UsagePanel.jsx +1 -1
  157. package/src/tui/components/tool-output-format.mjs +159 -21
  158. package/src/tui/components/tool-output-format.test.mjs +87 -0
  159. package/src/tui/display-width.mjs +69 -0
  160. package/src/tui/display-width.test.mjs +35 -0
  161. package/src/tui/dist/index.mjs +6965 -2391
  162. package/src/tui/engine.mjs +287 -126
  163. package/src/tui/index.jsx +117 -7
  164. package/src/tui/keyboard-protocol.mjs +42 -0
  165. package/src/tui/lib/voice-recorder.mjs +453 -0
  166. package/src/tui/markdown/format-token.mjs +129 -76
  167. package/src/tui/markdown/format-token.test.mjs +61 -19
  168. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  169. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  170. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  171. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  172. package/src/tui/markdown/table-layout.mjs +9 -9
  173. package/src/tui/paste-attachments.mjs +0 -11
  174. package/src/tui/prompt-history-store.mjs +129 -0
  175. package/src/tui/prompt-history-store.test.mjs +52 -0
  176. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  177. package/src/tui/theme.mjs +41 -657
  178. package/src/tui/themes/base.mjs +86 -0
  179. package/src/tui/themes/basic.mjs +85 -0
  180. package/src/tui/themes/catppuccin.mjs +72 -0
  181. package/src/tui/themes/dracula.mjs +70 -0
  182. package/src/tui/themes/everforest.mjs +71 -0
  183. package/src/tui/themes/gruvbox.mjs +71 -0
  184. package/src/tui/themes/index.mjs +71 -0
  185. package/src/tui/themes/indigo.mjs +78 -0
  186. package/src/tui/themes/kanagawa.mjs +80 -0
  187. package/src/tui/themes/light.mjs +81 -0
  188. package/src/tui/themes/nord.mjs +72 -0
  189. package/src/tui/themes/onedark.mjs +16 -0
  190. package/src/tui/themes/rosepine.mjs +70 -0
  191. package/src/tui/themes/teal.mjs +81 -0
  192. package/src/tui/themes/tokyonight.mjs +79 -0
  193. package/src/tui/themes/utils.mjs +106 -0
  194. package/src/tui/themes/warm.mjs +79 -0
  195. package/src/tui/transcript-tool-failures.mjs +13 -2
  196. package/src/ui/markdown.mjs +1 -1
  197. package/src/ui/model-display.mjs +2 -2
  198. package/src/ui/statusline.mjs +26 -27
  199. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  200. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  201. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  202. package/src/workflows/default/WORKFLOW.md +39 -12
  203. package/src/workflows/sequential/WORKFLOW.md +46 -0
  204. package/src/workflows/solo/WORKFLOW.md +7 -0
  205. package/vendor/ink/build/display-width.js +62 -0
  206. package/vendor/ink/build/ink.js +100 -12
  207. package/vendor/ink/build/measure-text.js +4 -1
  208. package/vendor/ink/build/output.js +115 -9
  209. package/vendor/ink/build/render-node-to-output.js +4 -1
  210. package/vendor/ink/build/render.js +4 -0
  211. package/src/output-styles/extreme-simple.md +0 -20
  212. package/src/rules/lead/04-workflow.md +0 -51
  213. package/src/workflows/default/workflow.json +0 -13
  214. 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 (5 min idle TTL, up to 8 parallel sockets per
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.
@@ -43,12 +43,16 @@ import {
43
43
  sleepWithAbort,
44
44
  } from './retry-classifier.mjs';
45
45
  import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
46
+ import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
46
47
  import {
47
48
  PROVIDER_RETRY_MAX_ATTEMPTS,
48
49
  PROVIDER_WS_ACQUIRE_TIMEOUT_MS,
49
- PROVIDER_WS_FIRST_MEANINGFUL_TIMEOUT_MS,
50
50
  PROVIDER_WS_HANDSHAKE_TIMEOUT_MS,
51
51
  PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS,
52
+ PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS,
53
+ PROVIDER_SSE_IDLE_WATCHDOG_ENABLED,
54
+ streamStalledError,
55
+ resolveTimeoutMs,
52
56
  } from '../stall-policy.mjs';
53
57
  import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
54
58
 
@@ -58,17 +62,19 @@ const CODEX_WS_URL = 'wss://chatgpt.com/backend-api/codex/responses';
58
62
  const CODEX_OAUTH_ORIGINATOR = 'codex_cli_rs';
59
63
  const OPENAI_WS_URL = 'wss://api.openai.com/v1/responses';
60
64
  const XAI_WS_URL = 'wss://api.x.ai/v1/responses';
61
- const WS_IDLE_MS = 5 * 60_000;
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
+ );
62
70
  const WS_HANDSHAKE_TIMEOUT_MS = PROVIDER_WS_HANDSHAKE_TIMEOUT_MS;
63
71
  const WS_ACQUIRE_TIMEOUT_MS = PROVIDER_WS_ACQUIRE_TIMEOUT_MS;
64
- // Pre-stream watchdog uses the shared provider deadline so it fails before
65
- // the 5-minute session slow warning.
66
- const WS_FIRST_MEANINGFUL_MS = PROVIDER_WS_FIRST_MEANINGFUL_TIMEOUT_MS;
67
72
  // Pre-`response.created` deadline. Once the socket is open and the
68
73
  // response.create frame is sent, a healthy server emits response.created
69
74
  // within seconds. If it stalls past this short bound the socket has wedged
70
75
  // post-upgrade with zero server events — treat it as a fast, retryable
71
- // first-byte timeout rather than waiting the longer first-meaningful window.
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.
72
78
  // Only this short window is shortened; the post-`response.created`
73
79
  // inter-chunk / reasoning span keeps the longer deadlines below.
74
80
  const WS_PRE_RESPONSE_CREATED_MS = (() => {
@@ -77,7 +83,9 @@ const WS_PRE_RESPONSE_CREATED_MS = (() => {
77
83
  if (Number.isFinite(n) && n > 0) return Math.min(Math.max(n, 1_000), 120_000);
78
84
  return 10_000;
79
85
  })();
80
- // Inter-chunk inactivity after first meaningful output.
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).
81
89
  const WS_INTER_CHUNK_MS = PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS;
82
90
  // Mid-stream retry budgets + backoff now live in the shared MIDSTREAM_RETRY_POLICY
83
91
  // table (retry-classifier.mjs). These aliases keep the local call sites readable
@@ -1233,11 +1241,11 @@ export async function _streamResponse({
1233
1241
  logSuppressedReasoningDeltas = true,
1234
1242
  traceProvider = 'openai-oauth',
1235
1243
  _timeouts = null,
1244
+ knownToolNames = null,
1236
1245
  }) {
1237
1246
  const errLabel = _wsErrLabel(traceProvider);
1238
1247
  const socket = entry.socket;
1239
1248
  const preResponseCreatedMs = _positiveInt(_timeouts?.preResponseCreatedMs, WS_PRE_RESPONSE_CREATED_MS);
1240
- const firstMeaningfulMs = _positiveInt(_timeouts?.firstMeaningfulMs, WS_FIRST_MEANINGFUL_MS);
1241
1249
  const interChunkMs = _positiveInt(_timeouts?.interChunkMs, WS_INTER_CHUNK_MS);
1242
1250
  const _streamingStart = Date.now();
1243
1251
  let _firstDeltaEmitted = false;
@@ -1253,6 +1261,19 @@ export async function _streamResponse({
1253
1261
  const citations = [];
1254
1262
  const citationKeys = new Set();
1255
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
+ };
1256
1277
  // Reasoning items collected from response.output_item.done (or salvaged
1257
1278
  // from response.completed.response.output). The request still includes
1258
1279
  // `reasoning.encrypted_content` so the server keeps emitting the blobs,
@@ -1340,8 +1361,7 @@ export async function _streamResponse({
1340
1361
  const call = customToolCallFromResponseItem(item);
1341
1362
  if (!call || toolCalls.some((existing) => existing.id === call.id)) return;
1342
1363
  toolCalls.push(call);
1343
- midState.emittedToolCall = true;
1344
- try { onToolCall?.(call); } catch {}
1364
+ emitToolCallDedupe(call);
1345
1365
  };
1346
1366
  const pushToolSearchCall = (item) => {
1347
1367
  if (!item || item.type !== 'tool_search_call') return;
@@ -1354,9 +1374,60 @@ export async function _streamResponse({
1354
1374
  nativeType: 'tool_search_call',
1355
1375
  };
1356
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);
1357
1397
  midState.emittedToolCall = true;
1358
1398
  try { onToolCall?.(call); } catch {}
1359
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
+ };
1360
1431
  const logReasoningDeltaSuppression = () => {
1361
1432
  if (!logSuppressedReasoningDeltas) return;
1362
1433
  const total = reasoningTextDeltaCount + reasoningSummaryTextDeltaCount + reasoningOtherDeltaCount;
@@ -1385,6 +1456,16 @@ export async function _streamResponse({
1385
1456
  let messageHandler = null;
1386
1457
  let closeHandler = null;
1387
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;
1388
1469
 
1389
1470
  return new Promise((resolve, reject) => {
1390
1471
  // Pre-stream watchdog: the timer fires if the server never sends a
@@ -1392,10 +1473,10 @@ export async function _streamResponse({
1392
1473
  // after our last frame. The socket is open and the response.create
1393
1474
  // frame was sent, but no server event has come back — a wedged
1394
1475
  // post-upgrade socket. Healthy servers ack within seconds, so this
1395
- // window is intentionally short (~25s). Once response.created (or
1396
- // any other meaningful event) arrives, the timer is cancelled and
1397
- // the longer inter-chunk inactivity watchdog takes over — silent
1398
- // gaps mid-reasoning (openai-oauth spending 50s+ producing reasoning
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
1399
1480
  // tokens) are normal and should not abort the turn.
1400
1481
  const armPreStreamWatchdog = () => {
1401
1482
  if (idleTimer) clearTimeout(idleTimer);
@@ -1427,8 +1508,6 @@ export async function _streamResponse({
1427
1508
  }, preResponseCreatedMs);
1428
1509
  };
1429
1510
  let interChunkTimer = null;
1430
- let firstMeaningfulTimer = null;
1431
- let firstMeaningfulSeen = false;
1432
1511
  const traceWsTimeout = (event, timeoutMs) => {
1433
1512
  try {
1434
1513
  const iteration = Number(midState.iteration);
@@ -1443,7 +1522,6 @@ export async function _streamResponse({
1443
1522
  attempt_index: Number.isFinite(attemptIndex) ? attemptIndex : null,
1444
1523
  warmup: midState.warmup === true,
1445
1524
  saw_response_created: midState.sawResponseCreated === true,
1446
- first_meaningful_seen: firstMeaningfulSeen === true,
1447
1525
  };
1448
1526
  appendAgentTrace({
1449
1527
  sessionId: midState.sessionId || null,
@@ -1460,29 +1538,6 @@ export async function _streamResponse({
1460
1538
  idleTimer = null;
1461
1539
  }
1462
1540
  };
1463
- const clearFirstMeaningfulWatchdog = () => {
1464
- if (firstMeaningfulTimer) {
1465
- clearTimeout(firstMeaningfulTimer);
1466
- firstMeaningfulTimer = null;
1467
- }
1468
- };
1469
- const armFirstMeaningfulWatchdog = () => {
1470
- if (firstMeaningfulSeen || firstMeaningfulMs <= 0) return;
1471
- clearFirstMeaningfulWatchdog();
1472
- firstMeaningfulTimer = setTimeout(() => {
1473
- if (process.env.MIXDOG_DEBUG_AGENT) {
1474
- process.stderr.write(`[agent-trace] ws-timeout kind=first-meaningful afterMs=${firstMeaningfulMs}\n`);
1475
- }
1476
- traceWsTimeout('first_meaningful_timeout', firstMeaningfulMs);
1477
- const err = new Error(`WS stream: no meaningful output within ${firstMeaningfulMs}ms after response.created`);
1478
- err.wsCloseCode = 4000;
1479
- err.firstMeaningfulTimeout = true;
1480
- midState.firstMeaningfulTimeout = true;
1481
- terminalError = err;
1482
- try { socket.close(4000, 'first_meaningful_timeout'); } catch {}
1483
- finish();
1484
- }, firstMeaningfulMs);
1485
- };
1486
1541
  const resetInterChunk = () => {
1487
1542
  if (interChunkTimer) clearTimeout(interChunkTimer);
1488
1543
  interChunkTimer = setTimeout(() => {
@@ -1497,30 +1552,49 @@ export async function _streamResponse({
1497
1552
  finish();
1498
1553
  }, interChunkMs);
1499
1554
  };
1500
- const onResponseCreated = () => {
1501
- clearPreStreamWatchdog();
1502
- if (!firstMeaningfulSeen) armFirstMeaningfulWatchdog();
1503
- else resetInterChunk();
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 {}
1504
1581
  };
1505
- // Called on every event that carries real output tokens or tool
1506
- // progress. `response.created` is only an ACK and must not count here:
1507
- // a wedged openai-oauth stream can ACK immediately and then never produce
1508
- // text/reasoning/tool deltas, holding the prompt-cache lane for the
1509
- // full inter-chunk window.
1510
- const onMeaningfulOutput = () => {
1511
- if (!firstMeaningfulSeen) {
1512
- firstMeaningfulSeen = true;
1513
- clearPreStreamWatchdog();
1514
- clearFirstMeaningfulWatchdog();
1515
- }
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();
1516
1589
  resetInterChunk();
1517
1590
  };
1518
- // resetIdle kept for compat; metadata frames no longer disarm pre-stream watchdog.
1519
- const resetIdle = () => { /* noop — only onMeaningfulOutput() disarms */ };
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(); };
1520
1594
  const cleanup = () => {
1521
1595
  if (idleTimer) clearTimeout(idleTimer);
1522
- clearFirstMeaningfulWatchdog();
1523
1596
  if (interChunkTimer) { clearTimeout(interChunkTimer); interChunkTimer = null; }
1597
+ if (semanticIdleTimer) { clearTimeout(semanticIdleTimer); semanticIdleTimer = null; }
1524
1598
  if (keepaliveTimer) { clearInterval(keepaliveTimer); keepaliveTimer = null; }
1525
1599
  if (messageHandler) socket.off('message', messageHandler);
1526
1600
  if (closeHandler) socket.off('close', closeHandler);
@@ -1529,6 +1603,9 @@ export async function _streamResponse({
1529
1603
  };
1530
1604
  const finish = () => {
1531
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();
1532
1609
  cleanup();
1533
1610
  if (terminalError) { reject(terminalError); return; }
1534
1611
  resolve({
@@ -1536,11 +1613,17 @@ export async function _streamResponse({
1536
1613
  model,
1537
1614
  reasoningItems: reasoningItems.length ? reasoningItems : undefined,
1538
1615
  responseItems: responseItemsAdded.length ? responseItemsAdded : undefined,
1539
- toolCalls: toolCalls.length ? toolCalls : undefined,
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,
1540
1619
  citations: citations.length ? citations : undefined,
1541
1620
  webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
1542
1621
  usage,
1543
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 } : {}),
1544
1627
  incompleteReason: incompleteReason || undefined,
1545
1628
  responseId: responseId || undefined,
1546
1629
  serviceTier: responseServiceTier || undefined,
@@ -1549,7 +1632,10 @@ export async function _streamResponse({
1549
1632
 
1550
1633
  messageHandler = (data) => {
1551
1634
  resetIdle();
1552
- // Do NOT call onStreamDelta for every frame metadata/keepalive frames
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
1553
1639
  // must not reset the agent stall watchdog's lastStreamDeltaAt. Only
1554
1640
  // meaningful output (text delta / tool call) updates that timestamp.
1555
1641
  const text = typeof data === 'string' ? data : data.toString('utf-8');
@@ -1571,13 +1657,11 @@ export async function _streamResponse({
1571
1657
  midState.sawResponseCreated = true;
1572
1658
  if (event.response?.model) model = event.response.model;
1573
1659
  if (event.response?.id) responseId = event.response.id;
1574
- // Server ack: cancel only the pre-created watchdog. Keep
1575
- // a separate first-meaningful watchdog armed until real
1576
- // model progress arrives.
1577
- 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.
1578
1663
  break;
1579
1664
  case 'response.output_text.delta':
1580
- content += event.delta || '';
1581
1665
  try {
1582
1666
  if (!_firstDeltaEmitted) {
1583
1667
  _firstDeltaEmitted = true;
@@ -1587,6 +1671,7 @@ export async function _streamResponse({
1587
1671
  }
1588
1672
  onStreamDelta?.();
1589
1673
  } catch {}
1674
+ bumpSemanticIdle();
1590
1675
  // Live text relay (gateway): forward the raw text chunk so
1591
1676
  // the client renders first tokens before the final replay.
1592
1677
  // Tool-call/argument deltas intentionally stay off this path.
@@ -1594,11 +1679,10 @@ export async function _streamResponse({
1594
1679
  // cannot be withdrawn, so flag the attempt so a later
1595
1680
  // mid-stream/truncated failure is NOT retried (retry would
1596
1681
  // concatenate a second attempt onto rendered text).
1597
- if (event.delta && onTextDelta) {
1598
- if (state) state.emittedText = true;
1599
- try { onTextDelta(event.delta); } catch {}
1600
- }
1601
- 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 || '');
1602
1686
  break;
1603
1687
  case 'response.reasoning_text.delta':
1604
1688
  case 'response.reasoning_summary_text.delta':
@@ -1606,16 +1690,12 @@ export async function _streamResponse({
1606
1690
  else reasoningSummaryTextDeltaCount += 1;
1607
1691
  // Reasoning text is live model progress — refresh
1608
1692
  // lastStreamDeltaAt so stream-watchdog does not flag a
1609
- // long reasoning span as a stall. It also counts as
1610
- // liveness for the local pre-stream / inter-chunk
1611
- // watchdogs: a long reasoning span without any
1612
- // output_text delta would otherwise trip the
1613
- // first-meaningful timer and abort an otherwise healthy
1614
- // stream. Reasoning is still suppressed from user
1615
- // content (no `content +=` here) — only the watchdog
1616
- // 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).
1617
1697
  try { onStreamDelta?.(); } catch {}
1618
- onMeaningfulOutput();
1698
+ bumpSemanticIdle();
1619
1699
  break;
1620
1700
  case 'response.output_item.added':
1621
1701
  if (event.item?.type === 'function_call') {
@@ -1623,16 +1703,20 @@ export async function _streamResponse({
1623
1703
  name: event.item.name || '',
1624
1704
  callId: event.item.call_id || '',
1625
1705
  });
1626
- onMeaningfulOutput();
1706
+ _toolInFlight = true;
1707
+ } else if (event.item?.type === 'custom_tool_call') {
1708
+ _toolInFlight = true;
1627
1709
  }
1628
1710
  break;
1629
1711
  case 'response.function_call_arguments.delta':
1712
+ _toolInFlight = true;
1630
1713
  try { onStreamDelta?.(); } catch {}
1631
- onMeaningfulOutput();
1714
+ bumpSemanticIdle();
1632
1715
  break;
1633
1716
  case 'response.custom_tool_call_input.delta':
1717
+ _toolInFlight = true;
1634
1718
  try { onStreamDelta?.(); } catch {}
1635
- onMeaningfulOutput();
1719
+ bumpSemanticIdle();
1636
1720
  break;
1637
1721
  case 'response.function_call_arguments.done': {
1638
1722
  const itemId = event.item_id || '';
@@ -1663,8 +1747,7 @@ export async function _streamResponse({
1663
1747
  if (pending?.callId && pending?.name) {
1664
1748
  const call = { id: pending.callId, name: pending.name, arguments: args };
1665
1749
  toolCalls.push(call);
1666
- midState.emittedToolCall = true;
1667
- try { onToolCall?.(call); } catch {}
1750
+ emitToolCallDedupe(call);
1668
1751
  } else {
1669
1752
  // Synthesizing a `tc_${Date.now()}` callId here would
1670
1753
  // make the next turn fail to match the model's
@@ -1684,7 +1767,7 @@ export async function _streamResponse({
1684
1767
  });
1685
1768
  }
1686
1769
  try { onStreamDelta?.(); } catch {}
1687
- onMeaningfulOutput();
1770
+ bumpSemanticIdle();
1688
1771
  break;
1689
1772
  }
1690
1773
  case 'response.output_item.done':
@@ -1698,11 +1781,9 @@ export async function _streamResponse({
1698
1781
  if (event.item?.type === 'web_search_call') pushWebSearchCall(event.item);
1699
1782
  if (event.item?.type === 'tool_search_call') {
1700
1783
  pushToolSearchCall(event.item);
1701
- onMeaningfulOutput();
1702
1784
  }
1703
1785
  if (event.item?.type === 'custom_tool_call') {
1704
1786
  pushCustomToolCall(event.item);
1705
- onMeaningfulOutput();
1706
1787
  }
1707
1788
  break;
1708
1789
  case 'response.completed': {
@@ -1734,7 +1815,18 @@ export async function _streamResponse({
1734
1815
  if (!content && item.type === 'message') {
1735
1816
  for (const c of item.content || []) {
1736
1817
  if (c.type === 'output_text') {
1737
- content += c.text || '';
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
+ }
1738
1830
  pushOutputTextAnnotations(c);
1739
1831
  }
1740
1832
  }
@@ -1770,8 +1862,7 @@ export async function _streamResponse({
1770
1862
  if (tc.id && tc.name) {
1771
1863
  delete tc._deferred;
1772
1864
  delete tc._pendingItemId;
1773
- midState.emittedToolCall = true;
1774
- try { onToolCall?.(tc); } catch {}
1865
+ emitToolCallDedupe(tc);
1775
1866
  }
1776
1867
  }
1777
1868
  }
@@ -1908,6 +1999,13 @@ export async function _streamResponse({
1908
1999
  && event.type.startsWith('response.reasoning')
1909
2000
  && event.type.endsWith('.delta')) {
1910
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();
1911
2009
  }
1912
2010
  // Trace-only events (response.in_progress, etc.)
1913
2011
  break;
@@ -2043,9 +2141,6 @@ export function _classifyHandshakeError(err) {
2043
2141
  * response.create frame sent, but the server never
2044
2142
  * emitted response.created within the short
2045
2143
  * pre-stream deadline. Fast-fail retryable.
2046
- * 'first_meaningful_timeout' — server ACKed response.created, then emitted
2047
- * no real text/reasoning/tool progress before the
2048
- * first-meaningful deadline.
2049
2144
  * 'response_failed_network' — response.failed with network_error
2050
2145
  * 'response_failed_disconnected' — response.failed with stream_disconnected
2051
2146
  *
@@ -2152,7 +2247,7 @@ export async function _acquireWithRetry({
2152
2247
  try { err.retryClassifier = classifier; } catch {}
2153
2248
  }
2154
2249
  try {
2155
- process.stderr.write(
2250
+ if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(
2156
2251
  `[openai-oauth-ws] handshake failed after ${attempt}/${HANDSHAKE_MAX_ATTEMPTS} attempts: ${err?.message || err}\n`,
2157
2252
  );
2158
2253
  } catch {}
@@ -2161,7 +2256,7 @@ export async function _acquireWithRetry({
2161
2256
  // Schedule backoff and emit progress.
2162
2257
  const backoff = _backoffFor(attempt);
2163
2258
  try {
2164
- process.stderr.write(
2259
+ if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(
2165
2260
  `[openai-oauth-ws] worker retry ${attempt}/${HANDSHAKE_MAX_ATTEMPTS} (transient: ${classifier}, backoff ${backoff}ms)\n`,
2166
2261
  );
2167
2262
  } catch {}
@@ -2249,6 +2344,14 @@ export async function sendViaWebSocket({
2249
2344
  const MAX_MIDSTREAM_RETRIES = MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT;
2250
2345
  let firstAttemptError = null;
2251
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
+ );
2252
2355
  // Live-text invariant across attempts: once ANY attempt has relayed a
2253
2356
  // non-empty text chunk to the client, no error thrown out of this function
2254
2357
  // may omit the liveTextEmitted/unsafeToRetry markers — otherwise an
@@ -2509,6 +2612,7 @@ export async function sendViaWebSocket({
2509
2612
  logSuppressedReasoningDeltas,
2510
2613
  traceProvider,
2511
2614
  _timeouts: streamTimeouts,
2615
+ knownToolNames,
2512
2616
  });
2513
2617
  } catch (err) {
2514
2618
  // Snapshot the xAI conversation anchor BEFORE releasing the
@@ -2555,7 +2659,7 @@ export async function sendViaWebSocket({
2555
2659
  const backoff = _midstreamBackoffFor(retryNumber);
2556
2660
  try {
2557
2661
  const line = `[openai-oauth-ws] mid-stream recovered: retry ${retryNumber}/${retryLimit} (cause: ${classifier}, backoff ${backoff}ms)\n`;
2558
- process.stderr.write(line);
2662
+ if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(line);
2559
2663
  emittedProgress.push(line);
2560
2664
  } catch {}
2561
2665
  await _sleepWithAbort(backoff, externalSignal, _sleepFn);
@@ -2664,8 +2768,8 @@ export async function sendViaWebSocket({
2664
2768
  transport: 'websocket',
2665
2769
  ws_mode: mode,
2666
2770
  ws_pre_response_created_timeout_ms: WS_PRE_RESPONSE_CREATED_MS,
2667
- ws_first_meaningful_timeout_ms: WS_FIRST_MEANINGFUL_MS,
2668
2771
  ws_inter_chunk_timeout_ms: WS_INTER_CHUNK_MS,
2772
+ ws_idle_ms: WS_IDLE_MS,
2669
2773
  iteration_delta_tokens: deltaTokens,
2670
2774
  reused_connection: reused,
2671
2775
  requested_service_tier: requestedServiceTier,