mixdog 0.7.18 → 0.8.0

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 (844) hide show
  1. package/README.md +37 -331
  2. package/package.json +67 -99
  3. package/scripts/boot-smoke.mjs +94 -0
  4. package/scripts/build-tui.mjs +52 -0
  5. package/scripts/compact-smoke.mjs +199 -0
  6. package/scripts/lead-workflow-smoke.mjs +598 -0
  7. package/scripts/live-worker-smoke.mjs +239 -0
  8. package/scripts/output-style-smoke.mjs +101 -0
  9. package/scripts/smoke-loop-report.mjs +221 -0
  10. package/scripts/smoke-loop.mjs +201 -0
  11. package/scripts/smoke.mjs +113 -0
  12. package/scripts/tool-failures.mjs +143 -0
  13. package/scripts/tool-smoke.mjs +456 -0
  14. package/src/agents/debugger/AGENT.md +3 -0
  15. package/src/agents/debugger/agent.json +6 -0
  16. package/src/agents/explore/AGENT.md +4 -0
  17. package/src/agents/explore/agent.json +6 -0
  18. package/src/agents/heavy-worker/AGENT.md +3 -0
  19. package/src/agents/heavy-worker/agent.json +6 -0
  20. package/src/agents/maintainer/AGENT.md +3 -0
  21. package/src/agents/maintainer/agent.json +6 -0
  22. package/src/agents/reviewer/AGENT.md +3 -0
  23. package/src/agents/reviewer/agent.json +6 -0
  24. package/src/agents/scheduler-task.md +3 -0
  25. package/src/agents/web-researcher/AGENT.md +3 -0
  26. package/src/agents/web-researcher/agent.json +6 -0
  27. package/src/agents/webhook-handler.md +3 -0
  28. package/src/agents/worker/AGENT.md +3 -0
  29. package/src/agents/worker/agent.json +6 -0
  30. package/src/app.mjs +90 -0
  31. package/src/cli.mjs +11 -0
  32. package/src/defaults/hidden-roles.json +72 -0
  33. package/src/defaults/mixdog-config.template.json +15 -0
  34. package/src/hooks/lib/permission-evaluator.cjs +488 -0
  35. package/src/hooks/lib/settings-loader.cjs +112 -0
  36. package/src/lib/keychain-cjs.cjs +332 -0
  37. package/src/lib/plugin-paths.cjs +28 -0
  38. package/src/lib/rules-builder.cjs +315 -0
  39. package/src/mixdog-session-runtime.mjs +3704 -0
  40. package/src/output-styles/default.md +38 -0
  41. package/src/output-styles/extreme-simple.md +17 -0
  42. package/src/output-styles/simple.md +17 -0
  43. package/src/repl.mjs +322 -0
  44. package/src/rules/bridge/00-common.md +5 -0
  45. package/src/rules/bridge/20-skip-protocol.md +11 -0
  46. package/src/rules/bridge/30-explorer.md +4 -0
  47. package/src/rules/bridge/40-cycle1-agent.md +28 -0
  48. package/src/rules/bridge/41-cycle2-agent.md +59 -0
  49. package/src/rules/lead/00-tool-lead.md +5 -0
  50. package/src/rules/lead/01-general.md +5 -0
  51. package/src/rules/lead/02-channels.md +3 -0
  52. package/src/rules/lead/04-workflow.md +12 -0
  53. package/src/rules/shared/00-language.md +3 -0
  54. package/src/rules/shared/01-tool.md +3 -0
  55. package/src/runtime/agent/orchestrator/bridge-trace.mjs +814 -0
  56. package/src/runtime/agent/orchestrator/cache-mtime.mjs +60 -0
  57. package/src/runtime/agent/orchestrator/config.mjs +446 -0
  58. package/src/runtime/agent/orchestrator/context/collect.mjs +796 -0
  59. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +417 -0
  60. package/src/runtime/agent/orchestrator/internal-roles.mjs +188 -0
  61. package/src/runtime/agent/orchestrator/internal-tools.mjs +88 -0
  62. package/src/runtime/agent/orchestrator/mcp/client.mjs +345 -0
  63. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2104 -0
  64. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +784 -0
  65. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +341 -0
  66. package/src/runtime/agent/orchestrator/providers/gemini.mjs +1679 -0
  67. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +959 -0
  68. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +213 -0
  69. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +38 -0
  70. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +471 -0
  71. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +615 -0
  72. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +808 -0
  73. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +1719 -0
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2587 -0
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1953 -0
  76. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +136 -0
  77. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +317 -0
  78. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +109 -0
  79. package/src/runtime/agent/orchestrator/providers/registry.mjs +247 -0
  80. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +332 -0
  81. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/providers/trace-utils.mjs +50 -0
  83. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
  84. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +318 -0
  85. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +367 -0
  86. package/src/runtime/agent/orchestrator/session/compact.mjs +882 -0
  87. package/src/runtime/agent/orchestrator/session/context-utils.mjs +233 -0
  88. package/src/runtime/agent/orchestrator/session/loop.mjs +2320 -0
  89. package/src/runtime/agent/orchestrator/session/manager.mjs +2960 -0
  90. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  91. package/src/runtime/agent/orchestrator/session/store.mjs +663 -0
  92. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +166 -0
  93. package/src/runtime/agent/orchestrator/smart-bridge/bridge-llm.mjs +339 -0
  94. package/src/runtime/agent/orchestrator/smart-bridge/cache-strategy.mjs +419 -0
  95. package/src/runtime/agent/orchestrator/stall-policy.mjs +227 -0
  96. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +235 -0
  97. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +723 -0
  98. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +389 -0
  99. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +637 -0
  100. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +165 -0
  101. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +104 -0
  102. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +194 -0
  103. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +596 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +110 -0
  105. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +153 -0
  106. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
  107. package/src/runtime/agent/orchestrator/tools/builtin/read-open.mjs +189 -0
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +731 -0
  109. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +168 -0
  110. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
  111. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +465 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +160 -0
  113. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +982 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +1087 -0
  115. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +231 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +223 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin.mjs +478 -0
  118. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
  119. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4102 -0
  120. package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +323 -0
  121. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +154 -0
  122. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +26 -0
  123. package/src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs +143 -0
  124. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +18 -0
  125. package/src/runtime/agent/orchestrator/tools/patch.mjs +2772 -0
  126. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +114 -0
  127. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +880 -0
  128. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +312 -0
  129. package/src/runtime/channels/backends/discord.mjs +781 -0
  130. package/src/runtime/channels/data/voice-runtime-manifest.json +138 -0
  131. package/src/runtime/channels/index.mjs +3309 -0
  132. package/src/runtime/channels/lib/config.mjs +285 -0
  133. package/src/runtime/channels/lib/drop-trace.mjs +71 -0
  134. package/src/runtime/channels/lib/event-pipeline.mjs +81 -0
  135. package/src/runtime/channels/lib/holidays.mjs +138 -0
  136. package/src/runtime/channels/lib/hook-pipe-server.mjs +671 -0
  137. package/src/runtime/channels/lib/output-forwarder.mjs +765 -0
  138. package/src/runtime/channels/lib/runtime-paths.mjs +497 -0
  139. package/src/runtime/channels/lib/scheduler.mjs +710 -0
  140. package/src/runtime/channels/lib/session-discovery.mjs +102 -0
  141. package/src/runtime/channels/lib/state-file.mjs +68 -0
  142. package/src/runtime/channels/lib/status-snapshot.mjs +224 -0
  143. package/src/runtime/channels/lib/tool-format.mjs +124 -0
  144. package/src/runtime/channels/lib/transcript-discovery.mjs +195 -0
  145. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +734 -0
  146. package/src/runtime/channels/lib/webhook.mjs +1288 -0
  147. package/src/runtime/channels/tool-defs.mjs +177 -0
  148. package/src/runtime/lib/keychain-cjs.cjs +289 -0
  149. package/src/runtime/memory/data/runtime-manifest.json +40 -0
  150. package/src/runtime/memory/index.mjs +3600 -0
  151. package/src/runtime/memory/lib/core-memory-store.mjs +336 -0
  152. package/src/runtime/memory/lib/embedding-provider.mjs +275 -0
  153. package/src/runtime/memory/lib/embedding-worker.mjs +331 -0
  154. package/src/runtime/memory/lib/memory-cycle-requests.mjs +276 -0
  155. package/src/runtime/memory/lib/memory-cycle1.mjs +783 -0
  156. package/src/runtime/memory/lib/memory-cycle2.mjs +1389 -0
  157. package/src/runtime/memory/lib/memory-cycle3.mjs +646 -0
  158. package/src/runtime/memory/lib/memory-embed.mjs +300 -0
  159. package/src/runtime/memory/lib/memory-ops-policy.mjs +149 -0
  160. package/src/runtime/memory/lib/memory-recall-store.mjs +644 -0
  161. package/src/runtime/memory/lib/memory.mjs +418 -0
  162. package/src/runtime/memory/lib/pg/adapter.mjs +314 -0
  163. package/src/runtime/memory/lib/pg/process.mjs +366 -0
  164. package/src/runtime/memory/lib/pg/supervisor.mjs +495 -0
  165. package/src/runtime/memory/lib/runtime-fetcher.mjs +464 -0
  166. package/src/runtime/memory/lib/trace-store.mjs +734 -0
  167. package/src/runtime/memory/tool-defs.mjs +79 -0
  168. package/src/runtime/search/index.mjs +925 -0
  169. package/src/runtime/search/lib/config.mjs +61 -0
  170. package/src/runtime/search/lib/web-tools.mjs +1278 -0
  171. package/src/runtime/search/tool-defs.mjs +64 -0
  172. package/src/runtime/shared/atomic-file.mjs +435 -0
  173. package/src/runtime/shared/background-tasks.mjs +376 -0
  174. package/src/runtime/shared/child-guardian.mjs +98 -0
  175. package/src/runtime/shared/config.mjs +393 -0
  176. package/src/runtime/shared/err-text.mjs +121 -0
  177. package/src/runtime/shared/launcher-control.mjs +259 -0
  178. package/src/runtime/shared/llm/http-agent.mjs +129 -0
  179. package/src/runtime/shared/open-url.mjs +37 -0
  180. package/src/runtime/shared/plugin-paths.mjs +25 -0
  181. package/src/runtime/shared/process-shutdown.mjs +147 -0
  182. package/src/runtime/shared/schedules-store.mjs +70 -0
  183. package/src/runtime/shared/tool-execution-contract.mjs +104 -0
  184. package/src/runtime/shared/tool-surface.mjs +950 -0
  185. package/src/runtime/shared/user-cwd.mjs +221 -0
  186. package/src/runtime/shared/user-data-guard.mjs +232 -0
  187. package/src/runtime/shared/workspace-router.mjs +259 -0
  188. package/src/standalone/bridge-tool.mjs +1414 -0
  189. package/src/standalone/channel-admin.mjs +366 -0
  190. package/src/standalone/channel-worker-preload.cjs +3 -0
  191. package/src/standalone/channel-worker.mjs +353 -0
  192. package/src/standalone/explore-tool.mjs +233 -0
  193. package/src/standalone/hook-bus.mjs +246 -0
  194. package/src/standalone/plugin-admin.mjs +247 -0
  195. package/src/standalone/provider-admin.mjs +338 -0
  196. package/src/standalone/seeds.mjs +94 -0
  197. package/src/standalone/usage-dashboard.mjs +510 -0
  198. package/src/tui/App.jsx +5438 -0
  199. package/src/tui/components/AnsiText.jsx +199 -0
  200. package/src/tui/components/ContextPanel.jsx +217 -0
  201. package/src/tui/components/Markdown.jsx +205 -0
  202. package/src/tui/components/MarkdownTable.jsx +204 -0
  203. package/src/tui/components/Message.jsx +103 -0
  204. package/src/tui/components/Picker.jsx +317 -0
  205. package/src/tui/components/PromptInput.jsx +584 -0
  206. package/src/tui/components/QueuedCommands.jsx +47 -0
  207. package/src/tui/components/SlashCommandPalette.jsx +114 -0
  208. package/src/tui/components/Spinner.jsx +317 -0
  209. package/src/tui/components/StatusLine.jsx +87 -0
  210. package/src/tui/components/TextEntryPanel.jsx +323 -0
  211. package/src/tui/components/ToolExecution.jsx +772 -0
  212. package/src/tui/components/TurnDone.jsx +78 -0
  213. package/src/tui/components/UsagePanel.jsx +331 -0
  214. package/src/tui/dist/index.mjs +12359 -0
  215. package/src/tui/engine.mjs +2410 -0
  216. package/src/tui/figures.mjs +50 -0
  217. package/src/tui/hooks/useEngine.mjs +16 -0
  218. package/src/tui/index.jsx +254 -0
  219. package/src/tui/input-editing.mjs +242 -0
  220. package/src/tui/markdown/format-token.mjs +194 -0
  221. package/src/tui/paste-attachments.mjs +198 -0
  222. package/src/tui/runtime/shared/process-shutdown.mjs +1 -0
  223. package/src/tui/spinner-verbs.mjs +45 -0
  224. package/src/tui/theme.mjs +67 -0
  225. package/src/tui/time-format.mjs +53 -0
  226. package/src/ui/ansi.mjs +115 -0
  227. package/src/ui/markdown.mjs +195 -0
  228. package/src/ui/statusline.mjs +730 -0
  229. package/src/ui/tool-card.mjs +101 -0
  230. package/src/vendor/statusline/bin/statusline-lib.mjs +805 -0
  231. package/src/vendor/statusline/bin/statusline-route.mjs +596 -0
  232. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +285 -0
  233. package/src/vendor/statusline/src/gateway/claude-current.mjs +320 -0
  234. package/src/vendor/statusline/src/gateway/route-meta.mjs +753 -0
  235. package/src/vendor/statusline/src/gateway/session-routes.mjs +244 -0
  236. package/src/workflows/default/WORKFLOW.md +7 -0
  237. package/src/workflows/default/workflow.json +14 -0
  238. package/vendor/ink/build/ansi-tokenizer.d.ts +38 -0
  239. package/vendor/ink/build/ansi-tokenizer.js +316 -0
  240. package/vendor/ink/build/ansi-tokenizer.js.map +1 -0
  241. package/vendor/ink/build/colorize.d.ts +3 -0
  242. package/vendor/ink/build/colorize.js +48 -0
  243. package/vendor/ink/build/colorize.js.map +1 -0
  244. package/vendor/ink/build/components/AccessibilityContext.d.ts +3 -0
  245. package/vendor/ink/build/components/AccessibilityContext.js +5 -0
  246. package/vendor/ink/build/components/AccessibilityContext.js.map +1 -0
  247. package/vendor/ink/build/components/AnimationContext.d.ts +9 -0
  248. package/vendor/ink/build/components/AnimationContext.js +13 -0
  249. package/vendor/ink/build/components/AnimationContext.js.map +1 -0
  250. package/vendor/ink/build/components/App.d.ts +24 -0
  251. package/vendor/ink/build/components/App.js +554 -0
  252. package/vendor/ink/build/components/App.js.map +1 -0
  253. package/vendor/ink/build/components/AppContext.d.ts +80 -0
  254. package/vendor/ink/build/components/AppContext.js +25 -0
  255. package/vendor/ink/build/components/AppContext.js.map +1 -0
  256. package/vendor/ink/build/components/BackgroundContext.d.ts +4 -0
  257. package/vendor/ink/build/components/BackgroundContext.js +3 -0
  258. package/vendor/ink/build/components/BackgroundContext.js.map +1 -0
  259. package/vendor/ink/build/components/Box.d.ts +130 -0
  260. package/vendor/ink/build/components/Box.js +34 -0
  261. package/vendor/ink/build/components/Box.js.map +1 -0
  262. package/vendor/ink/build/components/CursorContext.d.ts +11 -0
  263. package/vendor/ink/build/components/CursorContext.js +8 -0
  264. package/vendor/ink/build/components/CursorContext.js.map +1 -0
  265. package/vendor/ink/build/components/ErrorBoundary.d.ts +18 -0
  266. package/vendor/ink/build/components/ErrorBoundary.js +23 -0
  267. package/vendor/ink/build/components/ErrorBoundary.js.map +1 -0
  268. package/vendor/ink/build/components/ErrorOverview.d.ts +6 -0
  269. package/vendor/ink/build/components/ErrorOverview.js +90 -0
  270. package/vendor/ink/build/components/ErrorOverview.js.map +1 -0
  271. package/vendor/ink/build/components/FocusContext.d.ts +16 -0
  272. package/vendor/ink/build/components/FocusContext.js +17 -0
  273. package/vendor/ink/build/components/FocusContext.js.map +1 -0
  274. package/vendor/ink/build/components/Newline.d.ts +13 -0
  275. package/vendor/ink/build/components/Newline.js +8 -0
  276. package/vendor/ink/build/components/Newline.js.map +1 -0
  277. package/vendor/ink/build/components/Spacer.d.ts +7 -0
  278. package/vendor/ink/build/components/Spacer.js +11 -0
  279. package/vendor/ink/build/components/Spacer.js.map +1 -0
  280. package/vendor/ink/build/components/Static.d.ts +24 -0
  281. package/vendor/ink/build/components/Static.js +28 -0
  282. package/vendor/ink/build/components/Static.js.map +1 -0
  283. package/vendor/ink/build/components/StderrContext.d.ts +15 -0
  284. package/vendor/ink/build/components/StderrContext.js +13 -0
  285. package/vendor/ink/build/components/StderrContext.js.map +1 -0
  286. package/vendor/ink/build/components/StdinContext.d.ts +28 -0
  287. package/vendor/ink/build/components/StdinContext.js +20 -0
  288. package/vendor/ink/build/components/StdinContext.js.map +1 -0
  289. package/vendor/ink/build/components/StdoutContext.d.ts +15 -0
  290. package/vendor/ink/build/components/StdoutContext.js +13 -0
  291. package/vendor/ink/build/components/StdoutContext.js.map +1 -0
  292. package/vendor/ink/build/components/Text.d.ts +55 -0
  293. package/vendor/ink/build/components/Text.js +50 -0
  294. package/vendor/ink/build/components/Text.js.map +1 -0
  295. package/vendor/ink/build/components/Transform.d.ts +16 -0
  296. package/vendor/ink/build/components/Transform.js +15 -0
  297. package/vendor/ink/build/components/Transform.js.map +1 -0
  298. package/vendor/ink/build/cursor-helpers.d.ts +39 -0
  299. package/vendor/ink/build/cursor-helpers.js +62 -0
  300. package/vendor/ink/build/cursor-helpers.js.map +1 -0
  301. package/vendor/ink/build/devtools-window-polyfill.d.ts +1 -0
  302. package/vendor/ink/build/devtools-window-polyfill.js +68 -0
  303. package/vendor/ink/build/devtools-window-polyfill.js.map +1 -0
  304. package/vendor/ink/build/devtools.d.ts +1 -0
  305. package/vendor/ink/build/devtools.js +36 -0
  306. package/vendor/ink/build/devtools.js.map +1 -0
  307. package/vendor/ink/build/dom.d.ts +62 -0
  308. package/vendor/ink/build/dom.js +143 -0
  309. package/vendor/ink/build/dom.js.map +1 -0
  310. package/vendor/ink/build/get-max-width.d.ts +3 -0
  311. package/vendor/ink/build/get-max-width.js +10 -0
  312. package/vendor/ink/build/get-max-width.js.map +1 -0
  313. package/vendor/ink/build/hooks/use-animation.d.ts +49 -0
  314. package/vendor/ink/build/hooks/use-animation.js +87 -0
  315. package/vendor/ink/build/hooks/use-animation.js.map +1 -0
  316. package/vendor/ink/build/hooks/use-app.d.ts +5 -0
  317. package/vendor/ink/build/hooks/use-app.js +8 -0
  318. package/vendor/ink/build/hooks/use-app.js.map +1 -0
  319. package/vendor/ink/build/hooks/use-box-metrics.d.ts +59 -0
  320. package/vendor/ink/build/hooks/use-box-metrics.js +81 -0
  321. package/vendor/ink/build/hooks/use-box-metrics.js.map +1 -0
  322. package/vendor/ink/build/hooks/use-cursor.d.ts +12 -0
  323. package/vendor/ink/build/hooks/use-cursor.js +29 -0
  324. package/vendor/ink/build/hooks/use-cursor.js.map +1 -0
  325. package/vendor/ink/build/hooks/use-focus-manager.d.ts +43 -0
  326. package/vendor/ink/build/hooks/use-focus-manager.js +18 -0
  327. package/vendor/ink/build/hooks/use-focus-manager.js.map +1 -0
  328. package/vendor/ink/build/hooks/use-focus.d.ts +30 -0
  329. package/vendor/ink/build/hooks/use-focus.js +43 -0
  330. package/vendor/ink/build/hooks/use-focus.js.map +1 -0
  331. package/vendor/ink/build/hooks/use-input.d.ts +132 -0
  332. package/vendor/ink/build/hooks/use-input.js +126 -0
  333. package/vendor/ink/build/hooks/use-input.js.map +1 -0
  334. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.d.ts +6 -0
  335. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.js +12 -0
  336. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.js.map +1 -0
  337. package/vendor/ink/build/hooks/use-paste.d.ts +35 -0
  338. package/vendor/ink/build/hooks/use-paste.js +62 -0
  339. package/vendor/ink/build/hooks/use-paste.js.map +1 -0
  340. package/vendor/ink/build/hooks/use-stderr.d.ts +5 -0
  341. package/vendor/ink/build/hooks/use-stderr.js +8 -0
  342. package/vendor/ink/build/hooks/use-stderr.js.map +1 -0
  343. package/vendor/ink/build/hooks/use-stdin.d.ts +7 -0
  344. package/vendor/ink/build/hooks/use-stdin.js +9 -0
  345. package/vendor/ink/build/hooks/use-stdin.js.map +1 -0
  346. package/vendor/ink/build/hooks/use-stdout.d.ts +5 -0
  347. package/vendor/ink/build/hooks/use-stdout.js +8 -0
  348. package/vendor/ink/build/hooks/use-stdout.js.map +1 -0
  349. package/vendor/ink/build/hooks/use-window-size.d.ts +18 -0
  350. package/vendor/ink/build/hooks/use-window-size.js +22 -0
  351. package/vendor/ink/build/hooks/use-window-size.js.map +1 -0
  352. package/vendor/ink/build/index.d.ts +42 -0
  353. package/vendor/ink/build/index.js +24 -0
  354. package/vendor/ink/build/index.js.map +1 -0
  355. package/vendor/ink/build/ink.d.ts +146 -0
  356. package/vendor/ink/build/ink.js +1022 -0
  357. package/vendor/ink/build/ink.js.map +1 -0
  358. package/vendor/ink/build/input-parser.d.ts +10 -0
  359. package/vendor/ink/build/input-parser.js +194 -0
  360. package/vendor/ink/build/input-parser.js.map +1 -0
  361. package/vendor/ink/build/instances.d.ts +3 -0
  362. package/vendor/ink/build/instances.js +8 -0
  363. package/vendor/ink/build/instances.js.map +1 -0
  364. package/vendor/ink/build/kitty-keyboard.d.ts +23 -0
  365. package/vendor/ink/build/kitty-keyboard.js +32 -0
  366. package/vendor/ink/build/kitty-keyboard.js.map +1 -0
  367. package/vendor/ink/build/log-update.d.ts +20 -0
  368. package/vendor/ink/build/log-update.js +261 -0
  369. package/vendor/ink/build/log-update.js.map +1 -0
  370. package/vendor/ink/build/measure-element.d.ts +20 -0
  371. package/vendor/ink/build/measure-element.js +13 -0
  372. package/vendor/ink/build/measure-element.js.map +1 -0
  373. package/vendor/ink/build/measure-text.d.ts +6 -0
  374. package/vendor/ink/build/measure-text.js +21 -0
  375. package/vendor/ink/build/measure-text.js.map +1 -0
  376. package/vendor/ink/build/output.d.ts +35 -0
  377. package/vendor/ink/build/output.js +328 -0
  378. package/vendor/ink/build/output.js.map +1 -0
  379. package/vendor/ink/build/parse-keypress.d.ts +20 -0
  380. package/vendor/ink/build/parse-keypress.js +495 -0
  381. package/vendor/ink/build/parse-keypress.js.map +1 -0
  382. package/vendor/ink/build/reconciler.d.ts +4 -0
  383. package/vendor/ink/build/reconciler.js +306 -0
  384. package/vendor/ink/build/reconciler.js.map +1 -0
  385. package/vendor/ink/build/render-background.d.ts +4 -0
  386. package/vendor/ink/build/render-background.js +25 -0
  387. package/vendor/ink/build/render-background.js.map +1 -0
  388. package/vendor/ink/build/render-border.d.ts +4 -0
  389. package/vendor/ink/build/render-border.js +84 -0
  390. package/vendor/ink/build/render-border.js.map +1 -0
  391. package/vendor/ink/build/render-node-to-output.d.ts +14 -0
  392. package/vendor/ink/build/render-node-to-output.js +162 -0
  393. package/vendor/ink/build/render-node-to-output.js.map +1 -0
  394. package/vendor/ink/build/render-to-string.d.ts +38 -0
  395. package/vendor/ink/build/render-to-string.js +116 -0
  396. package/vendor/ink/build/render-to-string.js.map +1 -0
  397. package/vendor/ink/build/render.d.ts +176 -0
  398. package/vendor/ink/build/render.js +71 -0
  399. package/vendor/ink/build/render.js.map +1 -0
  400. package/vendor/ink/build/renderer.d.ts +8 -0
  401. package/vendor/ink/build/renderer.js +64 -0
  402. package/vendor/ink/build/renderer.js.map +1 -0
  403. package/vendor/ink/build/sanitize-ansi.d.ts +2 -0
  404. package/vendor/ink/build/sanitize-ansi.js +27 -0
  405. package/vendor/ink/build/sanitize-ansi.js.map +1 -0
  406. package/vendor/ink/build/squash-text-nodes.d.ts +3 -0
  407. package/vendor/ink/build/squash-text-nodes.js +36 -0
  408. package/vendor/ink/build/squash-text-nodes.js.map +1 -0
  409. package/vendor/ink/build/styles.d.ts +302 -0
  410. package/vendor/ink/build/styles.js +303 -0
  411. package/vendor/ink/build/styles.js.map +1 -0
  412. package/vendor/ink/build/utils.d.ts +9 -0
  413. package/vendor/ink/build/utils.js +19 -0
  414. package/vendor/ink/build/utils.js.map +1 -0
  415. package/vendor/ink/build/wrap-text.d.ts +3 -0
  416. package/vendor/ink/build/wrap-text.js +38 -0
  417. package/vendor/ink/build/wrap-text.js.map +1 -0
  418. package/vendor/ink/build/write-synchronized.d.ts +4 -0
  419. package/vendor/ink/build/write-synchronized.js +9 -0
  420. package/vendor/ink/build/write-synchronized.js.map +1 -0
  421. package/vendor/ink/license +10 -0
  422. package/vendor/ink/package.json +137 -0
  423. package/.claude-plugin/marketplace.json +0 -34
  424. package/.claude-plugin/plugin.json +0 -20
  425. package/.gitattributes +0 -34
  426. package/.mcp.json +0 -14
  427. package/ARCHITECTURE.md +0 -77
  428. package/CHANGELOG.md +0 -30
  429. package/CONTRIBUTING.md +0 -45
  430. package/DATA-FLOW.md +0 -79
  431. package/LICENSE +0 -21
  432. package/SECURITY.md +0 -138
  433. package/UNINSTALL.md +0 -115
  434. package/agents/maintenance.md +0 -5
  435. package/agents/memory-classification.md +0 -30
  436. package/agents/scheduler-task.md +0 -18
  437. package/agents/webhook-handler.md +0 -27
  438. package/agents/worker.md +0 -24
  439. package/bin/bridge +0 -133
  440. package/bin/statusline-launcher.mjs +0 -82
  441. package/bin/statusline-lib.mjs +0 -581
  442. package/bin/statusline-route.mjs +0 -273
  443. package/bin/statusline.mjs +0 -638
  444. package/bun.lock +0 -927
  445. package/commands/config.md +0 -16
  446. package/commands/doctor.md +0 -13
  447. package/commands/model.md +0 -61
  448. package/commands/setup.md +0 -17
  449. package/defaults/hidden-roles.json +0 -68
  450. package/defaults/memory-chunk-prompt.md +0 -63
  451. package/defaults/mixdog-config.template.json +0 -27
  452. package/defaults/user-workflow.json +0 -8
  453. package/defaults/user-workflow.md +0 -17
  454. package/hooks/hooks.json +0 -73
  455. package/hooks/lib/active-instance.cjs +0 -77
  456. package/hooks/lib/permission-evaluator.cjs +0 -411
  457. package/hooks/lib/permission-route.cjs +0 -63
  458. package/hooks/lib/settings-loader.cjs +0 -117
  459. package/hooks/post-tool-use.cjs +0 -84
  460. package/hooks/pre-mcp-sandbox.cjs +0 -158
  461. package/hooks/pre-tool-subagent.cjs +0 -258
  462. package/hooks/session-start.cjs +0 -1493
  463. package/hooks/shim-launcher.cjs +0 -65
  464. package/hooks/turn-timer.cjs +0 -82
  465. package/lib/claude-md-writer.cjs +0 -386
  466. package/lib/keychain-cjs.cjs +0 -290
  467. package/lib/plugin-paths.cjs +0 -69
  468. package/lib/rules-builder.cjs +0 -241
  469. package/native/README.md +0 -117
  470. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  471. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  472. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  473. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  474. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  475. package/prompts/code-review.txt +0 -16
  476. package/prompts/security-audit.txt +0 -17
  477. package/rules/bridge/00-common.md +0 -39
  478. package/rules/bridge/20-skip-protocol.md +0 -18
  479. package/rules/bridge/30-explorer.md +0 -33
  480. package/rules/bridge/40-cycle1-agent.md +0 -52
  481. package/rules/bridge/41-cycle2-agent.md +0 -62
  482. package/rules/lead/00-tool-lead.md +0 -61
  483. package/rules/lead/01-general.md +0 -26
  484. package/rules/lead/02-channels.md +0 -49
  485. package/rules/lead/03-team.md +0 -27
  486. package/rules/lead/04-workflow.md +0 -20
  487. package/rules/shared/00-language.md +0 -14
  488. package/rules/shared/01-tool.md +0 -138
  489. package/scripts/bootstrap.mjs +0 -130
  490. package/scripts/bridge-unify-smoke.mjs +0 -308
  491. package/scripts/build-runtime-linux.sh +0 -348
  492. package/scripts/build-runtime-macos.sh +0 -217
  493. package/scripts/build-runtime-windows.ps1 +0 -242
  494. package/scripts/builtin-utils-smoke.mjs +0 -398
  495. package/scripts/bump.mjs +0 -80
  496. package/scripts/check-json.mjs +0 -45
  497. package/scripts/check-syntax-changed.mjs +0 -102
  498. package/scripts/check-syntax.mjs +0 -58
  499. package/scripts/code-graph-batch.test.mjs +0 -33
  500. package/scripts/config-preserve-smoke.mjs +0 -180
  501. package/scripts/doctor.mjs +0 -489
  502. package/scripts/edit-normalize-fuzz.mjs +0 -130
  503. package/scripts/edit-normalize-smoke.mjs +0 -401
  504. package/scripts/edit-operation-smoke.mjs +0 -369
  505. package/scripts/edit2-smoke.mjs +0 -63
  506. package/scripts/ensure-deps.mjs +0 -259
  507. package/scripts/fuzzy-e2e.mjs +0 -28
  508. package/scripts/fuzzy-smoke.mjs +0 -26
  509. package/scripts/gateway-model.mjs +0 -596
  510. package/scripts/generate-runtime-manifest.mjs +0 -166
  511. package/scripts/guard-smoke.mjs +0 -66
  512. package/scripts/hidden-role-schema-smoke.mjs +0 -162
  513. package/scripts/hook-routing-smoke.mjs +0 -29
  514. package/scripts/inject-input.ps1 +0 -204
  515. package/scripts/io-complex-smoke.mjs +0 -667
  516. package/scripts/io-explore-bench.mjs +0 -424
  517. package/scripts/io-guardrails-smoke.mjs +0 -205
  518. package/scripts/io-mini-bench-baseline.json +0 -11
  519. package/scripts/io-mini-bench.mjs +0 -216
  520. package/scripts/io-route-harness.mjs +0 -933
  521. package/scripts/io-telemetry-report.mjs +0 -691
  522. package/scripts/lib/gateway-inventory.mjs +0 -178
  523. package/scripts/lib/gateway-settings.mjs +0 -78
  524. package/scripts/mutation-bench.mjs +0 -564
  525. package/scripts/mutation-io-smoke.mjs +0 -1097
  526. package/scripts/native-patch-bridge-smoke.mjs +0 -288
  527. package/scripts/native-patch-smoke.mjs +0 -304
  528. package/scripts/openai-oauth-catalog-smoke.mjs +0 -53
  529. package/scripts/patch-interior-context-smoke.mjs +0 -49
  530. package/scripts/patch-newline-utf8-smoke.mjs +0 -157
  531. package/scripts/perf-hook-smoke.mjs +0 -71
  532. package/scripts/permission-eval-smoke.mjs +0 -443
  533. package/scripts/prep-patch.mjs +0 -53
  534. package/scripts/prep-shim.mjs +0 -96
  535. package/scripts/provider-cache-smoke.mjs +0 -687
  536. package/scripts/report-runtime-health.mjs +0 -132
  537. package/scripts/resolve-bun.mjs +0 -60
  538. package/scripts/run-mcp.mjs +0 -1473
  539. package/scripts/salvage-v4a-shatter.test.mjs +0 -58
  540. package/scripts/scoped-cache-io-smoke.mjs +0 -103
  541. package/scripts/shell-policy-round3-smoke.mjs +0 -46
  542. package/scripts/smoke-runtime-negative.ps1 +0 -100
  543. package/scripts/smoke-runtime-negative.sh +0 -95
  544. package/scripts/stall-policy-smoke.mjs +0 -50
  545. package/scripts/start-memory-worker.mjs +0 -23
  546. package/scripts/statusline-launcher-smoke.mjs +0 -235
  547. package/scripts/stress-atomic-write.mjs +0 -1028
  548. package/scripts/test-fault-inject.mjs +0 -164
  549. package/scripts/test-large-file.mjs +0 -174
  550. package/scripts/tool-edge-smoke.mjs +0 -209
  551. package/scripts/uninstall.mjs +0 -238
  552. package/scripts/webhook-selfheal-smoke.mjs +0 -27
  553. package/scripts/write-overwrite-guard-smoke.mjs +0 -56
  554. package/server-main.mjs +0 -3350
  555. package/server.mjs +0 -468
  556. package/setup/config-merge.mjs +0 -246
  557. package/setup/install.mjs +0 -574
  558. package/setup/launch-core.mjs +0 -617
  559. package/setup/launch.mjs +0 -101
  560. package/setup/locate-claude.mjs +0 -56
  561. package/setup/mixdog-cli.mjs +0 -122
  562. package/setup/setup-server.mjs +0 -3305
  563. package/setup/setup.html +0 -3740
  564. package/setup/tui.mjs +0 -325
  565. package/skills/retro-skill-proposer/SKILL.md +0 -92
  566. package/skills/schedule-add/SKILL.md +0 -77
  567. package/skills/setup/SKILL.md +0 -356
  568. package/skills/webhook-add/SKILL.md +0 -81
  569. package/src/agent/bridge-stall-watchdog.mjs +0 -337
  570. package/src/agent/index.mjs +0 -2229
  571. package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +0 -1010
  572. package/src/agent/orchestrator/bridge-retry.mjs +0 -220
  573. package/src/agent/orchestrator/bridge-trace.mjs +0 -601
  574. package/src/agent/orchestrator/cache-mtime.mjs +0 -58
  575. package/src/agent/orchestrator/config.mjs +0 -405
  576. package/src/agent/orchestrator/context/collect.mjs +0 -651
  577. package/src/agent/orchestrator/dispatch-persist.mjs +0 -549
  578. package/src/agent/orchestrator/drain-registry.mjs +0 -50
  579. package/src/agent/orchestrator/explore-validator.mjs +0 -8
  580. package/src/agent/orchestrator/internal-roles.mjs +0 -118
  581. package/src/agent/orchestrator/internal-tools.mjs +0 -88
  582. package/src/agent/orchestrator/jobs.mjs +0 -116
  583. package/src/agent/orchestrator/mcp/client.mjs +0 -364
  584. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +0 -1884
  585. package/src/agent/orchestrator/providers/anthropic.mjs +0 -598
  586. package/src/agent/orchestrator/providers/gemini.mjs +0 -1530
  587. package/src/agent/orchestrator/providers/grok-oauth.mjs +0 -779
  588. package/src/agent/orchestrator/providers/model-catalog.mjs +0 -374
  589. package/src/agent/orchestrator/providers/openai-compat-stream.mjs +0 -366
  590. package/src/agent/orchestrator/providers/openai-compat.mjs +0 -1511
  591. package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -1891
  592. package/src/agent/orchestrator/providers/openai-oauth.mjs +0 -1456
  593. package/src/agent/orchestrator/providers/openai-ws.mjs +0 -127
  594. package/src/agent/orchestrator/providers/registry.mjs +0 -192
  595. package/src/agent/orchestrator/providers/retry-classifier.mjs +0 -325
  596. package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +0 -142
  597. package/src/agent/orchestrator/session/cache/read-cache.mjs +0 -319
  598. package/src/agent/orchestrator/session/cache/scoped-cache.mjs +0 -361
  599. package/src/agent/orchestrator/session/loop.mjs +0 -1619
  600. package/src/agent/orchestrator/session/manager.mjs +0 -1991
  601. package/src/agent/orchestrator/session/result-classification.mjs +0 -65
  602. package/src/agent/orchestrator/session/store.mjs +0 -632
  603. package/src/agent/orchestrator/session/stream-watchdog.mjs +0 -130
  604. package/src/agent/orchestrator/session/tool-result-offload.mjs +0 -166
  605. package/src/agent/orchestrator/session/trim.mjs +0 -491
  606. package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +0 -115
  607. package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +0 -331
  608. package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +0 -150
  609. package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +0 -228
  610. package/src/agent/orchestrator/smart-bridge/index.mjs +0 -215
  611. package/src/agent/orchestrator/smart-bridge/profiles.mjs +0 -37
  612. package/src/agent/orchestrator/smart-bridge/registry.mjs +0 -348
  613. package/src/agent/orchestrator/stall-policy.mjs +0 -201
  614. package/src/agent/orchestrator/tool-loop-guard.mjs +0 -75
  615. package/src/agent/orchestrator/tools/bash-session.mjs +0 -722
  616. package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +0 -511
  617. package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +0 -480
  618. package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +0 -256
  619. package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +0 -58
  620. package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +0 -240
  621. package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +0 -113
  622. package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +0 -74
  623. package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +0 -242
  624. package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +0 -211
  625. package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +0 -1364
  626. package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +0 -126
  627. package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +0 -141
  628. package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +0 -194
  629. package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +0 -60
  630. package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +0 -168
  631. package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +0 -173
  632. package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +0 -48
  633. package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -99
  634. package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +0 -193
  635. package/src/agent/orchestrator/tools/builtin/list-tool.mjs +0 -597
  636. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  637. package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +0 -300
  638. package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +0 -152
  639. package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +0 -118
  640. package/src/agent/orchestrator/tools/builtin/read-open.mjs +0 -190
  641. package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +0 -728
  642. package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +0 -173
  643. package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +0 -602
  644. package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +0 -196
  645. package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +0 -422
  646. package/src/agent/orchestrator/tools/builtin/search-builders.mjs +0 -158
  647. package/src/agent/orchestrator/tools/builtin/search-tool.mjs +0 -869
  648. package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +0 -962
  649. package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +0 -223
  650. package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +0 -206
  651. package/src/agent/orchestrator/tools/builtin/write-tool.mjs +0 -401
  652. package/src/agent/orchestrator/tools/builtin.mjs +0 -503
  653. package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +0 -24
  654. package/src/agent/orchestrator/tools/code-graph.mjs +0 -4095
  655. package/src/agent/orchestrator/tools/cwd-tool.mjs +0 -298
  656. package/src/agent/orchestrator/tools/destructive-warning.mjs +0 -323
  657. package/src/agent/orchestrator/tools/edit-normalize.mjs +0 -603
  658. package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +0 -154
  659. package/src/agent/orchestrator/tools/graph-manifest.json +0 -26
  660. package/src/agent/orchestrator/tools/host-input.mjs +0 -204
  661. package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +0 -143
  662. package/src/agent/orchestrator/tools/patch-manifest.json +0 -26
  663. package/src/agent/orchestrator/tools/patch-tool-defs.mjs +0 -20
  664. package/src/agent/orchestrator/tools/patch.mjs +0 -2754
  665. package/src/agent/orchestrator/tools/progress-message.mjs +0 -118
  666. package/src/agent/orchestrator/tools/shell-command.mjs +0 -865
  667. package/src/agent/orchestrator/tools/shell-policy-imports.mjs +0 -7
  668. package/src/agent/orchestrator/tools/shell-snapshot.mjs +0 -313
  669. package/src/agent/orchestrator/workflow-store.mjs +0 -93
  670. package/src/agent/tool-defs.mjs +0 -110
  671. package/src/channels/backends/discord.mjs +0 -784
  672. package/src/channels/data/voice-runtime-manifest.json +0 -138
  673. package/src/channels/index.mjs +0 -3268
  674. package/src/channels/lib/config.mjs +0 -292
  675. package/src/channels/lib/drop-trace.mjs +0 -71
  676. package/src/channels/lib/event-pipeline.mjs +0 -81
  677. package/src/channels/lib/holidays.mjs +0 -138
  678. package/src/channels/lib/hook-pipe-server.mjs +0 -822
  679. package/src/channels/lib/output-forwarder.mjs +0 -765
  680. package/src/channels/lib/runtime-paths.mjs +0 -552
  681. package/src/channels/lib/scheduler.mjs +0 -723
  682. package/src/channels/lib/session-discovery.mjs +0 -103
  683. package/src/channels/lib/state-file.mjs +0 -68
  684. package/src/channels/lib/status-snapshot.mjs +0 -219
  685. package/src/channels/lib/tool-format.mjs +0 -140
  686. package/src/channels/lib/transcript-discovery.mjs +0 -195
  687. package/src/channels/lib/voice-runtime-fetcher.mjs +0 -734
  688. package/src/channels/lib/webhook.mjs +0 -1318
  689. package/src/channels/tool-defs.mjs +0 -170
  690. package/src/daemon/host.mjs +0 -118
  691. package/src/daemon/mcp-transport.mjs +0 -47
  692. package/src/daemon/session.mjs +0 -100
  693. package/src/daemon/thin-client.mjs +0 -71
  694. package/src/daemon/transport.mjs +0 -163
  695. package/src/gateway/claude-current.mjs +0 -255
  696. package/src/gateway/oauth-usage.mjs +0 -598
  697. package/src/gateway/route-meta.mjs +0 -629
  698. package/src/gateway/server.mjs +0 -713
  699. package/src/memory/data/runtime-manifest.json +0 -40
  700. package/src/memory/index.mjs +0 -3332
  701. package/src/memory/lib/core-memory-store.mjs +0 -330
  702. package/src/memory/lib/embedding-provider.mjs +0 -269
  703. package/src/memory/lib/embedding-worker.mjs +0 -323
  704. package/src/memory/lib/memory-cycle1.mjs +0 -645
  705. package/src/memory/lib/memory-cycle2.mjs +0 -1284
  706. package/src/memory/lib/memory-cycle3.mjs +0 -540
  707. package/src/memory/lib/memory-embed.mjs +0 -299
  708. package/src/memory/lib/memory-ops-policy.mjs +0 -190
  709. package/src/memory/lib/memory-recall-store.mjs +0 -638
  710. package/src/memory/lib/memory.mjs +0 -412
  711. package/src/memory/lib/pg/adapter.mjs +0 -308
  712. package/src/memory/lib/pg/process.mjs +0 -360
  713. package/src/memory/lib/pg/supervisor.mjs +0 -396
  714. package/src/memory/lib/runtime-fetcher.mjs +0 -458
  715. package/src/memory/lib/trace-store.mjs +0 -728
  716. package/src/memory/tool-defs.mjs +0 -79
  717. package/src/search/index.mjs +0 -1173
  718. package/src/search/lib/backends/anthropic-oauth.mjs +0 -98
  719. package/src/search/lib/backends/exa.mjs +0 -50
  720. package/src/search/lib/backends/firecrawl.mjs +0 -61
  721. package/src/search/lib/backends/gemini-api.mjs +0 -83
  722. package/src/search/lib/backends/grok-oauth.mjs +0 -86
  723. package/src/search/lib/backends/index.mjs +0 -150
  724. package/src/search/lib/backends/openai-api.mjs +0 -144
  725. package/src/search/lib/backends/openai-oauth.mjs +0 -102
  726. package/src/search/lib/backends/openai-web-search.mjs +0 -76
  727. package/src/search/lib/backends/tavily.mjs +0 -55
  728. package/src/search/lib/backends/xai-api.mjs +0 -113
  729. package/src/search/lib/config.mjs +0 -192
  730. package/src/search/lib/provider-usage.mjs +0 -67
  731. package/src/search/lib/providers.mjs +0 -47
  732. package/src/search/lib/search-intent.mjs +0 -109
  733. package/src/search/lib/setup-handler.mjs +0 -261
  734. package/src/search/lib/web-tools.mjs +0 -1219
  735. package/src/search/tool-defs.mjs +0 -83
  736. package/src/setup/defender-exclusion.mjs +0 -183
  737. package/src/shared/atomic-file.mjs +0 -436
  738. package/src/shared/config.mjs +0 -372
  739. package/src/shared/daemon-recycle.mjs +0 -108
  740. package/src/shared/disable-claude-builtins.mjs +0 -91
  741. package/src/shared/err-text.mjs +0 -12
  742. package/src/shared/llm/http-agent.mjs +0 -123
  743. package/src/shared/open-url.mjs +0 -62
  744. package/src/shared/plugin-paths.mjs +0 -58
  745. package/src/shared/schedules-store.mjs +0 -70
  746. package/src/shared/seed.mjs +0 -161
  747. package/src/shared/user-cwd.mjs +0 -225
  748. package/src/shared/user-data-guard.mjs +0 -244
  749. package/src/status/aggregator.mjs +0 -584
  750. package/src/status/server.mjs +0 -413
  751. package/tools.json +0 -1671
  752. /package/{defaults → src/defaults}/cycle3-review-prompt.md +0 -0
  753. /package/{defaults → src/defaults}/memory-promote-prompt.md +0 -0
  754. /package/{hooks → src/hooks}/lib/permission-rules.cjs +0 -0
  755. /package/{lib → src/lib}/config-cjs.cjs +0 -0
  756. /package/{lib → src/lib}/hook-pipe-path.cjs +0 -0
  757. /package/{lib → src/lib}/mixdog-debug.cjs +0 -0
  758. /package/{lib → src/lib}/text-utils.cjs +0 -0
  759. /package/{rules → src/rules}/bridge/42-cycle3-agent.md +0 -0
  760. /package/src/{agent → runtime/agent}/orchestrator/activity-bus.mjs +0 -0
  761. /package/src/{agent → runtime/agent}/orchestrator/providers/anthropic-betas.mjs +0 -0
  762. /package/src/{agent → runtime/agent}/orchestrator/session/abort-lookup.mjs +0 -0
  763. /package/src/{agent → runtime/agent}/orchestrator/session/cache/post-edit-marks.mjs +0 -0
  764. /package/src/{agent → runtime/agent}/orchestrator/session/cache/scoped-cache-outcome.mjs +0 -0
  765. /package/src/{agent → runtime/agent}/orchestrator/session/cache/util.mjs +0 -0
  766. /package/src/{agent → runtime/agent}/orchestrator/session/read-dedup.mjs +0 -0
  767. /package/src/{agent → runtime/agent}/orchestrator/session/save-session-worker.mjs +0 -0
  768. /package/src/{agent → runtime/agent}/orchestrator/smart-bridge/session-builder.mjs +0 -0
  769. /package/src/{agent → runtime/agent}/orchestrator/tools/bash-policy-scan.mjs +0 -0
  770. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/advisory-lock.mjs +0 -0
  771. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/atomic-write.mjs +0 -0
  772. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/binary-file.mjs +0 -0
  773. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/cache-layers.mjs +0 -0
  774. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/cwd-utils.mjs +0 -0
  775. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/device-paths.mjs +0 -0
  776. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -0
  777. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/diff-utils.mjs +0 -0
  778. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/fs-reachability.mjs +0 -0
  779. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/grep-formatting.mjs +0 -0
  780. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/hash-utils.mjs +0 -0
  781. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/list-formatting.mjs +0 -0
  782. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/open-config-tool.mjs +0 -0
  783. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/path-locks.mjs +0 -0
  784. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/path-utils.mjs +0 -0
  785. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-args.mjs +0 -0
  786. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-batch.mjs +0 -0
  787. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-constants.mjs +0 -0
  788. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-image-resize.mjs +0 -0
  789. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-image.mjs +0 -0
  790. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-lines.mjs +0 -0
  791. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-mode-tool.mjs +0 -0
  792. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-range-index.mjs +0 -0
  793. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-ranges.mjs +0 -0
  794. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-special-files.mjs +0 -0
  795. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-tool.mjs +0 -0
  796. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-windows.mjs +0 -0
  797. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/shell-analysis.mjs +0 -0
  798. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/shell-output.mjs +0 -0
  799. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/snapshot-helpers.mjs +0 -0
  800. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/snapshot-validation.mjs +0 -0
  801. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/text-stats.mjs +0 -0
  802. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/windows-roots.mjs +0 -0
  803. /package/src/{agent → runtime/agent}/orchestrator/tools/code-graph-prewarm-worker.mjs +0 -0
  804. /package/src/{agent → runtime/agent}/orchestrator/tools/env-scrub.mjs +0 -0
  805. /package/src/{agent → runtime/agent}/orchestrator/tools/mutation-content-cache.mjs +0 -0
  806. /package/src/{agent → runtime/agent}/orchestrator/tools/mutation-planner.mjs +0 -0
  807. /package/src/{agent → runtime/agent}/orchestrator/tools/next-call-utils.mjs +0 -0
  808. /package/src/{agent → runtime/agent}/orchestrator/tools/result-compression.mjs +0 -0
  809. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-exec-policy.mjs +0 -0
  810. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-policy-danger-target.mjs +0 -0
  811. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-policy.mjs +0 -0
  812. /package/src/{channels → runtime/channels}/lib/cli-worker-host.mjs +0 -0
  813. /package/src/{channels → runtime/channels}/lib/config-lock.mjs +0 -0
  814. /package/src/{channels → runtime/channels}/lib/event-queue.mjs +0 -0
  815. /package/src/{channels → runtime/channels}/lib/executor.mjs +0 -0
  816. /package/src/{channels → runtime/channels}/lib/format.mjs +0 -0
  817. /package/src/{channels → runtime/channels}/lib/interaction-workflows.mjs +0 -0
  818. /package/src/{channels → runtime/channels}/lib/memory-client.mjs +0 -0
  819. /package/src/{channels → runtime/channels}/lib/session-control.mjs +0 -0
  820. /package/src/{channels → runtime/channels}/lib/settings.mjs +0 -0
  821. /package/src/{channels → runtime/channels}/lib/whisper-server.mjs +0 -0
  822. /package/src/{memory → runtime/memory}/lib/agent-ipc.mjs +0 -0
  823. /package/src/{memory → runtime/memory}/lib/bridge-trace-queries.mjs +0 -0
  824. /package/src/{memory → runtime/memory}/lib/llm-worker-host.mjs +0 -0
  825. /package/src/{memory → runtime/memory}/lib/memory-cycle.mjs +0 -0
  826. /package/src/{memory → runtime/memory}/lib/memory-extraction.mjs +0 -0
  827. /package/src/{memory → runtime/memory}/lib/memory-maintenance-store.mjs +0 -0
  828. /package/src/{memory → runtime/memory}/lib/memory-recall-id-patch.mjs +0 -0
  829. /package/src/{memory → runtime/memory}/lib/memory-recall-read-query.mjs +0 -0
  830. /package/src/{memory → runtime/memory}/lib/memory-recall-scope-filter.mjs +0 -0
  831. /package/src/{memory → runtime/memory}/lib/memory-retrievers.mjs +0 -0
  832. /package/src/{memory → runtime/memory}/lib/memory-score.mjs +0 -0
  833. /package/src/{memory → runtime/memory}/lib/memory-text-utils.mjs +0 -0
  834. /package/src/{memory → runtime/memory}/lib/model-profile.mjs +0 -0
  835. /package/src/{memory → runtime/memory}/lib/project-id-resolver.mjs +0 -0
  836. /package/src/{search → runtime/search}/lib/cache.mjs +0 -0
  837. /package/src/{search → runtime/search}/lib/formatter.mjs +0 -0
  838. /package/src/{search → runtime/search}/lib/state.mjs +0 -0
  839. /package/src/{shared → runtime/shared}/abort-controller.mjs +0 -0
  840. /package/src/{shared → runtime/shared}/llm/cost.mjs +0 -0
  841. /package/src/{shared → runtime/shared}/llm/index.mjs +0 -0
  842. /package/src/{shared → runtime/shared}/llm/pid-cleanup.mjs +0 -0
  843. /package/src/{shared → runtime/shared}/llm/usage-log.mjs +0 -0
  844. /package/src/{shared → runtime/shared}/wsl.mjs +0 -0
@@ -0,0 +1,2587 @@
1
+ /**
2
+ * OpenAI Codex OAuth — WebSocket transport.
3
+ *
4
+ * Single dispatch path for the openai-oauth provider (SSE removed in
5
+ * v0.6.117). Uses the `responses_websockets=2026-02-06` beta WebSocket
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
8
+ * key) so subsequent tool-loop iterations can send only the incremental
9
+ * `input` delta plus `previous_response_id`, skipping the full
10
+ * tools/system/history prefix each turn.
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).
16
+ *
17
+ * Exposes:
18
+ * sendViaWebSocket({ auth, body, sendOpts, onStreamDelta, onToolCall,
19
+ * onStageChange, externalSignal, poolKey, cacheKey, iteration,
20
+ * useModel, traceCtx })
21
+ *
22
+ * The caller (openai-oauth.mjs) supplies a fully built request body and the
23
+ * auth bundle; this module handles connection caching, delta framing, event
24
+ * parsing, and tracing.
25
+ */
26
+ import WebSocket from 'ws';
27
+ import { errText } from '../../../shared/err-text.mjs';
28
+ import { createHash, randomBytes } from 'crypto';
29
+ import {
30
+ extractCachedTokens,
31
+ traceBridgeFetch,
32
+ traceBridgeSse,
33
+ traceBridgeUsage,
34
+ appendBridgeTrace,
35
+ } from '../bridge-trace.mjs';
36
+ import { jitterDelayMs, populateHttpStatusFromMessage } from './retry-classifier.mjs';
37
+ import {
38
+ PROVIDER_RETRY_MAX_ATTEMPTS,
39
+ PROVIDER_WS_ACQUIRE_TIMEOUT_MS,
40
+ PROVIDER_WS_FIRST_MEANINGFUL_TIMEOUT_MS,
41
+ PROVIDER_WS_HANDSHAKE_TIMEOUT_MS,
42
+ PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS,
43
+ } from '../stall-policy.mjs';
44
+
45
+ globalThis.__mixdogOpenaiWsRuntimeLoaded = true;
46
+
47
+ const CODEX_WS_URL = 'wss://chatgpt.com/backend-api/codex/responses';
48
+ const CODEX_OAUTH_ORIGINATOR = 'codex_cli_rs';
49
+ const OPENAI_WS_URL = 'wss://api.openai.com/v1/responses';
50
+ const XAI_WS_URL = 'wss://api.x.ai/v1/responses';
51
+ const WS_IDLE_MS = 5 * 60_000;
52
+ const WS_HANDSHAKE_TIMEOUT_MS = PROVIDER_WS_HANDSHAKE_TIMEOUT_MS;
53
+ const WS_ACQUIRE_TIMEOUT_MS = PROVIDER_WS_ACQUIRE_TIMEOUT_MS;
54
+ // Pre-stream watchdog uses the shared provider deadline so it fails before
55
+ // the 5-minute session slow warning.
56
+ const WS_FIRST_MEANINGFUL_MS = PROVIDER_WS_FIRST_MEANINGFUL_TIMEOUT_MS;
57
+ // Pre-`response.created` deadline. Once the socket is open and the
58
+ // response.create frame is sent, a healthy server emits response.created
59
+ // within seconds. If it stalls past this short bound the socket has wedged
60
+ // post-upgrade with zero server events — treat it as a fast, retryable
61
+ // first-byte timeout rather than waiting the longer first-meaningful window.
62
+ // Only this short window is shortened; the post-`response.created`
63
+ // inter-chunk / reasoning span keeps the longer deadlines below.
64
+ const WS_PRE_RESPONSE_CREATED_MS = (() => {
65
+ const raw = process.env.MIXDOG_PROVIDER_WS_PRE_RESPONSE_CREATED_TIMEOUT_MS;
66
+ const n = Number(raw);
67
+ if (Number.isFinite(n) && n > 0) return Math.min(Math.max(n, 1_000), 120_000);
68
+ return 10_000;
69
+ })();
70
+ // Inter-chunk inactivity after first meaningful output.
71
+ const WS_INTER_CHUNK_MS = PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS;
72
+ const MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT = 2;
73
+ const MIDSTREAM_DEFAULT_RETRY_LIMIT = 1;
74
+ const MIDSTREAM_BACKOFF_MS = [250, 1000];
75
+
76
+ // Handshake retry policy. The `ws` library surfaces a bare
77
+ // `Opening handshake has timed out` Error after handshakeTimeout; transient
78
+ // network blips (DNS, reset, 5xx) similarly produce single-shot failures that
79
+ // waste the caller's turn when they'd succeed on retry. We wrap the acquire
80
+ // step with bounded exponential backoff. Permanent auth/quota (4xx) must NOT
81
+ // retry because a second attempt will hit the same deterministic server
82
+ // decision and just double the user-visible latency.
83
+ // Aligned to the cross-provider default (retry-classifier DEFAULT_MAX_ATTEMPTS=5,
84
+ // anthropic-oauth MAX_ATTEMPTS=5, withRetry-using providers all default to 5).
85
+ // Previously 3 — bumped for parity so every provider exhausts the same number
86
+ // of transient-5xx attempts before surfacing failure to the caller.
87
+ const HANDSHAKE_MAX_ATTEMPTS = PROVIDER_RETRY_MAX_ATTEMPTS;
88
+ const HANDSHAKE_BACKOFF_BASE_MS = 500;
89
+ const HANDSHAKE_BACKOFF_CAP_MS = 5000;
90
+ // WS socket pool buckets are keyed by `poolKey` (the per-call sessionId)
91
+ // to isolate parallel bridge invocations — each gets its own socket so
92
+ // a second caller cannot grab a sibling's mid-turn entry (Codex would
93
+ // otherwise reject the new response.create with "No tool output found
94
+ // for function call ..."). The Codex handshake `session_id` header/URL
95
+ // uses `cacheKey` — a prefix-scoped cache key derived from the configured
96
+ // provider namespace plus model/system/tools hash. Same-prefix sessions share
97
+ // server-side prompt cache, while unrelated main/worker prefixes no longer
98
+ // evict each other inside one static provider lane. Codex dedupes cache by
99
+ // handshake session_id, not by body.prompt_cache_key alone (measured
100
+ // 2026-04-19 after the v0.6.151 regression).
101
+ const MAX_POOLED_SOCKETS_PER_KEY = 8;
102
+
103
+ // poolKey -> Entry[]
104
+ // Entry: { socket, busy, idleTimer, lastResponseId, lastRequestSansInput,
105
+ // lastRequestInput, lastResponseItems, lastInputLen, turnState,
106
+ // closing, ephemeral }
107
+ const _wsPool = new Map();
108
+
109
+ // Final prompt_cache_key/session_id lane guard for OpenAI/Codex transports.
110
+ // The provider code may shard one logical prefix into N cache keys for 10+
111
+ // total parallelism; inside each final key we still serialize requests because
112
+ // live Codex probes show same-key concurrent WebSockets can randomly miss the
113
+ // server prompt cache even after warm-up.
114
+ const _openAiPromptCacheLanes = new Map();
115
+ const _openAiPromptCacheLaneRates = new Map();
116
+ const OPENAI_PROMPT_CACHE_LANE_RATE_WINDOW_MS = 60_000;
117
+ const OPENAI_PROMPT_CACHE_LANE_RATE_MAX_KEYS = 4096;
118
+
119
+ function _positiveInt(value, fallback = 0) {
120
+ const n = Number(value);
121
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
122
+ }
123
+
124
+ function _cacheLaneHash(value) {
125
+ return createHash('sha256').update(String(value || '')).digest('hex').slice(0, 12);
126
+ }
127
+
128
+ function _openAiPromptCacheLaneMaxInFlight(sendOpts = {}) {
129
+ return _positiveInt(
130
+ sendOpts?.openaiCacheLaneMaxInFlight
131
+ ?? sendOpts?.promptCacheLaneMaxInFlight
132
+ ?? process.env.MIXDOG_OPENAI_CACHE_LANE_MAX_INFLIGHT
133
+ ?? process.env.MIXDOG_OPENAI_CACHE_MAX_INFLIGHT,
134
+ 1,
135
+ );
136
+ }
137
+
138
+ function _openAiPromptCacheLaneQueueTimeoutMs(sendOpts = {}) {
139
+ return _positiveInt(
140
+ sendOpts?.openaiCacheLaneQueueTimeoutMs
141
+ ?? sendOpts?.promptCacheLaneQueueTimeoutMs
142
+ ?? process.env.MIXDOG_OPENAI_CACHE_LANE_QUEUE_TIMEOUT_MS
143
+ ?? process.env.MIXDOG_OPENAI_CACHE_QUEUE_TIMEOUT_MS,
144
+ 0,
145
+ );
146
+ }
147
+
148
+ function _openAiPromptCacheLaneRateLimitPerMin(sendOpts = {}) {
149
+ return _positiveInt(
150
+ sendOpts?.openaiCacheLaneRateLimitPerMin
151
+ ?? sendOpts?.promptCacheLaneRateLimitPerMin
152
+ ?? process.env.MIXDOG_OPENAI_CACHE_LANE_RPM
153
+ ?? process.env.MIXDOG_OPENAI_CACHE_KEY_RPM,
154
+ 12,
155
+ );
156
+ }
157
+
158
+ function _isOpenAiPromptCacheLaneAuth(auth) {
159
+ return auth?.type !== 'xai';
160
+ }
161
+
162
+ function _getOpenAiPromptCacheLaneState(key, maxInFlight) {
163
+ let state = _openAiPromptCacheLanes.get(key);
164
+ if (!state) {
165
+ state = { key, active: 0, queue: [], maxInFlight, nextId: 0 };
166
+ _openAiPromptCacheLanes.set(key, state);
167
+ }
168
+ state.maxInFlight = maxInFlight;
169
+ return state;
170
+ }
171
+
172
+ function _cleanupOpenAiPromptCacheLane(state) {
173
+ if (state.active === 0 && state.queue.length === 0) {
174
+ _openAiPromptCacheLanes.delete(state.key);
175
+ }
176
+ }
177
+
178
+ function _getOpenAiPromptCacheLaneRateState(key) {
179
+ let state = _openAiPromptCacheLaneRates.get(key);
180
+ if (!state) {
181
+ state = { key, starts: [], lastUsedAt: Date.now() };
182
+ _openAiPromptCacheLaneRates.set(key, state);
183
+ }
184
+ state.lastUsedAt = Date.now();
185
+ if (_openAiPromptCacheLaneRates.size > OPENAI_PROMPT_CACHE_LANE_RATE_MAX_KEYS) {
186
+ let oldestKey = null;
187
+ let oldestAt = Infinity;
188
+ for (const [k, v] of _openAiPromptCacheLaneRates) {
189
+ if ((v?.lastUsedAt || 0) < oldestAt) {
190
+ oldestAt = v.lastUsedAt || 0;
191
+ oldestKey = k;
192
+ }
193
+ }
194
+ if (oldestKey) _openAiPromptCacheLaneRates.delete(oldestKey);
195
+ }
196
+ return state;
197
+ }
198
+
199
+ function _pruneOpenAiPromptCacheLaneRateState(state, now = Date.now()) {
200
+ const cutoff = now - OPENAI_PROMPT_CACHE_LANE_RATE_WINDOW_MS;
201
+ while (state.starts.length > 0 && state.starts[0] <= cutoff) state.starts.shift();
202
+ state.lastUsedAt = now;
203
+ }
204
+
205
+ function _sleepWithSignal(ms, signal) {
206
+ if (ms <= 0) return Promise.resolve();
207
+ return new Promise((resolve, reject) => {
208
+ let timer = null;
209
+ let abortListener = null;
210
+ const cleanup = () => {
211
+ if (timer) clearTimeout(timer);
212
+ if (signal && abortListener) signal.removeEventListener('abort', abortListener);
213
+ };
214
+ abortListener = () => {
215
+ cleanup();
216
+ const reason = signal?.reason;
217
+ reject(reason instanceof Error ? reason : new Error('OpenAI prompt cache lane rate wait aborted'));
218
+ };
219
+ if (signal?.aborted) {
220
+ abortListener();
221
+ return;
222
+ }
223
+ if (signal) signal.addEventListener('abort', abortListener, { once: true });
224
+ timer = setTimeout(() => {
225
+ cleanup();
226
+ resolve();
227
+ }, ms);
228
+ timer.unref?.();
229
+ });
230
+ }
231
+
232
+ async function _reserveOpenAiPromptCacheLaneRate({ key, limitPerMin, signal }) {
233
+ if (!limitPerMin || limitPerMin <= 0) {
234
+ return { rateLimitPerMin: 0, rateWaitMs: 0, rateWindowCount: 0 };
235
+ }
236
+ const state = _getOpenAiPromptCacheLaneRateState(key);
237
+ const startedAt = Date.now();
238
+ while (true) {
239
+ const now = Date.now();
240
+ _pruneOpenAiPromptCacheLaneRateState(state, now);
241
+ if (state.starts.length < limitPerMin) {
242
+ state.starts.push(now);
243
+ return {
244
+ rateLimitPerMin: limitPerMin,
245
+ rateWaitMs: now - startedAt,
246
+ rateWindowCount: state.starts.length,
247
+ };
248
+ }
249
+ const waitMs = Math.max(25, state.starts[0] + OPENAI_PROMPT_CACHE_LANE_RATE_WINDOW_MS - now);
250
+ await _sleepWithSignal(waitMs, signal);
251
+ }
252
+ }
253
+
254
+ function _removeQueuedOpenAiPromptCacheLaneRequest(state, request) {
255
+ const index = state.queue.indexOf(request);
256
+ if (index >= 0) state.queue.splice(index, 1);
257
+ _cleanupOpenAiPromptCacheLane(state);
258
+ }
259
+
260
+ function _releaseOpenAiPromptCacheLane(state) {
261
+ state.active = Math.max(0, state.active - 1);
262
+ while (state.queue.length > 0 && state.active < state.maxInFlight) {
263
+ const next = state.queue.shift();
264
+ next.cleanup?.();
265
+ state.active += 1;
266
+ next.resolve(_makeOpenAiPromptCacheLaneHandle(state, next.requestId, next.enqueuedAt, true));
267
+ }
268
+ _cleanupOpenAiPromptCacheLane(state);
269
+ }
270
+
271
+ function _makeOpenAiPromptCacheLaneHandle(state, requestId, enqueuedAt, queued) {
272
+ let released = false;
273
+ return {
274
+ requestId,
275
+ queued,
276
+ waitedMs: Date.now() - enqueuedAt,
277
+ activeCount: state.active,
278
+ queueDepth: state.queue.length,
279
+ release() {
280
+ if (released) return;
281
+ released = true;
282
+ _releaseOpenAiPromptCacheLane(state);
283
+ },
284
+ };
285
+ }
286
+
287
+ function _acquireOpenAiPromptCacheLane({ key, maxInFlight, signal, timeoutMs }) {
288
+ const state = _getOpenAiPromptCacheLaneState(key, maxInFlight);
289
+ const requestId = ++state.nextId;
290
+ const enqueuedAt = Date.now();
291
+ if (state.active < state.maxInFlight) {
292
+ state.active += 1;
293
+ return Promise.resolve(_makeOpenAiPromptCacheLaneHandle(state, requestId, enqueuedAt, false));
294
+ }
295
+ return new Promise((resolve, reject) => {
296
+ const request = { requestId, enqueuedAt, resolve, reject, cleanup: null, timer: null, abortListener: null };
297
+ const cleanup = () => {
298
+ if (request.timer) clearTimeout(request.timer);
299
+ if (signal && request.abortListener) signal.removeEventListener('abort', request.abortListener);
300
+ };
301
+ request.cleanup = cleanup;
302
+ request.abortListener = () => {
303
+ cleanup();
304
+ _removeQueuedOpenAiPromptCacheLaneRequest(state, request);
305
+ const reason = signal?.reason;
306
+ reject(reason instanceof Error ? reason : new Error('OpenAI prompt cache lane wait aborted'));
307
+ };
308
+ if (signal?.aborted) {
309
+ request.abortListener();
310
+ return;
311
+ }
312
+ if (signal) signal.addEventListener('abort', request.abortListener, { once: true });
313
+ if (timeoutMs > 0) {
314
+ request.timer = setTimeout(() => {
315
+ cleanup();
316
+ _removeQueuedOpenAiPromptCacheLaneRequest(state, request);
317
+ reject(new Error(`OpenAI prompt cache lane wait timed out after ${timeoutMs}ms`));
318
+ }, timeoutMs);
319
+ request.timer.unref?.();
320
+ }
321
+ state.queue.push(request);
322
+ });
323
+ }
324
+
325
+ async function _withOpenAiPromptCacheLane({ auth, cacheKey, sendOpts, poolKey, iteration, traceProvider, useModel, externalSignal }, fn) {
326
+ if (!_isOpenAiPromptCacheLaneAuth(auth) || !cacheKey) {
327
+ return await fn({ enabled: false, maxInFlight: 0 });
328
+ }
329
+ const maxInFlight = _openAiPromptCacheLaneMaxInFlight(sendOpts);
330
+ if (maxInFlight <= 0) {
331
+ return await fn({ enabled: false, maxInFlight: 0 });
332
+ }
333
+ const laneKey = `openai-prompt:${traceProvider || 'openai'}:${useModel || 'default'}:${cacheKey}`;
334
+ const laneKeyHash = _cacheLaneHash(laneKey);
335
+ const rateMeta = await _reserveOpenAiPromptCacheLaneRate({
336
+ key: laneKey,
337
+ limitPerMin: _openAiPromptCacheLaneRateLimitPerMin(sendOpts),
338
+ signal: externalSignal,
339
+ });
340
+ if (rateMeta.rateWaitMs > 0) {
341
+ appendBridgeTrace({
342
+ sessionId: poolKey,
343
+ iteration,
344
+ kind: 'cache_lane',
345
+ provider: traceProvider,
346
+ model: useModel,
347
+ event: 'rate_wait',
348
+ lane_key_hash: laneKeyHash,
349
+ rate_limit_per_min: rateMeta.rateLimitPerMin,
350
+ rate_wait_ms: rateMeta.rateWaitMs,
351
+ rate_window_count: rateMeta.rateWindowCount,
352
+ });
353
+ }
354
+ const state = _getOpenAiPromptCacheLaneState(laneKey, maxInFlight);
355
+ const queued = state.active >= state.maxInFlight;
356
+ if (queued) {
357
+ appendBridgeTrace({
358
+ sessionId: poolKey,
359
+ iteration,
360
+ kind: 'cache_lane',
361
+ provider: traceProvider,
362
+ model: useModel,
363
+ event: 'queued',
364
+ lane_key_hash: laneKeyHash,
365
+ max_in_flight: maxInFlight,
366
+ active: state.active,
367
+ queue_depth: state.queue.length,
368
+ });
369
+ }
370
+ const timeoutMs = _openAiPromptCacheLaneQueueTimeoutMs(sendOpts);
371
+ const handle = await _acquireOpenAiPromptCacheLane({ key: laneKey, maxInFlight, signal: externalSignal, timeoutMs });
372
+ const laneMeta = {
373
+ enabled: true,
374
+ laneKeyHash,
375
+ maxInFlight,
376
+ rateLimitPerMin: rateMeta.rateLimitPerMin,
377
+ rateWaitMs: rateMeta.rateWaitMs,
378
+ rateWindowCount: rateMeta.rateWindowCount,
379
+ queued: queued || handle.queued === true,
380
+ waitMs: handle.waitedMs,
381
+ activeAfterAcquire: handle.activeCount,
382
+ queueDepthAfterAcquire: handle.queueDepth,
383
+ };
384
+ try {
385
+ return await fn(laneMeta);
386
+ } finally {
387
+ handle.release();
388
+ }
389
+ }
390
+
391
+ function _getPoolArr(poolKey) {
392
+ if (!poolKey) return null;
393
+ let arr = _wsPool.get(poolKey);
394
+ if (!arr) {
395
+ arr = [];
396
+ _wsPool.set(poolKey, arr);
397
+ }
398
+ return arr;
399
+ }
400
+
401
+ function _removeFromPool(poolKey, entry) {
402
+ if (!poolKey) return;
403
+ const arr = _wsPool.get(poolKey);
404
+ if (!arr) return;
405
+ const idx = arr.indexOf(entry);
406
+ if (idx >= 0) arr.splice(idx, 1);
407
+ if (arr.length === 0) _wsPool.delete(poolKey);
408
+ }
409
+
410
+ function _scheduleIdleClose(poolKey, entry) {
411
+ if (!entry) return;
412
+ if (entry.idleTimer) clearTimeout(entry.idleTimer);
413
+ entry.idleTimer = setTimeout(() => {
414
+ if (entry.busy) return;
415
+ try { entry.socket.close(1000, 'idle_timeout'); } catch {}
416
+ _removeFromPool(poolKey, entry);
417
+ }, WS_IDLE_MS);
418
+ try { entry.idleTimer.unref?.(); } catch {}
419
+ }
420
+
421
+ function _clearIdle(entry) {
422
+ if (entry?.idleTimer) {
423
+ clearTimeout(entry.idleTimer);
424
+ entry.idleTimer = null;
425
+ }
426
+ }
427
+
428
+ function _isOpen(entry) {
429
+ return entry?.socket?.readyState === WebSocket.OPEN;
430
+ }
431
+
432
+ // Awaited frame send. Asserts the socket is OPEN and resolves only after
433
+ // the underlying transport reports the buffered write succeeded (or fails)
434
+ // via the WebSocket send callback. Raw `socket.send(JSON.stringify(...))`
435
+ // is fire-and-forget — a wedged or half-closed socket silently queues the
436
+ // payload and the caller assumes it landed, then later times out waiting
437
+ // for a server event that will never arrive. Tag any failure with
438
+ // `wsSendFailed=true` so _classifyMidstreamError routes the next attempt
439
+ // through a fresh socket.
440
+ function _sendFrame(entry, frame) {
441
+ return new Promise((resolve, reject) => {
442
+ const socket = entry?.socket;
443
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
444
+ const err = new Error(`WS send: socket not OPEN (readyState=${socket?.readyState ?? 'n/a'})`);
445
+ err.wsSendFailed = true;
446
+ reject(err);
447
+ return;
448
+ }
449
+ let payload;
450
+ try { payload = JSON.stringify(frame); }
451
+ catch (e) {
452
+ const err = e instanceof Error ? e : new Error(String(e));
453
+ err.wsSendFailed = true;
454
+ reject(err);
455
+ return;
456
+ }
457
+ try {
458
+ // Do NOT await the send callback: on a wedged-but-OPEN socket the
459
+ // ws write callback may never fire, which would hang this Promise
460
+ // before _streamResponse arms its first-byte watchdog. Fire and
461
+ // resolve immediately; transport failures surface via the socket
462
+ // 'error'/'close' handlers and the first-byte watchdog.
463
+ socket.send(payload, () => {});
464
+ resolve();
465
+ } catch (e) {
466
+ const err = e instanceof Error ? e : new Error(String(e));
467
+ err.wsSendFailed = true;
468
+ reject(err);
469
+ }
470
+ });
471
+ }
472
+
473
+ function _buildHandshakeHeaders({ auth, sessionToken, turnState, cacheKey: _cacheKey }) {
474
+ // xAI WS: do NOT pin x-grok-conv-id. Measured parallel runs show that
475
+ // forcing a routing shard via that header alternates cold caches across
476
+ // parallel workers; the automatic prompt-prefix cache holds up better
477
+ // when each handshake is unpinned. Reference: vercel/ai xai provider.
478
+ const headers = auth.type === 'xai'
479
+ ? {
480
+ 'Authorization': `Bearer ${auth.apiKey}`,
481
+ }
482
+ : auth.type === 'openai-direct'
483
+ ? {
484
+ 'Authorization': `Bearer ${auth.apiKey}`,
485
+ 'OpenAI-Beta': 'responses_websockets=2026-02-06',
486
+ }
487
+ : {
488
+ 'Authorization': `Bearer ${auth.access_token}`,
489
+ 'chatgpt-account-id': auth.account_id || '',
490
+ 'originator': CODEX_OAUTH_ORIGINATOR,
491
+ 'OpenAI-Beta': 'responses_websockets=2026-02-06',
492
+ };
493
+ if (sessionToken) {
494
+ const sid = String(sessionToken);
495
+ headers['session_id'] = sid;
496
+ }
497
+ // x-client-request-id must be a per-request value so server-side request
498
+ // traces stay distinguishable across retries / reconnects sharing the same
499
+ // session_id. Reusing sessionToken (= cacheKey) collapsed every request
500
+ // for the same conversation onto one trace bucket.
501
+ headers['x-client-request-id'] = randomBytes(16).toString('hex');
502
+ if (turnState) headers['x-codex-turn-state'] = turnState;
503
+ return headers;
504
+ }
505
+
506
+ // handshake session_id is the conversation slot Codex uses for in-memory
507
+ // prefix state. All orchestrator-internal dispatches for this provider share
508
+ // the same cacheKey (built in manager.mjs via providerCacheKey()), so they
509
+ // share the server-side prefix-cache shard across roles/sources.
510
+ function _mintSessionToken(cacheKey, auth) {
511
+ // xAI's public WebSocket endpoint uses the open connection plus
512
+ // response ids for continuation; unlike Codex, it does not need the
513
+ // Codex-specific session_id handshake shard.
514
+ if (auth?.type === 'xai') return null;
515
+ return cacheKey || 'mixdog-default';
516
+ }
517
+
518
+ function _openSocket({ auth, sessionToken, turnState, externalSignal, cacheKey }) {
519
+ const headers = _buildHandshakeHeaders({ auth, sessionToken, turnState, cacheKey });
520
+ const baseUrl = auth.type === 'xai'
521
+ ? XAI_WS_URL
522
+ : auth.type === 'openai-direct'
523
+ ? OPENAI_WS_URL
524
+ : CODEX_WS_URL;
525
+ const _wsOpenStart = Date.now();
526
+ if (process.env.MIXDOG_DEBUG_BRIDGE) {
527
+ process.stderr.write(`[bridge-trace] ws-open-start url=${baseUrl} tokenHash=${createHash('sha256').update(String(sessionToken)).digest('hex').slice(0, 8)} ts=${_wsOpenStart}\n`);
528
+ }
529
+ const url = baseUrl + (sessionToken ? `?session_id=${encodeURIComponent(String(sessionToken))}` : '');
530
+ return new Promise((resolve, reject) => {
531
+ let settled = false;
532
+ let abortListener = null;
533
+ let acquireTimer = null;
534
+ const settle = (ok, val) => {
535
+ if (settled) return;
536
+ settled = true;
537
+ if (acquireTimer) {
538
+ clearTimeout(acquireTimer);
539
+ acquireTimer = null;
540
+ }
541
+ if (abortListener && externalSignal) {
542
+ try { externalSignal.removeEventListener('abort', abortListener); } catch {}
543
+ }
544
+ (ok ? resolve : reject)(val);
545
+ };
546
+ const socket = new WebSocket(url, { headers, handshakeTimeout: WS_HANDSHAKE_TIMEOUT_MS });
547
+ acquireTimer = setTimeout(() => {
548
+ if (settled) return;
549
+ if (process.env.MIXDOG_DEBUG_BRIDGE) {
550
+ process.stderr.write(`[bridge-trace] ws-open-fail kind=acquire_timeout timeoutMs=${WS_ACQUIRE_TIMEOUT_MS} elapsed=${Date.now() - _wsOpenStart}ms\n`);
551
+ }
552
+ try { socket.terminate(); } catch {}
553
+ settle(false, Object.assign(
554
+ new Error(`${_wsErrLabel(auth?.type === 'xai' ? 'xai' : auth?.type === 'openai-direct' ? 'openai-direct' : 'openai-oauth')} acquire timed out before open (${WS_ACQUIRE_TIMEOUT_MS}ms)`),
555
+ { code: 'EWSACQUIRETIMEOUT', acquireTimeoutMs: WS_ACQUIRE_TIMEOUT_MS },
556
+ ));
557
+ }, WS_ACQUIRE_TIMEOUT_MS);
558
+ try { acquireTimer.unref?.(); } catch {}
559
+ const capturedHeaders = { turnState: null };
560
+ socket.once('upgrade', (res) => {
561
+ try {
562
+ const ts = res?.headers?.['x-codex-turn-state'];
563
+ if (typeof ts === 'string' && ts.length) capturedHeaders.turnState = ts;
564
+ } catch {}
565
+ });
566
+ socket.once('open', () => {
567
+ if (process.env.MIXDOG_DEBUG_BRIDGE) {
568
+ process.stderr.write(`[bridge-trace] ws-open-ok elapsed=${Date.now() - _wsOpenStart}ms\n`);
569
+ }
570
+ settle(true, { socket, turnState: capturedHeaders.turnState });
571
+ });
572
+ socket.once('error', (err) => {
573
+ if (process.env.MIXDOG_DEBUG_BRIDGE) {
574
+ process.stderr.write(`[bridge-trace] ws-open-fail kind=error msg=${String(err?.message || err).slice(0, 120)} elapsed=${Date.now() - _wsOpenStart}ms\n`);
575
+ }
576
+ try { socket.terminate(); } catch {}
577
+ settle(false, err instanceof Error ? err : Object.assign(new Error(errText(err) || 'openai-oauth WS error'), { wsErrorEvent: true, original: err }));
578
+ });
579
+ socket.once('close', (code, reason) => {
580
+ // Half-open handshake: the peer closed before 'open'/'error' fired
581
+ // (TCP RST / TLS edge). Without this the connect Promise never
582
+ // settles and only the 600s outer watchdog can break the stall
583
+ // (observed stage=requesting 601s hang). Open-path closes are
584
+ // no-ops here because settle() has already flipped `settled`.
585
+ if (settled) return;
586
+ try { socket.terminate(); } catch {}
587
+ settle(false, Object.assign(
588
+ new Error(`${_wsErrLabel(auth?.type === 'xai' ? 'xai' : auth?.type === 'openai-direct' ? 'openai-direct' : 'openai-oauth')} handshake closed before open (code=${code})`),
589
+ { wsCloseCode: code, wsCloseReason: (reason && reason.toString) ? reason.toString('utf-8') : '' }));
590
+ });
591
+ socket.once('unexpected-response', (_req, res) => {
592
+ if (settled) return;
593
+ const status = res?.statusCode || 0;
594
+ let body = '';
595
+ res.on('data', c => { if (body.length < 2048) body += c.toString('utf-8'); });
596
+ res.on('end', () => {
597
+ if (process.env.MIXDOG_DEBUG_BRIDGE) {
598
+ process.stderr.write(`[bridge-trace] ws-open-fail kind=http status=${status} body=${body.slice(0, 120)} elapsed=${Date.now() - _wsOpenStart}ms\n`);
599
+ }
600
+ try { socket.terminate(); } catch {}
601
+ settle(false, Object.assign(new Error(`${_wsErrLabel(auth?.type === 'xai' ? 'xai' : auth?.type === 'openai-direct' ? 'openai-direct' : 'openai-oauth')} handshake ${status}: ${body.slice(0, 200)}`), { httpStatus: status, httpBody: body }));
602
+ });
603
+ });
604
+ if (externalSignal) {
605
+ const onAbort = () => {
606
+ try { socket.terminate(); } catch {}
607
+ const reason = externalSignal.reason;
608
+ settle(false, reason instanceof Error ? reason : new Error(`${_wsErrLabel(auth?.type === 'xai' ? 'xai' : auth?.type === 'openai-direct' ? 'openai-direct' : 'openai-oauth')} handshake aborted`));
609
+ };
610
+ if (externalSignal.aborted) { onAbort(); return; }
611
+ abortListener = onAbort;
612
+ externalSignal.addEventListener('abort', onAbort, { once: true });
613
+ }
614
+ });
615
+ }
616
+
617
+ async function acquireWebSocket({ auth, poolKey, cacheKey, forceFresh, externalSignal }) {
618
+ const _acqStart = Date.now();
619
+ if (process.env.MIXDOG_DEBUG_BRIDGE) {
620
+ process.stderr.write(`[bridge-trace] acquire-start poolKey=${poolKey} cacheKey=${cacheKey} forceFresh=${forceFresh} externalAborted=${!!externalSignal?.aborted} ts=${_acqStart}\n`);
621
+ }
622
+ if (externalSignal?.aborted) {
623
+ const reason = externalSignal.reason;
624
+ throw reason instanceof Error ? reason : new Error('Codex WS acquire aborted');
625
+ }
626
+ if (poolKey && !forceFresh) {
627
+ const arr = _wsPool.get(poolKey) || [];
628
+ // Prune dead entries first.
629
+ for (let i = arr.length - 1; i >= 0; i--) {
630
+ if (!_isOpen(arr[i]) || arr[i].closing) {
631
+ _clearIdle(arr[i]);
632
+ arr.splice(i, 1);
633
+ }
634
+ }
635
+ if (arr.length === 0) _wsPool.delete(poolKey);
636
+ // Reuse any idle open entry (cache-warm path).
637
+ const idle = arr.find(e => !e.busy);
638
+ if (idle) {
639
+ _clearIdle(idle);
640
+ idle.busy = true;
641
+ // Defensive: pre-existing pooled entries created before the
642
+ // prefix-hash field was introduced may not have it set. Normalize
643
+ // to null so the first delta check reads a deterministic value
644
+ // (and falls back to full-create instead of silently passing).
645
+ if (idle.lastInputPrefixHash === undefined) idle.lastInputPrefixHash = null;
646
+ if (idle.lastRequestInput === undefined) idle.lastRequestInput = null;
647
+ if (idle.lastResponseItems === undefined) idle.lastResponseItems = null;
648
+ if (process.env.MIXDOG_DEBUG_BRIDGE) {
649
+ process.stderr.write(`[bridge-trace] acquire-reuse poolKey=${poolKey} openSockets=${arr.length} elapsed=${Date.now() - _acqStart}ms\n`);
650
+ }
651
+ return { entry: idle, reused: true };
652
+ }
653
+ // All entries busy and bucket at cap: fall through to ephemeral socket.
654
+ if (arr.length >= MAX_POOLED_SOCKETS_PER_KEY) {
655
+ if (process.env.MIXDOG_DEBUG_BRIDGE) {
656
+ process.stderr.write(`[bridge-trace] acquire-ephemeral cacheKey=${cacheKey} reason=cap elapsed=${Date.now() - _acqStart}ms\n`);
657
+ }
658
+ const ephSessionToken = _mintSessionToken(cacheKey, auth);
659
+ const { socket, turnState } = await _openSocket({ auth, sessionToken: ephSessionToken, turnState: null, externalSignal, cacheKey });
660
+ // Drain-complete fence: same invariant as the normal acquire path —
661
+ // if drain fired during the await, do NOT push an ephemeral entry
662
+ // back into the pool.
663
+ if (_drainComplete) {
664
+ try { socket.close(1000, 'drain-complete'); } catch {}
665
+ throw new Error('WS pool drained — process exiting');
666
+ }
667
+ const entry = {
668
+ socket,
669
+ busy: true,
670
+ idleTimer: null,
671
+ lastResponseId: null,
672
+ lastRequestSansInput: null,
673
+ lastRequestInput: null,
674
+ lastResponseItems: null,
675
+ lastInputLen: 0,
676
+ lastInputPrefixHash: null,
677
+ turnState: turnState || null,
678
+ closing: false,
679
+ ephemeral: true,
680
+ sessionToken: ephSessionToken,
681
+ };
682
+ socket.on('close', () => { entry.closing = true; });
683
+ return { entry, reused: false };
684
+ }
685
+ }
686
+ // Parallel sockets must not inherit sibling turnState or the Codex server
687
+ // treats the new request as a continuation of another in-flight turn and
688
+ // returns "No tool output found for function call …". turnState only
689
+ // propagates within a single entry across its own iterations.
690
+ const sessionToken = _mintSessionToken(cacheKey, auth);
691
+ if (process.env.MIXDOG_DEBUG_BRIDGE) {
692
+ process.stderr.write(`[bridge-trace] acquire-new tokenHash=${createHash('sha256').update(String(sessionToken)).digest('hex').slice(0, 8)} elapsed=${Date.now() - _acqStart}ms\n`);
693
+ }
694
+ const { socket, turnState } = await _openSocket({ auth, sessionToken, turnState: null, externalSignal, cacheKey });
695
+ const entry = {
696
+ socket,
697
+ busy: true,
698
+ idleTimer: null,
699
+ lastResponseId: null,
700
+ lastRequestSansInput: null,
701
+ lastRequestInput: null,
702
+ lastResponseItems: null,
703
+ lastInputLen: 0,
704
+ lastInputPrefixHash: null,
705
+ turnState: turnState || null,
706
+ closing: false,
707
+ ephemeral: false,
708
+ sessionToken,
709
+ };
710
+ if (poolKey && !forceFresh) _getPoolArr(poolKey).push(entry);
711
+ socket.on('close', () => {
712
+ entry.closing = true;
713
+ _removeFromPool(poolKey, entry);
714
+ });
715
+ return { entry, reused: false };
716
+ }
717
+
718
+ function releaseWebSocket({ entry, poolKey, keep }) {
719
+ if (!entry) return;
720
+ entry.busy = false;
721
+ if (!keep || !_isOpen(entry) || !poolKey || entry.ephemeral) {
722
+ try { entry.socket.close(1000, keep ? 'no_session' : 'release_no_keep'); } catch {}
723
+ _removeFromPool(poolKey, entry);
724
+ return;
725
+ }
726
+ _scheduleIdleClose(poolKey, entry);
727
+ }
728
+
729
+ // Port of pi-mono get_incremental_items: if the cached request (sans input)
730
+ // matches the current one and the current input starts with the cached input,
731
+ // return only the tail. Otherwise return the full input (fresh turn).
732
+ function _sansInput(body) {
733
+ const { input: _ignored, previous_response_id: _prevIgnored, ...rest } = body;
734
+ return rest;
735
+ }
736
+
737
+ function _stableStringify(obj) {
738
+ // Shallow stable-ish: JSON.stringify with sorted top-level keys. Nested
739
+ // arrays (tools, include) are order-sensitive and reflect intent, so we
740
+ // do not sort them.
741
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return JSON.stringify(obj);
742
+ const keys = Object.keys(obj).sort();
743
+ const parts = [];
744
+ for (const k of keys) parts.push(JSON.stringify(k) + ':' + _stableStringify(obj[k]));
745
+ return '{' + parts.join(',') + '}';
746
+ }
747
+
748
+ function _cloneJson(value) {
749
+ if (value == null) return value;
750
+ try { return JSON.parse(JSON.stringify(value)); } catch { return value; }
751
+ }
752
+
753
+ function _responseItemKey(item, fallbackIndex = 0) {
754
+ if (!item || typeof item !== 'object') return `primitive:${fallbackIndex}`;
755
+ if (item.id) return `${item.type || 'item'}:id:${item.id}`;
756
+ if (item.call_id) return `${item.type || 'item'}:call:${item.call_id}`;
757
+ try { return `${item.type || 'item'}:json:${_stableStringify(item)}`; } catch {}
758
+ return `${item.type || 'item'}:${fallbackIndex}`;
759
+ }
760
+
761
+ function _normalizeArguments(value) {
762
+ if (value == null) return '';
763
+ if (typeof value === 'string') {
764
+ const trimmed = value.trim();
765
+ try { return _stableStringify(JSON.parse(trimmed || '{}')); } catch { return trimmed; }
766
+ }
767
+ return _stableStringify(value);
768
+ }
769
+
770
+ function _normalizeContentPart(part) {
771
+ if (!part || typeof part !== 'object') return part;
772
+ const type = part.type === 'input_text' ? 'output_text' : part.type;
773
+ if (type === 'output_text') return { type, text: part.text || '' };
774
+ return part;
775
+ }
776
+
777
+ function _contentPartsEqual(a, b) {
778
+ const aa = Array.isArray(a) ? a.map(_normalizeContentPart) : [];
779
+ const bb = Array.isArray(b) ? b.map(_normalizeContentPart) : [];
780
+ return _stableStringify(aa) === _stableStringify(bb);
781
+ }
782
+
783
+ function _logicalResponseItemMatch(inputItem, responseItem) {
784
+ if (!inputItem || !responseItem) return false;
785
+ const inputType = inputItem.type || (inputItem.role === 'assistant' ? 'message' : '');
786
+ const responseType = responseItem.type || '';
787
+ if (responseType === 'function_call') {
788
+ return inputType === 'function_call'
789
+ && String(inputItem.call_id || '') === String(responseItem.call_id || '')
790
+ && String(inputItem.name || '') === String(responseItem.name || '')
791
+ && _normalizeArguments(inputItem.arguments) === _normalizeArguments(responseItem.arguments);
792
+ }
793
+ if (responseType === 'message') {
794
+ const inputRole = inputItem.role || (inputType === 'message' ? 'assistant' : '');
795
+ const responseRole = responseItem.role || 'assistant';
796
+ return inputType === 'message'
797
+ && inputRole === responseRole
798
+ && _contentPartsEqual(inputItem.content, responseItem.content);
799
+ }
800
+ if (responseType === 'reasoning') {
801
+ return inputType === 'reasoning'
802
+ && (!!responseItem.id ? inputItem.id === responseItem.id : true)
803
+ && (!!responseItem.encrypted_content
804
+ ? inputItem.encrypted_content === responseItem.encrypted_content
805
+ : true);
806
+ }
807
+ if (responseType === 'web_search_call') {
808
+ return inputType === 'web_search_call'
809
+ && (!!responseItem.id ? inputItem.id === responseItem.id : true)
810
+ && _stableStringify(inputItem.action || null) === _stableStringify(responseItem.action || null);
811
+ }
812
+ if (inputType !== responseType) return false;
813
+ const stripVolatile = (item) => {
814
+ if (!item || typeof item !== 'object') return item;
815
+ const { id: _id, status: _status, ...rest } = item;
816
+ return rest;
817
+ };
818
+ return _stableStringify(stripVolatile(inputItem)) === _stableStringify(stripVolatile(responseItem));
819
+ }
820
+
821
+ function _requestInputItemsMatch(a, b) {
822
+ return _stableStringify(a) === _stableStringify(b);
823
+ }
824
+
825
+ function _stripRequestPrefix(curInput, prevInput) {
826
+ const current = Array.isArray(curInput) ? curInput : [];
827
+ const previous = Array.isArray(prevInput) ? prevInput : [];
828
+ if (current.length < previous.length) return null;
829
+ for (let i = 0; i < previous.length; i += 1) {
830
+ if (!_requestInputItemsMatch(current[i], previous[i])) return null;
831
+ }
832
+ return current.slice(previous.length);
833
+ }
834
+
835
+ function _isReplayLikeHead(item, responseItem) {
836
+ if (!item || !responseItem) return false;
837
+ const inputType = item.type || (item.role === 'assistant' ? 'message' : '');
838
+ const responseType = responseItem.type || '';
839
+ if (responseType === 'message') return inputType === 'message';
840
+ if (responseType === 'function_call') return inputType === 'function_call';
841
+ return inputType === responseType;
842
+ }
843
+
844
+ function _stripResponseItemsFromHead(items, responseItems) {
845
+ const tail = Array.isArray(items) ? items : [];
846
+ const outputs = Array.isArray(responseItems) ? responseItems : [];
847
+ let cursor = 0;
848
+ let stripped = 0;
849
+ let skipped = 0;
850
+ for (const output of outputs) {
851
+ if (cursor >= tail.length) break;
852
+ if (_logicalResponseItemMatch(tail[cursor], output)) {
853
+ cursor += 1;
854
+ stripped += 1;
855
+ continue;
856
+ }
857
+ if (_isReplayLikeHead(tail[cursor], output)) {
858
+ return {
859
+ ok: false,
860
+ reason: `response_output_mismatch:${output?.type || 'unknown'}`,
861
+ tail,
862
+ stripped,
863
+ skipped,
864
+ };
865
+ }
866
+ skipped += 1;
867
+ }
868
+ return { ok: true, reason: null, tail: tail.slice(cursor), stripped, skipped };
869
+ }
870
+
871
+ function _computeDelta({ entry, body }) {
872
+ if (!entry || !entry.lastRequestSansInput || !entry.lastResponseId) {
873
+ return { mode: 'full', reason: 'no_anchor', frame: { type: 'response.create', ...body } };
874
+ }
875
+ if (!Array.isArray(entry.lastRequestInput)) {
876
+ return { mode: 'full', reason: 'no_input_snapshot', frame: { type: 'response.create', ...body } };
877
+ }
878
+ const curSans = _stableStringify(_sansInput(body));
879
+ if (curSans !== entry.lastRequestSansInput) {
880
+ return { mode: 'full', reason: 'request_properties_changed', frame: { type: 'response.create', ...body } };
881
+ }
882
+ const curInput = Array.isArray(body.input) ? body.input : [];
883
+ const afterPreviousInput = _stripRequestPrefix(curInput, entry.lastRequestInput);
884
+ if (!afterPreviousInput) {
885
+ return { mode: 'full', reason: 'input_prefix_mismatch', frame: { type: 'response.create', ...body } };
886
+ }
887
+ const stripped = _stripResponseItemsFromHead(afterPreviousInput, entry.lastResponseItems);
888
+ if (!stripped.ok) {
889
+ return { mode: 'full', reason: stripped.reason, frame: { type: 'response.create', ...body } };
890
+ }
891
+ return {
892
+ mode: 'delta',
893
+ reason: null,
894
+ strippedResponseItems: stripped.stripped,
895
+ skippedResponseItems: stripped.skipped,
896
+ frame: {
897
+ ...body,
898
+ type: 'response.create',
899
+ previous_response_id: entry.lastResponseId,
900
+ input: stripped.tail,
901
+ },
902
+ };
903
+ }
904
+
905
+ function _estimateFrameTokens(frame) {
906
+ try {
907
+ const s = JSON.stringify(frame);
908
+ return Math.ceil(s.length / 4);
909
+ } catch { return 0; }
910
+ }
911
+
912
+ function _usageNum(value) {
913
+ const n = Number(value || 0);
914
+ return Number.isFinite(n) ? n : 0;
915
+ }
916
+
917
+ function _combineUsageWithWarmup(actual, warmup) {
918
+ if (!warmup) return actual;
919
+ if (!actual) return warmup;
920
+ const actualRaw = actual.raw || {};
921
+ const warmupRaw = warmup.raw || {};
922
+ const actualTicks = _usageNum(actualRaw.cost_in_usd_ticks);
923
+ const warmupTicks = _usageNum(warmupRaw.cost_in_usd_ticks);
924
+ return {
925
+ ...actual,
926
+ inputTokens: _usageNum(actual.inputTokens) + _usageNum(warmup.inputTokens),
927
+ outputTokens: _usageNum(actual.outputTokens) + _usageNum(warmup.outputTokens),
928
+ cachedTokens: _usageNum(actual.cachedTokens) + _usageNum(warmup.cachedTokens),
929
+ promptTokens: _usageNum(actual.promptTokens) + _usageNum(warmup.promptTokens),
930
+ warmupInputTokens: _usageNum(warmup.inputTokens),
931
+ warmupCachedTokens: _usageNum(warmup.cachedTokens),
932
+ warmupOutputTokens: _usageNum(warmup.outputTokens),
933
+ raw: {
934
+ ...actualRaw,
935
+ warmup_usage: warmupRaw,
936
+ ...(actualTicks || warmupTicks ? { cost_in_usd_ticks: actualTicks + warmupTicks } : {}),
937
+ },
938
+ };
939
+ }
940
+
941
+ function _parseEvent(raw) {
942
+ try { return JSON.parse(raw); } catch { return null; }
943
+ }
944
+
945
+ function _incompleteReasonFromEvent(event) {
946
+ const reasonObj = event?.response?.incomplete_details
947
+ || event?.incomplete_details
948
+ || event?.response?.status_details
949
+ || null;
950
+ return String(reasonObj?.reason || event?.response?.status || 'incomplete');
951
+ }
952
+
953
+ function _isMaxOutputIncompleteReason(reason) {
954
+ return /^(?:max_output_tokens|max_tokens|length|output_token_limit)$/i.test(String(reason || '').trim());
955
+ }
956
+
957
+ function _httpStatusFromWsClose(code, reason) {
958
+ const n = Number(code || 0);
959
+ const r = String(reason || '').toLowerCase();
960
+ if (n === 4401
961
+ || /\b(?:unauthorized|unauthorised|authentication|auth(?:enticated?)?|not authenticated|token expired|access token)\b/.test(r)) {
962
+ return 401;
963
+ }
964
+ if (n === 4403 || /\b(?:forbidden|policy|permission denied)\b/.test(r)) return 403;
965
+ if (n === 4429 || /\b(?:rate limit|quota)\b/.test(r)) return 429;
966
+ return 0;
967
+ }
968
+
969
+ function _wsErrLabel(p) {
970
+ if (p === 'xai') return 'xAI WS';
971
+ if (p === 'openai-direct' || p === 'openai') return 'OpenAI WS';
972
+ return 'Codex WS';
973
+ }
974
+ export async function _streamResponse({
975
+ entry,
976
+ externalSignal,
977
+ onStreamDelta,
978
+ onToolCall,
979
+ onTextDelta,
980
+ state,
981
+ logSuppressedReasoningDeltas = true,
982
+ traceProvider = 'openai-oauth',
983
+ _timeouts = null,
984
+ }) {
985
+ const errLabel = _wsErrLabel(traceProvider);
986
+ const socket = entry.socket;
987
+ const preResponseCreatedMs = _positiveInt(_timeouts?.preResponseCreatedMs, WS_PRE_RESPONSE_CREATED_MS);
988
+ const firstMeaningfulMs = _positiveInt(_timeouts?.firstMeaningfulMs, WS_FIRST_MEANINGFUL_MS);
989
+ const interChunkMs = _positiveInt(_timeouts?.interChunkMs, WS_INTER_CHUNK_MS);
990
+ const _streamingStart = Date.now();
991
+ let _firstDeltaEmitted = false;
992
+ let content = '';
993
+ let model = '';
994
+ let responseId = '';
995
+ let responseServiceTier = '';
996
+ const toolCalls = [];
997
+ const webSearchCalls = [];
998
+ const webSearchCallKeys = new Set();
999
+ const compactionItems = [];
1000
+ const responseItemsAdded = [];
1001
+ const responseItemKeys = new Set();
1002
+ const citations = [];
1003
+ const citationKeys = new Set();
1004
+ const pendingCalls = new Map();
1005
+ // Reasoning items collected from response.output_item.done (or salvaged
1006
+ // from response.completed.response.output). The request still includes
1007
+ // `reasoning.encrypted_content` so the server keeps emitting the blobs,
1008
+ // but explicit input-side replay is INTENTIONALLY OMITTED in
1009
+ // convertMessagesToResponsesInput (openai-oauth.mjs:233-238) — Codex
1010
+ // rejects the same `rs_*` id twice in one handshake session_id with a
1011
+ // "Duplicate item" error. Server-side conversation state already carries
1012
+ // the prefix forward across the WS_IDLE_MS window. The collected
1013
+ // reasoningItems below are surfaced for trace/debugging only; they do
1014
+ // not feed back into the next request body.
1015
+ const reasoningItems = [];
1016
+ let reasoningTextDeltaCount = 0;
1017
+ let reasoningSummaryTextDeltaCount = 0;
1018
+ let reasoningOtherDeltaCount = 0;
1019
+ let reasoningDeltaLogEmitted = false;
1020
+ const pushReasoningItem = (item) => {
1021
+ if (!item || item.type !== 'reasoning') return;
1022
+ if (!item.encrypted_content) return;
1023
+ reasoningItems.push({
1024
+ id: item.id || '',
1025
+ encrypted_content: item.encrypted_content,
1026
+ summary: Array.isArray(item.summary) ? item.summary : [],
1027
+ });
1028
+ };
1029
+ const pushResponseItem = (item) => {
1030
+ if (!item || typeof item !== 'object') return;
1031
+ const key = _responseItemKey(item, responseItemsAdded.length);
1032
+ if (responseItemKeys.has(key)) {
1033
+ const existing = responseItemsAdded.find((candidate, index) => _responseItemKey(candidate, index) === key);
1034
+ if (existing?.type === 'function_call' && item.type === 'function_call') {
1035
+ if (!existing.call_id && item.call_id) existing.call_id = item.call_id;
1036
+ if (!existing.name && item.name) existing.name = item.name;
1037
+ if ((existing.arguments == null || existing.arguments === '') && item.arguments != null) existing.arguments = item.arguments;
1038
+ }
1039
+ return;
1040
+ }
1041
+ responseItemKeys.add(key);
1042
+ responseItemsAdded.push(_cloneJson(item));
1043
+ };
1044
+ const enrichFunctionCallResponseItem = ({ itemId = '', callId = '', name = '', argumentsText = '' } = {}) => {
1045
+ for (const item of responseItemsAdded) {
1046
+ if (item?.type !== 'function_call') continue;
1047
+ if (itemId && item.id && item.id !== itemId) continue;
1048
+ if (callId && item.call_id && item.call_id !== callId) continue;
1049
+ if (!item.call_id && callId) item.call_id = callId;
1050
+ if (!item.name && name) item.name = name;
1051
+ if ((item.arguments == null || item.arguments === '') && argumentsText) item.arguments = argumentsText;
1052
+ }
1053
+ };
1054
+ const pushCitation = (raw, fallbackTitle = '') => {
1055
+ const url = raw?.url || raw?.uri || raw?.href || '';
1056
+ if (!url || citationKeys.has(url)) return;
1057
+ citationKeys.add(url);
1058
+ citations.push({
1059
+ title: raw?.title || fallbackTitle || '',
1060
+ url,
1061
+ snippet: raw?.snippet || raw?.text || raw?.description || '',
1062
+ source: 'openai-oauth',
1063
+ });
1064
+ };
1065
+ const pushOutputTextAnnotations = (contentPart) => {
1066
+ const annotations = Array.isArray(contentPart?.annotations) ? contentPart.annotations : [];
1067
+ for (const annotation of annotations) pushCitation(annotation);
1068
+ };
1069
+ const pushWebSearchCall = (item) => {
1070
+ if (!item || item.type !== 'web_search_call') return;
1071
+ let key = item.id || '';
1072
+ if (!key) {
1073
+ try { key = JSON.stringify(item.action || item); } catch { key = `${webSearchCalls.length}`; }
1074
+ }
1075
+ if (webSearchCallKeys.has(key)) return;
1076
+ webSearchCallKeys.add(key);
1077
+ webSearchCalls.push({
1078
+ id: item.id || '',
1079
+ status: item.status || '',
1080
+ action: item.action || null,
1081
+ });
1082
+ const action = item.action || {};
1083
+ if (action.url) pushCitation({ url: action.url, title: action.query || '' });
1084
+ if (Array.isArray(action.urls)) {
1085
+ for (const url of action.urls) pushCitation({ url, title: action.query || '' });
1086
+ }
1087
+ };
1088
+ const pushCompactionItem = (item) => {
1089
+ if (!item || !['compaction', 'compaction_summary', 'context_compaction'].includes(item.type)) return;
1090
+ if (!item.encrypted_content) return;
1091
+ compactionItems.push(item);
1092
+ };
1093
+ const logReasoningDeltaSuppression = () => {
1094
+ if (!logSuppressedReasoningDeltas) return;
1095
+ const total = reasoningTextDeltaCount + reasoningSummaryTextDeltaCount + reasoningOtherDeltaCount;
1096
+ if (reasoningDeltaLogEmitted || total === 0) return;
1097
+ reasoningDeltaLogEmitted = true;
1098
+ process.stderr.write(`[openai-oauth-ws] suppressed reasoning text deltas from user content count=${total} text=${reasoningTextDeltaCount} summary=${reasoningSummaryTextDeltaCount} other=${reasoningOtherDeltaCount}\n`);
1099
+ };
1100
+ let usage;
1101
+ let stopReason = null;
1102
+ let incompleteReason = null;
1103
+ let done = false;
1104
+ let terminalError = null;
1105
+ // Mid-stream retry classifier needs to distinguish "stream died before we
1106
+ // even saw response.created" from "stream died after we had a partial
1107
+ // response but before completion". Mutate the shared state object so the
1108
+ // caller can inspect flags on the error path without us having to attach
1109
+ // them manually at every reject site.
1110
+ const midState = state || {};
1111
+ midState.sawResponseCreated = midState.sawResponseCreated || false;
1112
+ midState.sawCompleted = midState.sawCompleted || false;
1113
+ midState.wsCloseCode = null;
1114
+ midState.responseFailedPayload = null;
1115
+ let idleTimer = null;
1116
+ let keepaliveTimer = null;
1117
+ let abortHandler = null;
1118
+ let messageHandler = null;
1119
+ let closeHandler = null;
1120
+ let errorHandler = null;
1121
+
1122
+ return new Promise((resolve, reject) => {
1123
+ // Pre-stream watchdog: the timer fires if the server never sends a
1124
+ // first event (response.created) within preResponseCreatedMs
1125
+ // after our last frame. The socket is open and the response.create
1126
+ // frame was sent, but no server event has come back — a wedged
1127
+ // post-upgrade socket. Healthy servers ack within seconds, so this
1128
+ // window is intentionally short (~25s). Once response.created (or
1129
+ // any other meaningful event) arrives, the timer is cancelled and
1130
+ // the longer inter-chunk inactivity watchdog takes over — silent
1131
+ // gaps mid-reasoning (Codex spending 50s+ producing reasoning
1132
+ // tokens) are normal and should not abort the turn.
1133
+ const armPreStreamWatchdog = () => {
1134
+ if (idleTimer) clearTimeout(idleTimer);
1135
+ idleTimer = setTimeout(() => {
1136
+ if (process.env.MIXDOG_DEBUG_BRIDGE) {
1137
+ process.stderr.write(`[bridge-trace] ws-timeout kind=first-byte afterMs=${preResponseCreatedMs}\n`);
1138
+ }
1139
+ traceWsTimeout('first_byte_timeout', preResponseCreatedMs);
1140
+ const err = new Error(`WS stream: no first server event within ${preResponseCreatedMs}ms`);
1141
+ // Tag the close code so _classifyMidstreamError sees a 4000
1142
+ // (our local pre-stream watchdog code) and routes through
1143
+ // the post-upgrade-no-first-event retryable bucket.
1144
+ err.wsCloseCode = 4000;
1145
+ // Tag the error object itself (not just midState): the warmup
1146
+ // path streams under a separate warmupState and rethrows on
1147
+ // timeout BEFORE it can copy flags to the outer midState, so the
1148
+ // outer catch's _classifyMidstreamError would otherwise see
1149
+ // sawResponseCreated=false + close 4000 and hit the pre-created
1150
+ // deny gate. err.firstByteTimeout makes both paths retryable.
1151
+ err.firstByteTimeout = true;
1152
+ midState.firstByteTimeout = true;
1153
+ terminalError = err;
1154
+ try { socket.close(4000, 'first_byte_timeout'); } catch {}
1155
+ // socket.close() may not settle a half-open WS (closeHandler never
1156
+ // fires) — reject directly so the turn retries instead of hanging
1157
+ // until the 600s watchdog. finish() is idempotent (Promise settles
1158
+ // once; cleanup is null-safe).
1159
+ finish();
1160
+ }, preResponseCreatedMs);
1161
+ };
1162
+ let interChunkTimer = null;
1163
+ let firstMeaningfulTimer = null;
1164
+ let firstMeaningfulSeen = false;
1165
+ const traceWsTimeout = (event, timeoutMs) => {
1166
+ try {
1167
+ const iteration = Number(midState.iteration);
1168
+ const attemptIndex = Number(midState.attemptIndex);
1169
+ const payload = {
1170
+ provider: midState.traceProvider || traceProvider,
1171
+ transport: 'websocket',
1172
+ event,
1173
+ timeout_ms: timeoutMs,
1174
+ elapsed_ms: Date.now() - _streamingStart,
1175
+ model: midState.model || model || null,
1176
+ attempt_index: Number.isFinite(attemptIndex) ? attemptIndex : null,
1177
+ warmup: midState.warmup === true,
1178
+ saw_response_created: midState.sawResponseCreated === true,
1179
+ first_meaningful_seen: firstMeaningfulSeen === true,
1180
+ };
1181
+ appendBridgeTrace({
1182
+ sessionId: midState.sessionId || null,
1183
+ iteration: Number.isFinite(iteration) ? iteration : null,
1184
+ kind: 'ws_timeout',
1185
+ ...payload,
1186
+ payload,
1187
+ });
1188
+ } catch {}
1189
+ };
1190
+ const clearPreStreamWatchdog = () => {
1191
+ if (idleTimer) {
1192
+ clearTimeout(idleTimer);
1193
+ idleTimer = null;
1194
+ }
1195
+ };
1196
+ const clearFirstMeaningfulWatchdog = () => {
1197
+ if (firstMeaningfulTimer) {
1198
+ clearTimeout(firstMeaningfulTimer);
1199
+ firstMeaningfulTimer = null;
1200
+ }
1201
+ };
1202
+ const armFirstMeaningfulWatchdog = () => {
1203
+ if (firstMeaningfulSeen || firstMeaningfulMs <= 0) return;
1204
+ clearFirstMeaningfulWatchdog();
1205
+ firstMeaningfulTimer = setTimeout(() => {
1206
+ if (process.env.MIXDOG_DEBUG_BRIDGE) {
1207
+ process.stderr.write(`[bridge-trace] ws-timeout kind=first-meaningful afterMs=${firstMeaningfulMs}\n`);
1208
+ }
1209
+ traceWsTimeout('first_meaningful_timeout', firstMeaningfulMs);
1210
+ const err = new Error(`WS stream: no meaningful output within ${firstMeaningfulMs}ms after response.created`);
1211
+ err.wsCloseCode = 4000;
1212
+ err.firstMeaningfulTimeout = true;
1213
+ midState.firstMeaningfulTimeout = true;
1214
+ terminalError = err;
1215
+ try { socket.close(4000, 'first_meaningful_timeout'); } catch {}
1216
+ finish();
1217
+ }, firstMeaningfulMs);
1218
+ };
1219
+ const resetInterChunk = () => {
1220
+ if (interChunkTimer) clearTimeout(interChunkTimer);
1221
+ interChunkTimer = setTimeout(() => {
1222
+ if (process.env.MIXDOG_DEBUG_BRIDGE) {
1223
+ process.stderr.write(`[bridge-trace] ws-timeout kind=inter-chunk afterMs=${interChunkMs}\n`);
1224
+ }
1225
+ traceWsTimeout('inter_chunk_timeout', interChunkMs);
1226
+ terminalError = new Error(`WS stream: inter-chunk inactivity for ${interChunkMs}ms`);
1227
+ try { socket.close(4000, 'inter_chunk_timeout'); } catch {}
1228
+ // Same half-open guard as the pre-stream watchdog: reject directly
1229
+ // so a stuck socket.close() cannot leave the Promise pending.
1230
+ finish();
1231
+ }, interChunkMs);
1232
+ };
1233
+ const onResponseCreated = () => {
1234
+ clearPreStreamWatchdog();
1235
+ if (!firstMeaningfulSeen) armFirstMeaningfulWatchdog();
1236
+ else resetInterChunk();
1237
+ };
1238
+ // Called on every event that carries real output tokens or tool
1239
+ // progress. `response.created` is only an ACK and must not count here:
1240
+ // a wedged Codex stream can ACK immediately and then never produce
1241
+ // text/reasoning/tool deltas, holding the prompt-cache lane for the
1242
+ // full inter-chunk window.
1243
+ const onMeaningfulOutput = () => {
1244
+ if (!firstMeaningfulSeen) {
1245
+ firstMeaningfulSeen = true;
1246
+ clearPreStreamWatchdog();
1247
+ clearFirstMeaningfulWatchdog();
1248
+ }
1249
+ resetInterChunk();
1250
+ };
1251
+ // resetIdle kept for compat; metadata frames no longer disarm pre-stream watchdog.
1252
+ const resetIdle = () => { /* noop — only onMeaningfulOutput() disarms */ };
1253
+ const cleanup = () => {
1254
+ if (idleTimer) clearTimeout(idleTimer);
1255
+ clearFirstMeaningfulWatchdog();
1256
+ if (interChunkTimer) { clearTimeout(interChunkTimer); interChunkTimer = null; }
1257
+ if (keepaliveTimer) { clearInterval(keepaliveTimer); keepaliveTimer = null; }
1258
+ if (messageHandler) socket.off('message', messageHandler);
1259
+ if (closeHandler) socket.off('close', closeHandler);
1260
+ if (errorHandler) socket.off('error', errorHandler);
1261
+ if (abortHandler && externalSignal) externalSignal.removeEventListener('abort', abortHandler);
1262
+ };
1263
+ const finish = () => {
1264
+ logReasoningDeltaSuppression();
1265
+ cleanup();
1266
+ if (!terminalError && midState.expectCompaction === true && compactionItems.length !== 1) {
1267
+ terminalError = new Error(
1268
+ `${errLabel} remote compaction expected exactly one compaction output item, got ${compactionItems.length}`,
1269
+ );
1270
+ }
1271
+ if (terminalError) { reject(terminalError); return; }
1272
+ resolve({
1273
+ content,
1274
+ model,
1275
+ reasoningItems: reasoningItems.length ? reasoningItems : undefined,
1276
+ responseItems: responseItemsAdded.length ? responseItemsAdded : undefined,
1277
+ compactionItem: compactionItems.length === 1 ? compactionItems[0] : undefined,
1278
+ compactionItems: compactionItems.length ? compactionItems : undefined,
1279
+ toolCalls: toolCalls.length ? toolCalls : undefined,
1280
+ citations: citations.length ? citations : undefined,
1281
+ webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
1282
+ usage,
1283
+ stopReason: stopReason || undefined,
1284
+ incompleteReason: incompleteReason || undefined,
1285
+ responseId: responseId || undefined,
1286
+ serviceTier: responseServiceTier || undefined,
1287
+ });
1288
+ };
1289
+
1290
+ messageHandler = (data) => {
1291
+ resetIdle();
1292
+ // Do NOT call onStreamDelta for every frame — metadata/keepalive frames
1293
+ // must not reset bridge-stall-watchdog's lastStreamDeltaAt. Only
1294
+ // meaningful output (text delta / tool call) updates that timestamp.
1295
+ const text = typeof data === 'string' ? data : data.toString('utf-8');
1296
+ const event = _parseEvent(text);
1297
+ if (!event) return;
1298
+ if (event.error) {
1299
+ const err = new Error(event.error.message || 'Responses WS error');
1300
+ try {
1301
+ err.payload = event.error;
1302
+ populateHttpStatusFromMessage(err);
1303
+ } catch {}
1304
+ terminalError = err;
1305
+ finish();
1306
+ return;
1307
+ }
1308
+ if (typeof event.type !== 'string') return;
1309
+ switch (event.type) {
1310
+ case 'response.created':
1311
+ midState.sawResponseCreated = true;
1312
+ if (event.response?.model) model = event.response.model;
1313
+ if (event.response?.id) responseId = event.response.id;
1314
+ // Server ack: cancel only the pre-created watchdog. Keep
1315
+ // a separate first-meaningful watchdog armed until real
1316
+ // model progress arrives.
1317
+ onResponseCreated();
1318
+ break;
1319
+ case 'response.output_text.delta':
1320
+ content += event.delta || '';
1321
+ try {
1322
+ if (!_firstDeltaEmitted) {
1323
+ _firstDeltaEmitted = true;
1324
+ if (process.env.MIXDOG_DEBUG_BRIDGE) {
1325
+ process.stderr.write(`[bridge-trace] ws-first-delta sinceStreaming=${Date.now() - _streamingStart}ms\n`);
1326
+ }
1327
+ }
1328
+ onStreamDelta?.();
1329
+ } catch {}
1330
+ // Live text relay (gateway): forward the raw text chunk so
1331
+ // the client renders first tokens before the final replay.
1332
+ // Tool-call/argument deltas intentionally stay off this path.
1333
+ // Invariant: once a non-empty chunk has been relayed live it
1334
+ // cannot be withdrawn, so flag the attempt so a later
1335
+ // mid-stream/truncated failure is NOT retried (retry would
1336
+ // concatenate a second attempt onto rendered text).
1337
+ if (event.delta && onTextDelta) {
1338
+ if (state) state.emittedText = true;
1339
+ try { onTextDelta(event.delta); } catch {}
1340
+ }
1341
+ onMeaningfulOutput();
1342
+ break;
1343
+ case 'response.reasoning_text.delta':
1344
+ case 'response.reasoning_summary_text.delta':
1345
+ if (event.type === 'response.reasoning_text.delta') reasoningTextDeltaCount += 1;
1346
+ else reasoningSummaryTextDeltaCount += 1;
1347
+ // Reasoning text is live model progress — refresh
1348
+ // lastStreamDeltaAt so stream-watchdog does not flag a
1349
+ // long reasoning span as a stall. It also counts as
1350
+ // liveness for the local pre-stream / inter-chunk
1351
+ // watchdogs: a long reasoning span without any
1352
+ // output_text delta would otherwise trip the
1353
+ // first-meaningful timer and abort an otherwise healthy
1354
+ // stream. Reasoning is still suppressed from user
1355
+ // content (no `content +=` here) — only the watchdog
1356
+ // timers are reset.
1357
+ try { onStreamDelta?.(); } catch {}
1358
+ onMeaningfulOutput();
1359
+ break;
1360
+ case 'response.output_item.added':
1361
+ if (event.item?.type === 'function_call') {
1362
+ pendingCalls.set(event.item.id || '', {
1363
+ name: event.item.name || '',
1364
+ callId: event.item.call_id || '',
1365
+ });
1366
+ onMeaningfulOutput();
1367
+ }
1368
+ break;
1369
+ case 'response.function_call_arguments.delta':
1370
+ try { onStreamDelta?.(); } catch {}
1371
+ onMeaningfulOutput();
1372
+ break;
1373
+ case 'response.function_call_arguments.done': {
1374
+ const itemId = event.item_id || '';
1375
+ const pending = pendingCalls.get(itemId);
1376
+ let args = {};
1377
+ try { args = JSON.parse(event.arguments || '{}'); } catch {}
1378
+ enrichFunctionCallResponseItem({
1379
+ itemId,
1380
+ callId: pending?.callId || event.call_id || '',
1381
+ name: pending?.name || event.name || '',
1382
+ argumentsText: event.arguments || JSON.stringify(args),
1383
+ });
1384
+ if (pending?.callId && pending?.name) {
1385
+ const call = { id: pending.callId, name: pending.name, arguments: args };
1386
+ toolCalls.push(call);
1387
+ midState.emittedToolCall = true;
1388
+ try { onToolCall?.(call); } catch {}
1389
+ } else {
1390
+ // Synthesizing a `tc_${Date.now()}` callId here would
1391
+ // make the next turn fail to match the model's
1392
+ // function_call_output reference. Defer instead and
1393
+ // salvage call_id/name from the final
1394
+ // response.completed.output bundle below. If salvage
1395
+ // also fails we fail the stream explicitly — masking
1396
+ // the gap with a synthetic id just shifts the failure
1397
+ // one turn later under a confusing "No tool output
1398
+ // found for function call" error.
1399
+ toolCalls.push({
1400
+ id: pending?.callId || '',
1401
+ name: pending?.name || '',
1402
+ arguments: args,
1403
+ _pendingItemId: itemId,
1404
+ _deferred: true,
1405
+ });
1406
+ }
1407
+ try { onStreamDelta?.(); } catch {}
1408
+ onMeaningfulOutput();
1409
+ break;
1410
+ }
1411
+ case 'response.output_item.done':
1412
+ pushResponseItem(event.item);
1413
+ // function_call / output_text already captured via their
1414
+ // dedicated streaming events. The one shape we still need
1415
+ // here is `reasoning` — carries encrypted_content that
1416
+ // must be replayed on the next input to keep the Codex
1417
+ // server-side prompt cache prefix warm.
1418
+ if (event.item?.type === 'reasoning') pushReasoningItem(event.item);
1419
+ if (event.item?.type === 'web_search_call') pushWebSearchCall(event.item);
1420
+ if (['compaction', 'compaction_summary', 'context_compaction'].includes(event.item?.type)) {
1421
+ pushCompactionItem(event.item);
1422
+ onMeaningfulOutput();
1423
+ }
1424
+ break;
1425
+ case 'response.completed': {
1426
+ const completedServiceTier = event.response?.service_tier || event.response?.serviceTier || '';
1427
+ if (completedServiceTier) responseServiceTier = String(completedServiceTier);
1428
+ if (event.response?.usage) {
1429
+ const u = event.response.usage;
1430
+ const rawUsage = responseServiceTier
1431
+ ? { ...u, service_tier: responseServiceTier }
1432
+ : u;
1433
+ usage = {
1434
+ inputTokens: u.input_tokens || 0,
1435
+ outputTokens: u.output_tokens || 0,
1436
+ cachedTokens: extractCachedTokens(u),
1437
+ // OpenAI Codex reports input_tokens as the total
1438
+ // prompt volume (cached portion is a subset, not
1439
+ // additive). Alias into the cross-provider
1440
+ // `promptTokens` field so downstream loggers have
1441
+ // uniform semantics.
1442
+ promptTokens: u.input_tokens || 0,
1443
+ raw: rawUsage,
1444
+ };
1445
+ }
1446
+ if (!model && event.response?.model) model = event.response.model;
1447
+ if (!responseId && event.response?.id) responseId = event.response.id;
1448
+ if (event.response?.output) {
1449
+ for (const item of event.response.output) {
1450
+ pushResponseItem(item);
1451
+ if (!content && item.type === 'message') {
1452
+ for (const c of item.content || []) {
1453
+ if (c.type === 'output_text') {
1454
+ content += c.text || '';
1455
+ pushOutputTextAnnotations(c);
1456
+ }
1457
+ }
1458
+ }
1459
+ if (item.type === 'message') {
1460
+ for (const c of item.content || []) {
1461
+ if (c.type === 'output_text') pushOutputTextAnnotations(c);
1462
+ }
1463
+ }
1464
+ if (item.type === 'web_search_call') pushWebSearchCall(item);
1465
+ if (['compaction', 'compaction_summary', 'context_compaction'].includes(item.type)) {
1466
+ pushCompactionItem(item);
1467
+ }
1468
+ // Salvage path: some streams emit reasoning only
1469
+ // inside the final response.completed.output
1470
+ // bundle (no per-item .done event). Dedup by id.
1471
+ if (item.type === 'reasoning'
1472
+ && !reasoningItems.some(r => r.id === item.id)) {
1473
+ pushReasoningItem(item);
1474
+ }
1475
+ // Salvage path for function_call: when
1476
+ // arguments.done fired before (or without) a
1477
+ // matching output_item.added, the deferred tool
1478
+ // call placeholder has empty id/name. The
1479
+ // completed.output bundle carries the canonical
1480
+ // call_id/name; fill them in and emit onToolCall.
1481
+ if (item.type === 'function_call') {
1482
+ const tc = toolCalls.find(
1483
+ (t) => t._deferred && t._pendingItemId === (item.id || ''),
1484
+ );
1485
+ if (tc) {
1486
+ if (!tc.id && item.call_id) tc.id = item.call_id;
1487
+ if (!tc.name && item.name) tc.name = item.name;
1488
+ if (tc.id && tc.name) {
1489
+ delete tc._deferred;
1490
+ delete tc._pendingItemId;
1491
+ midState.emittedToolCall = true;
1492
+ try { onToolCall?.(tc); } catch {}
1493
+ }
1494
+ }
1495
+ }
1496
+ }
1497
+ }
1498
+ // Salvage validation. Any deferred call still missing
1499
+ // id/name would propagate to the next turn as a
1500
+ // function_call_output the server can't anchor. Fail the
1501
+ // stream now so the caller sees a deterministic error
1502
+ // instead of a cryptic mismatch one turn later.
1503
+ const unresolved = toolCalls.find((t) => t._deferred);
1504
+ if (unresolved) {
1505
+ terminalError = new Error(
1506
+ `${errLabel} function_call salvage failed: missing call_id/name for item_id=${unresolved._pendingItemId || '?'}`,
1507
+ );
1508
+ finish();
1509
+ break;
1510
+ }
1511
+ midState.sawCompleted = true;
1512
+ done = true;
1513
+ finish();
1514
+ break;
1515
+ }
1516
+ case 'response.done': {
1517
+ // response.done is the terminal frame for some Codex
1518
+ // streams that never emit a separate response.completed.
1519
+ // Route through the same completed/failed/incomplete
1520
+ // normalization based on event.response.status so a
1521
+ // server-side abort (incomplete / failed) does not slip
1522
+ // through as success. status === 'completed' falls
1523
+ // through to the success path with sawCompleted set;
1524
+ // anything else is converted into a terminal error.
1525
+ const status = event.response?.status || '';
1526
+ if (status === 'failed') {
1527
+ midState.responseFailedPayload = event;
1528
+ const msg = event.response?.error?.message
1529
+ || event.error?.message
1530
+ || event.message
1531
+ || 'response.done failed';
1532
+ terminalError = Object.assign(
1533
+ new Error(`${errLabel} response.done failed: ${msg}`),
1534
+ { responseFailed: event },
1535
+ );
1536
+ populateHttpStatusFromMessage(terminalError, msg);
1537
+ done = true;
1538
+ finish();
1539
+ break;
1540
+ }
1541
+ if (status === 'incomplete') {
1542
+ const reasonStr = _incompleteReasonFromEvent(event);
1543
+ if (_isMaxOutputIncompleteReason(reasonStr)) {
1544
+ incompleteReason = reasonStr;
1545
+ stopReason = 'length';
1546
+ midState.sawCompleted = true;
1547
+ done = true;
1548
+ finish();
1549
+ break;
1550
+ }
1551
+ terminalError = Object.assign(
1552
+ new Error(`${errLabel} response.done incomplete: ${reasonStr}`),
1553
+ { responseIncomplete: event, incompleteReason: reasonStr },
1554
+ );
1555
+ done = true;
1556
+ finish();
1557
+ break;
1558
+ }
1559
+ if (status && status !== 'completed') {
1560
+ terminalError = Object.assign(
1561
+ new Error(`${errLabel} response.done unexpected status: ${status}`),
1562
+ { responseDoneStatus: status },
1563
+ );
1564
+ done = true;
1565
+ finish();
1566
+ break;
1567
+ }
1568
+ midState.sawCompleted = true;
1569
+ done = true;
1570
+ finish();
1571
+ break;
1572
+ }
1573
+ case 'response.incomplete': {
1574
+ // Most incomplete reasons are real failures. max_output_tokens
1575
+ // maps cleanly to Anthropic's stop_reason=max_tokens; treating
1576
+ // it as an error makes Claude retry the same over-budget turn.
1577
+ const reasonStr = _incompleteReasonFromEvent(event);
1578
+ if (_isMaxOutputIncompleteReason(reasonStr)) {
1579
+ incompleteReason = reasonStr;
1580
+ stopReason = 'length';
1581
+ midState.sawCompleted = true;
1582
+ done = true;
1583
+ finish();
1584
+ break;
1585
+ }
1586
+ terminalError = Object.assign(
1587
+ new Error(`${errLabel} response.incomplete: ${reasonStr}`),
1588
+ { responseIncomplete: event, incompleteReason: reasonStr },
1589
+ );
1590
+ finish();
1591
+ break;
1592
+ }
1593
+ case 'response.failed': {
1594
+ // Stash the payload so the mid-stream classifier can sniff
1595
+ // network_error / stream_disconnected without re-parsing.
1596
+ midState.responseFailedPayload = event;
1597
+ const msg = event.response?.error?.message
1598
+ || event.error?.message
1599
+ || event.message
1600
+ || 'response.failed';
1601
+ terminalError = Object.assign(new Error(`${errLabel} response.failed: ${msg}`), {
1602
+ responseFailed: event,
1603
+ });
1604
+ // Sniff the server message for transient/auth/permanent
1605
+ // hints so the handshake / mid-stream retry classifiers
1606
+ // can route by httpStatus. Without this, server-side
1607
+ // events like "Our servers are currently overloaded"
1608
+ // surfaced as unclassified errors and skipped the
1609
+ // 5xx retry bucket entirely.
1610
+ populateHttpStatusFromMessage(terminalError, msg);
1611
+ finish();
1612
+ break;
1613
+ }
1614
+ case 'error': {
1615
+ const errMsg = String(event.message || event.error?.message || 'unknown');
1616
+ terminalError = new Error(`${errLabel} error: ${errMsg}`);
1617
+ populateHttpStatusFromMessage(terminalError, errMsg);
1618
+ finish();
1619
+ break;
1620
+ }
1621
+ default:
1622
+ // Catch any other reasoning-delta variants (e.g.
1623
+ // `response.reasoning.<sub>.delta`) so they are counted
1624
+ // and suppressed, never reaching the user content buffer.
1625
+ if (typeof event.type === 'string'
1626
+ && event.type.startsWith('response.reasoning')
1627
+ && event.type.endsWith('.delta')) {
1628
+ reasoningOtherDeltaCount += 1;
1629
+ }
1630
+ // Trace-only events (response.in_progress, etc.)
1631
+ break;
1632
+ }
1633
+ };
1634
+ closeHandler = (code, reason) => {
1635
+ if (done) return;
1636
+ midState.wsCloseCode = code;
1637
+ if (!terminalError) {
1638
+ const r = reason?.toString?.('utf-8') || '';
1639
+ const httpStatus = _httpStatusFromWsClose(code, r);
1640
+ terminalError = Object.assign(
1641
+ new Error(`Codex WS closed before response.completed (code=${code}${r ? `, reason=${r}` : ''})`),
1642
+ { wsCloseCode: code, wsCloseReason: r, ...(httpStatus ? { httpStatus } : {}) },
1643
+ );
1644
+ } else if (terminalError && !terminalError.wsCloseCode) {
1645
+ try { terminalError.wsCloseCode = code; } catch {}
1646
+ try { terminalError.httpStatus = terminalError.httpStatus || _httpStatusFromWsClose(code, reason?.toString?.('utf-8') || ''); } catch {}
1647
+ }
1648
+ finish();
1649
+ };
1650
+ errorHandler = (err) => {
1651
+ if (done) return;
1652
+ const wrapped = err instanceof Error ? err : new Error(String(err));
1653
+ if (terminalError) {
1654
+ // Preserve the first terminalError; chain the later socket
1655
+ // error in via `cause` (or `suppressed` if cause already set)
1656
+ // so diagnostics keep the original failure visible.
1657
+ try {
1658
+ if (!terminalError.cause) terminalError.cause = wrapped;
1659
+ else {
1660
+ const list = Array.isArray(terminalError.suppressed)
1661
+ ? terminalError.suppressed
1662
+ : [];
1663
+ list.push(wrapped);
1664
+ terminalError.suppressed = list;
1665
+ }
1666
+ } catch {}
1667
+ } else {
1668
+ terminalError = wrapped;
1669
+ }
1670
+ try { socket.close(4001, 'stream_error'); } catch {}
1671
+ finish();
1672
+ };
1673
+ if (externalSignal) {
1674
+ abortHandler = () => {
1675
+ if (done) return;
1676
+ const reason = externalSignal.reason;
1677
+ terminalError = reason instanceof Error ? reason : new Error('Codex WS aborted by session close');
1678
+ // Tag: was this a user/caller abort, or a watchdog abort?
1679
+ // Mid-stream retry must skip user aborts but may retry watchdog
1680
+ // aborts. The caller-owned AbortController surfaces through
1681
+ // externalSignal; bridge-stall-watchdog signals via a reason
1682
+ // object whose name === 'BridgeStallAbortError'. stream-watchdog
1683
+ // uses StreamStalledAbortError. Anything else → treat as user.
1684
+ const reasonName = reason?.name || '';
1685
+ if (reasonName === 'BridgeStallAbortError'
1686
+ || reasonName === 'StreamStalledAbortError') {
1687
+ midState.watchdogAbort = reasonName;
1688
+ } else {
1689
+ midState.userAbort = true;
1690
+ }
1691
+ try { socket.close(4002, 'aborted'); } catch {}
1692
+ finish();
1693
+ };
1694
+ if (externalSignal.aborted) { abortHandler(); return; }
1695
+ externalSignal.addEventListener('abort', abortHandler, { once: true });
1696
+ }
1697
+ socket.on('message', messageHandler);
1698
+ socket.on('close', closeHandler);
1699
+ socket.on('error', errorHandler);
1700
+ armPreStreamWatchdog();
1701
+ // Periodic client-side WS ping while the stream is active. Codex's
1702
+ // server closes with 1011 "keepalive ping timeout" when it thinks the
1703
+ // peer is silent during long reasoning windows where no data frames
1704
+ // flow. Sending a ping every 18s from our side keeps the socket warm.
1705
+ // The interval is unref'd so it never holds the event loop open, and
1706
+ // cleanup() clears it on every terminal path (completed / close /
1707
+ // error / abort / mid-stream retry teardown).
1708
+ keepaliveTimer = setInterval(() => {
1709
+ try {
1710
+ if (socket.readyState !== WebSocket.OPEN) return;
1711
+ socket.ping();
1712
+ } catch {}
1713
+ }, 18_000);
1714
+ try { keepaliveTimer.unref?.(); } catch {}
1715
+ });
1716
+ }
1717
+
1718
+ /**
1719
+ * Classify a handshake error for retry eligibility.
1720
+ *
1721
+ * Default-deny: anything we don't recognize as transient returns null (treat
1722
+ * as permanent). Permanent buckets (401/403/404/429) also return null — the
1723
+ * server has made a deterministic decision that a retry can't change.
1724
+ *
1725
+ * Returns one of:
1726
+ * 'timeout' — `ws` handshakeTimeout fired
1727
+ * 'reset' — ECONNRESET / socket hang up
1728
+ * 'dns' — EAI_AGAIN / ENOTFOUND / EAI_NODATA
1729
+ * 'refused' — ECONNREFUSED
1730
+ * 'network' — ENETUNREACH / EHOSTUNREACH / EPIPE
1731
+ * 'acquire_timeout' — hard client-side open/acquire deadline fired
1732
+ * 'http_5xx' (with specific status e.g. 'http_503') — server overload
1733
+ * null — not retryable
1734
+ */
1735
+ export function _classifyHandshakeError(err) {
1736
+ if (!err) return null;
1737
+ const code = err.code || '';
1738
+ const msg = String(err.message || '');
1739
+ const status = Number(err.httpStatus || 0);
1740
+
1741
+ // Permanent HTTP (auth / quota / not-found) short-circuits.
1742
+ if (status === 401 || status === 403 || status === 404 || status === 429) {
1743
+ return null;
1744
+ }
1745
+ // 5xx transient.
1746
+ if (status >= 500 && status < 600) {
1747
+ return `http_${status}`;
1748
+ }
1749
+
1750
+ // Node errno codes.
1751
+ if (code === 'ECONNRESET') return 'reset';
1752
+ if (code === 'EAI_AGAIN' || code === 'ENOTFOUND' || code === 'EAI_NODATA') return 'dns';
1753
+ if (code === 'ECONNREFUSED') return 'refused';
1754
+ if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT') return 'timeout';
1755
+ if (code === 'EWSACQUIRETIMEOUT') return 'acquire_timeout';
1756
+ if (code === 'ENETUNREACH' || code === 'EHOSTUNREACH' || code === 'EPIPE') return 'network';
1757
+
1758
+ // `ws` library's handshake-timeout path: thrown as a bare Error.
1759
+ if (/opening handshake has timed out/i.test(msg)) return 'timeout';
1760
+ if (/socket hang up/i.test(msg)) return 'reset';
1761
+
1762
+ return null;
1763
+ }
1764
+
1765
+ /**
1766
+ * Classify a mid-stream error for bounded retry eligibility.
1767
+ *
1768
+ * Only fires AFTER `response.created` is observed and BEFORE
1769
+ * `response.completed`. The window is narrow on purpose: retrying a handshake
1770
+ * or a pre-create connect failure is owned by _acquireWithRetry; retrying
1771
+ * after completion would replay a finished turn.
1772
+ *
1773
+ * Retry buckets:
1774
+ * 'bridge_stall' — BridgeStallAbortError from bridge-stall-watchdog
1775
+ * 'stream_stalled' — StreamStalledAbortError from stream-watchdog
1776
+ * 'ws_1006' — abnormal close (connection lost)
1777
+ * 'ws_1011' — server unexpected condition
1778
+ * 'ws_1012' — service restart
1779
+ * 'ws_4000' — our armPreStreamWatchdog close with idle_timeout
1780
+ * 'ws_1000' — server-side normal close fired after response.created
1781
+ * but before response.completed (truncated stream)
1782
+ * 'first_byte_timeout' — post-upgrade-no-first-event: socket opened, our
1783
+ * response.create frame sent, but the server never
1784
+ * emitted response.created within the short
1785
+ * pre-stream deadline. Fast-fail retryable.
1786
+ * 'first_meaningful_timeout' — server ACKed response.created, then emitted
1787
+ * no real text/reasoning/tool progress before the
1788
+ * first-meaningful deadline.
1789
+ * 'response_failed_network' — response.failed with network_error
1790
+ * 'response_failed_disconnected' — response.failed with stream_disconnected
1791
+ *
1792
+ * Deny buckets (return null):
1793
+ * - externalSignal aborted by user (state.userAbort)
1794
+ * - state.sawCompleted === true (already done)
1795
+ * - state.sawResponseCreated === false (still pre-stream; handshake retry
1796
+ * owns that window) — EXCEPT for WS close 1011/1012, which can fire
1797
+ * after the 101 upgrade but before the first response.created event,
1798
+ * AND the pre-`response.created` first-byte timeout
1799
+ * (state.firstByteTimeout), which is permitted a bounded retry here
1800
+ * - HTTP 401 / 403 / 429 surfaced on the error
1801
+ * - state.attemptIndex has reached the classifier-specific retry budget
1802
+ */
1803
+ export function _classifyMidstreamError(err, state) {
1804
+ if (!state) return null;
1805
+ const attemptIndex = state.attemptIndex | 0;
1806
+ // Already completed (shouldn't throw, but defensive).
1807
+ if (state.sawCompleted) return null;
1808
+ // Any tool call already surfaced to the caller — retrying would
1809
+ // normally duplicate the side effect. EXCEPTION: ws_1000 truncation
1810
+ // (server-side normal close after response.created, before completion)
1811
+ // leaves the caller with an orphaned tool_use that the next turn cannot
1812
+ // pair to a tool_result, which the provider rejects with a hard 400.
1813
+ // The duplicate-side-effect risk is preferable to deterministic worker
1814
+ // death, especially for detached bridges that re-dispatch idempotently.
1815
+ if (state.emittedToolCall) {
1816
+ const _cc = Number(err?.wsCloseCode || state.wsCloseCode || 0);
1817
+ if (!(_cc === 1000 && state.sawResponseCreated && !state.sawCompleted)) return null;
1818
+ }
1819
+ // Live-text invariant: once a non-empty text chunk has been relayed to the
1820
+ // client (gateway live mode), the rendered output cannot be withdrawn and a
1821
+ // retry would concatenate a second attempt. Treat every subsequent
1822
+ // mid-stream/truncated failure as final — never retry.
1823
+ if (state.emittedText || err?.liveTextEmitted) return null;
1824
+ // Post-upgrade-no-first-event: the socket opened, our response.create
1825
+ // frame was sent, but the server never emitted a single event before
1826
+ // the short pre-`response.created` watchdog fired. The handshake retry
1827
+ // layer only sees pre-upgrade failures and the legacy pre-stream gate
1828
+ // below would deny this case (sawResponseCreated === false). Tag it
1829
+ // here as a fast retryable bucket so the worker reconnects within
1830
+ // seconds instead of stalling for the full first-meaningful window.
1831
+ if (state.firstByteTimeout || err?.firstByteTimeout) {
1832
+ return _allowMidstreamRetry('first_byte_timeout', attemptIndex);
1833
+ }
1834
+ if (state.firstMeaningfulTimeout || err?.firstMeaningfulTimeout) {
1835
+ return _allowMidstreamRetry('first_meaningful_timeout', attemptIndex);
1836
+ }
1837
+ // _sendFrame failure (socket not OPEN, send callback errored, JSON
1838
+ // serialize threw). Always retryable: caller will forceFresh next
1839
+ // attempt so the wedged socket is dropped.
1840
+ if (err?.wsSendFailed || state.wsSendFailed) {
1841
+ return _allowMidstreamRetry('ws_send_failed', attemptIndex);
1842
+ }
1843
+ // Pre-stream failures normally belong to the handshake retry layer. BUT
1844
+ // WS close 1011 / 1012 can fire after the 101 upgrade but BEFORE the
1845
+ // first response.created event when the server's keepalive times out or
1846
+ // the service restarts. Neither the handshake retry layer (it only sees
1847
+ // pre-upgrade failures) nor the existing mid-stream gate covers this
1848
+ // window, so permit bounded retry here for those two codes only.
1849
+ if (!state.sawResponseCreated) {
1850
+ const closeCode = Number(err?.wsCloseCode || state.wsCloseCode || 0);
1851
+ if (closeCode !== 1011 && closeCode !== 1012) return null;
1852
+ }
1853
+ // User/caller abort — never retry.
1854
+ if (state.userAbort) return null;
1855
+
1856
+ if (!err) return null;
1857
+ const status = Number(err?.httpStatus || 0);
1858
+ if (status === 401 || status === 403 || status === 429) return null;
1859
+ // Transient 5xx surfaced via populateHttpStatusFromMessage (case 'error'
1860
+ // and case 'response.failed' branches sniff server-supplied text like
1861
+ // "Our servers are currently overloaded" and assign httpStatus=503).
1862
+ // Allow one bounded mid-stream retry on the same budget as the WS close-
1863
+ // code buckets above so server-side overload no longer leaks straight
1864
+ // to the caller without a single retry attempt.
1865
+ if (status >= 500 && status < 600) {
1866
+ return _allowMidstreamRetry(`http_${status}`, attemptIndex);
1867
+ }
1868
+
1869
+ const name = err?.name || '';
1870
+ if (name === 'BridgeStallAbortError') return _allowMidstreamRetry('bridge_stall', attemptIndex);
1871
+ if (name === 'StreamStalledAbortError') return _allowMidstreamRetry('stream_stalled', attemptIndex);
1872
+
1873
+ // Watchdog abort surfaced via externalSignal handler → err is the reason
1874
+ // itself. state.watchdogAbort captures the class name when the error
1875
+ // shape was preserved but the name was stripped by some wrapper.
1876
+ if (state.watchdogAbort === 'BridgeStallAbortError') return _allowMidstreamRetry('bridge_stall', attemptIndex);
1877
+ if (state.watchdogAbort === 'StreamStalledAbortError') return _allowMidstreamRetry('stream_stalled', attemptIndex);
1878
+
1879
+ // WS close codes: prefer the decorated property, fall back to state.
1880
+ const closeCode = Number(err?.wsCloseCode || state.wsCloseCode || 0);
1881
+ if (closeCode === 1006) return _allowMidstreamRetry('ws_1006', attemptIndex);
1882
+ if (closeCode === 1011) return _allowMidstreamRetry('ws_1011', attemptIndex);
1883
+ if (closeCode === 1012) return _allowMidstreamRetry('ws_1012', attemptIndex);
1884
+ // Private 4xxx codes from a server/proxy are auth/policy/application closes;
1885
+ // never treat them as transient. 4000 is our local pre-stream watchdog code.
1886
+ if (closeCode >= 4000 && closeCode < 5000 && closeCode !== 4000) return null;
1887
+ if (closeCode === 4000) return _allowMidstreamRetry('ws_4000', attemptIndex);
1888
+ // Server-side normal close (1000) AFTER response.created but BEFORE
1889
+ // response.completed = truncated stream; legitimate transient. The
1890
+ // pre-stream gate above already rejects 1000 before sawResponseCreated
1891
+ // (handshake retry layer owns that window).
1892
+ if (closeCode === 1000 && state.sawResponseCreated && !state.sawCompleted) return _allowMidstreamRetry('ws_1000', attemptIndex);
1893
+
1894
+ // response.failed payload mentioning network_error / stream_disconnected.
1895
+ // xAI's gRPC backend periodically rotates auth context (server-side TTL)
1896
+ // and surfaces "Auth context expired" as a response.failed event. The
1897
+ // attemptIndex > 0 path in sendViaWebSocket forces a fresh WS handshake,
1898
+ // which re-authenticates — so a single bounded retry recovers the turn
1899
+ // instead of letting the worker die mid-session.
1900
+ const failed = err?.responseFailed || state.responseFailedPayload;
1901
+ if (failed) {
1902
+ try {
1903
+ const blob = JSON.stringify(failed).toLowerCase();
1904
+ if (blob.includes('stream_disconnected')) return _allowMidstreamRetry('response_failed_disconnected', attemptIndex);
1905
+ if (blob.includes('network_error')) return _allowMidstreamRetry('response_failed_network', attemptIndex);
1906
+ if (blob.includes('auth context expired')) return _allowMidstreamRetry('response_failed_auth_expired', attemptIndex);
1907
+ } catch {}
1908
+ }
1909
+
1910
+ // Unknown → default-deny (don't risk a second full-cost turn for an error
1911
+ // class we haven't proven is transient).
1912
+ return null;
1913
+ }
1914
+
1915
+ function _midstreamRetryLimit(classifier) {
1916
+ return classifier === 'ws_1006' || classifier === 'ws_1011'
1917
+ ? MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT
1918
+ : MIDSTREAM_DEFAULT_RETRY_LIMIT;
1919
+ }
1920
+
1921
+ function _allowMidstreamRetry(classifier, attemptIndex) {
1922
+ return attemptIndex < _midstreamRetryLimit(classifier) ? classifier : null;
1923
+ }
1924
+
1925
+ function _midstreamBackoffFor(retryNumber) {
1926
+ return MIDSTREAM_BACKOFF_MS[Math.min(Math.max(retryNumber, 1), MIDSTREAM_BACKOFF_MS.length) - 1];
1927
+ }
1928
+
1929
+ function _backoffFor(attempt) {
1930
+ // attempt is 1-based. retry 1 → 500, retry 2 → 1000, retry 3 → 2000 … capped.
1931
+ const raw = HANDSHAKE_BACKOFF_BASE_MS * (1 << (attempt - 1));
1932
+ return jitterDelayMs(Math.min(raw, HANDSHAKE_BACKOFF_CAP_MS));
1933
+ }
1934
+
1935
+ const _defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
1936
+
1937
+ async function _sleepWithAbort(ms, externalSignal, sleepFn = _defaultSleep) {
1938
+ if (!ms) return;
1939
+ if (!externalSignal) {
1940
+ await sleepFn(ms);
1941
+ return;
1942
+ }
1943
+ await new Promise((resolve, reject) => {
1944
+ const t = setTimeout(() => {
1945
+ externalSignal.removeEventListener('abort', onAbort);
1946
+ resolve();
1947
+ }, ms);
1948
+ const onAbort = () => {
1949
+ clearTimeout(t);
1950
+ const reason = externalSignal.reason;
1951
+ reject(reason instanceof Error ? reason : new Error('Codex WS retry backoff aborted'));
1952
+ };
1953
+ if (externalSignal.aborted) { onAbort(); return; }
1954
+ externalSignal.addEventListener('abort', onAbort, { once: true });
1955
+ });
1956
+ }
1957
+
1958
+ /**
1959
+ * Run `_acquire({auth, poolKey, cacheKey})` with bounded exponential-backoff
1960
+ * retry on transient handshake failures. The injection seams (`_acquire`,
1961
+ * `_sleepFn`, `onRetry`) let unit tests drive the state machine without
1962
+ * opening real sockets.
1963
+ *
1964
+ * On exhaustion the thrown error is tagged with:
1965
+ * err.attempts — 1..HANDSHAKE_MAX_ATTEMPTS
1966
+ * err.retryClassifier — final classifier string, or null for permanent
1967
+ */
1968
+ export async function _acquireWithRetry({
1969
+ auth,
1970
+ poolKey,
1971
+ cacheKey,
1972
+ forceFresh,
1973
+ onRetry,
1974
+ externalSignal,
1975
+ _acquire = acquireWebSocket,
1976
+ _sleepFn = _defaultSleep,
1977
+ } = {}) {
1978
+ let lastErr = null;
1979
+ let lastClassifier = null;
1980
+ for (let attempt = 1; attempt <= HANDSHAKE_MAX_ATTEMPTS; attempt++) {
1981
+ if (externalSignal?.aborted) {
1982
+ const reason = externalSignal.reason;
1983
+ throw reason instanceof Error ? reason : new Error('Codex WS acquire aborted');
1984
+ }
1985
+ try {
1986
+ if (attempt > 1) {
1987
+ if (process.env.MIXDOG_DEBUG_BRIDGE) {
1988
+ process.stderr.write(`[bridge-trace] ws-handshake-attempt n=${attempt}\n`);
1989
+ }
1990
+ }
1991
+ return await _acquire({ auth, poolKey, cacheKey, forceFresh, externalSignal });
1992
+ } catch (err) {
1993
+ lastErr = err;
1994
+ const classifier = _classifyHandshakeError(err);
1995
+ lastClassifier = classifier;
1996
+ // Permanent (or unknown → default-deny): stop immediately.
1997
+ if (!classifier) {
1998
+ if (err && typeof err === 'object') {
1999
+ try { err.attempts = attempt; } catch {}
2000
+ try { err.retryClassifier = null; } catch {}
2001
+ }
2002
+ throw err;
2003
+ }
2004
+ // Transient but exhausted: surface with tagging.
2005
+ if (attempt >= HANDSHAKE_MAX_ATTEMPTS) {
2006
+ if (err && typeof err === 'object') {
2007
+ try { err.attempts = attempt; } catch {}
2008
+ try { err.retryClassifier = classifier; } catch {}
2009
+ }
2010
+ try {
2011
+ process.stderr.write(
2012
+ `[openai-oauth-ws] handshake failed after ${attempt}/${HANDSHAKE_MAX_ATTEMPTS} attempts: ${err?.message || err}\n`,
2013
+ );
2014
+ } catch {}
2015
+ throw err;
2016
+ }
2017
+ // Schedule backoff and emit progress.
2018
+ const backoff = _backoffFor(attempt);
2019
+ try {
2020
+ process.stderr.write(
2021
+ `[openai-oauth-ws] worker retry ${attempt}/${HANDSHAKE_MAX_ATTEMPTS} (transient: ${classifier}, backoff ${backoff}ms)\n`,
2022
+ );
2023
+ } catch {}
2024
+ try {
2025
+ onRetry?.({
2026
+ attempt,
2027
+ max: HANDSHAKE_MAX_ATTEMPTS,
2028
+ classifier,
2029
+ backoffMs: backoff,
2030
+ error: err,
2031
+ });
2032
+ } catch {}
2033
+ // Sleep is abort-aware: an abort during backoff rejects immediately
2034
+ // instead of burning the remaining wait.
2035
+ if (externalSignal) {
2036
+ await new Promise((resolve, reject) => {
2037
+ const t = setTimeout(() => {
2038
+ externalSignal.removeEventListener('abort', onAbort);
2039
+ resolve();
2040
+ }, backoff);
2041
+ const onAbort = () => {
2042
+ clearTimeout(t);
2043
+ const reason = externalSignal.reason;
2044
+ reject(reason instanceof Error ? reason : new Error('Codex WS acquire aborted'));
2045
+ };
2046
+ if (externalSignal.aborted) { onAbort(); return; }
2047
+ externalSignal.addEventListener('abort', onAbort, { once: true });
2048
+ });
2049
+ } else {
2050
+ await _sleepFn(backoff);
2051
+ }
2052
+ }
2053
+ }
2054
+ // Unreachable — the loop either returns or throws above — but keep the
2055
+ // typing honest.
2056
+ if (lastErr && typeof lastErr === 'object') {
2057
+ try { lastErr.attempts = HANDSHAKE_MAX_ATTEMPTS; } catch {}
2058
+ try { lastErr.retryClassifier = lastClassifier; } catch {}
2059
+ }
2060
+ throw lastErr || new Error('acquireWithRetry: unreachable');
2061
+ }
2062
+
2063
+ /**
2064
+ * Dispatch one tool-loop iteration over a per-session cached WebSocket.
2065
+ * Returns the same shape as the SSE path: { content, model, toolCalls, usage }.
2066
+ */
2067
+ export async function sendViaWebSocket({
2068
+ auth,
2069
+ body,
2070
+ sendOpts,
2071
+ onStreamDelta,
2072
+ onToolCall,
2073
+ onTextDelta,
2074
+ onStageChange,
2075
+ externalSignal,
2076
+ poolKey,
2077
+ cacheKey,
2078
+ iteration,
2079
+ useModel,
2080
+ displayModel,
2081
+ forceFresh = false,
2082
+ includeResponseId = false,
2083
+ traceProvider = 'openai-oauth',
2084
+ logSuppressedReasoningDeltas = true,
2085
+ warmupBody = null,
2086
+ // Test seams (undefined in production). Let the unit test drive the
2087
+ // retry state machine without opening real sockets or touching the
2088
+ // handshake-retry layer.
2089
+ _acquireWithRetryFn = _acquireWithRetry,
2090
+ _streamFn = _streamResponse,
2091
+ _sendFrameFn = _sendFrame,
2092
+ _sleepFn = _defaultSleep,
2093
+ }) {
2094
+ // Bounded mid-stream retry: if an attempt's stream dies after
2095
+ // response.created but before response.completed from a transient cause
2096
+ // (watchdog abort / ws 1006/1011/1012/4000 / response.failed with network
2097
+ // error), tear down the socket and reissue the full request from scratch
2098
+ // with a classifier-specific budget. ws_1006/ws_1011 get two retries with
2099
+ // 250ms/1s backoff; other legacy transient buckets keep the prior one retry.
2100
+ // No delta resume — content restarts, which is the accepted tradeoff for
2101
+ // reviewer/worker flows that need the complete answer.
2102
+ // Retries are layered ABOVE the handshake retry loop (_acquireWithRetry
2103
+ // owns connect-level transience); the two never interleave because we
2104
+ // force a brand-new acquire for the retry attempt.
2105
+ const MAX_MIDSTREAM_RETRIES = MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT;
2106
+ let firstAttemptError = null;
2107
+ let firstAttemptClassifier = null;
2108
+ // Live-text invariant across attempts: once ANY attempt has relayed a
2109
+ // non-empty text chunk to the client, no error thrown out of this function
2110
+ // may omit the liveTextEmitted/unsafeToRetry markers — otherwise an
2111
+ // upstream gate (auth-refresh retry, HTTP fallback, shared withRetry)
2112
+ // could reissue the turn and concatenate a second attempt onto
2113
+ // already-rendered output. A text-emitting attempt is never retry-eligible
2114
+ // (_classifyMidstreamError returns null on emittedText), so the surfaced
2115
+ // error is frequently an EARLIER attempt's firstAttemptError that never saw
2116
+ // the marker; _stampLiveText re-applies it on every throw path.
2117
+ let liveTextEmittedAcrossAttempts = false;
2118
+ const _stampLiveText = (e) => {
2119
+ if (liveTextEmittedAcrossAttempts && e) {
2120
+ try { e.liveTextEmitted = true; e.unsafeToRetry = true; } catch {}
2121
+ }
2122
+ return e;
2123
+ };
2124
+ // Server-side xAI conversation anchor preserved across mid-stream
2125
+ // retries. xAI keys its conversation by previous_response_id alone
2126
+ // (sessionToken is null for xAI in _mintSessionToken); a forceFresh
2127
+ // socket on retry would otherwise drop prev_id and cold-start a new
2128
+ // server-side conversation, evicting every prefix the prior attempts
2129
+ // warmed. Codex / openai-direct anchor by per-socket session_id, where
2130
+ // this carry-forward would not help and is therefore gated to xAI.
2131
+ let carryForwardCache = null;
2132
+ const emittedProgress = [];
2133
+
2134
+ return await _withOpenAiPromptCacheLane({
2135
+ auth,
2136
+ cacheKey,
2137
+ sendOpts,
2138
+ poolKey,
2139
+ iteration,
2140
+ traceProvider,
2141
+ useModel,
2142
+ externalSignal,
2143
+ }, async (promptCacheLane) => {
2144
+ for (let attemptIndex = 0; attemptIndex <= MAX_MIDSTREAM_RETRIES; attemptIndex++) {
2145
+ const handshakeStart = Date.now();
2146
+ let acquired;
2147
+ let handshakeRetries = 0;
2148
+ const handshakeRetryClassifiers = [];
2149
+ try { onStageChange?.('requesting'); } catch {}
2150
+ try {
2151
+ acquired = await _acquireWithRetryFn({
2152
+ auth,
2153
+ poolKey,
2154
+ cacheKey,
2155
+ // Retry attempt must not reuse a pooled socket — the prior
2156
+ // one is either torn down or in an unknown state.
2157
+ forceFresh: forceFresh || attemptIndex > 0,
2158
+ externalSignal,
2159
+ onRetry: (info) => {
2160
+ handshakeRetries += 1;
2161
+ if (info?.classifier) handshakeRetryClassifiers.push(info.classifier);
2162
+ },
2163
+ });
2164
+ } catch (err) {
2165
+ const classifier = err?.retryClassifier || (err?.code === 'EWSACQUIRETIMEOUT' ? 'acquire_timeout' : null);
2166
+ const classifiers = [...handshakeRetryClassifiers];
2167
+ if (classifier && !classifiers.includes(classifier)) classifiers.push(classifier);
2168
+ if (err?.httpStatus != null || classifier || handshakeRetries > 0 || classifiers.length > 0) {
2169
+ traceBridgeFetch({
2170
+ sessionId: poolKey,
2171
+ headersMs: Date.now() - handshakeStart,
2172
+ httpStatus: Number(err?.httpStatus || 0),
2173
+ provider: traceProvider,
2174
+ model: useModel,
2175
+ transport: 'websocket',
2176
+ handshakeRetries: err?.attempts ? Math.max(Number(err.attempts) - 1, 0) : handshakeRetries,
2177
+ handshakeRetryClassifiers: classifiers,
2178
+ });
2179
+ }
2180
+ // Handshake-layer failure. Don't double-wrap: if this is the retry
2181
+ // attempt, surface the ORIGINAL first-attempt error (which is what
2182
+ // the caller's turn actually tripped on).
2183
+ if (attemptIndex > 0 && firstAttemptError) {
2184
+ try { firstAttemptError.midstreamRetries = attemptIndex; } catch {}
2185
+ throw _stampLiveText(firstAttemptError);
2186
+ }
2187
+ throw _stampLiveText(err);
2188
+ }
2189
+ const { entry, reused } = acquired;
2190
+ // Re-seed the retry attempt's fresh entry with the prior attempt's
2191
+ // last successful anchor so _computeDelta sees a non-null
2192
+ // lastInputPrefixHash and prev_response_id, keeping the same xAI
2193
+ // conversation slot warm instead of cold-starting one per retry.
2194
+ if (carryForwardCache && auth?.type === 'xai' && !reused) {
2195
+ entry.lastResponseId = carryForwardCache.lastResponseId;
2196
+ entry.lastInputPrefixHash = carryForwardCache.lastInputPrefixHash;
2197
+ entry.lastInputLen = carryForwardCache.lastInputLen;
2198
+ entry.lastRequestSansInput = carryForwardCache.lastRequestSansInput;
2199
+ entry.lastRequestInput = carryForwardCache.lastRequestInput;
2200
+ entry.lastResponseItems = carryForwardCache.lastResponseItems;
2201
+ }
2202
+ traceBridgeFetch({
2203
+ sessionId: poolKey,
2204
+ headersMs: Date.now() - handshakeStart,
2205
+ httpStatus: reused ? 0 : 101,
2206
+ provider: traceProvider,
2207
+ model: useModel,
2208
+ transport: 'websocket',
2209
+ handshakeRetries,
2210
+ handshakeRetryClassifiers,
2211
+ });
2212
+
2213
+ let requestBody = body;
2214
+ // Mid-stream retry: pin prev_id in the body so _computeDelta's
2215
+ // mode='full' fallback (triggered when the carried prefix hash no
2216
+ // longer matches the current input) still carries the conversation
2217
+ // anchor. The delta path overwrites this from entry.lastResponseId,
2218
+ // which equals the carried value, so the two paths agree.
2219
+ if (carryForwardCache && auth?.type === 'xai' && attemptIndex > 0 && !body.previous_response_id) {
2220
+ requestBody = { ...body, previous_response_id: carryForwardCache.lastResponseId };
2221
+ }
2222
+ let warmupResult = null;
2223
+ // midState is shared between warmup and the main stream so warmup
2224
+ // failures (first-byte timeout, send-failure, ws_4000) flow through
2225
+ // the SAME mid-stream classifier as the main send. A wedged warmup
2226
+ // socket must not bypass the retry loop and surface raw to the
2227
+ // caller — release the entry, force a fresh acquire, and retry.
2228
+ const midState = {
2229
+ attemptIndex,
2230
+ sawResponseCreated: false,
2231
+ sawCompleted: false,
2232
+ // Gateway live-text relay invariant (see _streamResponse): set once
2233
+ // a non-empty text chunk has been forwarded to the client.
2234
+ emittedText: false,
2235
+ sessionId: poolKey,
2236
+ iteration,
2237
+ model: useModel,
2238
+ traceProvider,
2239
+ };
2240
+ const sseStart = Date.now();
2241
+ let mode = 'full';
2242
+ let frame = null;
2243
+ let deltaTokens = 0;
2244
+ let deltaReason = null;
2245
+ let strippedResponseItems = 0;
2246
+ let skippedResponseItems = 0;
2247
+ let result;
2248
+ const streamTimeouts = sendOpts?.expectCompaction === true
2249
+ ? {
2250
+ firstMeaningfulMs: WS_INTER_CHUNK_MS,
2251
+ interChunkMs: WS_INTER_CHUNK_MS,
2252
+ }
2253
+ : null;
2254
+ try {
2255
+ if (warmupBody && typeof warmupBody === 'object' && attemptIndex === 0) {
2256
+ const warmupFrame = { type: 'response.create', ...warmupBody };
2257
+ await _sendFrameFn(entry, warmupFrame);
2258
+ const warmupStart = Date.now();
2259
+ const warmupState = {
2260
+ attemptIndex,
2261
+ sawResponseCreated: false,
2262
+ sawCompleted: false,
2263
+ sessionId: poolKey,
2264
+ iteration,
2265
+ model: useModel,
2266
+ traceProvider,
2267
+ warmup: true,
2268
+ };
2269
+ warmupResult = await _streamFn({
2270
+ entry,
2271
+ externalSignal,
2272
+ onStreamDelta: null,
2273
+ onToolCall: null,
2274
+ state: warmupState,
2275
+ logSuppressedReasoningDeltas,
2276
+ traceProvider,
2277
+ _timeouts: streamTimeouts,
2278
+ });
2279
+ // Surface warmup-time first-event timeout / send-failure
2280
+ // flags onto the shared midState so the outer catch's
2281
+ // classifier sees them. (warmupResult itself only resolves
2282
+ // on success; failures throw and skip this block.)
2283
+ if (warmupState.firstByteTimeout) midState.firstByteTimeout = true;
2284
+ if (warmupState.wsSendFailed) midState.wsSendFailed = true;
2285
+ if (!warmupResult?.responseId) {
2286
+ throw new Error('Responses WS warmup completed without response id');
2287
+ }
2288
+ entry.lastResponseId = warmupResult.responseId;
2289
+ entry.lastRequestSansInput = _stableStringify(_sansInput(warmupBody));
2290
+ const warmupInputArr = Array.isArray(warmupBody.input) ? warmupBody.input : [];
2291
+ entry.lastRequestInput = _cloneJson(warmupInputArr);
2292
+ entry.lastResponseItems = _cloneJson(Array.isArray(warmupResult.responseItems) ? warmupResult.responseItems : []);
2293
+ entry.lastInputLen = warmupInputArr.length;
2294
+ entry.lastInputPrefixHash = createHash('sha256')
2295
+ .update(JSON.stringify(warmupInputArr))
2296
+ .digest('hex');
2297
+ try {
2298
+ const warmupPayload = {
2299
+ provider: traceProvider,
2300
+ transport: 'websocket',
2301
+ event: 'warmup_completed',
2302
+ response_id: warmupResult.responseId,
2303
+ elapsed_ms: Date.now() - warmupStart,
2304
+ input_tokens: warmupResult.usage?.inputTokens || 0,
2305
+ cached_tokens: warmupResult.usage?.cachedTokens || 0,
2306
+ output_tokens: warmupResult.usage?.outputTokens || 0,
2307
+ prompt_tokens: warmupResult.usage?.promptTokens || 0,
2308
+ };
2309
+ appendBridgeTrace({
2310
+ sessionId: poolKey,
2311
+ iteration,
2312
+ kind: 'cache_warmup',
2313
+ ...warmupPayload,
2314
+ payload: warmupPayload,
2315
+ });
2316
+ } catch {}
2317
+ requestBody = { ...body, previous_response_id: warmupResult.responseId };
2318
+ delete requestBody.instructions;
2319
+ delete requestBody.generate;
2320
+ entry.lastRequestSansInput = _stableStringify(_sansInput({
2321
+ ...requestBody,
2322
+ input: warmupInputArr,
2323
+ }));
2324
+ }
2325
+
2326
+ const delta = _computeDelta({ entry, body: requestBody });
2327
+ ({ mode, frame } = delta);
2328
+ deltaReason = delta.reason || null;
2329
+ strippedResponseItems = delta.strippedResponseItems || 0;
2330
+ skippedResponseItems = delta.skippedResponseItems || 0;
2331
+ deltaTokens = _estimateFrameTokens(frame);
2332
+
2333
+ // Re-check abort after acquire/warmup — narrow window where
2334
+ // externalSignal could fire between successful acquire and
2335
+ // send(). Without this gate an aborted request could still
2336
+ // emit one frame to the provider.
2337
+ if (externalSignal?.aborted) {
2338
+ // Preserve the abort reason (Error) so downstream
2339
+ // classification (userAbort vs. generic) survives — a bare
2340
+ // new Error('Aborted') would erase that signal.
2341
+ const reason = externalSignal.reason;
2342
+ throw reason instanceof Error ? reason : new Error('Aborted');
2343
+ }
2344
+ await _sendFrameFn(entry, frame);
2345
+
2346
+ if (process.env.MIXDOG_DEBUG_BRIDGE) {
2347
+ process.stderr.write(`[bridge-trace] ws-streaming-start sinceAcquire=${Date.now() - handshakeStart}ms\n`);
2348
+ }
2349
+ try { onStageChange?.('streaming'); } catch {}
2350
+ result = await _streamFn({
2351
+ entry,
2352
+ externalSignal,
2353
+ onStreamDelta,
2354
+ onToolCall,
2355
+ onTextDelta,
2356
+ state: midState,
2357
+ logSuppressedReasoningDeltas,
2358
+ traceProvider,
2359
+ _timeouts: streamTimeouts,
2360
+ });
2361
+ } catch (err) {
2362
+ // Snapshot the xAI conversation anchor BEFORE releasing the
2363
+ // entry. release closes the socket but leaves state fields
2364
+ // intact; the next forceFresh acquire creates a new entry into
2365
+ // which we manually carry the anchor so the retry continues the
2366
+ // same conversation instead of cold-starting one.
2367
+ if (auth?.type === 'xai' && entry.lastResponseId) {
2368
+ carryForwardCache = {
2369
+ lastResponseId: entry.lastResponseId,
2370
+ lastInputPrefixHash: entry.lastInputPrefixHash,
2371
+ lastInputLen: entry.lastInputLen,
2372
+ lastRequestSansInput: entry.lastRequestSansInput,
2373
+ lastRequestInput: entry.lastRequestInput,
2374
+ lastResponseItems: entry.lastResponseItems,
2375
+ };
2376
+ }
2377
+ releaseWebSocket({ entry, poolKey, keep: false });
2378
+ // Mid-stream classification.
2379
+ // Live-text invariant: a non-empty chunk already relayed to the
2380
+ // client cannot be withdrawn. Tag the error so the upstream HTTP
2381
+ // fallback gate also refuses to re-issue and concatenate attempts.
2382
+ if (midState.emittedText) {
2383
+ // Latch across attempts: even though THIS error is never
2384
+ // retry-eligible once text is out, a later/earlier surfaced
2385
+ // error (firstAttemptError) must still carry the marker.
2386
+ liveTextEmittedAcrossAttempts = true;
2387
+ }
2388
+ _stampLiveText(err);
2389
+ const classifier = _classifyMidstreamError(err, midState);
2390
+ const retryLimit = classifier ? _midstreamRetryLimit(classifier) : 0;
2391
+ if (classifier && attemptIndex < retryLimit) {
2392
+ // Retry-eligible: stash the first-attempt error, emit progress,
2393
+ // and loop. The subsequent acquire uses forceFresh so no socket
2394
+ // is shared between attempts.
2395
+ firstAttemptError = err;
2396
+ firstAttemptClassifier = classifier;
2397
+ try { err.midstreamClassifier = classifier; } catch {}
2398
+ const retryNumber = attemptIndex + 1;
2399
+ const backoff = _midstreamBackoffFor(retryNumber);
2400
+ try {
2401
+ const line = `[openai-oauth-ws] mid-stream recovered: retry ${retryNumber}/${retryLimit} (cause: ${classifier}, backoff ${backoff}ms)\n`;
2402
+ process.stderr.write(line);
2403
+ emittedProgress.push(line);
2404
+ } catch {}
2405
+ await _sleepWithAbort(backoff, externalSignal, _sleepFn);
2406
+ continue;
2407
+ }
2408
+ // Not retryable, OR we've already exhausted the retry budget.
2409
+ if (attemptIndex > 0 && firstAttemptError) {
2410
+ // Exhausted path: surface the first-attempt error (the one
2411
+ // the user's turn actually tripped on), tag actual retry count.
2412
+ try { firstAttemptError.midstreamRetries = attemptIndex; } catch {}
2413
+ try { firstAttemptError.midstreamClassifier = firstAttemptClassifier; } catch {}
2414
+ // Attach the retry attempt's error so post-mortem diagnostics
2415
+ // can see WHY the retry also failed instead of silently
2416
+ // dropping it. Use `cause` if free, else `suppressed`.
2417
+ try {
2418
+ if (!firstAttemptError.cause) firstAttemptError.cause = err;
2419
+ else {
2420
+ const list = Array.isArray(firstAttemptError.suppressed)
2421
+ ? firstAttemptError.suppressed
2422
+ : [];
2423
+ list.push(err);
2424
+ firstAttemptError.suppressed = list;
2425
+ }
2426
+ } catch {}
2427
+ throw _stampLiveText(firstAttemptError);
2428
+ }
2429
+ throw _stampLiveText(err);
2430
+ }
2431
+ const liveModel = result.model || useModel;
2432
+ traceBridgeSse({
2433
+ sessionId: poolKey,
2434
+ sseParseMs: Date.now() - sseStart,
2435
+ provider: traceProvider,
2436
+ model: liveModel,
2437
+ transport: 'websocket',
2438
+ });
2439
+
2440
+ const resultToolCallCount = Array.isArray(result.toolCalls) ? result.toolCalls.length : 0;
2441
+ const keepResponseChain = !!result.responseId && !result.incompleteReason;
2442
+ const keepSocket = true;
2443
+
2444
+ // Update cache state for the next iteration in this session. Codex
2445
+ // keeps the previous response anchor even when the model emitted tool
2446
+ // calls: the next request is previous input + server output items
2447
+ // + tool results, and _computeDelta strips the first two parts so the
2448
+ // WebSocket frame only sends the true new tail.
2449
+ if (result.responseId && keepResponseChain) {
2450
+ entry.lastResponseId = result.responseId;
2451
+ entry.lastRequestSansInput = _stableStringify(_sansInput(requestBody));
2452
+ const inputArr = Array.isArray(requestBody.input) ? requestBody.input : [];
2453
+ entry.lastRequestInput = _cloneJson(inputArr);
2454
+ entry.lastResponseItems = _cloneJson(Array.isArray(result.responseItems) ? result.responseItems : []);
2455
+ entry.lastInputLen = inputArr.length;
2456
+ // Kept for diagnostics / xAI retry carry-forward. The canonical
2457
+ // prefix guard is lastRequestInput above, not this hash.
2458
+ entry.lastInputPrefixHash = createHash('sha256')
2459
+ .update(JSON.stringify(inputArr))
2460
+ .digest('hex');
2461
+ } else if (!keepResponseChain) {
2462
+ entry.lastResponseId = null;
2463
+ entry.lastRequestSansInput = null;
2464
+ entry.lastRequestInput = null;
2465
+ entry.lastResponseItems = null;
2466
+ entry.lastInputLen = 0;
2467
+ entry.lastInputPrefixHash = null;
2468
+ }
2469
+
2470
+ if (warmupResult?.usage) {
2471
+ result.usage = _combineUsageWithWarmup(result.usage, warmupResult.usage);
2472
+ }
2473
+
2474
+ const requestedServiceTier = body?.service_tier || null;
2475
+ const responseServiceTier = result.serviceTier || result.usage?.raw?.service_tier || null;
2476
+ traceBridgeUsage({
2477
+ sessionId: poolKey,
2478
+ iteration,
2479
+ inputTokens: result.usage?.inputTokens || 0,
2480
+ outputTokens: result.usage?.outputTokens || 0,
2481
+ cachedTokens: result.usage?.cachedTokens || 0,
2482
+ promptTokens: result.usage?.promptTokens || 0,
2483
+ model: liveModel,
2484
+ modelDisplay: displayModel ? displayModel(liveModel) : liveModel,
2485
+ responseId: result.responseId || null,
2486
+ rawUsage: result.usage?.raw || null,
2487
+ provider: traceProvider,
2488
+ serviceTier: responseServiceTier,
2489
+ });
2490
+ // Extra WS-specific observability: transport + per-iteration delta bytes.
2491
+ try {
2492
+ const transportPayload = {
2493
+ provider: traceProvider,
2494
+ transport: 'websocket',
2495
+ ws_mode: mode,
2496
+ ws_pre_response_created_timeout_ms: WS_PRE_RESPONSE_CREATED_MS,
2497
+ ws_first_meaningful_timeout_ms: WS_FIRST_MEANINGFUL_MS,
2498
+ ws_inter_chunk_timeout_ms: WS_INTER_CHUNK_MS,
2499
+ iteration_delta_tokens: deltaTokens,
2500
+ reused_connection: reused,
2501
+ requested_service_tier: requestedServiceTier,
2502
+ response_service_tier: responseServiceTier,
2503
+ handshake_retries: handshakeRetries,
2504
+ handshake_retry_classifiers: handshakeRetryClassifiers,
2505
+ midstream_retries: attemptIndex,
2506
+ response_id: result.responseId || null,
2507
+ cache_key_hash: cacheKey
2508
+ ? createHash('sha256').update(String(cacheKey)).digest('hex').slice(0, 12)
2509
+ : null,
2510
+ cache_lane_enabled: promptCacheLane?.enabled === true,
2511
+ cache_lane_key_hash: promptCacheLane?.laneKeyHash || null,
2512
+ cache_lane_max_in_flight: Number.isFinite(Number(promptCacheLane?.maxInFlight)) ? Number(promptCacheLane.maxInFlight) : null,
2513
+ cache_lane_rate_limit_per_min: Number.isFinite(Number(promptCacheLane?.rateLimitPerMin)) ? Number(promptCacheLane.rateLimitPerMin) : null,
2514
+ cache_lane_rate_wait_ms: Number.isFinite(Number(promptCacheLane?.rateWaitMs)) ? Number(promptCacheLane.rateWaitMs) : null,
2515
+ cache_lane_rate_window_count: Number.isFinite(Number(promptCacheLane?.rateWindowCount)) ? Number(promptCacheLane.rateWindowCount) : null,
2516
+ cache_lane_wait_ms: Number.isFinite(Number(promptCacheLane?.waitMs)) ? Number(promptCacheLane.waitMs) : null,
2517
+ cache_lane_queued: promptCacheLane?.queued === true,
2518
+ cache_lane_active: Number.isFinite(Number(promptCacheLane?.activeAfterAcquire)) ? Number(promptCacheLane.activeAfterAcquire) : null,
2519
+ cache_lane_queue_depth: Number.isFinite(Number(promptCacheLane?.queueDepthAfterAcquire)) ? Number(promptCacheLane.queueDepthAfterAcquire) : null,
2520
+ request_has_previous_response_id: typeof frame.previous_response_id === 'string' && frame.previous_response_id.length > 0,
2521
+ chain_delta_reason: mode === 'delta' ? null : deltaReason,
2522
+ chain_stripped_response_items: strippedResponseItems,
2523
+ chain_skipped_response_items: skippedResponseItems,
2524
+ chain_response_items: Array.isArray(result.responseItems) ? result.responseItems.length : 0,
2525
+ body_input_items: Array.isArray(requestBody.input) ? requestBody.input.length : null,
2526
+ frame_input_items: Array.isArray(frame.input) ? frame.input.length : null,
2527
+ frame_has_instructions: typeof frame.instructions === 'string' && frame.instructions.length > 0,
2528
+ warmup_used: !!warmupResult,
2529
+ warmup_response_id: warmupResult?.responseId || null,
2530
+ tool_call_count: resultToolCallCount,
2531
+ keep_socket: keepSocket,
2532
+ keep_response_chain: keepResponseChain,
2533
+ };
2534
+ appendBridgeTrace({
2535
+ sessionId: poolKey,
2536
+ iteration,
2537
+ kind: 'transport',
2538
+ ...transportPayload,
2539
+ payload: transportPayload,
2540
+ });
2541
+ } catch {}
2542
+
2543
+ releaseWebSocket({ entry, poolKey, keep: keepSocket });
2544
+ const { responseId: _ignored, responseItems: _responseItemsIgnored, ...out } = result;
2545
+ if (includeResponseId && result.responseId) out.responseId = result.responseId;
2546
+ if (warmupResult) {
2547
+ try {
2548
+ Object.defineProperty(out, '__warmup', {
2549
+ value: {
2550
+ requestBody,
2551
+ responseId: warmupResult.responseId,
2552
+ usage: warmupResult.usage,
2553
+ },
2554
+ enumerable: false,
2555
+ });
2556
+ } catch {}
2557
+ }
2558
+ // Leave a breadcrumb on the result so downstream callers can observe
2559
+ // that a retry was used (0 = first-try success, up to 2 for ws_1006/1011).
2560
+ try { Object.defineProperty(out, '__midstreamRetries', { value: attemptIndex, enumerable: false }); } catch {}
2561
+ return out;
2562
+ }
2563
+ // Unreachable — the loop either returns or throws above.
2564
+ throw _stampLiveText(firstAttemptError || new Error('sendViaWebSocket: unreachable'));
2565
+ });
2566
+ }
2567
+
2568
+ // Drain-complete fence — set true once _closeAllPooledSockets runs so any
2569
+ // in-flight acquire that resumes after drain throws instead of pushing a
2570
+ // fresh socket into the cleared pool. Single-set, process-lifetime invariant.
2571
+ let _drainComplete = false;
2572
+
2573
+ // Drain hook — self-registered exit drain.
2574
+ // Force-closes pooled sockets and fences subsequent acquires.
2575
+ // `drainOpenaiWsPool` alias matches the registry's `drain*` naming convention;
2576
+ // `_closeAllPooledSockets` kept for backward compat with existing call sites.
2577
+ export function _closeAllPooledSockets(reason = 'shutdown') {
2578
+ _drainComplete = true;
2579
+ for (const arr of _wsPool.values()) {
2580
+ for (const entry of arr) {
2581
+ try { entry.socket.close(1000, reason); } catch {}
2582
+ }
2583
+ }
2584
+ _wsPool.clear();
2585
+ }
2586
+ export const drainOpenaiWsPool = _closeAllPooledSockets;
2587
+ process.on('exit', drainOpenaiWsPool);