mixdog 1.2.5 → 1.2.6

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 (290) hide show
  1. package/package.json +1 -1
  2. package/scripts/lib/gate-scheduler.mjs +2 -2
  3. package/scripts/lib/run-node-tests.mjs +5 -14
  4. package/scripts/provider-pingpong-live.mjs +9 -9
  5. package/src/agents/advisor/AGENT.md +4 -4
  6. package/src/agents/advisor/agent.json +1 -1
  7. package/src/agents/reviewer/AGENT.md +2 -3
  8. package/src/agents/reviewer/agent.json +1 -1
  9. package/src/defaults/shipped-history.json +10 -0
  10. package/src/defaults/skills/browser-use/SKILL.md +2 -1
  11. package/src/defaults/skills/computer-use/SKILL.md +3 -1
  12. package/src/defaults/skills/visualize/SKILL.md +3 -2
  13. package/src/lib/rules-builder.cjs +1 -1
  14. package/src/output-styles/common.md +22 -6
  15. package/src/output-styles/detailed.md +7 -5
  16. package/src/output-styles/minimal.md +7 -3
  17. package/src/output-styles/simple.md +2 -3
  18. package/src/rules/agent/AGENT.md +3 -1
  19. package/src/rules/lead/LEAD.md +4 -3
  20. package/src/rules/shared/10-tool-workflow.md +2 -1
  21. package/src/rules/shared/60-verification.md +4 -5
  22. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -1
  23. package/src/runtime/agent/orchestrator/config.mjs +6 -0
  24. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +1 -1
  25. package/src/runtime/agent/orchestrator/internal-agents.mjs +10 -0
  26. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +53 -60
  27. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  28. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +2 -3
  29. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +9 -2
  30. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +40 -19
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +3 -2
  32. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-start.mjs +4 -1
  33. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events.mjs +2 -0
  34. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn.mjs +3 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -1
  36. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -1
  37. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +1 -2
  38. package/src/runtime/agent/orchestrator/providers/catalog-deadline.mjs +163 -0
  39. package/src/runtime/agent/orchestrator/providers/catalog-failures.mjs +34 -0
  40. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +38 -14
  41. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +100 -26
  42. package/src/runtime/agent/orchestrator/providers/lib/send-callbacks.mjs +24 -0
  43. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +13 -0
  44. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +2 -1
  45. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
  46. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +4 -0
  47. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  48. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  49. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +49 -4
  50. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +0 -7
  51. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +7 -1
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
  53. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -0
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +5 -1
  56. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +11 -2
  57. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -0
  58. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +45 -16
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +1 -0
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-connect.mjs +196 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +74 -9
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +14 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -0
  64. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-login.mjs +194 -0
  65. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-tokens.mjs +353 -0
  66. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +293 -0
  67. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +33 -100
  68. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +23 -14
  69. package/src/runtime/agent/orchestrator/providers/registry.mjs +41 -27
  70. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +2 -3
  71. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +3 -8
  72. package/src/runtime/agent/orchestrator/session/agent-pool-progress.mjs +15 -3
  73. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +74 -13
  74. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +102 -0
  75. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -2
  76. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +16 -0
  77. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +9 -0
  78. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +39 -2
  79. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +9 -19
  80. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -3
  81. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -0
  82. package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +13 -5
  83. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +10 -7
  84. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +62 -22
  85. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +23 -1
  86. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +81 -34
  87. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -2
  88. package/src/runtime/agent/orchestrator/session/request-fork.mjs +47 -0
  89. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +16 -1
  90. package/src/runtime/agent/orchestrator/session/side-question.mjs +5 -43
  91. package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +10 -0
  92. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +73 -6
  93. package/src/runtime/agent/orchestrator/session/store/load-session.mjs +40 -0
  94. package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +15 -0
  95. package/src/runtime/agent/orchestrator/session/store/session-paths.mjs +12 -0
  96. package/src/runtime/agent/orchestrator/session/store-agent-worker-header.mjs +36 -0
  97. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +258 -43
  98. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +306 -0
  99. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +70 -21
  100. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +46 -30
  101. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +144 -45
  102. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +32 -13
  103. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +364 -38
  104. package/src/runtime/agent/orchestrator/session/store.mjs +9 -2
  105. package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +43 -12
  106. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +23 -6
  107. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -6
  108. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +13 -7
  109. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +1 -1
  110. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/benign-exit.mjs +7 -1
  111. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +40 -7
  112. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +37 -34
  113. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/inline-hoist.mjs +1 -1
  114. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +0 -4
  115. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +29 -27
  117. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -1
  118. package/src/runtime/agent/orchestrator/tools/builtin/find-files-tool.mjs +28 -11
  119. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +12 -3
  120. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +203 -101
  121. package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +90 -41
  122. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +17 -1
  123. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-delivery.mjs +199 -0
  124. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +165 -39
  125. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +25 -13
  126. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +7 -6
  127. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +19 -4
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-line-collector.mjs +5 -3
  129. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +25 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +2 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-render.mjs +3 -1
  132. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +12 -5
  133. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +61 -22
  134. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +32 -8
  135. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +193 -19
  136. package/src/runtime/agent/orchestrator/tools/builtin/shell-direct-exe.mjs +57 -7
  137. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +119 -32
  138. package/src/runtime/agent/orchestrator/tools/builtin/shell-lossless-compact.mjs +26 -1
  139. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +13 -28
  140. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +90 -11
  141. package/src/runtime/agent/orchestrator/tools/builtin/wait-budgets.mjs +23 -0
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +54 -10
  143. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +22 -28
  144. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +11 -7
  145. package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +126 -9
  146. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  147. package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +15 -6
  148. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +40 -2
  149. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +2 -2
  150. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promotion-results.mjs +13 -3
  151. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +2 -0
  152. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -1
  153. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +35 -4
  155. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +7 -2
  156. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +39 -23
  157. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +6 -2
  158. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
  159. package/src/runtime/attachments/office-extract.mjs +11 -9
  160. package/src/runtime/memory/lib/embedding-tokenizer.mjs +32 -18
  161. package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -1
  162. package/src/runtime/memory/lib/tool-call-handler.mjs +7 -5
  163. package/src/runtime/office/authoring/pptx-script-normalize.mjs +11 -8
  164. package/src/runtime/office/core/tabular.mjs +1 -1
  165. package/src/runtime/office/design/library/design-template-inspect.mjs +7 -4
  166. package/src/runtime/office/portable/docx-formatting.mjs +1 -1
  167. package/src/runtime/office/portable/docx-runs.mjs +1 -1
  168. package/src/runtime/office/portable/docx-tracked-edits.mjs +2 -2
  169. package/src/runtime/office/portable/portable-docx-edits.mjs +11 -10
  170. package/src/runtime/office/portable/portable-docx-operations.mjs +21 -17
  171. package/src/runtime/office/portable/portable-docx-parts.mjs +15 -9
  172. package/src/runtime/office/portable/portable-docx-styles.mjs +4 -1
  173. package/src/runtime/office/portable/portable-docx-xml.mjs +9 -9
  174. package/src/runtime/office/portable/portable-opc.mjs +27 -13
  175. package/src/runtime/office/portable/portable-pivot.mjs +3 -3
  176. package/src/runtime/office/portable/portable-pptx-chart.mjs +3 -3
  177. package/src/runtime/office/portable/portable-pptx-charts.mjs +24 -18
  178. package/src/runtime/office/portable/portable-pptx-core.mjs +5 -5
  179. package/src/runtime/office/portable/portable-pptx-deck.mjs +8 -4
  180. package/src/runtime/office/portable/portable-pptx-package.mjs +7 -4
  181. package/src/runtime/office/portable/portable-pptx-shapes.mjs +18 -9
  182. package/src/runtime/office/portable/portable-sheet-parts.mjs +9 -3
  183. package/src/runtime/office/portable/portable-sheet-xml.mjs +5 -5
  184. package/src/runtime/office/portable/portable-xlsx-charts.mjs +4 -1
  185. package/src/runtime/office/portable/portable-xlsx-drawings.mjs +10 -6
  186. package/src/runtime/office/portable/portable-xlsx-operations.mjs +8 -5
  187. package/src/runtime/office/portable/portable-xlsx-pivot-ops.mjs +1 -1
  188. package/src/runtime/office/portable/portable-xlsx-reference-shift.mjs +11 -7
  189. package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +6 -3
  190. package/src/runtime/office/portable/portable-xml.mjs +1 -1
  191. package/src/runtime/shared/atomic-file.mjs +22 -13
  192. package/src/runtime/shared/background-tasks.mjs +60 -20
  193. package/src/runtime/shared/child-spawn-gate.mjs +26 -16
  194. package/src/runtime/shared/config.mjs +0 -10
  195. package/src/runtime/shared/file-lock.mjs +7 -1
  196. package/src/runtime/shared/file-permissions-worker.mjs +38 -0
  197. package/src/runtime/shared/file-permissions.d.mts +19 -4
  198. package/src/runtime/shared/file-permissions.mjs +310 -4
  199. package/src/runtime/shared/git-runner-worker.mjs +6 -78
  200. package/src/runtime/shared/llm/http-agent.mjs +7 -3
  201. package/src/runtime/shared/llm/provider-catalog-registry.mjs +10 -0
  202. package/src/runtime/shared/llm/usage-accounting.mjs +2 -0
  203. package/src/runtime/shared/llm/usage-ledger-import.mjs +32 -3
  204. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +12 -5
  205. package/src/runtime/shared/llm/usage-ledger-worker.mjs +1 -1
  206. package/src/runtime/shared/llm/usage-ledger.mjs +20 -5
  207. package/src/runtime/shared/memory-snapshot.mjs +5 -1
  208. package/src/runtime/shared/notify-trace.mjs +8 -1
  209. package/src/runtime/shared/pristine-execution-contract.json +5 -1
  210. package/src/runtime/shared/process-runner-worker.mjs +7 -0
  211. package/src/runtime/shared/process-runner.mjs +38 -0
  212. package/src/runtime/shared/provider-accounts.mjs +2 -1
  213. package/src/runtime/shared/provider-api-key.mjs +0 -1
  214. package/src/runtime/shared/read-row-numbers.mjs +1 -1
  215. package/src/runtime/shared/resource-admission.mjs +60 -18
  216. package/src/runtime/shared/run-child.mjs +84 -0
  217. package/src/runtime/shared/session-activity-signal.mjs +23 -0
  218. package/src/runtime/shared/shell-output-summary.mjs +23 -0
  219. package/src/runtime/shared/singleton-owner.mjs +126 -42
  220. package/src/runtime/shared/tool-output-limit.mjs +4 -2
  221. package/src/runtime/shared/tool-output-marker.mjs +58 -0
  222. package/src/runtime/shared/user-data-guard.mjs +28 -9
  223. package/src/runtime/shared/worktrees/activity-gate.mjs +33 -8
  224. package/src/runtime/shared/worktrees/git-head.mjs +20 -2
  225. package/src/runtime/shared/worktrees/lock.mjs +3 -2
  226. package/src/runtime/terminal-bridge/tool-defs.mjs +1 -1
  227. package/src/runtime/tidy/runners/biome.mjs +3 -1
  228. package/src/runtime/tidy/shadow.mjs +2 -1
  229. package/src/runtime/web-search/index.mjs +5 -7
  230. package/src/runtime/web-search/lib/formatter.mjs +42 -6
  231. package/src/runtime/web-search/tool-defs.mjs +6 -1
  232. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +4 -3
  233. package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -4
  234. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +3 -3
  235. package/src/session-runtime/lifecycle/shared.mjs +19 -2
  236. package/src/session-runtime/lifecycle/teardown/session-close.mjs +2 -0
  237. package/src/session-runtime/lifecycle/teardown.mjs +5 -2
  238. package/src/session-runtime/lifecycle-api.mjs +3 -1
  239. package/src/session-runtime/provider-auth/login.mjs +10 -6
  240. package/src/session-runtime/provider-auth/setup.mjs +19 -2
  241. package/src/session-runtime/provider-catalog-cache.mjs +101 -25
  242. package/src/session-runtime/provider-models/catalog-load.mjs +14 -3
  243. package/src/session-runtime/provider-models/row-hydration.mjs +4 -0
  244. package/src/session-runtime/provider-models.mjs +76 -10
  245. package/src/session-runtime/provider-readiness.mjs +4 -0
  246. package/src/session-runtime/services/agent-tool/close-flow.mjs +2 -1
  247. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +10 -2
  248. package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +13 -4
  249. package/src/session-runtime/services/agent-tool/notify.mjs +15 -4
  250. package/src/session-runtime/services/agent-tool/restart-notify.mjs +134 -0
  251. package/src/session-runtime/services/agent-tool/send-flow.mjs +24 -1
  252. package/src/session-runtime/services/agent-tool/spawn-flow/deferred-spawn.mjs +2 -1
  253. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -3
  254. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +5 -1
  255. package/src/session-runtime/services/agent-tool/tag-registry/tag-lookup.mjs +8 -3
  256. package/src/session-runtime/services/agent-tool/tag-registry.mjs +3 -0
  257. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +4 -0
  258. package/src/session-runtime/services/agent-tool/worker-index.mjs +44 -10
  259. package/src/session-runtime/services/agent-tool.mjs +20 -0
  260. package/src/session-runtime/services/provider-admin.mjs +92 -20
  261. package/src/session-runtime/services/usage-dashboard-model.mjs +2 -2
  262. package/src/session-runtime/services/usage-dashboard.mjs +27 -69
  263. package/src/session-runtime/turn/session-ops.mjs +2 -2
  264. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  265. package/src/session-runtime/usage-stats-api.mjs +6 -1
  266. package/src/standalone/channel-transport/http-routes.mjs +10 -9
  267. package/src/standalone/daemon-desktop-runtime.mjs +20 -7
  268. package/src/standalone/daemon-owner.mjs +117 -25
  269. package/src/standalone/daemon.mjs +43 -13
  270. package/src/standalone/session-call-cache.mjs +6 -0
  271. package/src/standalone/session-protocol.mjs +1 -1
  272. package/src/standalone/session-service/retention/busy.mjs +7 -2
  273. package/src/standalone/session-transport/routes.mjs +7 -6
  274. package/src/tui/app/model-options.mjs +39 -2
  275. package/src/tui/app/model-picker.mjs +16 -2
  276. package/src/tui/app/provider-setup-picker.mjs +1 -2
  277. package/src/tui/dist/index.mjs +385 -104
  278. package/src/tui/session/notice-text.mjs +0 -1
  279. package/src/tui/session/session-api/intake/submission.mjs +7 -13
  280. package/src/tui/session/session-api/integrations.mjs +1 -5
  281. package/src/tui/session/session-flow/drain.mjs +8 -0
  282. package/src/tui/session/session-flow/steering.mjs +46 -0
  283. package/src/tui/session/session-flow.mjs +2 -0
  284. package/src/tui/session/turn-stream.mjs +33 -0
  285. package/src/tui/session/turn.mjs +30 -3
  286. package/src/ui/statusline.mjs +1 -6
  287. package/src/ui/streaming-markdown-heal.mjs +24 -7
  288. package/src/vendor/statusline/bin/statusline-route.mjs +0 -1
  289. package/src/vendor/statusline/src/gateway/route-meta.mjs +0 -1
  290. package/src/workflows/default/WORKFLOW.md +9 -13
@@ -47,23 +47,68 @@ export function extractCompatReasoningEffort(model) {
47
47
  return null;
48
48
  }
49
49
 
50
+ const ERROR_BODY_LIMIT = 4096;
51
+ // An echoed key prefix this long (or the whole key, if shorter) is redacted.
52
+ const ECHOED_KEY_PREFIX_MIN = 8;
53
+
54
+ // Replaces the request key, and any run of its first ECHOED_KEY_PREFIX_MIN or
55
+ // more characters ("Incorrect API key provided: xai-abcd****"), in `value`.
56
+ function redactRequestKey(value, key) {
57
+ if (!key) return value;
58
+ const min = Math.min(ECHOED_KEY_PREFIX_MIN, key.length);
59
+ let out = '';
60
+ for (let i = 0; i < value.length; ) {
61
+ let n = 0;
62
+ while (n < key.length && value[i + n] === key[n]) n += 1;
63
+ if (n >= min) {
64
+ out += '[redacted]';
65
+ i += n;
66
+ } else {
67
+ out += value[i];
68
+ i += 1;
69
+ }
70
+ }
71
+ return out;
72
+ }
73
+
74
+ // A refused /models request keeps the provider's own reason (status plus the
75
+ // body's code/type/message) so the UI can say why the list is empty. The
76
+ // message stays `<name> models <status>`; the body rides on `providerError`,
77
+ // with the request key (`apiKey`) redacted from it.
78
+ async function compatModelsError(name, res, apiKey = '') {
79
+ const error = Object.assign(new Error(`${name} models ${res.status}`), { status: res.status });
80
+ try {
81
+ const body = JSON.parse((await res.text()).slice(0, ERROR_BODY_LIMIT));
82
+ const nested = body?.error && typeof body.error === 'object' ? body.error : null;
83
+ const text = (value) => (typeof value === 'string' && value ? redactRequestKey(value, apiKey) : undefined);
84
+ const providerError = {
85
+ message: text(body?.error) ?? text(nested?.message) ?? text(body?.message),
86
+ code: text(nested?.code) ?? text(body?.code),
87
+ type: text(nested?.type) ?? text(body?.type),
88
+ };
89
+ if (providerError.message || providerError.code || providerError.type) error.providerError = providerError;
90
+ } catch {
91
+ // A body that is not JSON adds no detail.
92
+ }
93
+ return error;
94
+ }
95
+
50
96
  // `keyless` omits the Authorization header entirely (keyless endpoints);
51
97
  // `signal` aborts the request together with the list timeout.
52
98
  export async function fetchCompatModelItems(provider, { signal = null, keyless = false } = {}) {
53
99
  const name = provider.name;
54
100
  const timeout = createTimeoutSignal(signal, MODEL_LIST_TIMEOUT_MS, `${name} model list`);
55
101
  try {
102
+ const apiKey = keyless ? '' : resolveProviderApiKey(name, provider.config || provider);
56
103
  const res = await fetch(`${String(provider.baseURL || '').replace(/\/+$/, '')}/models`, {
57
104
  method: 'GET',
58
105
  headers: {
59
- ...(keyless
60
- ? {}
61
- : { Authorization: `Bearer ${resolveProviderApiKey(name, provider.config || provider) || 'no-key'}` }),
106
+ ...(keyless ? {} : { Authorization: `Bearer ${apiKey || 'no-key'}` }),
62
107
  ...(provider.defaultHeaders || {}),
63
108
  },
64
109
  signal: timeout.signal,
65
110
  });
66
- if (!res.ok) throw Object.assign(new Error(`${provider.name} models ${res.status}`), { status: res.status });
111
+ if (!res.ok) throw await compatModelsError(provider.name, res, apiKey);
67
112
  const data = await res.json();
68
113
  if (Array.isArray(data?.data)) return data.data;
69
114
  if (Array.isArray(data)) return data;
@@ -9,13 +9,6 @@ export const OPENAI_COMPAT_PRESETS = {
9
9
  baseURL: 'https://api.x.ai/v1',
10
10
  defaultModel: 'grok-4.5',
11
11
  },
12
- // OpenCode Go - low-cost coding-model subscription gateway.
13
- 'opencode-go': {
14
- baseURL: 'https://opencode.ai/inference/go/v1',
15
- defaultModel: 'glm-5.2',
16
- // The gateway expects an application User-Agent (not the SDK default).
17
- extraHeaders: { 'User-Agent': 'mixdog' },
18
- },
19
12
  openrouter: {
20
13
  baseURL: 'https://openrouter.ai/api/v1',
21
14
  defaultModel: 'openrouter/auto',
@@ -14,6 +14,7 @@ import { truncatedCompatStreamError, parseCompletedToolCallArgumentsJson } from
14
14
  import { incompleteReasonFromEvent, isMaxOutputIncompleteReason } from './lib/responses-terminal-fields.mjs';
15
15
  import { responsesUsage } from './openai-compat-response-normalization.mjs';
16
16
  import { normalizeGatewayErrorDetail } from './provider-error-kind.mjs';
17
+ import { reportStreamItemStart, responsesItemStart } from './lib/send-callbacks.mjs';
17
18
 
18
19
  // An incomplete response was still billed: carry its reported usage on the
19
20
  // error so the provider-boundary accounting records it.
@@ -194,6 +195,7 @@ function onOutputItemAdded(event, state, ctx) {
194
195
  state.toolTracker?.mark(event.item);
195
196
  state.toolInFlight = true;
196
197
  }
198
+ reportStreamItemStart(ctx.onStreamItemStart, responsesItemStart(event.item));
197
199
  signal(ctx, state.toolInFlight ? 'tool' : 'semantic');
198
200
  }
199
201
 
@@ -249,7 +251,11 @@ function onRefusalDone(event, state, ctx) {
249
251
 
250
252
  function onOutputItemDone(event, state, ctx) {
251
253
  const item = event.item || {};
252
- if (item.type === 'message') refusalsOf(state).absorbMessage(item);
254
+ if (item.type === 'message') {
255
+ refusalsOf(state).absorbMessage(item);
256
+ // A stream that names the phase only on the finished item.
257
+ reportStreamItemStart(ctx.onStreamItemStart, responsesItemStart(item));
258
+ }
253
259
  if (item.type === 'function_call') {
254
260
  const itemId = reconcileCompatFunctionCallItem(state, item, ctx.label, ctx.onToolCall);
255
261
  // Drop the resolved function item from pendingCalls before
@@ -243,6 +243,7 @@ async function streamCompatResponses({ provider, params, label, signal, opts, he
243
243
  onStreamDelta: opts.onStreamDelta,
244
244
  onToolCall: opts.onToolCall,
245
245
  onTextDelta: opts.onTextDelta,
246
+ onStreamItemStart: opts.onStreamItemStart,
246
247
  parseResponsesToolCalls,
247
248
  responseOutputText,
248
249
  knownToolNames: knownToolNamesFromResponsesTools(params.tools),
@@ -51,6 +51,7 @@ export async function consumeCompatResponsesStream(
51
51
  onStreamDelta,
52
52
  onToolCall,
53
53
  onTextDelta,
54
+ onStreamItemStart,
54
55
  parseResponsesToolCalls,
55
56
  responseOutputText,
56
57
  knownToolNames,
@@ -89,6 +90,7 @@ export async function consumeCompatResponsesStream(
89
90
  onStreamDelta: reportProgress,
90
91
  onToolCall,
91
92
  onTextDelta,
93
+ onStreamItemStart,
92
94
  relayLeakText,
93
95
  };
94
96
  const stampOutcome = createResponsesOutcomeStamper(state, leakedCalls);
@@ -134,6 +134,7 @@ export async function sendXaiResponses(provider, messages, useModel, tools, opts
134
134
  onStreamDelta: opts.onStreamDelta,
135
135
  onToolCall: opts.onToolCall,
136
136
  onTextDelta: opts.onTextDelta,
137
+ onStreamItemStart: opts.onStreamItemStart,
137
138
  parseResponsesToolCalls,
138
139
  responseOutputText,
139
140
  knownToolNames: knownToolNamesFromResponsesTools(params.tools),
@@ -5,8 +5,9 @@
5
5
  */
6
6
  import { createOutputItemEvents } from './response-events/output-items.mjs';
7
7
  import { createTerminalFrameEvents } from './response-events/terminal-frames.mjs';
8
+ import { reportStreamItemStart, responsesItemStart } from '../lib/send-callbacks.mjs';
8
9
 
9
- export function createResponseEvents({ state, items, text, outcome, meaningful, onServerEvent }) {
10
+ export function createResponseEvents({ state, items, text, outcome, meaningful, onServerEvent, onStreamItemStart }) {
10
11
  const output = createOutputItemEvents({ state, items, meaningful });
11
12
  const terminal = createTerminalFrameEvents({ state, items, text, outcome, meaningful });
12
13
 
@@ -34,6 +35,7 @@ export function createResponseEvents({ state, items, text, outcome, meaningful,
34
35
  break;
35
36
  case 'response.output_item.added':
36
37
  output.onOutputItemAdded(event.item);
38
+ reportStreamItemStart(onStreamItemStart, responsesItemStart(event.item));
37
39
  break;
38
40
  case 'response.function_call_arguments.delta':
39
41
  case 'response.custom_tool_call_input.delta':
@@ -54,6 +56,8 @@ export function createResponseEvents({ state, items, text, outcome, meaningful,
54
56
  break;
55
57
  case 'response.output_item.done':
56
58
  output.onOutputItemDone(event.item || {});
59
+ // A stream that names the phase only on the finished message.
60
+ if (event.item?.type === 'message') reportStreamItemStart(onStreamItemStart, responsesItemStart(event.item));
57
61
  break;
58
62
  case 'response.completed':
59
63
  terminal.onCompleted(event);
@@ -25,13 +25,22 @@ import { createResponseEvents } from './openai-http-sse-response-state/response-
25
25
  * @param {(text: string) => void} [deps.onTextDelta]
26
26
  * @param {(kind: string) => void} deps.meaningful semantic progress (re-arms the idle watchdog)
27
27
  * @param {() => void} deps.onServerEvent first-event → semantic-idle handover
28
+ * @param {(detail: object) => void} [deps.onStreamItemStart] a tool call or phased message started
28
29
  */
29
- export function createHttpSseResponseState({ body, onToolCall, onTextDelta, meaningful, onServerEvent, label }) {
30
+ export function createHttpSseResponseState({
31
+ body,
32
+ onToolCall,
33
+ onTextDelta,
34
+ onStreamItemStart = null,
35
+ meaningful,
36
+ onServerEvent,
37
+ label,
38
+ }) {
30
39
  const state = createResponseState(label);
31
40
  const outcome = createResponseOutcome({ state });
32
41
  const items = createResponseItems({ state, onToolCall });
33
42
  const text = createTextRelay({ state, body, onTextDelta, meaningful, emitToolCall: items.emitToolCall });
34
- const events = createResponseEvents({ state, items, text, outcome, meaningful, onServerEvent });
43
+ const events = createResponseEvents({ state, items, text, outcome, meaningful, onServerEvent, onStreamItemStart });
35
44
 
36
45
  return {
37
46
  handleEvent: events.handleEvent,
@@ -195,6 +195,7 @@ export async function sendViaHttpSse({
195
195
  body,
196
196
  onToolCall,
197
197
  onTextDelta,
198
+ onStreamItemStart: typeof opts?.onStreamItemStart === 'function' ? opts.onStreamItemStart : null,
198
199
  meaningful,
199
200
  onServerEvent: watchdogs.noteServerEvent,
200
201
  label,
@@ -11,7 +11,7 @@
11
11
  import { createServer } from 'node:http';
12
12
  import { randomBytes } from 'node:crypto';
13
13
  import { createOAuthPkce, parseOAuthCodeInput } from './lib/oauth-pkce.mjs';
14
- import { OAUTH_PAGE_CONTENT_TYPE, oauthSuccessHtml } from './lib/oauth-page.mjs';
14
+ import { OAUTH_PAGE_CONTENT_TYPE, oauthErrorHtml, oauthSuccessHtml } from './lib/oauth-page.mjs';
15
15
  import { openLoginUrl } from './lib/open-login-url.mjs';
16
16
 
17
17
  const TOKEN_URL = 'https://auth.openai.com/oauth/token';
@@ -44,7 +44,8 @@ function _scrubOAuthLoginBody(text) {
44
44
  export function createOpenAIOAuthLogin(deps) {
45
45
  const { clientId, originator, extractAccountId, identityFromTokens, expiryFromAccessToken, saveTokens } = deps;
46
46
 
47
- async function exchangeAuthorizationCode({ pkce, code }) {
47
+ async function exchangeAuthorizationCode({ pkce, code, signal = null }) {
48
+ signal?.throwIfAborted();
48
49
  const cleanCode = String(code || '').trim();
49
50
  if (!cleanCode) throw new Error('[openai-oauth] authorization code is required');
50
51
  const tokenRes = await fetch(TOKEN_URL, {
@@ -58,7 +59,9 @@ export function createOpenAIOAuthLogin(deps) {
58
59
  code_verifier: pkce.verifier,
59
60
  }),
60
61
  redirect: 'error',
61
- signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
62
+ signal: signal
63
+ ? AbortSignal.any([signal, AbortSignal.timeout(TOKEN_TIMEOUT_MS)])
64
+ : AbortSignal.timeout(TOKEN_TIMEOUT_MS),
62
65
  });
63
66
  if (!tokenRes.ok) {
64
67
  const text = await tokenRes.text().catch(() => '');
@@ -78,6 +81,8 @@ export function createOpenAIOAuthLogin(deps) {
78
81
  account_id: extractAccountId(json.access_token),
79
82
  identity: identityFromTokens(json.id_token, json.access_token),
80
83
  };
84
+ // A cancelled login never stores the account it was exchanging.
85
+ signal?.throwIfAborted();
81
86
  saveTokens(tokens);
82
87
  return tokens;
83
88
  }
@@ -100,11 +105,30 @@ export function createOpenAIOAuthLogin(deps) {
100
105
  let server = null;
101
106
  let timeout = null;
102
107
  let finish = null;
108
+ let exchangePromise = null;
109
+ const controller = new AbortController();
110
+ // Browser and manual callbacks share one exchange: an authorization code is
111
+ // single-use, and a duplicate must not settle the login before the first.
112
+ const acceptCode = (code) => {
113
+ if (exchangePromise) return exchangePromise;
114
+ controller.signal.throwIfAborted();
115
+ // The browser deadline ends when a code arrives; the exchange has its own.
116
+ if (timeout) clearTimeout(timeout);
117
+ timeout = null;
118
+ exchangePromise = exchangeAuthorizationCode({ pkce, code, signal: controller.signal });
119
+ exchangePromise.then(
120
+ (tokens) => finish(tokens),
121
+ (err) => finish(null, err instanceof Error ? err : new Error(String(err)))
122
+ );
123
+ return exchangePromise;
124
+ };
103
125
  const waitForCallback = new Promise((resolve, reject) => {
104
126
  let settled = false;
105
127
  finish = (value, error = null) => {
106
128
  if (settled) return;
107
129
  settled = true;
130
+ // Work arriving after the login settled (or was cancelled) is rejected.
131
+ controller.abort();
108
132
  if (timeout) clearTimeout(timeout);
109
133
  try {
110
134
  server?.close();
@@ -122,19 +146,25 @@ export function createOpenAIOAuthLogin(deps) {
122
146
  return;
123
147
  }
124
148
  const code = u.searchParams.get('code');
125
- if (!code || u.searchParams.get('state') !== state) {
126
- res.writeHead(400);
127
- res.end('Invalid');
128
- finish(null);
149
+ const stateMatches = u.searchParams.get('state') === state;
150
+ if (!code || !stateMatches) {
151
+ res.writeHead(400, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
152
+ res.end(oauthErrorHtml('The sign-in callback was invalid or did not match this login.'));
153
+ // A request for another login is rejected alone; this login's
154
+ // legitimate callback may still arrive.
155
+ if (stateMatches && !exchangePromise) finish(null);
129
156
  return;
130
157
  }
131
- res.writeHead(200, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE });
132
- res.end(oauthSuccessHtml('OpenAI'));
133
158
  try {
134
- const tokens = await exchangeAuthorizationCode({ pkce, code });
135
- finish(tokens);
136
- } catch (err) {
137
- finish(null, err instanceof Error ? err : new Error(String(err)));
159
+ await acceptCode(code);
160
+ // The login settles (closing the server) before this response is
161
+ // written; do not leave a keep-alive connection bound to it.
162
+ res.writeHead(200, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE, Connection: 'close' });
163
+ res.end(oauthSuccessHtml('OpenAI'));
164
+ } catch {
165
+ // The shared exchange reports its error through waitForCallback.
166
+ res.writeHead(500, { 'Content-Type': OAUTH_PAGE_CONTENT_TYPE, Connection: 'close' });
167
+ res.end(oauthErrorHtml('OpenAI could not complete the token exchange.'));
138
168
  }
139
169
  });
140
170
  timeout = setTimeout(() => finish(null), LOGIN_TIMEOUT_MS);
@@ -161,9 +191,8 @@ export function createOpenAIOAuthLogin(deps) {
161
191
  completeCode: async (input) => {
162
192
  const parsed = parseOAuthCodeInput(input, { allowHashState: true });
163
193
  if (parsed.state && parsed.state !== state) throw new Error('[openai-oauth] OAuth state mismatch');
164
- const tokens = await exchangeAuthorizationCode({ pkce, code: parsed.code });
165
- finish?.(tokens);
166
- return tokens;
194
+ if (!parsed.code) throw new Error('[openai-oauth] authorization code is required');
195
+ return await acceptCode(parsed.code);
167
196
  },
168
197
  cancel: () => {
169
198
  finish?.(null);
@@ -297,6 +297,7 @@ export async function sendViaWebSocket({
297
297
  onStreamDelta,
298
298
  onToolCall,
299
299
  onTextDelta,
300
+ onStreamItemStart: typeof sendOpts?.onStreamItemStart === 'function' ? sendOpts.onStreamItemStart : null,
300
301
  state: midState,
301
302
  logSuppressedReasoningDeltas,
302
303
  traceProvider,
@@ -0,0 +1,196 @@
1
+ /**
2
+ * openai-ws-connect.mjs — how a Responses WebSocket reaches the server.
3
+ *
4
+ * Every pool handshake is a brand-new TCP+TLS connection (the upgrade detaches
5
+ * it from any HTTP keep-alive pool), so a burst of agents starting together
6
+ * pays one DNS resolution and one full TLS handshake per socket. This module
7
+ * removes the repeated work and the long stall:
8
+ *
9
+ * - lookup: one resolution per host, shared by concurrent handshakes and
10
+ * reused for a short TTL (libuv's small thread pool otherwise queues one
11
+ * getaddrinfo per socket behind unrelated file I/O);
12
+ * - TLS resumption: the latest session ticket per host rides the next
13
+ * connect, so later sockets skip the full key exchange;
14
+ * - hedged open: an upgrade that has not opened within the hedge window
15
+ * gets a parallel replacement attempt instead of waiting out the whole
16
+ * acquire timeout. The first leg to open wins; the rest are cancelled.
17
+ * Server decisions (any HTTP status on the upgrade, e.g. 429/401) end
18
+ * the race at once so backoff stays driven by real server signals.
19
+ */
20
+ import { lookup as dnsLookup } from 'node:dns';
21
+ import net from 'node:net';
22
+ import tls from 'node:tls';
23
+ import { resolveTimeoutMs } from '../stall-policy.mjs';
24
+
25
+ const DNS_TTL_MS = resolveTimeoutMs('MIXDOG_PROVIDER_WS_DNS_TTL_MS', 30_000, { minMs: 1_000, maxMs: 300_000 });
26
+ // Observed production upgrades: p95 ≈ 0.55s, max ≈ 3.4s over 6k handshakes,
27
+ // so a leg still unopened after 3s is almost certainly stuck.
28
+ const WS_HEDGE_MS = resolveTimeoutMs('MIXDOG_PROVIDER_WS_HEDGE_MS', 3_000, { minMs: 500, maxMs: 60_000 });
29
+
30
+ const _dnsCache = new Map();
31
+
32
+ function _lookupKey(hostname, options) {
33
+ return `${hostname}\0${options?.family || 0}\0${options?.hints || 0}`;
34
+ }
35
+
36
+ function _answer(addresses, options, callback) {
37
+ if (options?.all) {
38
+ callback(null, addresses);
39
+ return;
40
+ }
41
+ const first = addresses[0];
42
+ callback(null, first.address, first.family);
43
+ }
44
+
45
+ /** dns.lookup-compatible resolver with in-flight coalescing and a short TTL. */
46
+ export function cachedLookup(hostname, options, callback) {
47
+ const opts = typeof options === 'function' ? {} : typeof options === 'number' ? { family: options } : options || {};
48
+ const cb = typeof options === 'function' ? options : callback;
49
+ const key = _lookupKey(hostname, opts);
50
+ const cached = _dnsCache.get(key);
51
+ if (cached?.addresses && cached.expiresAt > Date.now()) {
52
+ _answer(cached.addresses, opts, cb);
53
+ return;
54
+ }
55
+ if (cached?.waiters) {
56
+ cached.waiters.push({ opts, cb });
57
+ return;
58
+ }
59
+ const record = { waiters: [{ opts, cb }] };
60
+ _dnsCache.set(key, record);
61
+ dnsLookup(hostname, { family: opts.family || 0, hints: opts.hints || 0, all: true }, (err, addresses) => {
62
+ const waiters = record.waiters;
63
+ if (err || !addresses?.length) {
64
+ if (_dnsCache.get(key) === record) _dnsCache.delete(key);
65
+ const failure = err || Object.assign(new Error(`lookup ${hostname} returned no address`), { code: 'ENOTFOUND' });
66
+ for (const waiter of waiters) waiter.cb(failure);
67
+ return;
68
+ }
69
+ _dnsCache.set(key, { addresses, expiresAt: Date.now() + DNS_TTL_MS });
70
+ for (const waiter of waiters) _answer(addresses, waiter.opts, waiter.cb);
71
+ });
72
+ }
73
+
74
+ const _tlsSessions = new Map();
75
+
76
+ /**
77
+ * `ws` createConnection: the shared lookup for every socket, plus TLS
78
+ * resumption for secure ones. Mirrors ws's own servername defaulting.
79
+ */
80
+ export function createWsConnection(options) {
81
+ if (options.protocol === 'ws:' || options.protocol === 'http:') {
82
+ return net.connect({ ...options, path: options.socketPath, lookup: cachedLookup });
83
+ }
84
+ const sessionKey = `${options.host}:${options.port}`;
85
+ const servername =
86
+ options.servername || options.servername === '' ? options.servername : net.isIP(options.host) ? '' : options.host;
87
+ const socket = tls.connect({
88
+ ...options,
89
+ path: undefined,
90
+ servername,
91
+ lookup: cachedLookup,
92
+ session: _tlsSessions.get(sessionKey),
93
+ });
94
+ socket.on('session', (session) => _tlsSessions.set(sessionKey, session));
95
+ return socket;
96
+ }
97
+
98
+ /** Test-only reset of the resolver and TLS session caches. */
99
+ export function _clearConnectCachesForTest() {
100
+ _dnsCache.clear();
101
+ _tlsSessions.clear();
102
+ }
103
+
104
+ function _abortReason(signal, fallback) {
105
+ return signal?.reason instanceof Error ? signal.reason : new Error(fallback);
106
+ }
107
+
108
+ /**
109
+ * Race handshake legs until one opens. `openLeg({ externalSignal })` opens one
110
+ * socket and resolves `{ socket }`. A new leg starts every `hedgeMs` while
111
+ * none has opened and the acquire deadline leaves room; a leg's transport
112
+ * failure only ends the race when nothing else is still connecting.
113
+ */
114
+ export function openHedged({ openLeg, externalSignal, hedgeMs = WS_HEDGE_MS, deadlineMs, deadlineError }) {
115
+ return new Promise((resolve, reject) => {
116
+ const startedAt = Date.now();
117
+ const legs = new Set();
118
+ let settled = false;
119
+ let hedgeTimer = null;
120
+ let deadlineTimer = null;
121
+ let lastError = null;
122
+
123
+ const cleanup = () => {
124
+ if (hedgeTimer) clearTimeout(hedgeTimer);
125
+ if (deadlineTimer) clearTimeout(deadlineTimer);
126
+ externalSignal?.removeEventListener?.('abort', onExternalAbort);
127
+ };
128
+ const cancelLegs = (reason) => {
129
+ for (const leg of legs) leg.abort(reason);
130
+ legs.clear();
131
+ };
132
+ const finish = (ok, value) => {
133
+ if (settled) return;
134
+ settled = true;
135
+ cleanup();
136
+ (ok ? resolve : reject)(value);
137
+ };
138
+ const fail = (err) => {
139
+ cancelLegs(err);
140
+ finish(false, err);
141
+ };
142
+ function onExternalAbort() {
143
+ fail(_abortReason(externalSignal, 'OpenAI WS handshake aborted'));
144
+ }
145
+
146
+ const scheduleHedge = () => {
147
+ if (settled || !(hedgeMs > 0)) return;
148
+ if (Date.now() - startedAt + hedgeMs >= deadlineMs) return;
149
+ hedgeTimer = setTimeout(() => {
150
+ hedgeTimer = null;
151
+ startLeg();
152
+ }, hedgeMs);
153
+ hedgeTimer.unref?.();
154
+ };
155
+
156
+ function startLeg() {
157
+ if (settled) return;
158
+ const controller = new AbortController();
159
+ legs.add(controller);
160
+ Promise.resolve()
161
+ .then(() => openLeg({ externalSignal: controller.signal }))
162
+ .then(
163
+ (opened) => {
164
+ legs.delete(controller);
165
+ if (settled) {
166
+ try {
167
+ opened?.socket?.terminate?.();
168
+ } catch {}
169
+ return;
170
+ }
171
+ cancelLegs(new Error('OpenAI WS handshake superseded by a faster attempt'));
172
+ finish(true, opened);
173
+ },
174
+ (err) => {
175
+ legs.delete(controller);
176
+ if (settled || controller.signal.aborted) return;
177
+ lastError = err;
178
+ // An HTTP status is the server's decision for this request
179
+ // (rate limit, auth, upgrade refusal): surface it unchanged.
180
+ if (err?.httpStatus != null || legs.size === 0) fail(err);
181
+ }
182
+ );
183
+ if (hedgeTimer) clearTimeout(hedgeTimer);
184
+ scheduleHedge();
185
+ }
186
+
187
+ if (externalSignal?.aborted) {
188
+ onExternalAbort();
189
+ return;
190
+ }
191
+ externalSignal?.addEventListener?.('abort', onExternalAbort, { once: true });
192
+ deadlineTimer = setTimeout(() => fail(lastError?.httpStatus != null ? lastError : deadlineError()), deadlineMs);
193
+ deadlineTimer.unref?.();
194
+ startLeg();
195
+ });
196
+ }
@@ -9,6 +9,7 @@
9
9
  import { createHash, randomBytes } from 'node:crypto';
10
10
  import { performance } from 'node:perf_hooks';
11
11
  import { attachHandshakeFailureHandlers, captureHandshakeUpgrade } from './openai-ws-handshake.mjs';
12
+ import { createWsConnection, openHedged } from './openai-ws-connect.mjs';
12
13
  import { codexOriginator, codexUserAgent, codexVersionHeader } from './codex-client-meta.mjs';
13
14
  import {
14
15
  PROVIDER_WS_ACQUIRE_TIMEOUT_MS,
@@ -539,7 +540,26 @@ const WS_URL_BY_AUTH = { xai: XAI_WS_URL, 'openai-direct': OPENAI_WS_URL };
539
540
  /** The auth kind an error label names; anything but xAI/direct is the OAuth (Codex) path. */
540
541
  const _wsAuthKind = (auth) => (auth?.type === 'xai' || auth?.type === 'openai-direct' ? auth.type : 'openai-oauth');
541
542
 
542
- async function _openSocket({ auth, sessionToken, externalSignal, cacheKey, codexHeaders }) {
543
+ function _acquireTimeoutError(auth) {
544
+ return Object.assign(
545
+ new Error(`${_wsErrLabel(_wsAuthKind(auth))} acquire timed out before open (${WS_ACQUIRE_TIMEOUT_MS}ms)`),
546
+ { code: 'EWSACQUIRETIMEOUT', acquireTimeoutMs: WS_ACQUIRE_TIMEOUT_MS }
547
+ );
548
+ }
549
+
550
+ // One acquire = a hedged race of handshake legs under the acquire deadline
551
+ // (openai-ws-connect.mjs): a stuck upgrade is replaced after the hedge window
552
+ // instead of costing the whole timeout plus a retry.
553
+ function _openSocket(args) {
554
+ return openHedged({
555
+ openLeg: ({ externalSignal }) => _openSocketLeg({ ...args, externalSignal }),
556
+ externalSignal: args.externalSignal,
557
+ deadlineMs: WS_ACQUIRE_TIMEOUT_MS,
558
+ deadlineError: () => _acquireTimeoutError(args.auth),
559
+ });
560
+ }
561
+
562
+ async function _openSocketLeg({ auth, sessionToken, externalSignal, cacheKey, codexHeaders }) {
543
563
  // `ws` loads on the first socket, not at runtime boot via provider-admin.
544
564
  const { default: WebSocket } = await import('ws');
545
565
  const headers = _buildHandshakeHeaders({ auth, sessionToken, cacheKey, codexHeaders });
@@ -586,6 +606,7 @@ async function _openSocket({ auth, sessionToken, externalSignal, cacheKey, codex
586
606
  headers,
587
607
  handshakeTimeout: WS_HANDSHAKE_TIMEOUT_MS,
588
608
  maxPayload: WS_MAX_INCOMING_FRAME_BYTES,
609
+ createConnection: createWsConnection,
589
610
  });
590
611
  acquireTimer = setTimeout(() => {
591
612
  if (settled) return;
@@ -597,13 +618,7 @@ async function _openSocket({ auth, sessionToken, externalSignal, cacheKey, codex
597
618
  try {
598
619
  socket.terminate();
599
620
  } catch {}
600
- settle(
601
- false,
602
- Object.assign(
603
- new Error(`${_wsErrLabel(_wsAuthKind(auth))} acquire timed out before open (${WS_ACQUIRE_TIMEOUT_MS}ms)`),
604
- { code: 'EWSACQUIRETIMEOUT', acquireTimeoutMs: WS_ACQUIRE_TIMEOUT_MS }
605
- )
606
- );
621
+ settle(false, _acquireTimeoutError(auth));
607
622
  }, WS_ACQUIRE_TIMEOUT_MS);
608
623
  try {
609
624
  acquireTimer.unref?.();
@@ -764,6 +779,52 @@ function _onEntryClosed(poolKey, entry) {
764
779
  if (!entry.ephemeral) _removeFromPool(poolKey, entry);
765
780
  }
766
781
 
782
+ // Sessionless sends (no poolKey) carry no turn state or response chain of
783
+ // their own, so their connections are shared: one idle bucket per
784
+ // credential + cache key, no owner claim (concurrent sessionless callers
785
+ // each take a different idle socket or open one), and a reused socket starts
786
+ // with an empty chain so no request ever deltas against another caller's
787
+ // response. Without this every sessionless send paid a fresh handshake and
788
+ // closed the socket on release.
789
+ const SHARED_POOL_PREFIX = '\0shared:';
790
+
791
+ function _resetChain(entry) {
792
+ entry.lastResponseId = null;
793
+ entry.lastRequestSansInput = null;
794
+ entry.lastRequestInput = null;
795
+ entry.lastResponseItems = null;
796
+ entry.lastInputLen = 0;
797
+ entry.lastInputPrefixHash = null;
798
+ entry.turnState = null;
799
+ entry.turnStateScope = null;
800
+ entry.turnStateTurnId = null;
801
+ }
802
+
803
+ async function _acquireShared({ auth, cacheKey, codexHeaders, externalSignal, acqStart }) {
804
+ const compatibility = _poolCompatibility(auth, cacheKey);
805
+ const sharedKey = `${SHARED_POOL_PREFIX}${compatibility.authIdentity}:${compatibility.cacheKeyIdentity}`;
806
+ const arr = _wsPool.get(sharedKey) || [];
807
+ _pruneStaleEntries(sharedKey, arr, compatibility);
808
+ const idle = await _takeIdleEntry(sharedKey, arr, compatibility, acqStart);
809
+ if (idle) {
810
+ _resetChain(idle);
811
+ return { entry: idle, reused: true, ownerWaitMs: 0 };
812
+ }
813
+ const entry = await _openPoolEntry({
814
+ auth,
815
+ cacheKey,
816
+ codexHeaders,
817
+ externalSignal,
818
+ compatibility,
819
+ ephemeral: false,
820
+ acqStart,
821
+ });
822
+ entry.sharedPoolKey = sharedKey;
823
+ _getPoolArr(sharedKey).push(entry);
824
+ entry.socket.on('close', () => _onEntryClosed(sharedKey, entry));
825
+ return { entry, reused: false, ownerWaitMs: 0 };
826
+ }
827
+
767
828
  export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders, forceFresh, externalSignal }) {
768
829
  const _acqStart = Date.now();
769
830
  _acquireTrace(
@@ -772,6 +833,9 @@ export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders,
772
833
  if (externalSignal?.aborted) {
773
834
  throw _acquireAbortError(externalSignal);
774
835
  }
836
+ if (!poolKey && !forceFresh) {
837
+ return _acquireShared({ auth, cacheKey, codexHeaders, externalSignal, acqStart: _acqStart });
838
+ }
775
839
  const ownerClaim = await _claimPoolOwner(poolKey, externalSignal);
776
840
  const acquired = (entry, reused) => {
777
841
  _bindPoolOwner(poolKey, ownerClaim, entry);
@@ -825,8 +889,9 @@ export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders,
825
889
  }
826
890
  }
827
891
 
828
- export function releaseWebSocket({ entry, poolKey, keep }) {
892
+ export function releaseWebSocket({ entry, poolKey: callerPoolKey, keep }) {
829
893
  if (!entry) return;
894
+ const poolKey = callerPoolKey || entry.sharedPoolKey || null;
830
895
  entry.busy = false;
831
896
  if (!keep || !_isOpen(entry) || !poolKey || entry.ephemeral) {
832
897
  try {