mixdog 0.9.1 → 0.9.3

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 (301) hide show
  1. package/package.json +9 -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/anthropic-maxtokens-test.mjs +119 -0
  6. package/scripts/background-task-meta-smoke.mjs +1 -1
  7. package/scripts/bench-run.mjs +262 -0
  8. package/scripts/build-tui.mjs +13 -1
  9. package/scripts/compact-smoke.mjs +12 -0
  10. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  11. package/scripts/explore-bench.mjs +124 -0
  12. package/scripts/hook-bus-test.mjs +191 -0
  13. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  14. package/scripts/internal-comms-bench.mjs +727 -0
  15. package/scripts/internal-comms-smoke.mjs +75 -0
  16. package/scripts/lead-workflow-smoke.mjs +4 -4
  17. package/scripts/live-worker-smoke.mjs +9 -9
  18. package/scripts/output-style-bench.mjs +285 -0
  19. package/scripts/output-style-smoke.mjs +13 -10
  20. package/scripts/patch-replay.mjs +90 -0
  21. package/scripts/path-suffix-test.mjs +57 -0
  22. package/scripts/provider-stream-stall-test.mjs +276 -0
  23. package/scripts/provider-toolcall-test.mjs +599 -1
  24. package/scripts/recall-bench.mjs +207 -0
  25. package/scripts/routing-corpus.mjs +281 -0
  26. package/scripts/session-bench.mjs +1526 -0
  27. package/scripts/session-diag.mjs +595 -0
  28. package/scripts/task-bench.mjs +207 -0
  29. package/scripts/tool-failures.mjs +6 -6
  30. package/scripts/tool-smoke.mjs +310 -67
  31. package/scripts/toolcall-args-test.mjs +81 -0
  32. package/src/agents/debugger/AGENT.md +4 -4
  33. package/src/agents/heavy-worker/AGENT.md +20 -9
  34. package/src/agents/reviewer/AGENT.md +4 -4
  35. package/src/agents/worker/AGENT.md +17 -9
  36. package/src/app.mjs +10 -6
  37. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  38. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  39. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  40. package/src/headless-role.mjs +14 -14
  41. package/src/help.mjs +1 -0
  42. package/src/lib/rules-builder.cjs +32 -54
  43. package/src/mixdog-session-runtime.mjs +1040 -2036
  44. package/src/output-styles/default.md +12 -7
  45. package/src/output-styles/minimal.md +25 -0
  46. package/src/output-styles/oneline.md +21 -0
  47. package/src/output-styles/simple.md +10 -9
  48. package/src/repl.mjs +17 -7
  49. package/src/rules/agent/00-common.md +7 -5
  50. package/src/rules/agent/30-explorer.md +8 -12
  51. package/src/rules/lead/01-general.md +3 -1
  52. package/src/rules/lead/lead-tool.md +13 -2
  53. package/src/rules/shared/01-tool.md +23 -12
  54. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  55. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  56. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  57. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  58. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  59. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  60. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  61. package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
  62. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  63. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  64. package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
  65. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  66. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  67. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  68. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
  69. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
  70. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
  71. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
  72. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  73. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  74. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
  75. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  76. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
  78. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
  79. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
  80. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  82. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
  83. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  84. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
  85. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  86. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  87. package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
  88. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
  89. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
  90. package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
  91. package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
  93. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
  94. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
  97. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
  98. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
  99. package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
  100. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
  101. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
  102. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
  103. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
  104. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
  105. package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
  106. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  107. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  108. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  109. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  110. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  113. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
  115. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  116. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  118. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
  119. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  120. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  121. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  122. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
  123. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
  124. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  125. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
  126. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
  127. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  129. package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
  130. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
  131. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
  132. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
  133. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
  134. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
  135. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
  136. package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
  137. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
  138. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
  139. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
  140. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
  141. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
  142. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
  143. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
  146. package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
  147. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
  148. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
  149. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
  150. package/src/runtime/channels/backends/discord.mjs +99 -9
  151. package/src/runtime/channels/backends/telegram.mjs +501 -0
  152. package/src/runtime/channels/index.mjs +437 -1439
  153. package/src/runtime/channels/lib/boot-profile.mjs +23 -0
  154. package/src/runtime/channels/lib/config.mjs +54 -2
  155. package/src/runtime/channels/lib/crash-log.mjs +106 -0
  156. package/src/runtime/channels/lib/format.mjs +4 -2
  157. package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
  158. package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
  159. package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
  160. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  161. package/src/runtime/channels/lib/telegram-format.mjs +280 -0
  162. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
  164. package/src/runtime/channels/lib/webhook.mjs +59 -31
  165. package/src/runtime/channels/lib/whisper-language.mjs +42 -0
  166. package/src/runtime/channels/tool-defs.mjs +1 -1
  167. package/src/runtime/memory/index.mjs +465 -345
  168. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  169. package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
  170. package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
  171. package/src/runtime/memory/lib/http-wire.mjs +57 -0
  172. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  173. package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
  174. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  175. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
  176. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
  177. package/src/runtime/memory/lib/memory.mjs +121 -4
  178. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  179. package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
  180. package/src/runtime/memory/lib/recall-format.mjs +183 -0
  181. package/src/runtime/memory/lib/session-ingest.mjs +107 -0
  182. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  183. package/src/runtime/memory/tool-defs.mjs +10 -7
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/background-tasks.mjs +2 -3
  186. package/src/runtime/shared/buffered-appender.mjs +149 -0
  187. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  188. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  189. package/src/runtime/shared/config.mjs +9 -0
  190. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  191. package/src/runtime/shared/schedules-store.mjs +21 -19
  192. package/src/runtime/shared/task-notification-envelope.mjs +98 -0
  193. package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
  194. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  195. package/src/runtime/shared/tool-surface.mjs +98 -13
  196. package/src/runtime/shared/transcript-writer.mjs +156 -0
  197. package/src/runtime/shared/update-checker.mjs +214 -0
  198. package/src/session-runtime/config-helpers.mjs +209 -0
  199. package/src/session-runtime/effort.mjs +128 -0
  200. package/src/session-runtime/fs-utils.mjs +10 -0
  201. package/src/session-runtime/model-capabilities.mjs +130 -0
  202. package/src/session-runtime/output-styles.mjs +124 -0
  203. package/src/session-runtime/plugin-mcp.mjs +114 -0
  204. package/src/session-runtime/session-text.mjs +100 -0
  205. package/src/session-runtime/statusline-route.mjs +35 -0
  206. package/src/session-runtime/tool-catalog.mjs +720 -0
  207. package/src/session-runtime/workflow.mjs +358 -0
  208. package/src/standalone/agent-tool.mjs +302 -117
  209. package/src/standalone/channel-admin.mjs +133 -40
  210. package/src/standalone/channel-worker.mjs +10 -292
  211. package/src/standalone/explore-tool.mjs +12 -5
  212. package/src/standalone/hook-bus.mjs +165 -8
  213. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  214. package/src/standalone/opencode-go-login.mjs +121 -0
  215. package/src/standalone/provider-admin.mjs +25 -3
  216. package/src/standalone/usage-dashboard.mjs +1 -1
  217. package/src/tui/App.jsx +2883 -778
  218. package/src/tui/components/ConfirmBar.jsx +47 -0
  219. package/src/tui/components/ContextPanel.jsx +5 -3
  220. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  221. package/src/tui/components/Markdown.jsx +22 -98
  222. package/src/tui/components/Message.jsx +14 -35
  223. package/src/tui/components/Picker.jsx +87 -12
  224. package/src/tui/components/PromptInput.jsx +355 -22
  225. package/src/tui/components/QueuedCommands.jsx +1 -1
  226. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  227. package/src/tui/components/Spinner.jsx +7 -7
  228. package/src/tui/components/StatusLine.jsx +40 -21
  229. package/src/tui/components/TextEntryPanel.jsx +51 -7
  230. package/src/tui/components/ToolExecution.jsx +183 -101
  231. package/src/tui/components/TurnDone.jsx +4 -4
  232. package/src/tui/components/UsagePanel.jsx +1 -1
  233. package/src/tui/components/tool-output-format.mjs +161 -23
  234. package/src/tui/components/tool-output-format.test.mjs +87 -0
  235. package/src/tui/display-width.mjs +69 -0
  236. package/src/tui/display-width.test.mjs +35 -0
  237. package/src/tui/dist/index.mjs +6731 -2333
  238. package/src/tui/engine/agent-envelope.mjs +296 -0
  239. package/src/tui/engine/boot-profile.mjs +21 -0
  240. package/src/tui/engine/labels.mjs +67 -0
  241. package/src/tui/engine/notice-text.mjs +112 -0
  242. package/src/tui/engine/queue-helpers.mjs +161 -0
  243. package/src/tui/engine/session-stats.mjs +46 -0
  244. package/src/tui/engine/tool-call-fields.mjs +23 -0
  245. package/src/tui/engine/tool-result-text.mjs +126 -0
  246. package/src/tui/engine.mjs +569 -948
  247. package/src/tui/index.jsx +117 -7
  248. package/src/tui/input-editing.mjs +58 -8
  249. package/src/tui/input-editing.selection.test.mjs +75 -0
  250. package/src/tui/keyboard-protocol.mjs +42 -0
  251. package/src/tui/lib/voice-recorder.mjs +469 -0
  252. package/src/tui/markdown/format-token.mjs +128 -76
  253. package/src/tui/markdown/format-token.test.mjs +61 -19
  254. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  255. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  256. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  257. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  258. package/src/tui/markdown/table-layout.mjs +9 -9
  259. package/src/tui/paste-attachments.mjs +36 -9
  260. package/src/tui/paste-fix.test.mjs +119 -0
  261. package/src/tui/prompt-history-store.mjs +129 -0
  262. package/src/tui/prompt-history-store.test.mjs +52 -0
  263. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  264. package/src/tui/theme.mjs +41 -657
  265. package/src/tui/themes/base.mjs +86 -0
  266. package/src/tui/themes/basic.mjs +85 -0
  267. package/src/tui/themes/catppuccin.mjs +72 -0
  268. package/src/tui/themes/dracula.mjs +70 -0
  269. package/src/tui/themes/everforest.mjs +71 -0
  270. package/src/tui/themes/gruvbox.mjs +71 -0
  271. package/src/tui/themes/index.mjs +71 -0
  272. package/src/tui/themes/indigo.mjs +78 -0
  273. package/src/tui/themes/kanagawa.mjs +80 -0
  274. package/src/tui/themes/light.mjs +81 -0
  275. package/src/tui/themes/nord.mjs +72 -0
  276. package/src/tui/themes/onedark.mjs +16 -0
  277. package/src/tui/themes/rosepine.mjs +70 -0
  278. package/src/tui/themes/teal.mjs +80 -0
  279. package/src/tui/themes/tokyonight.mjs +79 -0
  280. package/src/tui/themes/utils.mjs +106 -0
  281. package/src/tui/themes/warm.mjs +79 -0
  282. package/src/tui/transcript-tool-failures.mjs +13 -2
  283. package/src/ui/markdown.mjs +1 -1
  284. package/src/ui/model-display.mjs +2 -2
  285. package/src/ui/statusline.mjs +75 -27
  286. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  287. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  288. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  289. package/src/workflows/default/WORKFLOW.md +46 -12
  290. package/src/workflows/sequential/WORKFLOW.md +51 -0
  291. package/src/workflows/solo/WORKFLOW.md +12 -1
  292. package/vendor/ink/build/display-width.js +62 -0
  293. package/vendor/ink/build/ink.js +100 -12
  294. package/vendor/ink/build/measure-text.js +4 -1
  295. package/vendor/ink/build/output.js +115 -9
  296. package/vendor/ink/build/render-node-to-output.js +4 -1
  297. package/vendor/ink/build/render.js +4 -0
  298. package/src/output-styles/extreme-simple.md +0 -20
  299. package/src/rules/lead/04-workflow.md +0 -51
  300. package/src/workflows/default/workflow.json +0 -13
  301. package/src/workflows/solo/workflow.json +0 -7
@@ -4,15 +4,15 @@
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.
11
11
  *
12
- * References:
13
- * - pi-mono packages/ai/src/providers/openai-codex-responses.ts
14
- * (acquireWebSocket/release, get_incremental_items delta logic).
15
- * - openai/codex codex-rs/core/src/client.rs (turn-state echo header).
12
+ * Incremental-input reuse is decided by diffing against the cached request
13
+ * the socket last sent (see _sansInput below), and requests carry a
14
+ * turn-state echo header so the backend can correlate WS frames to the
15
+ * in-flight turn.
16
16
  *
17
17
  * Exposes:
18
18
  * sendViaWebSocket({ auth, body, sendOpts, onStreamDelta, onToolCall,
@@ -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,8 @@ 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 — any frame,
87
+ // including metadata/keepalive, proves the socket is live.
81
88
  const WS_INTER_CHUNK_MS = PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS;
82
89
  // Mid-stream retry budgets + backoff now live in the shared MIDSTREAM_RETRY_POLICY
83
90
  // table (retry-classifier.mjs). These aliases keep the local call sites readable
@@ -101,8 +108,8 @@ const WS_MIDSTREAM_POLICY = {
101
108
  // decision and just double the user-visible latency.
102
109
  // Aligned to the cross-provider default (retry-classifier DEFAULT_MAX_ATTEMPTS=5,
103
110
  // anthropic-oauth MAX_ATTEMPTS=5, withRetry-using providers all default to 5).
104
- // Previously 3 — bumped for parity so every provider exhausts the same number
105
- // of transient-5xx attempts before surfacing failure to the caller.
111
+ // Bumped from 3 so this provider exhausts the same number of transient-5xx
112
+ // attempts as the others before surfacing failure to the caller.
106
113
  const HANDSHAKE_MAX_ATTEMPTS = PROVIDER_RETRY_MAX_ATTEMPTS;
107
114
  const HANDSHAKE_BACKOFF_BASE_MS = 500;
108
115
  const HANDSHAKE_BACKOFF_CAP_MS = 5000;
@@ -934,9 +941,9 @@ function releaseWebSocket({ entry, poolKey, keep }) {
934
941
  _scheduleIdleClose(poolKey, entry);
935
942
  }
936
943
 
937
- // Port of pi-mono get_incremental_items: if the cached request (sans input)
938
- // matches the current one and the current input starts with the cached input,
939
- // return only the tail. Otherwise return the full input (fresh turn).
944
+ // If the cached request (sans input) matches the current one and the current
945
+ // input starts with the cached input, return only the tail. Otherwise return
946
+ // the full input (fresh turn).
940
947
  function _sansInput(body) {
941
948
  const { input: _ignored, previous_response_id: _prevIgnored, ...rest } = body;
942
949
  return rest;
@@ -1206,19 +1213,23 @@ function _wsErrLabel(p) {
1206
1213
  return 'OpenAI OAuth WS';
1207
1214
  }
1208
1215
  // tool_search_call.arguments parse. Module-scope (exported) for direct test
1209
- // coverage. Native convergence (openai-oauth / anthropic-oauth / opencode): same policy
1210
- // as the function_call_arguments.done path and openai-oauth _parseJsonObject —
1216
+ // coverage. Same policy as the function_call_arguments.done path and
1217
+ // openai-oauth _parseJsonObject —
1211
1218
  // object passes through; null/non-string/empty/whitespace → {} (no args); a
1212
1219
  // non-empty string that fails JSON.parse is deterministic bad JSON, surfaced
1213
1220
  // as an invalid-args MARKER (not silently swallowed to {}) so the dispatch
1214
1221
  // loop returns an is_error tool_result and the model self-corrects in the same
1215
1222
  // turn.
1216
1223
  export function parseToolSearchArgs(value) {
1217
- if (value && typeof value === 'object') return value;
1224
+ if (value && typeof value === 'object') {
1225
+ // Reject arrays — the tool_search schema is an object
1226
+ // ({query,select,limit}); an array must never pass through as args.
1227
+ return Array.isArray(value) ? {} : value;
1228
+ }
1218
1229
  if (typeof value !== 'string' || !value.trim()) return {};
1219
1230
  try {
1220
1231
  const parsed = JSON.parse(value);
1221
- return parsed && typeof parsed === 'object' ? parsed : {};
1232
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
1222
1233
  } catch (err) {
1223
1234
  return makeInvalidToolArgsMarker(value, err instanceof Error ? err.message : String(err));
1224
1235
  }
@@ -1233,11 +1244,11 @@ export async function _streamResponse({
1233
1244
  logSuppressedReasoningDeltas = true,
1234
1245
  traceProvider = 'openai-oauth',
1235
1246
  _timeouts = null,
1247
+ knownToolNames = null,
1236
1248
  }) {
1237
1249
  const errLabel = _wsErrLabel(traceProvider);
1238
1250
  const socket = entry.socket;
1239
1251
  const preResponseCreatedMs = _positiveInt(_timeouts?.preResponseCreatedMs, WS_PRE_RESPONSE_CREATED_MS);
1240
- const firstMeaningfulMs = _positiveInt(_timeouts?.firstMeaningfulMs, WS_FIRST_MEANINGFUL_MS);
1241
1252
  const interChunkMs = _positiveInt(_timeouts?.interChunkMs, WS_INTER_CHUNK_MS);
1242
1253
  const _streamingStart = Date.now();
1243
1254
  let _firstDeltaEmitted = false;
@@ -1253,6 +1264,19 @@ export async function _streamResponse({
1253
1264
  const citations = [];
1254
1265
  const citationKeys = new Set();
1255
1266
  const pendingCalls = new Map();
1267
+ // Tool-work-in-flight flag: set the moment a function/custom tool call's
1268
+ // input starts streaming (before it lands in pendingCalls/toolCalls).
1269
+ // Gates partial-final SUCCESS so a stall mid tool-input never looks text-only.
1270
+ let _toolInFlight = false;
1271
+ // Fix 2: cross-path name+args dedupe. A text-leaked synthetic and an
1272
+ // identical native function_call must fire onToolCall exactly once. Every
1273
+ // dispatch site routes through emitToolCallDedupe.
1274
+ const _toolDedupe = createToolCallDedupe();
1275
+ const emitToolCallDedupe = (call) => {
1276
+ if (!_toolDedupe.shouldDispatch(call?.name, call?.arguments)) return;
1277
+ midState.emittedToolCall = true;
1278
+ try { onToolCall?.(call); } catch {}
1279
+ };
1256
1280
  // Reasoning items collected from response.output_item.done (or salvaged
1257
1281
  // from response.completed.response.output). The request still includes
1258
1282
  // `reasoning.encrypted_content` so the server keeps emitting the blobs,
@@ -1340,8 +1364,7 @@ export async function _streamResponse({
1340
1364
  const call = customToolCallFromResponseItem(item);
1341
1365
  if (!call || toolCalls.some((existing) => existing.id === call.id)) return;
1342
1366
  toolCalls.push(call);
1343
- midState.emittedToolCall = true;
1344
- try { onToolCall?.(call); } catch {}
1367
+ emitToolCallDedupe(call);
1345
1368
  };
1346
1369
  const pushToolSearchCall = (item) => {
1347
1370
  if (!item || item.type !== 'tool_search_call') return;
@@ -1354,9 +1377,60 @@ export async function _streamResponse({
1354
1377
  nativeType: 'tool_search_call',
1355
1378
  };
1356
1379
  toolCalls.push(call);
1380
+ emitToolCallDedupe(call);
1381
+ };
1382
+ // Leaked tool-call guard. The model sometimes emits a tool call as plain
1383
+ // text (XML `<invoke>`/`<function_calls>` or gpt-oss harmony
1384
+ // `<|channel|>...to=functions.NAME...<|call|>`) inside
1385
+ // `response.output_text.delta` instead of a native function_call. Route
1386
+ // text through the guard so leaked calls are suppressed from the visible
1387
+ // stream, synthesized (native `call_...` id shape), and dispatched like
1388
+ // native ones. Additive: the native function_call path is untouched.
1389
+ const leakGuard = createLeakGuard({ knownToolNames, harmony: true });
1390
+ const dispatchLeakedCall = (recovered) => {
1391
+ let args = recovered?.arguments;
1392
+ if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
1393
+ const call = {
1394
+ id: `call_leaked_${randomBytes(8).toString('hex')}`,
1395
+ name: recovered.name,
1396
+ arguments: args,
1397
+ };
1398
+ if (!_toolDedupe.shouldDispatch(call.name, call.arguments)) return;
1399
+ toolCalls.push(call);
1357
1400
  midState.emittedToolCall = true;
1358
1401
  try { onToolCall?.(call); } catch {}
1359
1402
  };
1403
+ const relayLeakText = (delta) => {
1404
+ if (!leakGuard.enabled) {
1405
+ content += delta || '';
1406
+ if (delta && onTextDelta) {
1407
+ if (state) state.emittedText = true;
1408
+ try { onTextDelta(delta); } catch {}
1409
+ }
1410
+ return;
1411
+ }
1412
+ const { text, calls } = leakGuard.push(delta);
1413
+ if (text) {
1414
+ content += text;
1415
+ if (onTextDelta) {
1416
+ if (state) state.emittedText = true;
1417
+ try { onTextDelta(text); } catch {}
1418
+ }
1419
+ }
1420
+ for (const c of calls) dispatchLeakedCall(c);
1421
+ };
1422
+ const flushLeak = () => {
1423
+ if (!leakGuard.enabled) return;
1424
+ const { text, calls } = leakGuard.flush();
1425
+ if (text) {
1426
+ content += text;
1427
+ if (onTextDelta) {
1428
+ if (state) state.emittedText = true;
1429
+ try { onTextDelta(text); } catch {}
1430
+ }
1431
+ }
1432
+ for (const c of calls) dispatchLeakedCall(c);
1433
+ };
1360
1434
  const logReasoningDeltaSuppression = () => {
1361
1435
  if (!logSuppressedReasoningDeltas) return;
1362
1436
  const total = reasoningTextDeltaCount + reasoningSummaryTextDeltaCount + reasoningOtherDeltaCount;
@@ -1385,6 +1459,16 @@ export async function _streamResponse({
1385
1459
  let messageHandler = null;
1386
1460
  let closeHandler = null;
1387
1461
  let errorHandler = null;
1462
+ // SEMANTIC idle timer: distinct from the inter-chunk
1463
+ // timer, which resets on EVERY frame (rate_limits/metadata/keepalive keep
1464
+ // the socket "alive"). This timer resets ONLY on meaningful output deltas
1465
+ // (text/reasoning/tool args — the same events that call onStreamDelta) so a
1466
+ // stream that emits some deltas then goes silent (server keepalive frames
1467
+ // only) trips a short, named terminal StreamStalledError instead of coasting
1468
+ // to the 300s inter-chunk cap / 30-min agent watchdog.
1469
+ let semanticIdleTimer = null;
1470
+ const semanticIdleMs = PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS;
1471
+ const semanticIdleEnabled = PROVIDER_SSE_IDLE_WATCHDOG_ENABLED && semanticIdleMs > 0;
1388
1472
 
1389
1473
  return new Promise((resolve, reject) => {
1390
1474
  // Pre-stream watchdog: the timer fires if the server never sends a
@@ -1392,10 +1476,10 @@ export async function _streamResponse({
1392
1476
  // after our last frame. The socket is open and the response.create
1393
1477
  // frame was sent, but no server event has come back — a wedged
1394
1478
  // 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
1479
+ // window is intentionally short (WS_PRE_RESPONSE_CREATED_MS, ~10s).
1480
+ // Once ANY server event arrives, resetIdle() cancels this watchdog and
1481
+ // the single inter-chunk idle timer takes over — silent gaps
1482
+ // mid-reasoning (openai-oauth spending 50s+ producing reasoning
1399
1483
  // tokens) are normal and should not abort the turn.
1400
1484
  const armPreStreamWatchdog = () => {
1401
1485
  if (idleTimer) clearTimeout(idleTimer);
@@ -1427,8 +1511,6 @@ export async function _streamResponse({
1427
1511
  }, preResponseCreatedMs);
1428
1512
  };
1429
1513
  let interChunkTimer = null;
1430
- let firstMeaningfulTimer = null;
1431
- let firstMeaningfulSeen = false;
1432
1514
  const traceWsTimeout = (event, timeoutMs) => {
1433
1515
  try {
1434
1516
  const iteration = Number(midState.iteration);
@@ -1443,7 +1525,6 @@ export async function _streamResponse({
1443
1525
  attempt_index: Number.isFinite(attemptIndex) ? attemptIndex : null,
1444
1526
  warmup: midState.warmup === true,
1445
1527
  saw_response_created: midState.sawResponseCreated === true,
1446
- first_meaningful_seen: firstMeaningfulSeen === true,
1447
1528
  };
1448
1529
  appendAgentTrace({
1449
1530
  sessionId: midState.sessionId || null,
@@ -1460,29 +1541,6 @@ export async function _streamResponse({
1460
1541
  idleTimer = null;
1461
1542
  }
1462
1543
  };
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
1544
  const resetInterChunk = () => {
1487
1545
  if (interChunkTimer) clearTimeout(interChunkTimer);
1488
1546
  interChunkTimer = setTimeout(() => {
@@ -1497,30 +1555,49 @@ export async function _streamResponse({
1497
1555
  finish();
1498
1556
  }, interChunkMs);
1499
1557
  };
1500
- const onResponseCreated = () => {
1501
- clearPreStreamWatchdog();
1502
- if (!firstMeaningfulSeen) armFirstMeaningfulWatchdog();
1503
- else resetInterChunk();
1558
+ // pi per-event idle: (re)armed only on meaningful output deltas via
1559
+ // bumpSemanticIdle(). Keepalive/metadata frames DON'T touch it, so a
1560
+ // deltas-then-silent wedge trips this short semantic window.
1561
+ const resetSemanticIdle = () => {
1562
+ if (!semanticIdleEnabled) return;
1563
+ if (semanticIdleTimer) clearTimeout(semanticIdleTimer);
1564
+ semanticIdleTimer = setTimeout(() => {
1565
+ traceWsTimeout('semantic_idle_timeout', semanticIdleMs);
1566
+ terminalError = streamStalledError('Responses WS', semanticIdleMs, { emittedToolCall: !!midState?.emittedToolCall });
1567
+ // Partial-final recovery: attach streamed partial state so
1568
+ // a wedged FINAL no-tool summary can be accepted as partial-final
1569
+ // success by the loop. pendingToolUse gates out mid-flight tools.
1570
+ try {
1571
+ terminalError.partialContent = content;
1572
+ terminalError.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
1573
+ terminalError.pendingToolUse = pendingCalls.size > 0
1574
+ || !!midState?.emittedToolCall
1575
+ || toolCalls.length > 0
1576
+ || _toolInFlight === true;
1577
+ terminalError.partialModel = model || undefined;
1578
+ } catch { /* best-effort enrichment */ }
1579
+ try { terminalError.wsCloseCode = 4000; } catch {}
1580
+ try { socket.close(4000, 'semantic_idle_timeout'); } catch {}
1581
+ finish();
1582
+ }, semanticIdleMs);
1583
+ try { semanticIdleTimer.unref?.(); } catch {}
1504
1584
  };
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
- }
1585
+ // Single idle reset called on EVERY parsed server event (matches
1586
+ // codex, which resets one idle timer on every received WS frame). Any
1587
+ // frame proves the socket is live; there is no separate "meaningful
1588
+ // output" gate. Also clears the pre-stream watchdog defensively in case
1589
+ // the first event is not response.created.
1590
+ const resetIdle = () => {
1591
+ clearPreStreamWatchdog();
1516
1592
  resetInterChunk();
1517
1593
  };
1518
- // resetIdle kept for compat; metadata frames no longer disarm pre-stream watchdog.
1519
- const resetIdle = () => { /* noop — only onMeaningfulOutput() disarms */ };
1594
+ // Meaningful-output progress bump: called by the same delta cases that
1595
+ // call onStreamDelta (text/reasoning/tool args). Arms the semantic idle.
1596
+ const bumpSemanticIdle = () => { resetSemanticIdle(); };
1520
1597
  const cleanup = () => {
1521
1598
  if (idleTimer) clearTimeout(idleTimer);
1522
- clearFirstMeaningfulWatchdog();
1523
1599
  if (interChunkTimer) { clearTimeout(interChunkTimer); interChunkTimer = null; }
1600
+ if (semanticIdleTimer) { clearTimeout(semanticIdleTimer); semanticIdleTimer = null; }
1524
1601
  if (keepaliveTimer) { clearInterval(keepaliveTimer); keepaliveTimer = null; }
1525
1602
  if (messageHandler) socket.off('message', messageHandler);
1526
1603
  if (closeHandler) socket.off('close', closeHandler);
@@ -1529,6 +1606,9 @@ export async function _streamResponse({
1529
1606
  };
1530
1607
  const finish = () => {
1531
1608
  logReasoningDeltaSuppression();
1609
+ // Flush any partial-sentinel tail held back mid-stream so
1610
+ // legitimate trailing text is never lost (streamed-text path).
1611
+ flushLeak();
1532
1612
  cleanup();
1533
1613
  if (terminalError) { reject(terminalError); return; }
1534
1614
  resolve({
@@ -1536,11 +1616,17 @@ export async function _streamResponse({
1536
1616
  model,
1537
1617
  reasoningItems: reasoningItems.length ? reasoningItems : undefined,
1538
1618
  responseItems: responseItemsAdded.length ? responseItemsAdded : undefined,
1539
- toolCalls: toolCalls.length ? toolCalls : undefined,
1619
+ // Dedupe by name+args (Fix 2, array side) so an identical
1620
+ // synthetic-leaked + native pair can't run the tool twice.
1621
+ toolCalls: toolCalls.length ? dedupeToolCallList(toolCalls) : undefined,
1540
1622
  citations: citations.length ? citations : undefined,
1541
1623
  webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
1542
1624
  usage,
1543
1625
  stopReason: stopReason || undefined,
1626
+ // P1 audit fix: mirror the HTTP/SSE fallback's truncated flag
1627
+ // for the WS path (sendViaWebSocket spreads this result
1628
+ // through to the provider caller unchanged).
1629
+ ...(stopReason === 'length' && content.length > 0 ? { truncated: true } : {}),
1544
1630
  incompleteReason: incompleteReason || undefined,
1545
1631
  responseId: responseId || undefined,
1546
1632
  serviceTier: responseServiceTier || undefined,
@@ -1549,7 +1635,10 @@ export async function _streamResponse({
1549
1635
 
1550
1636
  messageHandler = (data) => {
1551
1637
  resetIdle();
1552
- // Do NOT call onStreamDelta for every frame metadata/keepalive frames
1638
+ // resetIdle() above resets the SINGLE inter-chunk idle timer on
1639
+ // EVERY received frame — response.created, metadata,
1640
+ // rate_limits, and all deltas keep the socket alive. Separately, do
1641
+ // NOT call onStreamDelta for every frame — metadata/keepalive frames
1553
1642
  // must not reset the agent stall watchdog's lastStreamDeltaAt. Only
1554
1643
  // meaningful output (text delta / tool call) updates that timestamp.
1555
1644
  const text = typeof data === 'string' ? data : data.toString('utf-8');
@@ -1571,13 +1660,11 @@ export async function _streamResponse({
1571
1660
  midState.sawResponseCreated = true;
1572
1661
  if (event.response?.model) model = event.response.model;
1573
1662
  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();
1663
+ // Server ack (first event). resetIdle() at the top of
1664
+ // messageHandler already cleared the pre-stream watchdog and
1665
+ // armed the single idle timer — no extra bookkeeping here.
1578
1666
  break;
1579
1667
  case 'response.output_text.delta':
1580
- content += event.delta || '';
1581
1668
  try {
1582
1669
  if (!_firstDeltaEmitted) {
1583
1670
  _firstDeltaEmitted = true;
@@ -1587,6 +1674,7 @@ export async function _streamResponse({
1587
1674
  }
1588
1675
  onStreamDelta?.();
1589
1676
  } catch {}
1677
+ bumpSemanticIdle();
1590
1678
  // Live text relay (gateway): forward the raw text chunk so
1591
1679
  // the client renders first tokens before the final replay.
1592
1680
  // Tool-call/argument deltas intentionally stay off this path.
@@ -1594,11 +1682,10 @@ export async function _streamResponse({
1594
1682
  // cannot be withdrawn, so flag the attempt so a later
1595
1683
  // mid-stream/truncated failure is NOT retried (retry would
1596
1684
  // 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();
1685
+ // Routed through the leaked-tool-call guard: appends to
1686
+ // `content`, forwards visible text via onTextDelta, and
1687
+ // recovers/dispatches any leaked known-tool call.
1688
+ relayLeakText(event.delta || '');
1602
1689
  break;
1603
1690
  case 'response.reasoning_text.delta':
1604
1691
  case 'response.reasoning_summary_text.delta':
@@ -1606,16 +1693,12 @@ export async function _streamResponse({
1606
1693
  else reasoningSummaryTextDeltaCount += 1;
1607
1694
  // Reasoning text is live model progress — refresh
1608
1695
  // 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.
1696
+ // long reasoning span as a stall. The local WS idle timer
1697
+ // was already reset by resetIdle() at the top of
1698
+ // messageHandler. Reasoning is still suppressed from user
1699
+ // content (no `content +=` here).
1617
1700
  try { onStreamDelta?.(); } catch {}
1618
- onMeaningfulOutput();
1701
+ bumpSemanticIdle();
1619
1702
  break;
1620
1703
  case 'response.output_item.added':
1621
1704
  if (event.item?.type === 'function_call') {
@@ -1623,26 +1706,37 @@ export async function _streamResponse({
1623
1706
  name: event.item.name || '',
1624
1707
  callId: event.item.call_id || '',
1625
1708
  });
1626
- onMeaningfulOutput();
1709
+ _toolInFlight = true;
1710
+ } else if (event.item?.type === 'custom_tool_call') {
1711
+ _toolInFlight = true;
1712
+ } else if (event.item?.type === 'tool_search_call') {
1713
+ // Mark tool_search in-flight at item-added time, same
1714
+ // as function_call/custom_tool_call above, so the
1715
+ // semantic-idle stall gate's pendingToolUse never
1716
+ // drops a mid-flight tool_search before
1717
+ // response.output_item.done.
1718
+ _toolInFlight = true;
1627
1719
  }
1628
1720
  break;
1629
1721
  case 'response.function_call_arguments.delta':
1722
+ _toolInFlight = true;
1630
1723
  try { onStreamDelta?.(); } catch {}
1631
- onMeaningfulOutput();
1724
+ bumpSemanticIdle();
1632
1725
  break;
1633
1726
  case 'response.custom_tool_call_input.delta':
1727
+ _toolInFlight = true;
1634
1728
  try { onStreamDelta?.(); } catch {}
1635
- onMeaningfulOutput();
1729
+ bumpSemanticIdle();
1636
1730
  break;
1637
1731
  case 'response.function_call_arguments.done': {
1638
1732
  const itemId = event.item_id || '';
1639
1733
  const pending = pendingCalls.get(itemId);
1640
1734
  // function_call_arguments.done is a completion signal:
1641
1735
  // empty/whitespace → no args ({}); a non-empty string that
1642
- // fails JSON.parse is deterministic bad JSON. Native
1643
- // convergence: surface an invalid-args MARKER (not silent
1644
- // {}) so the dispatch loop returns an is_error tool_result
1645
- // and the model re-issues valid JSON in the same turn.
1736
+ // fails JSON.parse is deterministic bad JSON. Surface an
1737
+ // invalid-args MARKER (not silent {}) so the dispatch loop
1738
+ // returns an is_error tool_result and the model re-issues
1739
+ // valid JSON in the same turn.
1646
1740
  let args = {};
1647
1741
  {
1648
1742
  const _argText = typeof event.arguments === 'string' ? event.arguments : '';
@@ -1663,8 +1757,7 @@ export async function _streamResponse({
1663
1757
  if (pending?.callId && pending?.name) {
1664
1758
  const call = { id: pending.callId, name: pending.name, arguments: args };
1665
1759
  toolCalls.push(call);
1666
- midState.emittedToolCall = true;
1667
- try { onToolCall?.(call); } catch {}
1760
+ emitToolCallDedupe(call);
1668
1761
  } else {
1669
1762
  // Synthesizing a `tc_${Date.now()}` callId here would
1670
1763
  // make the next turn fail to match the model's
@@ -1684,7 +1777,7 @@ export async function _streamResponse({
1684
1777
  });
1685
1778
  }
1686
1779
  try { onStreamDelta?.(); } catch {}
1687
- onMeaningfulOutput();
1780
+ bumpSemanticIdle();
1688
1781
  break;
1689
1782
  }
1690
1783
  case 'response.output_item.done':
@@ -1698,11 +1791,9 @@ export async function _streamResponse({
1698
1791
  if (event.item?.type === 'web_search_call') pushWebSearchCall(event.item);
1699
1792
  if (event.item?.type === 'tool_search_call') {
1700
1793
  pushToolSearchCall(event.item);
1701
- onMeaningfulOutput();
1702
1794
  }
1703
1795
  if (event.item?.type === 'custom_tool_call') {
1704
1796
  pushCustomToolCall(event.item);
1705
- onMeaningfulOutput();
1706
1797
  }
1707
1798
  break;
1708
1799
  case 'response.completed': {
@@ -1734,7 +1825,18 @@ export async function _streamResponse({
1734
1825
  if (!content && item.type === 'message') {
1735
1826
  for (const c of item.content || []) {
1736
1827
  if (c.type === 'output_text') {
1737
- content += c.text || '';
1828
+ // Completed-output fallback (no streamed
1829
+ // text). Route through the leak guard so
1830
+ // a tool call leaked only in the final
1831
+ // bundle is recovered, not surfaced as
1832
+ // visible content. final=true → full flush.
1833
+ if (leakGuard.enabled) {
1834
+ const { text, calls } = leakGuard.push(c.text || '', true);
1835
+ content += text;
1836
+ for (const lc of calls) dispatchLeakedCall(lc);
1837
+ } else {
1838
+ content += c.text || '';
1839
+ }
1738
1840
  pushOutputTextAnnotations(c);
1739
1841
  }
1740
1842
  }
@@ -1770,8 +1872,7 @@ export async function _streamResponse({
1770
1872
  if (tc.id && tc.name) {
1771
1873
  delete tc._deferred;
1772
1874
  delete tc._pendingItemId;
1773
- midState.emittedToolCall = true;
1774
- try { onToolCall?.(tc); } catch {}
1875
+ emitToolCallDedupe(tc);
1775
1876
  }
1776
1877
  }
1777
1878
  }
@@ -1908,6 +2009,13 @@ export async function _streamResponse({
1908
2009
  && event.type.startsWith('response.reasoning')
1909
2010
  && event.type.endsWith('.delta')) {
1910
2011
  reasoningOtherDeltaCount += 1;
2012
+ // These ARE live model progress (reviewer Medium): a
2013
+ // provider that emits only these reasoning variants for a
2014
+ // long span would otherwise trip the SEMANTIC idle abort.
2015
+ // Refresh both the watchdog and the semantic idle timer,
2016
+ // matching the named reasoning_text.delta case above.
2017
+ try { onStreamDelta?.(); } catch {}
2018
+ bumpSemanticIdle();
1911
2019
  }
1912
2020
  // Trace-only events (response.in_progress, etc.)
1913
2021
  break;
@@ -2043,9 +2151,6 @@ export function _classifyHandshakeError(err) {
2043
2151
  * response.create frame sent, but the server never
2044
2152
  * emitted response.created within the short
2045
2153
  * 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
2154
  * 'response_failed_network' — response.failed with network_error
2050
2155
  * 'response_failed_disconnected' — response.failed with stream_disconnected
2051
2156
  *
@@ -2152,7 +2257,7 @@ export async function _acquireWithRetry({
2152
2257
  try { err.retryClassifier = classifier; } catch {}
2153
2258
  }
2154
2259
  try {
2155
- process.stderr.write(
2260
+ if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(
2156
2261
  `[openai-oauth-ws] handshake failed after ${attempt}/${HANDSHAKE_MAX_ATTEMPTS} attempts: ${err?.message || err}\n`,
2157
2262
  );
2158
2263
  } catch {}
@@ -2161,7 +2266,7 @@ export async function _acquireWithRetry({
2161
2266
  // Schedule backoff and emit progress.
2162
2267
  const backoff = _backoffFor(attempt);
2163
2268
  try {
2164
- process.stderr.write(
2269
+ if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(
2165
2270
  `[openai-oauth-ws] worker retry ${attempt}/${HANDSHAKE_MAX_ATTEMPTS} (transient: ${classifier}, backoff ${backoff}ms)\n`,
2166
2271
  );
2167
2272
  } catch {}
@@ -2249,6 +2354,14 @@ export async function sendViaWebSocket({
2249
2354
  const MAX_MIDSTREAM_RETRIES = MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT;
2250
2355
  let firstAttemptError = null;
2251
2356
  let firstAttemptClassifier = null;
2357
+ // Known tool names for the leaked-tool-call guard in _streamResponse.
2358
+ // Derived from the exact request body so a recovered leaked call only
2359
+ // synthesizes when it names a tool actually offered to this request.
2360
+ const knownToolNames = new Set(
2361
+ (Array.isArray(body?.tools) ? body.tools : [])
2362
+ .map((t) => (typeof t?.name === 'string' ? t.name : null))
2363
+ .filter(Boolean),
2364
+ );
2252
2365
  // Live-text invariant across attempts: once ANY attempt has relayed a
2253
2366
  // non-empty text chunk to the client, no error thrown out of this function
2254
2367
  // may omit the liveTextEmitted/unsafeToRetry markers — otherwise an
@@ -2509,6 +2622,7 @@ export async function sendViaWebSocket({
2509
2622
  logSuppressedReasoningDeltas,
2510
2623
  traceProvider,
2511
2624
  _timeouts: streamTimeouts,
2625
+ knownToolNames,
2512
2626
  });
2513
2627
  } catch (err) {
2514
2628
  // Snapshot the xAI conversation anchor BEFORE releasing the
@@ -2555,7 +2669,7 @@ export async function sendViaWebSocket({
2555
2669
  const backoff = _midstreamBackoffFor(retryNumber);
2556
2670
  try {
2557
2671
  const line = `[openai-oauth-ws] mid-stream recovered: retry ${retryNumber}/${retryLimit} (cause: ${classifier}, backoff ${backoff}ms)\n`;
2558
- process.stderr.write(line);
2672
+ if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(line);
2559
2673
  emittedProgress.push(line);
2560
2674
  } catch {}
2561
2675
  await _sleepWithAbort(backoff, externalSignal, _sleepFn);
@@ -2664,8 +2778,8 @@ export async function sendViaWebSocket({
2664
2778
  transport: 'websocket',
2665
2779
  ws_mode: mode,
2666
2780
  ws_pre_response_created_timeout_ms: WS_PRE_RESPONSE_CREATED_MS,
2667
- ws_first_meaningful_timeout_ms: WS_FIRST_MEANINGFUL_MS,
2668
2781
  ws_inter_chunk_timeout_ms: WS_INTER_CHUNK_MS,
2782
+ ws_idle_ms: WS_IDLE_MS,
2669
2783
  iteration_delta_tokens: deltaTokens,
2670
2784
  reused_connection: reused,
2671
2785
  requested_service_tier: requestedServiceTier,