mixdog 0.9.151 → 0.9.153

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 (307) hide show
  1. package/LICENSES/browser-import-NOTICE.txt +11 -0
  2. package/NOTICE.md +11 -0
  3. package/README.md +108 -68
  4. package/package.json +17 -4
  5. package/scripts/computer-task-cost-report.mjs +144 -0
  6. package/scripts/run-office-live-tests.mjs +32 -0
  7. package/scripts/tool-stress.mjs +0 -2
  8. package/src/agents/front-worker/AGENT.md +13 -0
  9. package/src/agents/front-worker/agent.json +6 -0
  10. package/src/agents/heavy-worker/AGENT.md +10 -11
  11. package/src/agents/heavy-worker/agent.json +1 -1
  12. package/src/agents/maintainer/AGENT.md +0 -2
  13. package/src/agents/reviewer/AGENT.md +9 -13
  14. package/src/agents/reviewer/agent.json +1 -1
  15. package/src/agents/security/AGENT.md +14 -0
  16. package/src/agents/security/agent.json +6 -0
  17. package/src/agents/worker/AGENT.md +8 -8
  18. package/src/agents/worker/agent.json +1 -1
  19. package/src/agents/writer/AGENT.md +14 -0
  20. package/src/agents/writer/agent.json +6 -0
  21. package/src/defaults/skills/setup/SKILL.md +18 -27
  22. package/src/headless-exec.mjs +3 -10
  23. package/src/headless-exec.test.mjs +2 -7
  24. package/src/rules/agent/00-common.md +4 -7
  25. package/src/rules/lead/01-general.md +0 -3
  26. package/src/rules/shared/10-tool-workflow.md +10 -2
  27. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +183 -26
  28. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.test.mjs +378 -0
  29. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
  30. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +109 -0
  31. package/src/runtime/agent/orchestrator/cache-break-trace.test.mjs +115 -0
  32. package/src/runtime/agent/orchestrator/config.mjs +13 -68
  33. package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
  34. package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
  35. package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
  36. package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
  37. package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
  38. package/src/runtime/agent/orchestrator/providers/admission-scheduler-metrics.test.mjs +47 -0
  39. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +16 -3
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
  41. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  43. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
  44. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
  45. package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
  46. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +19 -3
  47. package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
  48. package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
  49. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -9
  50. package/src/runtime/agent/orchestrator/providers/media-parity.test.mjs +59 -0
  51. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
  52. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +71 -0
  53. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +18 -26
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
  56. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
  57. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
  58. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +47 -2
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +103 -12
  60. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-turn-state.mjs +68 -0
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-delta-parity.test.mjs +122 -6
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +36 -11
  64. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +144 -93
  65. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +18 -13
  66. package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +288 -0
  67. package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
  68. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
  69. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
  70. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +86 -7
  71. package/src/runtime/agent/orchestrator/session/agent-loop.test.mjs +222 -0
  72. package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.mjs +61 -0
  73. package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.test.mjs +83 -0
  74. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
  75. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +26 -2
  76. package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +112 -0
  77. package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +192 -0
  78. package/src/runtime/agent/orchestrator/session/context-usage-restart.test.mjs +72 -0
  79. package/src/runtime/agent/orchestrator/session/context-utils.mjs +100 -15
  80. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +17 -1
  81. package/src/runtime/agent/orchestrator/session/eager-dispatch.test.mjs +74 -0
  82. package/src/runtime/agent/orchestrator/session/image-strip-recovery.mjs +12 -8
  83. package/src/runtime/agent/orchestrator/session/image-strip-recovery.test.mjs +20 -1
  84. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +110 -2
  85. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +3 -16
  86. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.test.mjs +28 -0
  87. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
  88. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +6 -0
  89. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -1
  90. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -0
  91. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +111 -39
  92. package/src/runtime/agent/orchestrator/session/manager/ask-session.test.mjs +46 -0
  93. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +30 -6
  94. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
  95. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +27 -3
  96. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +26 -54
  97. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.test.mjs +63 -1
  98. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +3 -0
  99. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -110
  100. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +580 -0
  101. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +593 -0
  102. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +119 -190
  103. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
  104. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +43 -8
  105. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +129 -12
  106. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +9 -2
  107. package/src/runtime/agent/orchestrator/session/store/listing.mjs +8 -1
  108. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +15 -1
  109. package/src/runtime/agent/orchestrator/session/store-live-publication.test.mjs +83 -0
  110. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +36 -2
  111. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +157 -30
  112. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +64 -0
  113. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +57 -0
  114. package/src/runtime/agent/orchestrator/session/store-summary-visibility.test.mjs +219 -0
  115. package/src/runtime/agent/orchestrator/session/store.mjs +26 -2
  116. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +22 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
  118. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
  119. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
  120. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
  121. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
  122. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
  123. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
  124. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
  125. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
  126. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +6 -6
  129. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
  130. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
  131. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
  132. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
  133. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
  134. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
  135. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
  136. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
  137. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  138. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  139. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +12 -0
  140. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  141. package/src/runtime/attachments/pdf-extract.mjs +3 -0
  142. package/src/runtime/attachments/pdfjs-runtime.mjs +13 -0
  143. package/src/runtime/bridge-clients.test.mjs +827 -0
  144. package/src/runtime/browser-bridge/action-schema.mjs +251 -0
  145. package/src/runtime/browser-bridge/client.mjs +152 -0
  146. package/src/runtime/browser-bridge/tool-defs.mjs +159 -0
  147. package/src/runtime/computer-bridge/action-schema.mjs +653 -0
  148. package/src/runtime/computer-bridge/client.mjs +231 -0
  149. package/src/runtime/computer-bridge/tool-defs.mjs +33 -0
  150. package/src/runtime/media/store.mjs +18 -8
  151. package/src/runtime/media/store.test.mjs +14 -0
  152. package/src/runtime/memory/index.mjs +1 -1
  153. package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
  154. package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
  155. package/src/runtime/office/assurance-benchmark.mjs +358 -0
  156. package/src/runtime/office/assurance.mjs +833 -0
  157. package/src/runtime/office/benchmark.mjs +496 -0
  158. package/src/runtime/office/capabilities.mjs +551 -0
  159. package/src/runtime/office/com-adapter.mjs +283 -0
  160. package/src/runtime/office/content-model.mjs +183 -0
  161. package/src/runtime/office/contract-benchmark.mjs +105 -0
  162. package/src/runtime/office/design-library.mjs +1562 -0
  163. package/src/runtime/office/design-system.mjs +1966 -0
  164. package/src/runtime/office/index.mjs +2413 -0
  165. package/src/runtime/office/journal.mjs +82 -0
  166. package/src/runtime/office/office-assurance.test.mjs +371 -0
  167. package/src/runtime/office/office-com-host.ps1 +3781 -0
  168. package/src/runtime/office/office-com-session-host.ps1 +855 -0
  169. package/src/runtime/office/office-live-runtime.test.mjs +819 -0
  170. package/src/runtime/office/office-runtime.test.mjs +2070 -0
  171. package/src/runtime/office/ooxml-validator.mjs +271 -0
  172. package/src/runtime/office/pagination.mjs +132 -0
  173. package/src/runtime/office/pdf-adapter.mjs +591 -0
  174. package/src/runtime/office/pdf-analysis.mjs +372 -0
  175. package/src/runtime/office/pdf-render.mjs +167 -0
  176. package/src/runtime/office/pdf-security.mjs +63 -0
  177. package/src/runtime/office/polish-benchmark.mjs +190 -0
  178. package/src/runtime/office/portable-ooxml.mjs +1867 -0
  179. package/src/runtime/office/quality-live-benchmark.mjs +486 -0
  180. package/src/runtime/office/quality-pipeline.mjs +136 -0
  181. package/src/runtime/office/tabular.mjs +302 -0
  182. package/src/runtime/office/templates/mixdog-executive.pptx +0 -0
  183. package/src/runtime/office/templates/mixdog-executive.pptx.mixdog.json +190 -0
  184. package/src/runtime/office/tool-defs.mjs +113 -0
  185. package/src/runtime/office/visual-diff.mjs +89 -0
  186. package/src/runtime/office/xlsx-assertions.mjs +172 -0
  187. package/src/runtime/office/xlsx-contract.mjs +147 -0
  188. package/src/runtime/shared/agent-route-config.mjs +9 -0
  189. package/src/runtime/shared/child-spawn-gate.mjs +17 -0
  190. package/src/runtime/shared/child-spawn-remote.mjs +69 -7
  191. package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
  192. package/src/runtime/shared/pristine-execution-contract.json +2 -1
  193. package/src/runtime/shared/provider-api-key.mjs +1 -0
  194. package/src/runtime/shared/session-runtime-health.mjs +91 -0
  195. package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
  196. package/src/runtime/shared/skill-document.mjs +86 -0
  197. package/src/runtime/shared/skill-document.test.mjs +81 -0
  198. package/src/runtime/shared/tool-execution-contract.mjs +16 -0
  199. package/src/runtime/shared/tool-surface.mjs +27 -0
  200. package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
  201. package/src/runtime/shared/turn-snapshot.mjs +32 -9
  202. package/src/runtime/shared/user-cwd.mjs +33 -14
  203. package/src/runtime/shared/user-cwd.test.mjs +43 -0
  204. package/src/session-runtime/agent-disable.test.mjs +20 -1
  205. package/src/session-runtime/context-status.mjs +9 -3
  206. package/src/session-runtime/cwd-plugins.mjs +26 -26
  207. package/src/session-runtime/cwd-plugins.test.mjs +75 -0
  208. package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
  209. package/src/session-runtime/deferred-tool-delta.mjs +104 -0
  210. package/src/session-runtime/deferred-tool-delta.test.mjs +40 -0
  211. package/src/session-runtime/global-extensions.mjs +26 -0
  212. package/src/session-runtime/global-extensions.test.mjs +28 -0
  213. package/src/session-runtime/global-settings-session-identity.test.mjs +76 -0
  214. package/src/session-runtime/goal-runtime.mjs +1141 -0
  215. package/src/session-runtime/goal-runtime.test.mjs +645 -0
  216. package/src/session-runtime/lifecycle-api.mjs +58 -11
  217. package/src/session-runtime/lifecycle-api.test.mjs +183 -0
  218. package/src/session-runtime/mcp-glue.mjs +49 -47
  219. package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
  220. package/src/session-runtime/plugin-mcp.mjs +96 -22
  221. package/src/session-runtime/prewarm.mjs +1 -10
  222. package/src/session-runtime/provider-request-snapshot.mjs +33 -9
  223. package/src/session-runtime/provider-request-snapshot.test.mjs +27 -5
  224. package/src/session-runtime/resource-api-global.test.mjs +109 -0
  225. package/src/session-runtime/resource-api.mjs +168 -116
  226. package/src/session-runtime/runtime-core.mjs +208 -29
  227. package/src/session-runtime/runtime-tunables.mjs +6 -8
  228. package/src/session-runtime/session-lifecycle.mjs +34 -12
  229. package/src/session-runtime/session-title.mjs +2 -2
  230. package/src/session-runtime/session-turn-api.mjs +37 -25
  231. package/src/session-runtime/skills-api.mjs +79 -25
  232. package/src/session-runtime/skills-api.test.mjs +99 -0
  233. package/src/session-runtime/tool-catalog-data.mjs +1 -1
  234. package/src/session-runtime/tool-catalog-schema.mjs +0 -5
  235. package/src/session-runtime/tool-catalog.mjs +42 -112
  236. package/src/session-runtime/tool-defs.mjs +6 -1
  237. package/src/session-runtime/tool-policy-surface.test.mjs +93 -17
  238. package/src/session-runtime/tool-surface.mjs +30 -8
  239. package/src/session-runtime/workflow-agents-api.mjs +8 -25
  240. package/src/session-runtime/workflow.mjs +8 -6
  241. package/src/standalone/agent-dispatch-broker.mjs +5 -9
  242. package/src/standalone/agent-tool/helpers.mjs +2 -2
  243. package/src/standalone/agent-tool/job-views.mjs +3 -1
  244. package/src/standalone/agent-tool/notify.mjs +11 -9
  245. package/src/standalone/agent-tool/notify.test.mjs +37 -0
  246. package/src/standalone/agent-tool/render.mjs +3 -0
  247. package/src/standalone/agent-tool/spawn-flow.mjs +61 -16
  248. package/src/standalone/agent-tool/spawn-flow.test.mjs +41 -0
  249. package/src/standalone/agent-tool/tool-def.mjs +1 -1
  250. package/src/standalone/agent-tool/worker-index.mjs +123 -0
  251. package/src/standalone/agent-tool/worker-index.test.mjs +132 -0
  252. package/src/standalone/agent-tool/worker-rows.mjs +2 -1
  253. package/src/standalone/agent-tool.mjs +57 -10
  254. package/src/standalone/daemon.mjs +113 -15
  255. package/src/standalone/hook-bus/config.mjs +1 -0
  256. package/src/standalone/plugin-admin.mjs +17 -0
  257. package/src/standalone/plugin-admin.test.mjs +7 -0
  258. package/src/standalone/provider-admin.mjs +1 -0
  259. package/src/standalone/session-protocol.mjs +5 -0
  260. package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
  261. package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
  262. package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
  263. package/src/standalone/session-runtime-host-factory.mjs +17 -0
  264. package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
  265. package/src/standalone/session-runtime-host-health.test.mjs +5 -0
  266. package/src/standalone/session-runtime-host.mjs +709 -58
  267. package/src/standalone/session-runtime-inline-host.mjs +258 -0
  268. package/src/standalone/session-runtime-inline-host.test.mjs +385 -0
  269. package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
  270. package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
  271. package/src/standalone/session-runtime-shard-host.test.mjs +642 -0
  272. package/src/standalone/session-runtime-shard-router.mjs +149 -0
  273. package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
  274. package/src/standalone/session-runtime-worker.mjs +302 -26
  275. package/src/standalone/session-service-agent-tree.test.mjs +984 -0
  276. package/src/standalone/session-service-goal-recovery.test.mjs +177 -0
  277. package/src/standalone/session-service.mjs +711 -11
  278. package/src/tui/app/app-view.jsx +1 -0
  279. package/src/tui/app/core-memory-picker.mjs +2 -0
  280. package/src/tui/app/extension-pickers.mjs +1 -1
  281. package/src/tui/app/maintenance-pickers.mjs +3 -3
  282. package/src/tui/app/model-options.mjs +2 -0
  283. package/src/tui/app/model-picker.mjs +14 -4
  284. package/src/tui/app/panel-handoff.test.mjs +311 -0
  285. package/src/tui/app/project-picker.mjs +1 -0
  286. package/src/tui/app/route-pickers.mjs +21 -6
  287. package/src/tui/app/settings-picker.mjs +26 -3
  288. package/src/tui/app/slash-commands.mjs +1 -0
  289. package/src/tui/app/slash-dispatch.mjs +73 -22
  290. package/src/tui/app/theme-effort-pickers.mjs +7 -2
  291. package/src/tui/components/Picker.jsx +10 -1
  292. package/src/tui/dist/index.mjs +187 -45
  293. package/src/tui/lib/voice-setup.mjs +4 -1
  294. package/src/tui/session/agent-message-source.test.mjs +37 -0
  295. package/src/tui/session/completion-card-restore.test.mjs +50 -0
  296. package/src/tui/session/context-state.mjs +3 -13
  297. package/src/tui/session/goal-continuation.mjs +141 -0
  298. package/src/tui/session/goal-continuation.test.mjs +203 -0
  299. package/src/tui/session/queue-helpers.mjs +5 -2
  300. package/src/tui/session/session-action-surface.test.mjs +190 -0
  301. package/src/tui/session/session-api-ext.mjs +48 -8
  302. package/src/tui/session/session-api.mjs +73 -1
  303. package/src/tui/session/session-flow.mjs +31 -1
  304. package/src/tui/session/turn.mjs +88 -15
  305. package/src/tui/session-local.mjs +39 -3
  306. package/src/vendor/statusline/src/gateway/route-meta.mjs +31 -0
  307. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
@@ -26,6 +26,11 @@ import {
26
26
  // Human-readable transport label for handshake/acquire error messages. Shared
27
27
  // with openai-oauth-ws.mjs (stream-side errors use the same labels).
28
28
  import { _envOn, _codexBetaFeatures, _dumpHandshakeHeaders, _dumpFrame, _formatRedactedHeaders, _cfCookieHeader, _cfCookieCapture } from './openai-ws-headers.mjs';
29
+ import {
30
+ clearAllCodexTurnStates,
31
+ clearCodexTurnStateScope,
32
+ retireCodexTurnStateOwner,
33
+ } from './openai-turn-state.mjs';
29
34
  export function _wsErrLabel(p) {
30
35
  if (p === 'xai') return 'xAI WS';
31
36
  if (p === 'openai-direct' || p === 'openai') return 'OpenAI WS';
@@ -81,44 +86,94 @@ const MAX_POOLED_SOCKETS_PER_KEY = 8;
81
86
  export const _wsPool = new Map();
82
87
  let _releaseSequence = 0;
83
88
 
84
- // The server issues an `x-codex-turn-state` sticky-routing token on the
85
- // handshake 101 response; it pins the backend node that holds the warm prefix.
86
- // codex keeps that token in a turn-scoped cell and replays it on the RECONNECT
87
- // handshake (build_responses_headers -> connect_websocket), so a replacement
88
- // connection re-pins the same node instead of cold-starting the prefix. Our
89
- // pool entry dies with its socket, so the token is retained here at poolKey
90
- // scope and replayed only for a replacement socket. A parallel peer must still
91
- // open clean: inheriting a sibling's token makes openai-oauth treat the request
92
- // as a continuation of another in-flight turn ("No tool output found for
93
- // function call ...").
94
- const _turnStateByPoolKey = new Map();
95
- const MAX_RETAINED_TURN_STATE_KEYS = 4096;
96
-
97
- function _retainTurnState(poolKey, entry) {
98
- if (!poolKey || !entry) return;
99
- const turnState = typeof entry.turnState === 'string' ? entry.turnState : '';
100
- // A token already retired by the per-turn drop guard must not be revived.
101
- if (!turnState) return;
102
- // Bounded like the other pool maps so a long-lived process cannot
103
- // accumulate one token per dead session.
104
- if (!_turnStateByPoolKey.has(poolKey) && _turnStateByPoolKey.size >= MAX_RETAINED_TURN_STATE_KEYS) {
105
- const oldest = _turnStateByPoolKey.keys().next().value;
106
- if (oldest !== undefined) _turnStateByPoolKey.delete(oldest);
107
- }
108
- _turnStateByPoolKey.set(poolKey, {
109
- turnState,
110
- // Carry the turn owner so the per-turn drop guard still retires a
111
- // replayed token when turn_id moves on.
112
- turnStateTurnId: entry.turnStateTurnId ?? null,
89
+ // Codex gives one turn-scoped session exclusive use of one WebSocket until its
90
+ // response stream ends. Mirror that ownership per poolKey: a concurrent acquire
91
+ // waits instead of opening a sibling connection with a mismatched response
92
+ // chain. The claim is retained through close when keep=false so only an actual
93
+ // close can hand turn state to a replacement socket.
94
+ const _poolOwnerByKey = new Map();
95
+
96
+ function _acquireAbortError(externalSignal) {
97
+ const reason = externalSignal?.reason;
98
+ return reason instanceof Error ? reason : new Error('OpenAI OAuth WS acquire aborted');
99
+ }
100
+
101
+ function _waitForPoolOwnerRelease(claim, externalSignal) {
102
+ return new Promise((resolve, reject) => {
103
+ let settled = false;
104
+ const finish = (fn, value) => {
105
+ if (settled) return;
106
+ settled = true;
107
+ claim.waiters.delete(onRelease);
108
+ try { externalSignal?.removeEventListener('abort', onAbort); } catch {}
109
+ fn(value);
110
+ };
111
+ const onRelease = () => finish(resolve);
112
+ const onAbort = () => finish(reject, _acquireAbortError(externalSignal));
113
+ claim.waiters.add(onRelease);
114
+ if (externalSignal) {
115
+ if (externalSignal.aborted) {
116
+ onAbort();
117
+ return;
118
+ }
119
+ externalSignal.addEventListener('abort', onAbort, { once: true });
120
+ }
113
121
  });
114
122
  }
115
123
 
116
- function _replacementTurnState(poolKey) {
124
+ async function _claimPoolOwner(poolKey, externalSignal) {
117
125
  if (!poolKey) return null;
118
- // A live entry in the bucket means this handshake is a parallel peer, not
119
- // a reconnect.
120
- if (hasPooledWebSocket(poolKey)) return null;
121
- return _turnStateByPoolKey.get(poolKey) || null;
126
+ const waitStartedAt = performance.now();
127
+ for (;;) {
128
+ if (externalSignal?.aborted) throw _acquireAbortError(externalSignal);
129
+ const current = _poolOwnerByKey.get(poolKey);
130
+ if (!current) {
131
+ const claim = {
132
+ entry: null,
133
+ waiters: new Set(),
134
+ waitMs: Math.max(0, performance.now() - waitStartedAt),
135
+ };
136
+ _poolOwnerByKey.set(poolKey, claim);
137
+ return claim;
138
+ }
139
+ await _waitForPoolOwnerRelease(current, externalSignal);
140
+ }
141
+ }
142
+
143
+ function _bindPoolOwner(poolKey, claim, entry) {
144
+ if (!poolKey || !claim) return;
145
+ if (_poolOwnerByKey.get(poolKey) !== claim) {
146
+ try { entry?.socket?.close?.(1000, 'pool_owner_changed'); } catch {}
147
+ _scheduleForcedTerminate(entry?.socket);
148
+ throw new Error('OpenAI OAuth WS ownership changed during acquire');
149
+ }
150
+ claim.entry = entry;
151
+ entry.poolOwnerClaim = claim;
152
+ }
153
+
154
+ function _releasePoolOwner(poolKey, owner) {
155
+ if (!poolKey || !owner) return;
156
+ const claim = owner.poolOwnerClaim || owner;
157
+ if (_poolOwnerByKey.get(poolKey) !== claim) return;
158
+ _poolOwnerByKey.delete(poolKey);
159
+ if (claim.entry?.poolOwnerClaim === claim) {
160
+ try { delete claim.entry.poolOwnerClaim; } catch {}
161
+ }
162
+ for (const waiter of [...claim.waiters]) {
163
+ try { waiter(); } catch {}
164
+ }
165
+ claim.waiters.clear();
166
+ }
167
+
168
+ function _clearPoolOwnerScope(poolKey) {
169
+ const claim = poolKey ? _poolOwnerByKey.get(poolKey) : null;
170
+ if (claim) _releasePoolOwner(poolKey, claim);
171
+ }
172
+
173
+ function _clearAllPoolOwners() {
174
+ for (const [poolKey, claim] of [..._poolOwnerByKey.entries()]) {
175
+ _releasePoolOwner(poolKey, claim);
176
+ }
122
177
  }
123
178
 
124
179
  function _poolCompatibility(auth, cacheKey) {
@@ -141,12 +196,9 @@ function _entryCompatible(entry, compatibility) {
141
196
  && entry?.cacheKeyIdentity === compatibility.cacheKeyIdentity;
142
197
  }
143
198
 
144
- // A previous_response_id is valid on the connection that produced it. When
145
- // concurrency temporarily creates sibling sockets for one pool key, continue
146
- // on the most recently completed sibling instead of whichever socket happened
147
- // to be inserted first. This is the closest representation of Codex's
148
- // turn-local single-connection ownership without weakening the existing
149
- // full-frame fallback checks.
199
+ // A previous_response_id is valid on the connection that produced it. If a
200
+ // bucket contains multiple legacy idle entries, continue on the most recently
201
+ // completed one instead of whichever socket happened to be inserted first.
150
202
  function _selectIdleEntry(entries, compatibility) {
151
203
  let selected;
152
204
  for (const entry of entries || []) {
@@ -184,7 +236,7 @@ function _getPoolArr(poolKey) {
184
236
  // cap-overflow ephemeral. They were invisible to per-session close and to the
185
237
  // global drain, so a wedged one survived both. Tracking them here keeps the
186
238
  // reuse map unchanged (nothing can select them) while making every owned socket
187
- // reachable by closeOpenaiWsPoolForSession/_closeAllPooledSockets.
239
+ // reachable by closeOpenaiWsPoolForSession/drainOpenaiWsPool.
188
240
  const _unpooledSockets = new Set();
189
241
 
190
242
  function _trackUnpooled(entry, poolKey) {
@@ -203,9 +255,6 @@ function _removeFromPool(poolKey, entry) {
203
255
  // idle-close or liveness-ping interval.
204
256
  _clearIdle(entry);
205
257
  _clearLiveness(entry);
206
- // The entry is going away; keep its shard pin so the session's next socket
207
- // can reconnect onto the same backend node.
208
- _retainTurnState(poolKey, entry);
209
258
  if (!poolKey) return;
210
259
  const arr = _wsPool.get(poolKey);
211
260
  if (!arr) return;
@@ -375,7 +424,7 @@ export function _sendFrame(entry, frame, sendSpan = null, timeoutMs = WS_SEND_TI
375
424
  });
376
425
  }
377
426
 
378
- function _buildHandshakeHeaders({ auth, sessionToken, turnState, cacheKey: _cacheKey, codexHeaders }) {
427
+ function _buildHandshakeHeaders({ auth, sessionToken, cacheKey: _cacheKey, codexHeaders }) {
379
428
  // xAI WS: do NOT pin x-grok-conv-id. Measured parallel runs show that
380
429
  // forcing a routing shard via that header alternates cold caches across
381
430
  // parallel workers; the automatic prompt-prefix cache holds up better
@@ -431,7 +480,6 @@ function _buildHandshakeHeaders({ auth, sessionToken, turnState, cacheKey: _cach
431
480
  // distinguishable across reconnects.
432
481
  headers['x-client-request-id'] = randomBytes(16).toString('hex');
433
482
  }
434
- if (turnState) headers['x-codex-turn-state'] = turnState;
435
483
  // Probe knobs (cache-route hunt 2026-07-04): see jar block at top of file.
436
484
  if (isOpenAiOauth) {
437
485
  const jar = _cfCookieHeader(auth);
@@ -454,8 +502,8 @@ function _mintSessionToken(cacheKey, auth) {
454
502
  return cacheKey || 'mixdog-default';
455
503
  }
456
504
 
457
- function _openSocket({ auth, sessionToken, turnState, externalSignal, cacheKey, codexHeaders }) {
458
- const headers = _buildHandshakeHeaders({ auth, sessionToken, turnState, cacheKey, codexHeaders });
505
+ function _openSocket({ auth, sessionToken, externalSignal, cacheKey, codexHeaders }) {
506
+ const headers = _buildHandshakeHeaders({ auth, sessionToken, cacheKey, codexHeaders });
459
507
  const baseUrl = auth.type === 'xai'
460
508
  ? XAI_WS_URL
461
509
  : auth.type === 'openai-direct'
@@ -510,11 +558,8 @@ function _openSocket({ auth, sessionToken, turnState, externalSignal, cacheKey,
510
558
  ));
511
559
  }, WS_ACQUIRE_TIMEOUT_MS);
512
560
  try { acquireTimer.unref?.(); } catch {}
513
- const capturedHeaders = { turnState: null };
514
561
  socket.once('upgrade', (res) => {
515
562
  try {
516
- const ts = res?.headers?.['x-codex-turn-state'];
517
- if (typeof ts === 'string' && ts.length) capturedHeaders.turnState = ts;
518
563
  _cfCookieCapture(auth, res?.headers?.['set-cookie']);
519
564
  // Probe: dump the full 101-upgrade response header set so we can
520
565
  // see what the server actually issues (turn-state investigation).
@@ -541,7 +586,7 @@ function _openSocket({ auth, sessionToken, turnState, externalSignal, cacheKey,
541
586
  if (process.env.MIXDOG_DEBUG_AGENT) {
542
587
  process.stderr.write(`[agent-trace] ws-open-ok elapsed=${Date.now() - _wsOpenStart}ms\n`);
543
588
  }
544
- settle(true, { socket, turnState: capturedHeaders.turnState });
589
+ settle(true, { socket });
545
590
  });
546
591
  socket.once('error', (err) => {
547
592
  if (process.env.MIXDOG_DEBUG_AGENT) {
@@ -590,15 +635,23 @@ function _openSocket({ auth, sessionToken, turnState, externalSignal, cacheKey,
590
635
 
591
636
  let _openSocketImpl = _openSocket;
592
637
 
593
- export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders, forceFresh, externalSignal }) {
638
+ export async function acquireWebSocket({
639
+ auth,
640
+ poolKey,
641
+ cacheKey,
642
+ codexHeaders,
643
+ forceFresh,
644
+ externalSignal,
645
+ }) {
594
646
  const _acqStart = Date.now();
595
647
  if (process.env.MIXDOG_DEBUG_AGENT) {
596
648
  process.stderr.write(`[agent-trace] acquire-start poolKey=${poolKey} cacheKey=${cacheKey} forceFresh=${forceFresh} externalAborted=${!!externalSignal?.aborted} ts=${_acqStart}\n`);
597
649
  }
598
650
  if (externalSignal?.aborted) {
599
- const reason = externalSignal.reason;
600
- throw reason instanceof Error ? reason : new Error('OpenAI OAuth WS acquire aborted');
651
+ throw _acquireAbortError(externalSignal);
601
652
  }
653
+ const ownerClaim = await _claimPoolOwner(poolKey, externalSignal);
654
+ try {
602
655
  if (poolKey && !forceFresh) {
603
656
  const arr = _wsPool.get(poolKey) || [];
604
657
  const compatibility = _poolCompatibility(auth, cacheKey);
@@ -610,13 +663,6 @@ export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders,
610
663
  if (!_isOpen(arr[i]) || arr[i].closing || incompatibleIdle) {
611
664
  if (incompatibleIdle) {
612
665
  try { arr[i].socket.close(1000, 'pool_boundary_changed'); } catch {}
613
- } else {
614
- // A dead socket pruned here never reaches _removeFromPool,
615
- // so retain its shard pin explicitly — this is the common
616
- // reconnect path and the one the replacement handshake
617
- // below reads. A boundary change deliberately drops the
618
- // pin: it belongs to the old auth/cache identity.
619
- _retainTurnState(poolKey, arr[i]);
620
666
  }
621
667
  _clearIdle(arr[i]);
622
668
  _clearLiveness(arr[i]);
@@ -660,7 +706,8 @@ export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders,
660
706
  if (process.env.MIXDOG_DEBUG_AGENT) {
661
707
  process.stderr.write(`[agent-trace] acquire-reuse poolKey=${poolKey} openSockets=${arr.length} elapsed=${Date.now() - _acqStart}ms\n`);
662
708
  }
663
- return { entry: idle, reused: true };
709
+ _bindPoolOwner(poolKey, ownerClaim, idle);
710
+ return { entry: idle, reused: true, ownerWaitMs: ownerClaim?.waitMs || 0 };
664
711
  }
665
712
  // All entries busy and bucket at cap: fall through to ephemeral socket.
666
713
  if (arr.length >= MAX_POOLED_SOCKETS_PER_KEY) {
@@ -668,7 +715,7 @@ export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders,
668
715
  process.stderr.write(`[agent-trace] acquire-ephemeral cacheKey=${cacheKey} reason=cap elapsed=${Date.now() - _acqStart}ms\n`);
669
716
  }
670
717
  const ephSessionToken = _mintSessionToken(cacheKey, auth);
671
- const { socket, turnState } = await _openSocketImpl({ auth, sessionToken: ephSessionToken, turnState: null, externalSignal, cacheKey, codexHeaders });
718
+ const { socket } = await _openSocketImpl({ auth, sessionToken: ephSessionToken, externalSignal, cacheKey, codexHeaders });
672
719
  // Drain-complete fence: same invariant as the normal acquire path —
673
720
  // if drain fired during the await, do NOT push an ephemeral entry
674
721
  // back into the pool.
@@ -688,7 +735,7 @@ export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders,
688
735
  lastInputPrefixHash: null,
689
736
  releaseSequence: 0,
690
737
  ...compatibility,
691
- turnState: turnState || null,
738
+ turnState: null,
692
739
  closing: false,
693
740
  ephemeral: true,
694
741
  sessionToken: ephSessionToken,
@@ -698,26 +745,27 @@ export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders,
698
745
  entry.probing = false;
699
746
  socket.on('pong', () => { entry.lastAliveAt = Date.now(); });
700
747
  socket.on('message', () => { entry.lastAliveAt = Date.now(); });
701
- socket.on('close', () => { entry.closing = true; _untrackUnpooled(entry); });
748
+ socket.on('close', () => {
749
+ entry.closing = true;
750
+ retireCodexTurnStateOwner(entry.turnStateScope || poolKey, entry);
751
+ _releasePoolOwner(poolKey, entry);
752
+ _untrackUnpooled(entry);
753
+ });
702
754
  // Cap-overflow ephemeral: never pooled, still owned by this session.
703
755
  _trackUnpooled(entry, poolKey);
704
- return { entry, reused: false };
756
+ _bindPoolOwner(poolKey, ownerClaim, entry);
757
+ return { entry, reused: false, ownerWaitMs: ownerClaim?.waitMs || 0 };
705
758
  }
706
759
  }
707
- // Parallel sockets must not inherit sibling turnState or the openai-oauth server
708
- // treats the new request as a continuation of another in-flight turn and
709
- // returns "No tool output found for function call …". turnState only
710
- // propagates within a single entry across its own iterations — plus the
711
- // codex reconnect case below, where the bucket holds no live entry and the
712
- // retained token re-pins the shard the dead socket was using. forceFresh
713
- // asks for a deliberately unpinned socket, so it always opens clean.
760
+ // A handshake is scoped to a physical connection, while turn state belongs
761
+ // to one logical turn. Every new socket therefore opens without turn state;
762
+ // request metadata restores it later only when the turn id matches.
714
763
  const sessionToken = _mintSessionToken(cacheKey, auth);
715
764
  const compatibility = _poolCompatibility(auth, cacheKey);
716
- const retainedTurnState = forceFresh ? null : _replacementTurnState(poolKey);
717
765
  if (process.env.MIXDOG_DEBUG_AGENT) {
718
766
  process.stderr.write(`[agent-trace] acquire-new tokenHash=${createHash('sha256').update(String(sessionToken)).digest('hex').slice(0, 8)} elapsed=${Date.now() - _acqStart}ms\n`);
719
767
  }
720
- const { socket, turnState } = await _openSocketImpl({ auth, sessionToken, turnState: retainedTurnState?.turnState || null, externalSignal, cacheKey, codexHeaders });
768
+ const { socket } = await _openSocketImpl({ auth, sessionToken, externalSignal, cacheKey, codexHeaders });
721
769
  // Drain may complete while the normal handshake is awaiting 'open'. Never
722
770
  // return or insert that late socket into the already-drained process pool.
723
771
  if (_drainComplete) {
@@ -736,11 +784,8 @@ export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders,
736
784
  lastInputPrefixHash: null,
737
785
  releaseSequence: 0,
738
786
  ...compatibility,
739
- // A token issued on THIS handshake supersedes the replayed one and is
740
- // attributed on first use; a surviving retained token keeps its
741
- // original turn owner so the per-turn drop guard still retires it.
742
- turnState: turnState || retainedTurnState?.turnState || null,
743
- turnStateTurnId: turnState ? null : (retainedTurnState?.turnStateTurnId ?? null),
787
+ turnState: null,
788
+ turnStateTurnId: null,
744
789
  closing: false,
745
790
  ephemeral: false,
746
791
  sessionToken,
@@ -757,10 +802,17 @@ export async function acquireWebSocket({ auth, poolKey, cacheKey, codexHeaders,
757
802
  else _trackUnpooled(entry, poolKey);
758
803
  socket.on('close', () => {
759
804
  entry.closing = true;
805
+ retireCodexTurnStateOwner(entry.turnStateScope || poolKey, entry);
806
+ _releasePoolOwner(poolKey, entry);
760
807
  _untrackUnpooled(entry);
761
808
  _removeFromPool(poolKey, entry);
762
809
  });
763
- return { entry, reused: false };
810
+ _bindPoolOwner(poolKey, ownerClaim, entry);
811
+ return { entry, reused: false, ownerWaitMs: ownerClaim?.waitMs || 0 };
812
+ } catch (err) {
813
+ _releasePoolOwner(poolKey, ownerClaim);
814
+ throw err;
815
+ }
764
816
  }
765
817
 
766
818
  export function releaseWebSocket({ entry, poolKey, keep }) {
@@ -790,14 +842,13 @@ export function releaseWebSocket({ entry, poolKey, keep }) {
790
842
  // Idle pooled sockets retain reuse state without retaining the process.
791
843
  // acquireWebSocket re-refs the transport before probing or handing it out.
792
844
  _setTransportReferenced(entry, false);
845
+ _releasePoolOwner(poolKey, entry);
793
846
  }
794
847
 
795
848
  export function closeOpenaiWsPoolForSession(poolKey, reason = 'session_closed') {
796
849
  if (!poolKey) return;
797
- // The session is over, so its shard pin must not survive into a later
798
- // session that reuses the key. Clearing each entry's token before the
799
- // close() calls also keeps the async 'close' handlers from re-retaining it.
800
- _turnStateByPoolKey.delete(poolKey);
850
+ _clearPoolOwnerScope(poolKey);
851
+ clearCodexTurnStateScope(poolKey);
801
852
  const closeReason = String(reason || 'session_closed');
802
853
  const entries = _wsPool.get(poolKey);
803
854
  if (entries) {
@@ -859,6 +910,7 @@ export function hasPooledWebSocket(poolKey) {
859
910
  }
860
911
 
861
912
  export function _clearWebSocketPoolForTest() {
913
+ _clearAllPoolOwners();
862
914
  for (const arr of _wsPool.values()) {
863
915
  for (const entry of arr) {
864
916
  entry.turnState = null;
@@ -875,7 +927,7 @@ export function _clearWebSocketPoolForTest() {
875
927
  }
876
928
  _unpooledSockets.clear();
877
929
  _wsPool.clear();
878
- _turnStateByPoolKey.clear();
930
+ clearAllCodexTurnStates();
879
931
  _releaseSequence = 0;
880
932
  }
881
933
 
@@ -883,17 +935,17 @@ export function _setOpenSocketForTest(fn) {
883
935
  _openSocketImpl = typeof fn === 'function' ? fn : _openSocket;
884
936
  }
885
937
 
886
- // Drain-complete fence — set true once _closeAllPooledSockets runs so any
938
+ // Drain-complete fence — set true once drainOpenaiWsPool runs so any
887
939
  // in-flight acquire that resumes after drain throws instead of pushing a
888
940
  // fresh socket into the cleared pool. Single-set, process-lifetime invariant.
889
941
  let _drainComplete = false;
890
942
 
891
943
  // Drain hook — self-registered exit drain.
892
944
  // Force-closes pooled sockets and fences subsequent acquires.
893
- // `drainOpenaiWsPool` alias matches the registry's `drain*` naming convention;
894
- // `_closeAllPooledSockets` kept for backward compat with existing call sites.
895
- export function _closeAllPooledSockets(reason = 'shutdown', { immediate = false } = {}) {
945
+ export function drainOpenaiWsPool(reason = 'shutdown', { immediate = false } = {}) {
896
946
  _drainComplete = true;
947
+ _clearAllPoolOwners();
948
+ clearAllCodexTurnStates();
897
949
  for (const arr of _wsPool.values()) {
898
950
  // _shutdownEntry tears down per-entry timers before the map is dropped
899
951
  // (otherwise the idle-close and liveness-ping intervals outlive the
@@ -909,7 +961,6 @@ export function _closeAllPooledSockets(reason = 'shutdown', { immediate = false
909
961
  _unpooledSockets.clear();
910
962
  _wsPool.clear();
911
963
  }
912
- export const drainOpenaiWsPool = _closeAllPooledSockets;
913
964
  // Session-close / drain hooks are shared across provider transports (cursor-wire
914
965
  // registers the same globals), so chain whatever is already installed instead of
915
966
  // replacing it — import order must not decide which transport gets torn down.
@@ -925,4 +976,4 @@ globalThis.__mixdogDrainProviderConnections = (reason) => {
925
976
  };
926
977
  // 'exit' hands the listener an exit CODE, never a reason string, and no timer
927
978
  // can fire after it: terminate immediately instead of scheduling.
928
- process.on('exit', () => _closeAllPooledSockets('process-exit', { immediate: true }));
979
+ process.on('exit', () => drainOpenaiWsPool('process-exit', { immediate: true }));
@@ -33,7 +33,9 @@ import {
33
33
  streamStalledError,
34
34
  } from '../stall-policy.mjs';
35
35
  import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
36
+ import { createProviderReplay } from './lib/provider-replay.mjs';
36
37
  import { _wsErrLabel, WS_MAX_INCOMING_FRAME_BYTES } from './openai-ws-pool.mjs';
38
+ import { captureCodexTurnState } from './openai-turn-state.mjs';
37
39
  import {
38
40
  _sansInput,
39
41
  _stableStringify,
@@ -180,24 +182,19 @@ function _hasHeaderKey(headers, name) {
180
182
 
181
183
  export function _captureTurnStateFromEvent(entry, event) {
182
184
  if (!entry || entry.turnState || !event || typeof event !== 'object') return;
183
- // The token normally arrives on the WebSocket 101 handshake and is held
184
- // write-once per entry, which is what keeps sticky routing stable across
185
- // previous_response_id calls. This event path is the fallback for
186
- // deployments that surface the header on a response event instead; the
187
- // write-once guard above means it can never swap a live chain's token.
185
+ // WebSocket turn state is returned by response metadata and replayed only
186
+ // in later response.create metadata for the same logical turn.
188
187
  const turnState = _headerString(event.headers, X_CODEX_TURN_STATE_HEADER)
189
188
  || _headerString(event.response?.headers, X_CODEX_TURN_STATE_HEADER)
190
189
  || _headerString(event.response?.metadata?.headers, X_CODEX_TURN_STATE_HEADER)
191
190
  || _headerString(event.metadata?.headers, X_CODEX_TURN_STATE_HEADER);
192
191
  if (turnState) {
193
- entry.turnState = turnState;
194
- // Attribute the captured token to the turn currently on the wire so
195
- // the per-turn drop guard (_withCodexWsClientMetadata) can retire it
196
- // once turn_id advances; null falls back to first-use attribution. An
197
- // empty turn_id (parity prewarm) is a valid owner — preserve '' rather
198
- // than collapsing it to null, so a prewarm-captured token is retired on
199
- // the next real turn instead of leaking onto it.
200
- entry.turnStateTurnId = entry.currentTurnId != null ? entry.currentTurnId : null;
192
+ entry.turnState = captureCodexTurnState(
193
+ entry.turnStateScope,
194
+ entry.turnStateTurnId,
195
+ turnState,
196
+ entry,
197
+ ) || ((!entry.turnStateScope || !entry.turnStateTurnId) ? turnState : null);
201
198
  }
202
199
  }
203
200
 
@@ -675,6 +672,10 @@ export async function _streamResponse({
675
672
  try {
676
673
  err.partialContent = content;
677
674
  err.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
675
+ err.partialProviderReplay = createProviderReplay(
676
+ traceProvider === 'xai' ? 'xai-responses' : 'openai-responses',
677
+ responseItemsAdded,
678
+ );
678
679
  err.pendingToolUse = pendingCalls.size > 0
679
680
  || activeToolItems.size > 0
680
681
  || _toolInFlight === true;
@@ -794,6 +795,10 @@ export async function _streamResponse({
794
795
  model,
795
796
  reasoningItems: reasoningItems.length ? reasoningItems : undefined,
796
797
  responseItems: responseItemsAdded.length ? responseItemsAdded : undefined,
798
+ providerReplay: createProviderReplay(
799
+ traceProvider === 'xai' ? 'xai-responses' : 'openai-responses',
800
+ responseItemsAdded,
801
+ ),
797
802
  // Dedupe by name+args (Fix 2, array side) so an identical
798
803
  // synthetic-leaked + native pair can't run the tool twice.
799
804
  toolCalls: toolCalls.length ? dedupeToolCallList(toolCalls) : undefined,