mixdog 0.9.3 → 0.9.5

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 (382) hide show
  1. package/README.md +112 -38
  2. package/package.json +10 -3
  3. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  4. package/scripts/bench/lead-review-tasks.json +20 -0
  5. package/scripts/bench/r4-mixed-tasks.json +20 -0
  6. package/scripts/bench/r5-orchestrated-task.json +7 -0
  7. package/scripts/bench/review-tasks.json +20 -0
  8. package/scripts/bench/round-codex.json +114 -0
  9. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  10. package/scripts/bench/round-mixdog-lead.json +269 -0
  11. package/scripts/bench/round-mixdog.json +126 -0
  12. package/scripts/bench/round-r10-bigsample.json +679 -0
  13. package/scripts/bench/round-r11-codexalign.json +257 -0
  14. package/scripts/bench/round-r13-clientmeta.json +464 -0
  15. package/scripts/bench/round-r14-betafeatures.json +466 -0
  16. package/scripts/bench/round-r15-fulldefault.json +462 -0
  17. package/scripts/bench/round-r16-sessionid.json +466 -0
  18. package/scripts/bench/round-r17-wirebytes.json +456 -0
  19. package/scripts/bench/round-r18-prewarm.json +468 -0
  20. package/scripts/bench/round-r19-clean.json +472 -0
  21. package/scripts/bench/round-r20-prewarm-clean.json +475 -0
  22. package/scripts/bench/round-r21-delta-retry.json +473 -0
  23. package/scripts/bench/round-r22-full-probe.json +693 -0
  24. package/scripts/bench/round-r23-itemprobe.json +701 -0
  25. package/scripts/bench/round-r24-shapefix.json +677 -0
  26. package/scripts/bench/round-r25-serial.json +464 -0
  27. package/scripts/bench/round-r26-parallel3.json +671 -0
  28. package/scripts/bench/round-r27-parallel10.json +894 -0
  29. package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
  30. package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
  31. package/scripts/bench/round-r30-instid.json +253 -0
  32. package/scripts/bench/round-r31-upgradeprobe.json +256 -0
  33. package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
  34. package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
  35. package/scripts/bench/round-r34-orchestrated.json +120 -0
  36. package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
  37. package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
  38. package/scripts/bench/round-r4-codex.json +114 -0
  39. package/scripts/bench/round-r4-mixed.json +225 -0
  40. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  41. package/scripts/bench/round-r6-codex.json +114 -0
  42. package/scripts/bench/round-r6-solo.json +257 -0
  43. package/scripts/bench/round-r7-full.json +254 -0
  44. package/scripts/bench/round-r8-fulldefault.json +255 -0
  45. package/scripts/bench-run.mjs +251 -32
  46. package/scripts/freevar-smoke.mjs +95 -0
  47. package/scripts/internal-comms-bench.mjs +3 -4
  48. package/scripts/internal-comms-smoke.mjs +10 -9
  49. package/scripts/model-catalog-audit.mjs +209 -0
  50. package/scripts/model-list-sanitize-test.mjs +37 -0
  51. package/scripts/mouse-probe.mjs +45 -0
  52. package/scripts/output-style-bench.mjs +13 -6
  53. package/scripts/output-style-smoke.mjs +4 -4
  54. package/scripts/provider-toolcall-test.mjs +7 -3
  55. package/scripts/recall-bench.mjs +76 -13
  56. package/scripts/recall-quality-cases.json +12 -0
  57. package/scripts/recall-usecase-cases.json +18 -0
  58. package/scripts/session-bench.mjs +152 -6
  59. package/scripts/tool-smoke.mjs +25 -65
  60. package/scripts/tui-render-smoke.mjs +90 -0
  61. package/scripts/webhook-smoke.mjs +208 -0
  62. package/src/agents/debugger/AGENT.md +4 -1
  63. package/src/agents/heavy-worker/AGENT.md +9 -8
  64. package/src/agents/maintainer/AGENT.md +4 -0
  65. package/src/agents/reviewer/AGENT.md +2 -1
  66. package/src/agents/scheduler-task/AGENT.md +2 -3
  67. package/src/agents/webhook-handler/AGENT.md +2 -3
  68. package/src/agents/worker/AGENT.md +10 -7
  69. package/src/app.mjs +12 -1
  70. package/src/headless-role.mjs +7 -1
  71. package/src/lib/rules-builder.cjs +4 -0
  72. package/src/mixdog-session-runtime.mjs +647 -2056
  73. package/src/output-styles/default.md +30 -9
  74. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  75. package/src/output-styles/minimal.md +8 -6
  76. package/src/output-styles/simple.md +21 -7
  77. package/src/rules/agent/00-common.md +6 -3
  78. package/src/rules/agent/30-explorer.md +16 -5
  79. package/src/rules/lead/01-general.md +5 -5
  80. package/src/rules/lead/lead-brief.md +15 -0
  81. package/src/rules/lead/lead-tool.md +6 -15
  82. package/src/rules/shared/01-tool.md +17 -21
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
  84. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  85. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  86. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  87. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  88. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  89. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  90. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  91. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  92. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  93. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  95. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  97. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  100. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  101. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  102. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  103. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +105 -11
  104. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  108. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  109. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  110. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +740 -0
  111. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  112. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
  113. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
  114. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
  115. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  116. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -0
  117. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  119. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
  120. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  121. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  122. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  123. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  124. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  125. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  126. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  127. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  128. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  129. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  130. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +275 -0
  132. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  133. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  134. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  135. package/src/runtime/agent/orchestrator/session/loop.mjs +278 -402
  136. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  137. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  138. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  139. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  140. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  141. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  142. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  143. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  144. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  145. package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
  146. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  147. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  148. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +35 -44
  149. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  152. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  155. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  156. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  157. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  158. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  159. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  161. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  163. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  164. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  165. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  166. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  167. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  168. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  169. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  170. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  171. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  172. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  173. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  174. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  175. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  176. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  177. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  178. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  179. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  180. package/src/runtime/channels/backends/discord.mjs +27 -318
  181. package/src/runtime/channels/backends/telegram.mjs +8 -12
  182. package/src/runtime/channels/index.mjs +247 -701
  183. package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
  184. package/src/runtime/channels/lib/config.mjs +37 -149
  185. package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
  186. package/src/runtime/channels/lib/event-queue.mjs +78 -13
  187. package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
  188. package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
  189. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  190. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  191. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  192. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  193. package/src/runtime/channels/lib/scheduler.mjs +27 -113
  194. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  195. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  196. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  197. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  198. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  199. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  200. package/src/runtime/channels/lib/webhook/deliveries.mjs +313 -0
  201. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  202. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  203. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  204. package/src/runtime/channels/lib/webhook.mjs +43 -616
  205. package/src/runtime/channels/tool-defs.mjs +11 -130
  206. package/src/runtime/memory/index.mjs +210 -1948
  207. package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
  208. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  209. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  210. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  211. package/src/runtime/memory/lib/ko-morph.mjs +195 -0
  212. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  213. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  214. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  215. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  216. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  217. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  218. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  219. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  220. package/src/runtime/memory/lib/memory-recall-store.mjs +69 -12
  221. package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
  222. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  223. package/src/runtime/memory/lib/query-handlers.mjs +802 -0
  224. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  225. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  226. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  227. package/src/runtime/memory/tool-defs.mjs +5 -13
  228. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  229. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  230. package/src/runtime/search/lib/web-tools.mjs +24 -602
  231. package/src/runtime/shared/atomic-file.mjs +26 -1
  232. package/src/runtime/shared/config.mjs +14 -4
  233. package/src/runtime/shared/launcher-control.mjs +2 -2
  234. package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
  235. package/src/runtime/shared/schedules-store.mjs +13 -3
  236. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  237. package/src/runtime/shared/tool-primitives.mjs +308 -0
  238. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  239. package/src/runtime/shared/tool-surface.mjs +80 -898
  240. package/src/runtime/shared/transcript-writer.mjs +23 -0
  241. package/src/runtime/shared/update-checker.mjs +7 -4
  242. package/src/session-runtime/config-helpers.mjs +119 -2
  243. package/src/session-runtime/config-lifecycle.mjs +232 -0
  244. package/src/session-runtime/cwd-plugins.mjs +226 -0
  245. package/src/session-runtime/mcp-glue.mjs +177 -0
  246. package/src/session-runtime/model-recency.mjs +111 -0
  247. package/src/session-runtime/native-search.mjs +247 -0
  248. package/src/session-runtime/output-styles.mjs +11 -9
  249. package/src/session-runtime/prewarm.mjs +142 -0
  250. package/src/session-runtime/provider-models.mjs +278 -0
  251. package/src/session-runtime/provider-usage.mjs +120 -0
  252. package/src/session-runtime/quick-model-rows.mjs +205 -0
  253. package/src/session-runtime/quick-search-models.mjs +47 -0
  254. package/src/session-runtime/session-hooks.mjs +93 -0
  255. package/src/session-runtime/settings-api.mjs +352 -0
  256. package/src/session-runtime/tool-catalog.mjs +29 -29
  257. package/src/session-runtime/tool-defs.mjs +84 -0
  258. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  259. package/src/session-runtime/workflow.mjs +1 -1
  260. package/src/standalone/agent-tool/helpers.mjs +237 -0
  261. package/src/standalone/agent-tool/notify.mjs +107 -0
  262. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  263. package/src/standalone/agent-tool/render.mjs +152 -0
  264. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  265. package/src/standalone/agent-tool.mjs +138 -669
  266. package/src/standalone/channel-admin.mjs +102 -90
  267. package/src/standalone/channel-worker.mjs +4 -7
  268. package/src/standalone/explore-tool.mjs +64 -14
  269. package/src/standalone/hook-bus/config.mjs +207 -0
  270. package/src/standalone/hook-bus/constants.mjs +90 -0
  271. package/src/standalone/hook-bus/handlers.mjs +481 -0
  272. package/src/standalone/hook-bus/payload.mjs +31 -0
  273. package/src/standalone/hook-bus/rules.mjs +77 -0
  274. package/src/standalone/hook-bus.mjs +77 -870
  275. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  276. package/src/standalone/opencode-go-login.mjs +5 -1
  277. package/src/standalone/provider-admin.mjs +1 -16
  278. package/src/standalone/usage-dashboard.mjs +3 -1
  279. package/src/tui/App.jsx +1059 -8110
  280. package/src/tui/app/app-format.mjs +213 -0
  281. package/src/tui/app/channel-pickers.mjs +508 -0
  282. package/src/tui/app/clipboard.mjs +67 -0
  283. package/src/tui/app/core-memory-picker.mjs +210 -0
  284. package/src/tui/app/extension-pickers.mjs +506 -0
  285. package/src/tui/app/input-parsers.mjs +193 -0
  286. package/src/tui/app/maintenance-pickers.mjs +356 -0
  287. package/src/tui/app/model-options.mjs +334 -0
  288. package/src/tui/app/model-picker.mjs +365 -0
  289. package/src/tui/app/onboarding-steps.mjs +400 -0
  290. package/src/tui/app/project-picker.mjs +247 -0
  291. package/src/tui/app/provider-setup-picker.mjs +580 -0
  292. package/src/tui/app/resume-picker.mjs +55 -0
  293. package/src/tui/app/route-pickers.mjs +419 -0
  294. package/src/tui/app/settings-picker.mjs +489 -0
  295. package/src/tui/app/slash-commands.mjs +101 -0
  296. package/src/tui/app/slash-dispatch.mjs +427 -0
  297. package/src/tui/app/text-layout.mjs +46 -0
  298. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  299. package/src/tui/app/transcript-window.mjs +677 -0
  300. package/src/tui/app/use-mouse-input.mjs +460 -0
  301. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  302. package/src/tui/app/use-transcript-scroll.mjs +512 -0
  303. package/src/tui/app/use-transcript-window.mjs +607 -0
  304. package/src/tui/components/ConfirmBar.jsx +10 -7
  305. package/src/tui/components/Picker.jsx +64 -15
  306. package/src/tui/components/PromptInput.jsx +33 -102
  307. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  308. package/src/tui/components/StatusLine.jsx +69 -15
  309. package/src/tui/components/TextEntryPanel.jsx +11 -0
  310. package/src/tui/components/ToolExecution.jsx +52 -594
  311. package/src/tui/components/TranscriptItem.jsx +105 -0
  312. package/src/tui/components/UsagePanel.jsx +18 -4
  313. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  314. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  315. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  316. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  317. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  318. package/src/tui/display-width.mjs +20 -3
  319. package/src/tui/dist/index.mjs +13553 -12384
  320. package/src/tui/engine/agent-job-feed.mjs +133 -0
  321. package/src/tui/engine/notification-plan.mjs +76 -0
  322. package/src/tui/engine/render-timing.mjs +17 -0
  323. package/src/tui/engine/tool-approval.mjs +94 -0
  324. package/src/tui/engine/tool-card-results.mjs +234 -0
  325. package/src/tui/engine/tool-result-status.mjs +135 -0
  326. package/src/tui/engine.mjs +170 -574
  327. package/src/tui/figures.mjs +5 -0
  328. package/src/tui/index.jsx +65 -1
  329. package/src/tui/input-editing.mjs +2 -2
  330. package/src/tui/markdown/format-token.mjs +4 -1
  331. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  332. package/src/tui/theme.mjs +6 -0
  333. package/src/ui/statusline-agents.mjs +213 -0
  334. package/src/ui/statusline-format.mjs +146 -0
  335. package/src/ui/statusline-segments.mjs +148 -0
  336. package/src/ui/statusline.mjs +77 -501
  337. package/src/ui/tool-card.mjs +0 -1
  338. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  339. package/src/workflows/default/WORKFLOW.md +16 -18
  340. package/src/workflows/sequential/WORKFLOW.md +16 -18
  341. package/vendor/ink/build/display-width.js +19 -3
  342. package/vendor/ink/build/ink.js +112 -7
  343. package/vendor/ink/build/log-update.js +17 -3
  344. package/vendor/ink/build/wrap-text.js +125 -0
  345. package/scripts/_test-folder-dialog.mjs +0 -30
  346. package/scripts/fix-brief-fn.mjs +0 -35
  347. package/scripts/fix-format-tool-surface.mjs +0 -24
  348. package/scripts/fix-tool-exec-visible.mjs +0 -42
  349. package/scripts/patch-agent-brief.mjs +0 -48
  350. package/scripts/patch-app.mjs +0 -21
  351. package/scripts/patch-app2.mjs +0 -18
  352. package/scripts/patch-dist-brief.mjs +0 -96
  353. package/scripts/patch-tool-exec.mjs +0 -70
  354. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  355. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  356. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  357. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  358. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  359. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  360. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  361. package/src/runtime/channels/lib/holidays.mjs +0 -138
  362. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  363. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  364. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  365. package/src/standalone/agent-task-status.test.mjs +0 -76
  366. package/src/tui/components/tool-output-format.test.mjs +0 -399
  367. package/src/tui/display-width.test.mjs +0 -35
  368. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  369. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  370. package/src/tui/input-editing.selection.test.mjs +0 -75
  371. package/src/tui/markdown/format-token.test.mjs +0 -354
  372. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  373. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  374. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  375. package/src/tui/paste-fix.test.mjs +0 -119
  376. package/src/tui/prompt-history-store.test.mjs +0 -52
  377. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  378. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  379. package/src/ui/markdown.test.mjs +0 -70
  380. package/src/ui/statusline-context-label.test.mjs +0 -15
  381. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  382. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -0,0 +1,1105 @@
1
+ /**
2
+ * openai-ws-stream.mjs — WS Responses stream consumer + incremental-input
3
+ * delta engine for the OpenAI OAuth WebSocket transport.
4
+ *
5
+ * Extracted from openai-oauth-ws.mjs:
6
+ * - request/response item matching + delta computation (_computeDelta,
7
+ * _sansInput, _logicalResponseItemMatch, ...) used to send only the
8
+ * input tail on a warm socket,
9
+ * - the per-response stream loop (_streamResponse): event parsing, idle/
10
+ * pre-created watchdogs, leak guard, tool-call dedupe, usage assembly.
11
+ *
12
+ * sendViaWebSocket (openai-oauth-ws.mjs) is the only production caller;
13
+ * scripts import parseToolSearchArgs/_logicalResponseItemMatch via the
14
+ * openai-oauth-ws.mjs re-exports.
15
+ */
16
+ import { randomBytes } from 'crypto';
17
+ import {
18
+ extractCachedTokens,
19
+ appendAgentTrace,
20
+ } from '../agent-trace.mjs';
21
+ import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
22
+ import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
23
+ import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
24
+ import {
25
+ PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS,
26
+ PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS,
27
+ PROVIDER_SSE_IDLE_WATCHDOG_ENABLED,
28
+ streamStalledError,
29
+ } from '../stall-policy.mjs';
30
+ import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
31
+ import { _wsErrLabel } from './openai-ws-pool.mjs';
32
+ import {
33
+ _sansInput,
34
+ _stableStringify,
35
+ _cloneJson,
36
+ _logicalResponseItemMatch,
37
+ _stripResponseItemsFromHead,
38
+ _computeDelta,
39
+ _estimateFrameTokens,
40
+ } from './openai-ws-delta.mjs';
41
+ import {
42
+ _combineUsageWithWarmup,
43
+ _parseEvent,
44
+ _incompleteReasonFromEvent,
45
+ _isMaxOutputIncompleteReason,
46
+ _httpStatusFromWsClose,
47
+ } from './openai-ws-events.mjs';
48
+
49
+ // Facade re-exports: the delta/matching helpers and usage/event helpers below
50
+ // were extracted to openai-ws-delta.mjs / openai-ws-events.mjs (no behavior
51
+ // change). Re-exported here so existing importers of openai-ws-stream.mjs
52
+ // (openai-oauth-ws.mjs et al) keep resolving these symbols unchanged.
53
+ export {
54
+ _sansInput,
55
+ _stableStringify,
56
+ _cloneJson,
57
+ _logicalResponseItemMatch,
58
+ _stripResponseItemsFromHead,
59
+ _computeDelta,
60
+ _estimateFrameTokens,
61
+ _combineUsageWithWarmup,
62
+ };
63
+
64
+ // Pre-`response.created` deadline. Once the socket is open and the
65
+ // response.create frame is sent, a healthy server emits response.created
66
+ // within seconds. If it stalls past this short bound the socket has wedged
67
+ // post-upgrade with zero server events — treat it as a fast, retryable
68
+ // first-byte timeout. This is the ONLY pre-stream watchdog; once any server
69
+ // event arrives the single inter-chunk idle timer below takes over.
70
+ // Only this short window is shortened; the post-`response.created`
71
+ // inter-chunk / reasoning span keeps the longer deadlines below.
72
+ // Positive-int coercion for per-call timeout overrides: finite > 0 → floor,
73
+ // else fallback.
74
+ export function _positiveInt(value, fallback = 0) {
75
+ const n = Number(value);
76
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
77
+ }
78
+ export const WS_PRE_RESPONSE_CREATED_MS = (() => {
79
+ const raw = process.env.MIXDOG_PROVIDER_WS_PRE_RESPONSE_CREATED_TIMEOUT_MS;
80
+ const n = Number(raw);
81
+ if (Number.isFinite(n) && n > 0) return Math.min(Math.max(n, 1_000), 120_000);
82
+ return 10_000;
83
+ })();
84
+ // Single inter-chunk idle timer. Resets on EVERY received frame — any frame,
85
+ // including metadata/keepalive, proves the socket is live.
86
+ export const WS_INTER_CHUNK_MS = PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS;
87
+ const X_CODEX_TURN_STATE_HEADER = 'x-codex-turn-state';
88
+
89
+ function _responseItemKey(item, fallbackIndex = 0) {
90
+ if (!item || typeof item !== 'object') return `primitive:${fallbackIndex}`;
91
+ if (item.id) return `${item.type || 'item'}:id:${item.id}`;
92
+ if (item.call_id) return `${item.type || 'item'}:call:${item.call_id}`;
93
+ try { return `${item.type || 'item'}:json:${_stableStringify(item)}`; } catch {}
94
+ return `${item.type || 'item'}:${fallbackIndex}`;
95
+ }
96
+
97
+ function _headerString(headers, name) {
98
+ if (!headers || typeof headers !== 'object') return null;
99
+ const wanted = String(name).toLowerCase();
100
+ for (const [key, value] of Object.entries(headers)) {
101
+ if (String(key).toLowerCase() !== wanted) continue;
102
+ if (typeof value === 'string' && value) return value;
103
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
104
+ if (Array.isArray(value)) {
105
+ const first = value.find((item) => typeof item === 'string' && item);
106
+ if (first) return first;
107
+ }
108
+ }
109
+ return null;
110
+ }
111
+
112
+ function _headerKeys(headers) {
113
+ if (!headers || typeof headers !== 'object') return [];
114
+ const keys = [];
115
+ for (const key of Object.keys(headers)) {
116
+ const normalized = String(key || '').trim().toLowerCase();
117
+ if (normalized) keys.push(normalized);
118
+ }
119
+ return [...new Set(keys)].sort();
120
+ }
121
+
122
+ function _hasHeaderKey(headers, name) {
123
+ const wanted = String(name || '').trim().toLowerCase();
124
+ if (!wanted) return false;
125
+ return _headerKeys(headers).includes(wanted);
126
+ }
127
+
128
+ function _captureTurnStateFromEvent(entry, event) {
129
+ if (!entry || entry.turnState || !event || typeof event !== 'object') return;
130
+ const turnState = _headerString(event.headers, X_CODEX_TURN_STATE_HEADER)
131
+ || _headerString(event.response?.headers, X_CODEX_TURN_STATE_HEADER)
132
+ || _headerString(event.response?.metadata?.headers, X_CODEX_TURN_STATE_HEADER)
133
+ || _headerString(event.metadata?.headers, X_CODEX_TURN_STATE_HEADER);
134
+ if (turnState) entry.turnState = turnState;
135
+ }
136
+
137
+ function _traceWsHeaderKeys(entry, event, midState, traceProvider, model) {
138
+ try {
139
+ if (!entry || !event || typeof event !== 'object') return;
140
+ const eventType = typeof event.type === 'string' ? event.type : '';
141
+ if (eventType !== 'response.created' && eventType !== 'response.metadata') return;
142
+ const topLevelHeaderKeys = _headerKeys(event.headers);
143
+ const responseHeaderKeys = _headerKeys(event.response?.headers);
144
+ const responseMetadataHeaderKeys = _headerKeys(event.response?.metadata?.headers);
145
+ const eventMetadataHeaderKeys = _headerKeys(event.metadata?.headers);
146
+ const hasAnyHeaders = topLevelHeaderKeys.length > 0
147
+ || responseHeaderKeys.length > 0
148
+ || responseMetadataHeaderKeys.length > 0
149
+ || eventMetadataHeaderKeys.length > 0;
150
+ if (hasAnyHeaders && entry.wsHeaderKeysFinalTraced) return;
151
+ if (!hasAnyHeaders && entry.wsHeaderKeysEmptyTraced) return;
152
+ const iteration = Number(midState?.iteration);
153
+ const payload = {
154
+ provider: midState?.traceProvider || traceProvider,
155
+ transport: 'websocket',
156
+ event_type: eventType,
157
+ model: midState?.model || model || null,
158
+ top_level_header_keys: topLevelHeaderKeys,
159
+ response_header_keys: responseHeaderKeys,
160
+ response_metadata_header_keys: responseMetadataHeaderKeys,
161
+ event_metadata_header_keys: eventMetadataHeaderKeys,
162
+ has_turn_state_header: _hasHeaderKey(event.headers, X_CODEX_TURN_STATE_HEADER)
163
+ || _hasHeaderKey(event.response?.headers, X_CODEX_TURN_STATE_HEADER)
164
+ || _hasHeaderKey(event.response?.metadata?.headers, X_CODEX_TURN_STATE_HEADER)
165
+ || _hasHeaderKey(event.metadata?.headers, X_CODEX_TURN_STATE_HEADER),
166
+ values_redacted: true,
167
+ };
168
+ appendAgentTrace({
169
+ sessionId: midState?.sessionId || null,
170
+ iteration: Number.isFinite(iteration) ? iteration : null,
171
+ kind: 'ws_header_keys',
172
+ provider: payload.provider,
173
+ model: payload.model,
174
+ transport: 'websocket',
175
+ event_type: eventType,
176
+ payload,
177
+ });
178
+ if (hasAnyHeaders) entry.wsHeaderKeysFinalTraced = true;
179
+ else entry.wsHeaderKeysEmptyTraced = true;
180
+ } catch {}
181
+ }
182
+ // _wsErrLabel moved to openai-ws-pool.mjs (imported above).
183
+ // Delta/matching helpers (_sansInput, _stableStringify, _cloneJson,
184
+ // _logicalResponseItemMatch, _stripResponseItemsFromHead, _computeDelta,
185
+ // _estimateFrameTokens) → openai-ws-delta.mjs. Usage/event helpers
186
+ // (_combineUsageWithWarmup, _parseEvent, _incompleteReasonFromEvent,
187
+ // _isMaxOutputIncompleteReason, _httpStatusFromWsClose) → openai-ws-events.mjs.
188
+ // All imported + re-exported at the top of this file.
189
+ // tool_search_call.arguments parse. Module-scope (exported) for direct test
190
+ // coverage. Same policy as the function_call_arguments.done path and
191
+ // openai-oauth _parseJsonObject —
192
+ // object passes through; null/non-string/empty/whitespace → {} (no args); a
193
+ // non-empty string that fails JSON.parse is deterministic bad JSON, surfaced
194
+ // as an invalid-args MARKER (not silently swallowed to {}) so the dispatch
195
+ // loop returns an is_error tool_result and the model self-corrects in the same
196
+ // turn.
197
+ export function parseToolSearchArgs(value) {
198
+ if (value && typeof value === 'object') {
199
+ // Reject arrays — the tool_search schema is an object
200
+ // ({query,select,limit}); an array must never pass through as args.
201
+ return Array.isArray(value) ? {} : value;
202
+ }
203
+ if (typeof value !== 'string' || !value.trim()) return {};
204
+ try {
205
+ const parsed = JSON.parse(value);
206
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
207
+ } catch (err) {
208
+ return makeInvalidToolArgsMarker(value, err instanceof Error ? err.message : String(err));
209
+ }
210
+ }
211
+
212
+ export async function _streamResponse({
213
+ entry,
214
+ externalSignal,
215
+ onStreamDelta,
216
+ onToolCall,
217
+ onTextDelta,
218
+ state,
219
+ logSuppressedReasoningDeltas = true,
220
+ traceProvider = 'openai-oauth',
221
+ _timeouts = null,
222
+ knownToolNames = null,
223
+ }) {
224
+ const errLabel = _wsErrLabel(traceProvider);
225
+ const socket = entry.socket;
226
+ const preResponseCreatedMs = _positiveInt(_timeouts?.preResponseCreatedMs, WS_PRE_RESPONSE_CREATED_MS);
227
+ const interChunkMs = _positiveInt(_timeouts?.interChunkMs, WS_INTER_CHUNK_MS);
228
+ const _streamingStart = Date.now();
229
+ let _firstDeltaEmitted = false;
230
+ let content = '';
231
+ let model = '';
232
+ let responseId = '';
233
+ let responseServiceTier = '';
234
+ const toolCalls = [];
235
+ const webSearchCalls = [];
236
+ const webSearchCallKeys = new Set();
237
+ const responseItemsAdded = [];
238
+ const responseItemKeys = new Set();
239
+ const citations = [];
240
+ const citationKeys = new Set();
241
+ const pendingCalls = new Map();
242
+ // Tool-work-in-flight flag: set the moment a function/custom tool call's
243
+ // input starts streaming (before it lands in pendingCalls/toolCalls).
244
+ // Gates partial-final SUCCESS so a stall mid tool-input never looks text-only.
245
+ let _toolInFlight = false;
246
+ // Fix 2: cross-path name+args dedupe. A text-leaked synthetic and an
247
+ // identical native function_call must fire onToolCall exactly once. Every
248
+ // dispatch site routes through emitToolCallDedupe.
249
+ const _toolDedupe = createToolCallDedupe();
250
+ const emitToolCallDedupe = (call) => {
251
+ if (!_toolDedupe.shouldDispatch(call?.name, call?.arguments)) return;
252
+ midState.emittedToolCall = true;
253
+ try { onToolCall?.(call); } catch {}
254
+ };
255
+ // Reasoning items collected from response.output_item.done (or salvaged
256
+ // from response.completed.response.output). The request still includes
257
+ // `reasoning.encrypted_content` so the server keeps emitting the blobs,
258
+ // but explicit input-side replay is INTENTIONALLY OMITTED in
259
+ // convertMessagesToResponsesInput (openai-oauth.mjs:233-238) — openai-oauth
260
+ // rejects the same `rs_*` id twice in one handshake session_id with a
261
+ // "Duplicate item" error. Server-side conversation state already carries
262
+ // the prefix forward across the WS_IDLE_MS window. The collected
263
+ // reasoningItems below are surfaced for trace/debugging only; they do
264
+ // not feed back into the next request body.
265
+ const reasoningItems = [];
266
+ let reasoningTextDeltaCount = 0;
267
+ let reasoningSummaryTextDeltaCount = 0;
268
+ let reasoningOtherDeltaCount = 0;
269
+ let reasoningDeltaLogEmitted = false;
270
+ const pushReasoningItem = (item) => {
271
+ if (!item || item.type !== 'reasoning') return;
272
+ if (!item.encrypted_content) return;
273
+ reasoningItems.push({
274
+ id: item.id || '',
275
+ encrypted_content: item.encrypted_content,
276
+ summary: Array.isArray(item.summary) ? item.summary : [],
277
+ });
278
+ };
279
+ const pushResponseItem = (item) => {
280
+ if (!item || typeof item !== 'object') return;
281
+ const key = _responseItemKey(item, responseItemsAdded.length);
282
+ if (responseItemKeys.has(key)) {
283
+ const existing = responseItemsAdded.find((candidate, index) => _responseItemKey(candidate, index) === key);
284
+ if (existing?.type === 'function_call' && item.type === 'function_call') {
285
+ if (!existing.call_id && item.call_id) existing.call_id = item.call_id;
286
+ if (!existing.name && item.name) existing.name = item.name;
287
+ if ((existing.arguments == null || existing.arguments === '') && item.arguments != null) existing.arguments = item.arguments;
288
+ }
289
+ return;
290
+ }
291
+ responseItemKeys.add(key);
292
+ responseItemsAdded.push(_cloneJson(item));
293
+ };
294
+ const enrichFunctionCallResponseItem = ({ itemId = '', callId = '', name = '', argumentsText = '' } = {}) => {
295
+ for (const item of responseItemsAdded) {
296
+ if (item?.type !== 'function_call') continue;
297
+ if (itemId && item.id && item.id !== itemId) continue;
298
+ if (callId && item.call_id && item.call_id !== callId) continue;
299
+ if (!item.call_id && callId) item.call_id = callId;
300
+ if (!item.name && name) item.name = name;
301
+ if ((item.arguments == null || item.arguments === '') && argumentsText) item.arguments = argumentsText;
302
+ }
303
+ };
304
+ const pushCitation = (raw, fallbackTitle = '') => {
305
+ const url = raw?.url || raw?.uri || raw?.href || '';
306
+ if (!url || citationKeys.has(url)) return;
307
+ citationKeys.add(url);
308
+ citations.push({
309
+ title: raw?.title || fallbackTitle || '',
310
+ url,
311
+ snippet: raw?.snippet || raw?.text || raw?.description || '',
312
+ source: 'openai-oauth',
313
+ });
314
+ };
315
+ const pushOutputTextAnnotations = (contentPart) => {
316
+ const annotations = Array.isArray(contentPart?.annotations) ? contentPart.annotations : [];
317
+ for (const annotation of annotations) pushCitation(annotation);
318
+ };
319
+ const pushWebSearchCall = (item) => {
320
+ if (!item || item.type !== 'web_search_call') return;
321
+ let key = item.id || '';
322
+ if (!key) {
323
+ try { key = JSON.stringify(item.action || item); } catch { key = `${webSearchCalls.length}`; }
324
+ }
325
+ if (webSearchCallKeys.has(key)) return;
326
+ webSearchCallKeys.add(key);
327
+ webSearchCalls.push({
328
+ id: item.id || '',
329
+ status: item.status || '',
330
+ action: item.action || null,
331
+ });
332
+ const action = item.action || {};
333
+ if (action.url) pushCitation({ url: action.url, title: action.query || '' });
334
+ if (Array.isArray(action.urls)) {
335
+ for (const url of action.urls) pushCitation({ url, title: action.query || '' });
336
+ }
337
+ };
338
+ const pushCustomToolCall = (item) => {
339
+ const call = customToolCallFromResponseItem(item);
340
+ if (!call || toolCalls.some((existing) => existing.id === call.id)) return;
341
+ toolCalls.push(call);
342
+ emitToolCallDedupe(call);
343
+ };
344
+ const pushToolSearchCall = (item) => {
345
+ if (!item || item.type !== 'tool_search_call') return;
346
+ const callId = item.call_id || item.id || '';
347
+ if (!callId || toolCalls.some((call) => call.id === callId)) return;
348
+ const call = {
349
+ id: callId,
350
+ name: 'tool_search',
351
+ arguments: parseToolSearchArgs(item.arguments),
352
+ nativeType: 'tool_search_call',
353
+ };
354
+ toolCalls.push(call);
355
+ emitToolCallDedupe(call);
356
+ };
357
+ // Leaked tool-call guard. The model sometimes emits a tool call as plain
358
+ // text (XML `<invoke>`/`<function_calls>` or gpt-oss harmony
359
+ // `<|channel|>...to=functions.NAME...<|call|>`) inside
360
+ // `response.output_text.delta` instead of a native function_call. Route
361
+ // text through the guard so leaked calls are suppressed from the visible
362
+ // stream, synthesized (native `call_...` id shape), and dispatched like
363
+ // native ones. Additive: the native function_call path is untouched.
364
+ const leakGuard = createLeakGuard({ knownToolNames, harmony: true });
365
+ const dispatchLeakedCall = (recovered) => {
366
+ let args = recovered?.arguments;
367
+ if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
368
+ const call = {
369
+ id: `call_leaked_${randomBytes(8).toString('hex')}`,
370
+ name: recovered.name,
371
+ arguments: args,
372
+ };
373
+ if (!_toolDedupe.shouldDispatch(call.name, call.arguments)) return;
374
+ toolCalls.push(call);
375
+ midState.emittedToolCall = true;
376
+ try { onToolCall?.(call); } catch {}
377
+ };
378
+ const relayLeakText = (delta) => {
379
+ if (!leakGuard.enabled) {
380
+ content += delta || '';
381
+ if (delta && onTextDelta) {
382
+ if (state) state.emittedText = true;
383
+ try { onTextDelta(delta); } catch {}
384
+ }
385
+ return;
386
+ }
387
+ const { text, calls } = leakGuard.push(delta);
388
+ if (text) {
389
+ content += text;
390
+ if (onTextDelta) {
391
+ if (state) state.emittedText = true;
392
+ try { onTextDelta(text); } catch {}
393
+ }
394
+ }
395
+ for (const c of calls) dispatchLeakedCall(c);
396
+ };
397
+ const flushLeak = () => {
398
+ if (!leakGuard.enabled) return;
399
+ const { text, calls } = leakGuard.flush();
400
+ if (text) {
401
+ content += text;
402
+ if (onTextDelta) {
403
+ if (state) state.emittedText = true;
404
+ try { onTextDelta(text); } catch {}
405
+ }
406
+ }
407
+ for (const c of calls) dispatchLeakedCall(c);
408
+ };
409
+ const logReasoningDeltaSuppression = () => {
410
+ if (!logSuppressedReasoningDeltas) return;
411
+ const total = reasoningTextDeltaCount + reasoningSummaryTextDeltaCount + reasoningOtherDeltaCount;
412
+ if (reasoningDeltaLogEmitted || total === 0) return;
413
+ reasoningDeltaLogEmitted = true;
414
+ process.stderr.write(`[openai-oauth-ws] suppressed reasoning text deltas from user content count=${total} text=${reasoningTextDeltaCount} summary=${reasoningSummaryTextDeltaCount} other=${reasoningOtherDeltaCount}\n`);
415
+ };
416
+ let usage;
417
+ let stopReason = null;
418
+ let incompleteReason = null;
419
+ let done = false;
420
+ let terminalError = null;
421
+ // Mid-stream retry classifier needs to distinguish "stream died before we
422
+ // even saw response.created" from "stream died after we had a partial
423
+ // response but before completion". Mutate the shared state object so the
424
+ // caller can inspect flags on the error path without us having to attach
425
+ // them manually at every reject site.
426
+ const midState = state || {};
427
+ midState.sawResponseCreated = midState.sawResponseCreated || false;
428
+ midState.sawCompleted = midState.sawCompleted || false;
429
+ midState.wsCloseCode = null;
430
+ midState.responseFailedPayload = null;
431
+ let idleTimer = null;
432
+ let keepaliveTimer = null;
433
+ let abortHandler = null;
434
+ let messageHandler = null;
435
+ let closeHandler = null;
436
+ let errorHandler = null;
437
+ // SEMANTIC idle timer: distinct from the inter-chunk
438
+ // timer, which resets on EVERY frame (rate_limits/metadata/keepalive keep
439
+ // the socket "alive"). This timer resets ONLY on meaningful output deltas
440
+ // (text/reasoning/tool args — the same events that call onStreamDelta) so a
441
+ // stream that emits some deltas then goes silent (server keepalive frames
442
+ // only) trips a short, named terminal StreamStalledError instead of coasting
443
+ // to the 300s inter-chunk cap / 30-min agent watchdog.
444
+ let semanticIdleTimer = null;
445
+ const semanticIdleMs = PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS;
446
+ const semanticIdleEnabled = PROVIDER_SSE_IDLE_WATCHDOG_ENABLED && semanticIdleMs > 0;
447
+
448
+ return new Promise((resolve, reject) => {
449
+ // Pre-stream watchdog: the timer fires if the server never sends a
450
+ // first event (response.created) within preResponseCreatedMs
451
+ // after our last frame. The socket is open and the response.create
452
+ // frame was sent, but no server event has come back — a wedged
453
+ // post-upgrade socket. Healthy servers ack within seconds, so this
454
+ // window is intentionally short (WS_PRE_RESPONSE_CREATED_MS, ~10s).
455
+ // Once ANY server event arrives, resetIdle() cancels this watchdog and
456
+ // the single inter-chunk idle timer takes over — silent gaps
457
+ // mid-reasoning (openai-oauth spending 50s+ producing reasoning
458
+ // tokens) are normal and should not abort the turn.
459
+ const armPreStreamWatchdog = () => {
460
+ if (idleTimer) clearTimeout(idleTimer);
461
+ idleTimer = setTimeout(() => {
462
+ if (process.env.MIXDOG_DEBUG_AGENT) {
463
+ process.stderr.write(`[agent-trace] ws-timeout kind=first-byte afterMs=${preResponseCreatedMs}\n`);
464
+ }
465
+ traceWsTimeout('first_byte_timeout', preResponseCreatedMs);
466
+ const err = new Error(`WS stream: no first server event within ${preResponseCreatedMs}ms`);
467
+ // Tag the close code so _classifyMidstreamError sees a 4000
468
+ // (our local pre-stream watchdog code) and routes through
469
+ // the post-upgrade-no-first-event retryable bucket.
470
+ err.wsCloseCode = 4000;
471
+ // Tag the error object itself (not just midState): the warmup
472
+ // path streams under a separate warmupState and rethrows on
473
+ // timeout BEFORE it can copy flags to the outer midState, so the
474
+ // outer catch's _classifyMidstreamError would otherwise see
475
+ // sawResponseCreated=false + close 4000 and hit the pre-created
476
+ // deny gate. err.firstByteTimeout makes both paths retryable.
477
+ err.firstByteTimeout = true;
478
+ midState.firstByteTimeout = true;
479
+ terminalError = err;
480
+ try { socket.close(4000, 'first_byte_timeout'); } catch {}
481
+ // socket.close() may not settle a half-open WS (closeHandler never
482
+ // fires) — reject directly so the turn retries instead of hanging
483
+ // until the 600s watchdog. finish() is idempotent (Promise settles
484
+ // once; cleanup is null-safe).
485
+ finish();
486
+ }, preResponseCreatedMs);
487
+ };
488
+ let interChunkTimer = null;
489
+ const traceWsTimeout = (event, timeoutMs) => {
490
+ try {
491
+ const iteration = Number(midState.iteration);
492
+ const attemptIndex = Number(midState.attemptIndex);
493
+ const payload = {
494
+ provider: midState.traceProvider || traceProvider,
495
+ transport: 'websocket',
496
+ event,
497
+ timeout_ms: timeoutMs,
498
+ elapsed_ms: Date.now() - _streamingStart,
499
+ model: midState.model || model || null,
500
+ attempt_index: Number.isFinite(attemptIndex) ? attemptIndex : null,
501
+ warmup: midState.warmup === true,
502
+ saw_response_created: midState.sawResponseCreated === true,
503
+ };
504
+ appendAgentTrace({
505
+ sessionId: midState.sessionId || null,
506
+ iteration: Number.isFinite(iteration) ? iteration : null,
507
+ kind: 'ws_timeout',
508
+ ...payload,
509
+ payload,
510
+ });
511
+ } catch {}
512
+ };
513
+ const clearPreStreamWatchdog = () => {
514
+ if (idleTimer) {
515
+ clearTimeout(idleTimer);
516
+ idleTimer = null;
517
+ }
518
+ };
519
+ const resetInterChunk = () => {
520
+ if (interChunkTimer) clearTimeout(interChunkTimer);
521
+ interChunkTimer = setTimeout(() => {
522
+ if (process.env.MIXDOG_DEBUG_AGENT) {
523
+ process.stderr.write(`[agent-trace] ws-timeout kind=inter-chunk afterMs=${interChunkMs}\n`);
524
+ }
525
+ traceWsTimeout('inter_chunk_timeout', interChunkMs);
526
+ terminalError = new Error(`WS stream: inter-chunk inactivity for ${interChunkMs}ms`);
527
+ try { socket.close(4000, 'inter_chunk_timeout'); } catch {}
528
+ // Same half-open guard as the pre-stream watchdog: reject directly
529
+ // so a stuck socket.close() cannot leave the Promise pending.
530
+ finish();
531
+ }, interChunkMs);
532
+ };
533
+ // pi per-event idle: (re)armed only on meaningful output deltas via
534
+ // bumpSemanticIdle(). Keepalive/metadata frames DON'T touch it, so a
535
+ // deltas-then-silent wedge trips this short semantic window.
536
+ const resetSemanticIdle = () => {
537
+ if (!semanticIdleEnabled) return;
538
+ if (semanticIdleTimer) clearTimeout(semanticIdleTimer);
539
+ semanticIdleTimer = setTimeout(() => {
540
+ traceWsTimeout('semantic_idle_timeout', semanticIdleMs);
541
+ terminalError = streamStalledError('Responses WS', semanticIdleMs, { emittedToolCall: !!midState?.emittedToolCall });
542
+ // Partial-final recovery: attach streamed partial state so
543
+ // a wedged FINAL no-tool summary can be accepted as partial-final
544
+ // success by the loop. pendingToolUse gates out mid-flight tools.
545
+ try {
546
+ terminalError.partialContent = content;
547
+ terminalError.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
548
+ terminalError.pendingToolUse = pendingCalls.size > 0
549
+ || !!midState?.emittedToolCall
550
+ || toolCalls.length > 0
551
+ || _toolInFlight === true;
552
+ terminalError.partialModel = model || undefined;
553
+ } catch { /* best-effort enrichment */ }
554
+ try { terminalError.wsCloseCode = 4000; } catch {}
555
+ try { socket.close(4000, 'semantic_idle_timeout'); } catch {}
556
+ finish();
557
+ }, semanticIdleMs);
558
+ try { semanticIdleTimer.unref?.(); } catch {}
559
+ };
560
+ // Single idle reset — called on EVERY parsed server event (matches
561
+ // codex, which resets one idle timer on every received WS frame). Any
562
+ // frame proves the socket is live; there is no separate "meaningful
563
+ // output" gate. Also clears the pre-stream watchdog defensively in case
564
+ // the first event is not response.created.
565
+ const resetIdle = () => {
566
+ clearPreStreamWatchdog();
567
+ resetInterChunk();
568
+ };
569
+ // Meaningful-output progress bump: called by the same delta cases that
570
+ // call onStreamDelta (text/reasoning/tool args). Arms the semantic idle.
571
+ const bumpSemanticIdle = () => { resetSemanticIdle(); };
572
+ const cleanup = () => {
573
+ if (idleTimer) clearTimeout(idleTimer);
574
+ if (interChunkTimer) { clearTimeout(interChunkTimer); interChunkTimer = null; }
575
+ if (semanticIdleTimer) { clearTimeout(semanticIdleTimer); semanticIdleTimer = null; }
576
+ if (keepaliveTimer) { clearInterval(keepaliveTimer); keepaliveTimer = null; }
577
+ if (messageHandler) socket.off('message', messageHandler);
578
+ if (closeHandler) socket.off('close', closeHandler);
579
+ if (errorHandler) socket.off('error', errorHandler);
580
+ if (abortHandler && externalSignal) externalSignal.removeEventListener('abort', abortHandler);
581
+ };
582
+ const finish = () => {
583
+ logReasoningDeltaSuppression();
584
+ // Flush any partial-sentinel tail held back mid-stream so
585
+ // legitimate trailing text is never lost (streamed-text path).
586
+ flushLeak();
587
+ cleanup();
588
+ if (terminalError) { reject(terminalError); return; }
589
+ resolve({
590
+ content,
591
+ model,
592
+ reasoningItems: reasoningItems.length ? reasoningItems : undefined,
593
+ responseItems: responseItemsAdded.length ? responseItemsAdded : undefined,
594
+ // Dedupe by name+args (Fix 2, array side) so an identical
595
+ // synthetic-leaked + native pair can't run the tool twice.
596
+ toolCalls: toolCalls.length ? dedupeToolCallList(toolCalls) : undefined,
597
+ citations: citations.length ? citations : undefined,
598
+ webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
599
+ usage,
600
+ stopReason: stopReason || undefined,
601
+ // P1 audit fix: mirror the HTTP/SSE fallback's truncated flag
602
+ // for the WS path (sendViaWebSocket spreads this result
603
+ // through to the provider caller unchanged).
604
+ ...(stopReason === 'length' && content.length > 0 ? { truncated: true } : {}),
605
+ incompleteReason: incompleteReason || undefined,
606
+ responseId: responseId || undefined,
607
+ serviceTier: responseServiceTier || undefined,
608
+ });
609
+ };
610
+
611
+ messageHandler = (data) => {
612
+ resetIdle();
613
+ // resetIdle() above resets the SINGLE inter-chunk idle timer on
614
+ // EVERY received frame — response.created, metadata,
615
+ // rate_limits, and all deltas keep the socket alive. Separately, do
616
+ // NOT call onStreamDelta for every frame — metadata/keepalive frames
617
+ // must not reset the agent stall watchdog's lastStreamDeltaAt. Only
618
+ // meaningful output (text delta / tool call) updates that timestamp.
619
+ const text = typeof data === 'string' ? data : data.toString('utf-8');
620
+ const event = _parseEvent(text);
621
+ if (!event) return;
622
+ _traceWsHeaderKeys(entry, event, midState, traceProvider, model);
623
+ _captureTurnStateFromEvent(entry, event);
624
+ if (event.error) {
625
+ const err = new Error(event.error.message || 'Responses WS error');
626
+ try {
627
+ err.payload = event.error;
628
+ populateHttpStatusFromMessage(err);
629
+ } catch {}
630
+ terminalError = err;
631
+ finish();
632
+ return;
633
+ }
634
+ if (typeof event.type !== 'string') return;
635
+ switch (event.type) {
636
+ case 'response.created':
637
+ midState.sawResponseCreated = true;
638
+ if (event.response?.model) model = event.response.model;
639
+ if (event.response?.id) responseId = event.response.id;
640
+ // Server ack (first event). resetIdle() at the top of
641
+ // messageHandler already cleared the pre-stream watchdog and
642
+ // armed the single idle timer — no extra bookkeeping here.
643
+ break;
644
+ case 'response.output_text.delta':
645
+ try {
646
+ if (!_firstDeltaEmitted) {
647
+ _firstDeltaEmitted = true;
648
+ if (process.env.MIXDOG_DEBUG_AGENT) {
649
+ process.stderr.write(`[agent-trace] ws-first-delta sinceStreaming=${Date.now() - _streamingStart}ms\n`);
650
+ }
651
+ }
652
+ onStreamDelta?.();
653
+ } catch {}
654
+ bumpSemanticIdle();
655
+ // Live text relay (gateway): forward the raw text chunk so
656
+ // the client renders first tokens before the final replay.
657
+ // Tool-call/argument deltas intentionally stay off this path.
658
+ // Invariant: once a non-empty chunk has been relayed live it
659
+ // cannot be withdrawn, so flag the attempt so a later
660
+ // mid-stream/truncated failure is NOT retried (retry would
661
+ // concatenate a second attempt onto rendered text).
662
+ // Routed through the leaked-tool-call guard: appends to
663
+ // `content`, forwards visible text via onTextDelta, and
664
+ // recovers/dispatches any leaked known-tool call.
665
+ relayLeakText(event.delta || '');
666
+ break;
667
+ case 'response.reasoning_text.delta':
668
+ case 'response.reasoning_summary_text.delta':
669
+ if (event.type === 'response.reasoning_text.delta') reasoningTextDeltaCount += 1;
670
+ else reasoningSummaryTextDeltaCount += 1;
671
+ // Reasoning text is live model progress — refresh
672
+ // lastStreamDeltaAt so stream-watchdog does not flag a
673
+ // long reasoning span as a stall. The local WS idle timer
674
+ // was already reset by resetIdle() at the top of
675
+ // messageHandler. Reasoning is still suppressed from user
676
+ // content (no `content +=` here).
677
+ try { onStreamDelta?.(); } catch {}
678
+ bumpSemanticIdle();
679
+ break;
680
+ case 'response.output_item.added':
681
+ if (event.item?.type === 'function_call') {
682
+ pendingCalls.set(event.item.id || '', {
683
+ name: event.item.name || '',
684
+ callId: event.item.call_id || '',
685
+ });
686
+ _toolInFlight = true;
687
+ } else if (event.item?.type === 'custom_tool_call') {
688
+ _toolInFlight = true;
689
+ } else if (event.item?.type === 'tool_search_call') {
690
+ // Mark tool_search in-flight at item-added time, same
691
+ // as function_call/custom_tool_call above, so the
692
+ // semantic-idle stall gate's pendingToolUse never
693
+ // drops a mid-flight tool_search before
694
+ // response.output_item.done.
695
+ _toolInFlight = true;
696
+ }
697
+ break;
698
+ case 'response.function_call_arguments.delta':
699
+ _toolInFlight = true;
700
+ try { onStreamDelta?.(); } catch {}
701
+ bumpSemanticIdle();
702
+ break;
703
+ case 'response.custom_tool_call_input.delta':
704
+ _toolInFlight = true;
705
+ try { onStreamDelta?.(); } catch {}
706
+ bumpSemanticIdle();
707
+ break;
708
+ case 'response.function_call_arguments.done': {
709
+ const itemId = event.item_id || '';
710
+ const pending = pendingCalls.get(itemId);
711
+ // function_call_arguments.done is a completion signal:
712
+ // empty/whitespace → no args ({}); a non-empty string that
713
+ // fails JSON.parse is deterministic bad JSON. Surface an
714
+ // invalid-args MARKER (not silent {}) so the dispatch loop
715
+ // returns an is_error tool_result and the model re-issues
716
+ // valid JSON in the same turn.
717
+ let args = {};
718
+ {
719
+ const _argText = typeof event.arguments === 'string' ? event.arguments : '';
720
+ if (_argText.trim() !== '') {
721
+ try {
722
+ args = JSON.parse(_argText);
723
+ } catch (err) {
724
+ args = makeInvalidToolArgsMarker(_argText, err instanceof Error ? err.message : String(err));
725
+ }
726
+ }
727
+ }
728
+ enrichFunctionCallResponseItem({
729
+ itemId,
730
+ callId: pending?.callId || event.call_id || '',
731
+ name: pending?.name || event.name || '',
732
+ argumentsText: event.arguments || JSON.stringify(args),
733
+ });
734
+ if (pending?.callId && pending?.name) {
735
+ const call = { id: pending.callId, name: pending.name, arguments: args };
736
+ toolCalls.push(call);
737
+ emitToolCallDedupe(call);
738
+ } else {
739
+ // Synthesizing a `tc_${Date.now()}` callId here would
740
+ // make the next turn fail to match the model's
741
+ // function_call_output reference. Defer instead and
742
+ // salvage call_id/name from the final
743
+ // response.completed.output bundle below. If salvage
744
+ // also fails we fail the stream explicitly — masking
745
+ // the gap with a synthetic id just shifts the failure
746
+ // one turn later under a confusing "No tool output
747
+ // found for function call" error.
748
+ toolCalls.push({
749
+ id: pending?.callId || '',
750
+ name: pending?.name || '',
751
+ arguments: args,
752
+ _pendingItemId: itemId,
753
+ _deferred: true,
754
+ });
755
+ }
756
+ try { onStreamDelta?.(); } catch {}
757
+ bumpSemanticIdle();
758
+ break;
759
+ }
760
+ case 'response.output_item.done':
761
+ pushResponseItem(event.item);
762
+ // function_call / output_text already captured via their
763
+ // dedicated streaming events. The one shape we still need
764
+ // here is `reasoning` — carries encrypted_content that
765
+ // must be replayed on the next input to keep the openai-oauth
766
+ // server-side prompt cache prefix warm.
767
+ if (event.item?.type === 'reasoning') pushReasoningItem(event.item);
768
+ if (event.item?.type === 'web_search_call') pushWebSearchCall(event.item);
769
+ if (event.item?.type === 'tool_search_call') {
770
+ pushToolSearchCall(event.item);
771
+ }
772
+ if (event.item?.type === 'custom_tool_call') {
773
+ pushCustomToolCall(event.item);
774
+ }
775
+ break;
776
+ case 'response.completed': {
777
+ const completedServiceTier = event.response?.service_tier || event.response?.serviceTier || '';
778
+ if (completedServiceTier) responseServiceTier = String(completedServiceTier);
779
+ if (event.response?.usage) {
780
+ const u = event.response.usage;
781
+ const rawUsage = responseServiceTier
782
+ ? { ...u, service_tier: responseServiceTier }
783
+ : u;
784
+ usage = {
785
+ inputTokens: u.input_tokens || 0,
786
+ outputTokens: u.output_tokens || 0,
787
+ cachedTokens: extractCachedTokens(u),
788
+ // openai-oauth reports input_tokens as the total
789
+ // prompt volume (cached portion is a subset, not
790
+ // additive). Alias into the cross-provider
791
+ // `promptTokens` field so downstream loggers have
792
+ // uniform semantics.
793
+ promptTokens: u.input_tokens || 0,
794
+ raw: rawUsage,
795
+ };
796
+ }
797
+ if (!model && event.response?.model) model = event.response.model;
798
+ if (!responseId && event.response?.id) responseId = event.response.id;
799
+ if (event.response?.output) {
800
+ for (const item of event.response.output) {
801
+ pushResponseItem(item);
802
+ if (!content && item.type === 'message') {
803
+ for (const c of item.content || []) {
804
+ if (c.type === 'output_text') {
805
+ // Completed-output fallback (no streamed
806
+ // text). Route through the leak guard so
807
+ // a tool call leaked only in the final
808
+ // bundle is recovered, not surfaced as
809
+ // visible content. final=true → full flush.
810
+ if (leakGuard.enabled) {
811
+ const { text, calls } = leakGuard.push(c.text || '', true);
812
+ content += text;
813
+ for (const lc of calls) dispatchLeakedCall(lc);
814
+ } else {
815
+ content += c.text || '';
816
+ }
817
+ pushOutputTextAnnotations(c);
818
+ }
819
+ }
820
+ }
821
+ if (item.type === 'message') {
822
+ for (const c of item.content || []) {
823
+ if (c.type === 'output_text') pushOutputTextAnnotations(c);
824
+ }
825
+ }
826
+ if (item.type === 'web_search_call') pushWebSearchCall(item);
827
+ if (item.type === 'tool_search_call') pushToolSearchCall(item);
828
+ if (item.type === 'custom_tool_call') pushCustomToolCall(item);
829
+ // Salvage path: some streams emit reasoning only
830
+ // inside the final response.completed.output
831
+ // bundle (no per-item .done event). Dedup by id.
832
+ if (item.type === 'reasoning'
833
+ && !reasoningItems.some(r => r.id === item.id)) {
834
+ pushReasoningItem(item);
835
+ }
836
+ // Salvage path for function_call: when
837
+ // arguments.done fired before (or without) a
838
+ // matching output_item.added, the deferred tool
839
+ // call placeholder has empty id/name. The
840
+ // completed.output bundle carries the canonical
841
+ // call_id/name; fill them in and emit onToolCall.
842
+ if (item.type === 'function_call') {
843
+ const tc = toolCalls.find(
844
+ (t) => t._deferred && t._pendingItemId === (item.id || ''),
845
+ );
846
+ if (tc) {
847
+ if (!tc.id && item.call_id) tc.id = item.call_id;
848
+ if (!tc.name && item.name) tc.name = item.name;
849
+ if (tc.id && tc.name) {
850
+ delete tc._deferred;
851
+ delete tc._pendingItemId;
852
+ emitToolCallDedupe(tc);
853
+ }
854
+ }
855
+ }
856
+ }
857
+ }
858
+ // Salvage validation. Any deferred call still missing
859
+ // id/name would propagate to the next turn as a
860
+ // function_call_output the server can't anchor. Fail the
861
+ // stream now so the caller sees a deterministic error
862
+ // instead of a cryptic mismatch one turn later.
863
+ const unresolved = toolCalls.find((t) => t._deferred);
864
+ if (unresolved) {
865
+ terminalError = new Error(
866
+ `${errLabel} function_call salvage failed: missing call_id/name for item_id=${unresolved._pendingItemId || '?'}`,
867
+ );
868
+ finish();
869
+ break;
870
+ }
871
+ midState.sawCompleted = true;
872
+ done = true;
873
+ finish();
874
+ break;
875
+ }
876
+ case 'response.done': {
877
+ // response.done is the terminal frame for some openai-oauth
878
+ // streams that never emit a separate response.completed.
879
+ // Route through the same completed/failed/incomplete
880
+ // normalization based on event.response.status so a
881
+ // server-side abort (incomplete / failed) does not slip
882
+ // through as success. status === 'completed' falls
883
+ // through to the success path with sawCompleted set;
884
+ // anything else is converted into a terminal error.
885
+ const status = event.response?.status || '';
886
+ if (status === 'failed') {
887
+ midState.responseFailedPayload = event;
888
+ const msg = event.response?.error?.message
889
+ || event.error?.message
890
+ || event.message
891
+ || 'response.done failed';
892
+ terminalError = Object.assign(
893
+ new Error(`${errLabel} response.done failed: ${msg}`),
894
+ { responseFailed: event },
895
+ );
896
+ populateHttpStatusFromMessage(terminalError, msg);
897
+ done = true;
898
+ finish();
899
+ break;
900
+ }
901
+ if (status === 'incomplete') {
902
+ const reasonStr = _incompleteReasonFromEvent(event);
903
+ if (_isMaxOutputIncompleteReason(reasonStr)) {
904
+ incompleteReason = reasonStr;
905
+ stopReason = 'length';
906
+ midState.sawCompleted = true;
907
+ done = true;
908
+ finish();
909
+ break;
910
+ }
911
+ terminalError = Object.assign(
912
+ new Error(`${errLabel} response.done incomplete: ${reasonStr}`),
913
+ { responseIncomplete: event, incompleteReason: reasonStr },
914
+ );
915
+ done = true;
916
+ finish();
917
+ break;
918
+ }
919
+ if (status && status !== 'completed') {
920
+ terminalError = Object.assign(
921
+ new Error(`${errLabel} response.done unexpected status: ${status}`),
922
+ { responseDoneStatus: status },
923
+ );
924
+ done = true;
925
+ finish();
926
+ break;
927
+ }
928
+ midState.sawCompleted = true;
929
+ done = true;
930
+ finish();
931
+ break;
932
+ }
933
+ case 'response.incomplete': {
934
+ // Most incomplete reasons are real failures. max_output_tokens
935
+ // maps cleanly to Anthropic's stop_reason=max_tokens; treating
936
+ // it as an error makes Claude retry the same over-budget turn.
937
+ const reasonStr = _incompleteReasonFromEvent(event);
938
+ if (_isMaxOutputIncompleteReason(reasonStr)) {
939
+ incompleteReason = reasonStr;
940
+ stopReason = 'length';
941
+ midState.sawCompleted = true;
942
+ done = true;
943
+ finish();
944
+ break;
945
+ }
946
+ terminalError = Object.assign(
947
+ new Error(`${errLabel} response.incomplete: ${reasonStr}`),
948
+ { responseIncomplete: event, incompleteReason: reasonStr },
949
+ );
950
+ finish();
951
+ break;
952
+ }
953
+ case 'response.failed': {
954
+ // Stash the payload so the mid-stream classifier can sniff
955
+ // network_error / stream_disconnected without re-parsing.
956
+ midState.responseFailedPayload = event;
957
+ const msg = event.response?.error?.message
958
+ || event.error?.message
959
+ || event.message
960
+ || 'response.failed';
961
+ terminalError = Object.assign(new Error(`${errLabel} response.failed: ${msg}`), {
962
+ responseFailed: event,
963
+ });
964
+ // Sniff the server message for transient/auth/permanent
965
+ // hints so the handshake / mid-stream retry classifiers
966
+ // can route by httpStatus. Without this, server-side
967
+ // events like "Our servers are currently overloaded"
968
+ // surfaced as unclassified errors and skipped the
969
+ // 5xx retry bucket entirely.
970
+ populateHttpStatusFromMessage(terminalError, msg);
971
+ finish();
972
+ break;
973
+ }
974
+ case 'error': {
975
+ const errMsg = String(event.message || event.error?.message || 'unknown');
976
+ terminalError = new Error(`${errLabel} error: ${errMsg}`);
977
+ populateHttpStatusFromMessage(terminalError, errMsg);
978
+ finish();
979
+ break;
980
+ }
981
+ default:
982
+ // Catch any other reasoning-delta variants (e.g.
983
+ // `response.reasoning.<sub>.delta`) so they are counted
984
+ // and suppressed, never reaching the user content buffer.
985
+ if (typeof event.type === 'string'
986
+ && event.type.startsWith('response.reasoning')
987
+ && event.type.endsWith('.delta')) {
988
+ reasoningOtherDeltaCount += 1;
989
+ // These ARE live model progress (reviewer Medium): a
990
+ // provider that emits only these reasoning variants for a
991
+ // long span would otherwise trip the SEMANTIC idle abort.
992
+ // Refresh both the watchdog and the semantic idle timer,
993
+ // matching the named reasoning_text.delta case above.
994
+ try { onStreamDelta?.(); } catch {}
995
+ bumpSemanticIdle();
996
+ }
997
+ // Trace-only events (response.in_progress, etc.)
998
+ break;
999
+ }
1000
+ };
1001
+ closeHandler = (code, reason) => {
1002
+ if (done) return;
1003
+ midState.wsCloseCode = code;
1004
+ if (!terminalError) {
1005
+ const r = reason?.toString?.('utf-8') || '';
1006
+ const httpStatus = _httpStatusFromWsClose(code, r);
1007
+ terminalError = Object.assign(
1008
+ new Error(`OpenAI OAuth WS closed before response.completed (code=${code}${r ? `, reason=${r}` : ''})`),
1009
+ { wsCloseCode: code, wsCloseReason: r, ...(httpStatus ? { httpStatus } : {}) },
1010
+ );
1011
+ } else if (terminalError && !terminalError.wsCloseCode) {
1012
+ try { terminalError.wsCloseCode = code; } catch {}
1013
+ try { terminalError.httpStatus = terminalError.httpStatus || _httpStatusFromWsClose(code, reason?.toString?.('utf-8') || ''); } catch {}
1014
+ }
1015
+ finish();
1016
+ };
1017
+ errorHandler = (err) => {
1018
+ if (done) return;
1019
+ const wrapped = err instanceof Error ? err : new Error(String(err));
1020
+ if (terminalError) {
1021
+ // Preserve the first terminalError; chain the later socket
1022
+ // error in via `cause` (or `suppressed` if cause already set)
1023
+ // so diagnostics keep the original failure visible.
1024
+ try {
1025
+ if (!terminalError.cause) terminalError.cause = wrapped;
1026
+ else {
1027
+ const list = Array.isArray(terminalError.suppressed)
1028
+ ? terminalError.suppressed
1029
+ : [];
1030
+ list.push(wrapped);
1031
+ terminalError.suppressed = list;
1032
+ }
1033
+ } catch {}
1034
+ } else {
1035
+ terminalError = wrapped;
1036
+ }
1037
+ try { socket.close(4001, 'stream_error'); } catch {}
1038
+ finish();
1039
+ };
1040
+ if (externalSignal) {
1041
+ abortHandler = () => {
1042
+ if (done) return;
1043
+ const reason = externalSignal.reason;
1044
+ terminalError = reason instanceof Error ? reason : new Error('OpenAI OAuth WS aborted by session close');
1045
+ // Tag: was this a user/caller abort, or a watchdog abort?
1046
+ // Mid-stream retry must skip user aborts but may retry watchdog
1047
+ // aborts. The caller-owned AbortController surfaces through
1048
+ // externalSignal; the agent stall watchdog signals via a reason
1049
+ // object whose name === 'AgentStallAbortError'. stream-watchdog
1050
+ // uses StreamStalledAbortError. Anything else → treat as user.
1051
+ const reasonName = reason?.name || '';
1052
+ if (reasonName === 'AgentStallAbortError'
1053
+ || reasonName === 'StreamStalledAbortError') {
1054
+ midState.watchdogAbort = reasonName;
1055
+ } else {
1056
+ midState.userAbort = true;
1057
+ }
1058
+ try { socket.close(4002, 'aborted'); } catch {}
1059
+ finish();
1060
+ };
1061
+ if (externalSignal.aborted) { abortHandler(); return; }
1062
+ externalSignal.addEventListener('abort', abortHandler, { once: true });
1063
+ }
1064
+ socket.on('message', messageHandler);
1065
+ socket.on('close', closeHandler);
1066
+ socket.on('error', errorHandler);
1067
+ armPreStreamWatchdog();
1068
+ // Periodic client-side WS ping while the stream is active. The server's
1069
+ // server closes with 1011 "keepalive ping timeout" when it thinks the
1070
+ // peer is silent during long reasoning windows where no data frames
1071
+ // flow. Sending a ping every 10s from our side keeps the socket warm.
1072
+ // The interval is unref'd so it never holds the event loop open, and
1073
+ // cleanup() clears it on every terminal path (completed / close /
1074
+ // error / abort / mid-stream retry teardown).
1075
+ keepaliveTimer = setInterval(() => {
1076
+ try {
1077
+ if (socket.readyState !== WebSocket.OPEN) return;
1078
+ socket.ping();
1079
+ } catch {}
1080
+ }, 10_000);
1081
+ try { keepaliveTimer.unref?.(); } catch {}
1082
+ });
1083
+ }
1084
+
1085
+ /**
1086
+ * Classify a handshake error for retry eligibility.
1087
+ *
1088
+ * Default-deny: anything we don't recognize as transient returns null (treat
1089
+ * as permanent). Permanent buckets (401/403/404/429) also return null — the
1090
+ * server has made a deterministic decision that a retry can't change.
1091
+ *
1092
+ * Returns one of:
1093
+ * 'timeout' — `ws` handshakeTimeout fired
1094
+ * 'reset' — ECONNRESET / socket hang up
1095
+ * 'dns' — EAI_AGAIN / ENOTFOUND / EAI_NODATA
1096
+ * 'refused' — ECONNREFUSED
1097
+ * 'network' — ENETUNREACH / EHOSTUNREACH / EPIPE
1098
+ * 'acquire_timeout' — hard client-side open/acquire deadline fired
1099
+ * 'http_5xx' (with specific status e.g. 'http_503') — server overload
1100
+ * null — not retryable
1101
+ */
1102
+ // Thin re-export wrapper: handshake classification now lives in the shared
1103
+ // retry-classifier (classifyHandshakeError). Kept here as a named export so
1104
+ // internal call sites (_acquireWithRetry) and any external importer keep
1105
+ // resolving the same symbol.