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
@@ -32,6 +32,7 @@ import {
32
32
  grokCacheChainTraceFields,
33
33
  appendAgentTrace,
34
34
  } from '../agent-trace.mjs';
35
+ import { traceCacheBreak } from '../cache-break-trace.mjs';
35
36
  import {
36
37
  classifyHandshakeError,
37
38
  classifyMidstreamError,
@@ -50,7 +51,6 @@ import {
50
51
  acquireWebSocket,
51
52
  releaseWebSocket,
52
53
  _sendFrame,
53
- _closeAllPooledSockets,
54
54
  drainOpenaiWsPool,
55
55
  } from './openai-ws-pool.mjs';
56
56
  import {
@@ -66,7 +66,10 @@ import {
66
66
  parseToolSearchArgs,
67
67
  _streamResponse,
68
68
  } from './openai-ws-stream.mjs';
69
- import { _buildResponseCreateFrame } from './openai-ws-delta.mjs';
69
+ import {
70
+ _buildResponseCreateFrame,
71
+ _requestInputMismatchDiagnostics,
72
+ } from './openai-ws-delta.mjs';
70
73
  import { resolveOpenAiTransportPolicy } from './openai-transport-policy.mjs';
71
74
  import { envPositiveInt } from './lib/env-utils.mjs';
72
75
 
@@ -74,7 +77,6 @@ import { envPositiveInt } from './lib/env-utils.mjs';
74
77
  // (drainOpenaiWsPool), scripts/provider-toolcall-test.mjs (parseToolSearchArgs,
75
78
  // _logicalResponseItemMatch, _streamResponse) and other external callers.
76
79
  export {
77
- _closeAllPooledSockets,
78
80
  drainOpenaiWsPool,
79
81
  _logicalResponseItemMatch,
80
82
  parseToolSearchArgs,
@@ -591,6 +593,10 @@ export async function sendViaWebSocket({
591
593
  // One compact row per logical iteration. Values aggregate all handshake
592
594
  // and mid-stream attempts, including warmup, without retaining request data.
593
595
  const sendSpan = {
596
+ admissionQueueWaitMs: Math.max(0, Number(sendOpts?._providerAdmission?.queueWaitMs) || 0),
597
+ providerConcurrentRequests: Math.max(1, Number(sendOpts?._providerAdmission?.active) || 1),
598
+ providerQueuedRequests: Math.max(0, Number(sendOpts?._providerAdmission?.queued) || 0),
599
+ poolOwnerWaitMs: 0,
594
600
  poolAcquireMs: 0,
595
601
  requestBuildSerializationMs: 0,
596
602
  preResponseCreatedMs: 0,
@@ -600,14 +606,44 @@ export async function sendViaWebSocket({
600
606
  acquireAttempts: 0,
601
607
  acquireMode: null,
602
608
  emitted: false,
609
+ timing: null,
603
610
  };
604
- const emitSendSpan = (outcome) => {
605
- if (sendSpan.emitted) return;
611
+ const emitSendSpan = (outcome, target = null) => {
612
+ if (sendSpan.emitted) {
613
+ if (target && sendSpan.timing) {
614
+ try { target.transportTiming = sendSpan.timing; } catch {}
615
+ }
616
+ return sendSpan.timing;
617
+ }
606
618
  sendSpan.emitted = true;
619
+ const socketAcquireMs = Math.max(0, sendSpan.poolAcquireMs - sendSpan.poolOwnerWaitMs);
620
+ const timing = {
621
+ transport: 'websocket',
622
+ outcome,
623
+ admissionQueueWaitMs: sendSpan.admissionQueueWaitMs,
624
+ providerConcurrentRequests: sendSpan.providerConcurrentRequests,
625
+ providerQueuedRequests: sendSpan.providerQueuedRequests,
626
+ poolOwnerWaitMs: sendSpan.poolOwnerWaitMs,
627
+ socketAcquireMs,
628
+ poolAcquireMs: sendSpan.poolAcquireMs,
629
+ requestBuildSerializationMs: sendSpan.requestBuildSerializationMs,
630
+ preResponseCreatedMs: sendSpan.preResponseCreatedMs,
631
+ providerFirstEventMs: sendSpan.firstEventMs,
632
+ retryBackoffMs: sendSpan.retryBackoffMs,
633
+ handshakeRetries: sendSpan.handshakeRetries,
634
+ acquireAttempts: sendSpan.acquireAttempts,
635
+ acquireMode: sendSpan.acquireMode || 'failed',
636
+ };
637
+ sendSpan.timing = timing;
607
638
  const payload = {
608
639
  provider: traceProvider,
609
640
  model: useModel,
610
641
  transport: 'websocket',
642
+ admission_queue_wait_ms: timing.admissionQueueWaitMs,
643
+ provider_concurrent_requests: timing.providerConcurrentRequests,
644
+ provider_queued_requests: timing.providerQueuedRequests,
645
+ pool_owner_wait_ms: timing.poolOwnerWaitMs,
646
+ socket_acquire_ms: timing.socketAcquireMs,
611
647
  acquire_mode: sendSpan.acquireMode || 'failed',
612
648
  acquire_attempts: sendSpan.acquireAttempts,
613
649
  handshake_retries: sendSpan.handshakeRetries,
@@ -627,6 +663,10 @@ export async function sendViaWebSocket({
627
663
  payload,
628
664
  });
629
665
  } catch {}
666
+ if (target) {
667
+ try { target.transportTiming = timing; } catch {}
668
+ }
669
+ return timing;
630
670
  };
631
671
  // Single caller-visible recovery path for both handshake/acquire retries
632
672
  // and retryable stream failures. The session/TUI stage bridge renders this
@@ -701,6 +741,7 @@ export async function sendViaWebSocket({
701
741
  } catch (err) {
702
742
  _stampWarmup(err);
703
743
  sendSpan.poolAcquireMs += performance.now() - handshakeStart;
744
+ sendSpan.poolOwnerWaitMs += Math.max(0, Number(err?.ownerWaitMs) || 0);
704
745
  // Provenance only; policy remains provider-owned below. This lets
705
746
  // the direct wrapper distinguish an upgrade rejection from an
706
747
  // application error carrying the same HTTP status.
@@ -746,6 +787,7 @@ export async function sendViaWebSocket({
746
787
  const retryable = classifier
747
788
  && Number(err?.httpStatus || 0) !== 401
748
789
  && Number(err?.httpStatus || 0) !== 426
790
+ && err?.unsafeToRetry !== true
749
791
  && !externalSignal?.aborted;
750
792
  if (retryable && attemptIndex < MAX_MIDSTREAM_RETRIES) {
751
793
  if (!firstAttemptError) {
@@ -779,7 +821,7 @@ export async function sendViaWebSocket({
779
821
  try { err.midstreamClassifier = classifier; } catch {}
780
822
  try { err.wsRetriesExhausted = true; } catch {}
781
823
  }
782
- emitSendSpan('error');
824
+ emitSendSpan('error', err);
783
825
  throw _stampTool(_stampLiveText(err));
784
826
  }
785
827
  if (attemptIndex > 0 && firstAttemptError) {
@@ -796,6 +838,7 @@ export async function sendViaWebSocket({
796
838
  }
797
839
  const { entry, reused } = acquired;
798
840
  sendSpan.poolAcquireMs += performance.now() - handshakeStart;
841
+ sendSpan.poolOwnerWaitMs += Math.max(0, Number(acquired?.ownerWaitMs) || 0);
799
842
  sendSpan.acquireMode = acquired.prewarmed
800
843
  ? 'prewarmed'
801
844
  : entry?.ephemeral ? 'ephemeral' : (reused ? 'reused' : 'new');
@@ -865,6 +908,7 @@ export async function sendViaWebSocket({
865
908
  let strippedResponseItems = 0;
866
909
  let skippedResponseItems = 0;
867
910
  let responseOutputMismatch = null;
911
+ let requestInputMismatch = null;
868
912
  let wireFrameHadTurnState = false;
869
913
  let wireFrameMetadataTrace = _metadataTrace(null);
870
914
  let framePrefixHash = null;
@@ -952,7 +996,6 @@ export async function sendViaWebSocket({
952
996
  entry.lastInputPrefixHash = createHash('sha256')
953
997
  .update(JSON.stringify(warmupInputArr))
954
998
  .digest('hex');
955
- entry.turnStateFromPrewarm = true;
956
999
  try {
957
1000
  const warmupPayload = {
958
1001
  provider: traceProvider,
@@ -978,7 +1021,7 @@ export async function sendViaWebSocket({
978
1021
  // Codex performs generate:false during session startup, then hands
979
1022
  // this live client session to the first real turn. Startup callers
980
1023
  // stop here; the pooled entry retains its response id, request
981
- // snapshot, socket, and turn-state for the later real send.
1024
+ // snapshot and socket for the later real send.
982
1025
  if (sendOpts?._startupPrewarmOnly === true) {
983
1026
  const responseId = warmupResult?.responseId
984
1027
  || startupWarmupResponseId
@@ -986,8 +1029,7 @@ export async function sendViaWebSocket({
986
1029
  || null;
987
1030
  if (responseId) entry.startupWarmupResponseId = responseId;
988
1031
  else releaseWebSocket({ entry, poolKey, keep: false });
989
- emitSendSpan('ok');
990
- return {
1032
+ const out = {
991
1033
  content: '',
992
1034
  model: warmupResult?.model || useModel,
993
1035
  toolCalls: [],
@@ -1003,6 +1045,8 @@ export async function sendViaWebSocket({
1003
1045
  ? { entry, poolKey, cacheKey }
1004
1046
  : null,
1005
1047
  };
1048
+ out.transportTiming = emitSendSpan('ok');
1049
+ return out;
1006
1050
  }
1007
1051
 
1008
1052
  // A completed generate:false prewarm is a valid continuation
@@ -1017,6 +1061,7 @@ export async function sendViaWebSocket({
1017
1061
  strippedResponseItems = delta.strippedResponseItems || 0;
1018
1062
  skippedResponseItems = delta.skippedResponseItems || 0;
1019
1063
  responseOutputMismatch = delta.responseOutputMismatch || null;
1064
+ requestInputMismatch = delta.requestInputMismatch || null;
1020
1065
  const wireFrame = _withCodexWsClientMetadata(frame, entry, useCodexWsClientMetadata, codexMetadataContext);
1021
1066
  wireFrameHadTurnState = !!wireFrame?.client_metadata?.['x-codex-turn-state'];
1022
1067
  wireFrameMetadataTrace = _metadataTrace(wireFrame?.client_metadata);
@@ -1314,6 +1359,12 @@ export async function sendViaWebSocket({
1314
1359
  body: requestBody,
1315
1360
  traceProvider,
1316
1361
  });
1362
+ if (cacheContinuityResetReason === 'input_prefix_mismatch' && !requestInputMismatch) {
1363
+ requestInputMismatch = _requestInputMismatchDiagnostics(
1364
+ requestBody?.input,
1365
+ entry?.lastRequestInput,
1366
+ );
1367
+ }
1317
1368
  if (result.responseId && keepResponseChain) {
1318
1369
  entry.lastResponseId = result.responseId;
1319
1370
  entry.lastRequestSansInput = _stableStringify(_sansInput(requestBody, {
@@ -1433,6 +1484,23 @@ export async function sendViaWebSocket({
1433
1484
  });
1434
1485
  } catch {}
1435
1486
  }
1487
+ if (cacheObservation.actualMiss) {
1488
+ traceCacheBreak({
1489
+ sessionId: poolKey,
1490
+ iteration,
1491
+ classification: 'provider_miss',
1492
+ reason: cacheObservation.missReason || 'warm_session_cache_miss',
1493
+ source: 'provider_usage',
1494
+ provider: traceProvider,
1495
+ model: liveModel,
1496
+ transport: 'websocket',
1497
+ cachedTokens: cacheObservation.cachedTokens,
1498
+ promptTokens: cacheObservation.promptTokens,
1499
+ uncachedTokens: cacheObservation.uncachedTokens,
1500
+ cacheRatio: cacheObservation.cacheRatio,
1501
+ actualCacheMiss: true,
1502
+ }, { traceFn: _agentTraceFn });
1503
+ }
1436
1504
  // Rebase after a genuine provider retreat so one eviction produces one
1437
1505
  // diagnostic instead of a long run of duplicate "dropped" rows. The
1438
1506
  // request that exposed the retreat has already rebuilt the prefix; its
@@ -1496,7 +1564,7 @@ export async function sendViaWebSocket({
1496
1564
  ws_client_metadata_has_turn_state: wireFrameHadTurnState,
1497
1565
  ws_entry_turn_state_available: useCodexWsClientMetadata && !!entry.turnState,
1498
1566
  // Fingerprint only. The token is a routing credential, so it is
1499
- // hashed like every other handshake secret; the length still
1567
+ // hashed rather than logged; the length still
1500
1568
  // distinguishes a real server token from a stub value, and the
1501
1569
  // hash shows whether one session keeps a single pin or is
1502
1570
  // re-issued (reconnect / turn rollover).
@@ -1507,6 +1575,7 @@ export async function sendViaWebSocket({
1507
1575
  chain_stripped_response_items: strippedResponseItems,
1508
1576
  chain_skipped_response_items: skippedResponseItems,
1509
1577
  ...(responseOutputMismatch || {}),
1578
+ ...(requestInputMismatch || {}),
1510
1579
  chain_response_items: Array.isArray(result.responseItems) ? result.responseItems.length : 0,
1511
1580
  body_input_items: Array.isArray(requestBody.input) ? requestBody.input.length : null,
1512
1581
  frame_input_items: Array.isArray(frame.input) ? frame.input.length : null,
@@ -1534,10 +1603,29 @@ export async function sendViaWebSocket({
1534
1603
  const intentionalTransition = typeof sendOpts?.cacheBreakIntent === 'string'
1535
1604
  ? sendOpts.cacheBreakIntent
1536
1605
  : null;
1606
+ traceCacheBreak({
1607
+ sessionId: poolKey,
1608
+ iteration,
1609
+ classification: intentionalTransition ? 'intentional' : 'provider_transition',
1610
+ reason: mode === 'delta' ? deltaReason : (deltaReason || 'full_frame'),
1611
+ source: 'openai_ws_delta',
1612
+ provider: traceProvider,
1613
+ model: liveModel,
1614
+ transport: 'websocket',
1615
+ intentionalTransition,
1616
+ cachedTokens: cacheObservation.cachedTokens,
1617
+ promptTokens: cacheObservation.promptTokens,
1618
+ uncachedTokens: cacheObservation.uncachedTokens,
1619
+ cacheRatio: cacheObservation.cacheRatio,
1620
+ actualCacheMiss: cacheObservation.actualMiss,
1621
+ ...(requestInputMismatch || {}),
1622
+ }, { traceFn: null });
1537
1623
  _agentTraceFn({
1538
1624
  sessionId: poolKey,
1539
1625
  iteration,
1540
1626
  kind: 'cache_break',
1627
+ classification: intentionalTransition ? 'intentional' : 'provider_transition',
1628
+ source: 'openai_ws_delta',
1541
1629
  provider: traceProvider,
1542
1630
  model: liveModel,
1543
1631
  payload: {
@@ -1546,6 +1634,8 @@ export async function sendViaWebSocket({
1546
1634
  transport: 'websocket',
1547
1635
  ws_mode: mode,
1548
1636
  reason: mode === 'delta' ? deltaReason : (deltaReason || 'full_frame'),
1637
+ classification: intentionalTransition ? 'intentional' : 'provider_transition',
1638
+ source: 'openai_ws_delta',
1549
1639
  intentional_transition: intentionalTransition,
1550
1640
  request_tool_choice: requestBody.tool_choice ?? null,
1551
1641
  cache_key_hash: transportCacheKeyHash,
@@ -1558,6 +1648,7 @@ export async function sendViaWebSocket({
1558
1648
  chain_stripped_response_items: strippedResponseItems,
1559
1649
  chain_skipped_response_items: skippedResponseItems,
1560
1650
  ...(responseOutputMismatch || {}),
1651
+ ...(requestInputMismatch || {}),
1561
1652
  chain_response_items: Array.isArray(result.responseItems) ? result.responseItems.length : 0,
1562
1653
  body_input_items: Array.isArray(requestBody.input) ? requestBody.input.length : null,
1563
1654
  frame_input_items: Array.isArray(frame.input) ? frame.input.length : null,
@@ -1586,7 +1677,7 @@ export async function sendViaWebSocket({
1586
1677
  // Leave a breadcrumb on the result so downstream callers can observe
1587
1678
  // that a retry was used (0 = first-try success, up to 2 for ws_1006/1011).
1588
1679
  try { Object.defineProperty(out, '__midstreamRetries', { value: attemptIndex, enumerable: false }); } catch {}
1589
- emitSendSpan('ok');
1680
+ out.transportTiming = emitSendSpan('ok');
1590
1681
  return out;
1591
1682
  }
1592
1683
  // Unreachable — the loop either returns or throws above.
@@ -17,6 +17,7 @@ import { sanitizeModelList } from './model-list-sanitize.mjs';
17
17
  import { writeJsonAtomicSync, withFileLock } from '../../../shared/atomic-file.mjs';
18
18
  import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs';
19
19
  import { makeModelCache } from './model-cache.mjs';
20
+ import { providerReplayItems } from './lib/provider-replay.mjs';
20
21
 
21
22
  import { sendViaWebSocket } from './openai-oauth-ws.mjs';
22
23
  import { _combineUsageWithWarmup } from './openai-ws-events.mjs';
@@ -175,6 +176,18 @@ export function convertMessagesToResponsesInput(messages, opts = {}) {
175
176
  continue;
176
177
  }
177
178
  flushToolMedia();
179
+ const orderedReplay = replayEncryptedReasoning && m.role === 'assistant'
180
+ ? providerReplayItems(m, 'openai-responses')
181
+ : undefined;
182
+ if (orderedReplay?.length) {
183
+ for (const item of orderedReplay) {
184
+ if (item?.type === 'custom_tool_call' && item.call_id) {
185
+ customToolCallNameById.set(item.call_id, item.name || '');
186
+ }
187
+ out.push(item);
188
+ }
189
+ continue;
190
+ }
178
191
  pushReasoningItems(m, 'before');
179
192
  if (m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length) {
180
193
  // Default path deliberately omits reasoning replay: openai-oauth
@@ -0,0 +1,68 @@
1
+ const MAX_TURN_STATE_SCOPES = 4096;
2
+ const _turnStateByScope = new Map();
3
+
4
+ function _clean(value) {
5
+ return typeof value === 'string' ? value.trim() : '';
6
+ }
7
+
8
+ function _ensureCapacity(scopeKey) {
9
+ if (_turnStateByScope.has(scopeKey) || _turnStateByScope.size < MAX_TURN_STATE_SCOPES) return;
10
+ const oldest = _turnStateByScope.keys().next().value;
11
+ if (oldest !== undefined) _turnStateByScope.delete(oldest);
12
+ }
13
+
14
+ // A turn-state token belongs to one logical turn, not to the physical
15
+ // connection that happened to carry it. Activating a new turn clears the prior
16
+ // value even when the caller reuses the same WebSocket or HTTP session.
17
+ export function activateCodexTurnState(scopeKey, turnId, owner = null) {
18
+ const scope = _clean(scopeKey);
19
+ const turn = _clean(turnId);
20
+ if (!scope || !turn) return null;
21
+ const current = _turnStateByScope.get(scope);
22
+ if (current?.turnId === turn) {
23
+ // One physical WS connection owns the token while it is live. HTTP
24
+ // fallback has no WS owner and may read the same turn-scoped value.
25
+ if (owner && current.owner && current.owner !== owner) return null;
26
+ if (owner && !current.owner) current.owner = owner;
27
+ return current.value || null;
28
+ }
29
+ _ensureCapacity(scope);
30
+ _turnStateByScope.set(scope, { turnId: turn, value: null, owner: owner || null });
31
+ return null;
32
+ }
33
+
34
+ // The first server value wins for the whole logical turn.
35
+ export function captureCodexTurnState(scopeKey, turnId, value, owner = null) {
36
+ const scope = _clean(scopeKey);
37
+ const turn = _clean(turnId);
38
+ const token = _clean(value);
39
+ if (!scope || !turn || !token) return null;
40
+ activateCodexTurnState(scope, turn, owner);
41
+ const current = _turnStateByScope.get(scope);
42
+ if (!current || current.turnId !== turn) return null;
43
+ if (owner && current.owner && current.owner !== owner) return null;
44
+ if (owner && !current.owner) current.owner = owner;
45
+ if (current.value) return current.value;
46
+ current.value = token;
47
+ return token;
48
+ }
49
+
50
+ // A replacement connection may adopt the turn token only after the physical
51
+ // connection that owned it has actually closed.
52
+ export function retireCodexTurnStateOwner(scopeKey, owner) {
53
+ const scope = _clean(scopeKey);
54
+ if (!scope || !owner) return;
55
+ const current = _turnStateByScope.get(scope);
56
+ if (current?.owner === owner) current.owner = null;
57
+ }
58
+
59
+ export function clearCodexTurnStateScope(scopeKey) {
60
+ const scope = _clean(scopeKey);
61
+ if (scope) _turnStateByScope.delete(scope);
62
+ }
63
+
64
+ export function clearAllCodexTurnStates() {
65
+ _turnStateByScope.clear();
66
+ }
67
+
68
+ export const _clearCodexTurnStatesForTest = clearAllCodexTurnStates;
@@ -1,4 +1,5 @@
1
1
  import assert from 'node:assert/strict';
2
+ import { EventEmitter } from 'node:events';
2
3
  import { test } from 'node:test';
3
4
 
4
5
  import {
@@ -9,6 +10,12 @@ import {
9
10
  import { _applyReasoningReplayPolicy } from './openai-oauth-ws.mjs';
10
11
  import { _captureTurnStateFromEvent } from './openai-ws-stream.mjs';
11
12
  import { _convertMessagesToResponsesInputForTest } from './openai-responses-payload.mjs';
13
+ import {
14
+ acquireWebSocket,
15
+ releaseWebSocket,
16
+ _clearWebSocketPoolForTest,
17
+ _setOpenSocketForTest,
18
+ } from './openai-ws-pool.mjs';
12
19
 
13
20
  // A turn may reason, speak, then reason again before calling a tool. Replaying
14
21
  // every retained item in front of the text invents an order the response never
@@ -41,19 +48,18 @@ test('a turn that reasons, speaks, then reasons again replays in that order', ()
41
48
  assert.equal(input[3].afterText, undefined);
42
49
  });
43
50
 
44
- // A live chain's sticky-routing token can never be swapped mid-session: the
45
- // entry holds it write-once, whichever path first supplied it.
51
+ // A logical turn's sticky-routing token is first-value-wins.
46
52
  test('a live turn-state token is never replaced by a later event token', () => {
47
53
  const entry = {};
48
54
  _captureTurnStateFromEvent(entry, {
49
- type: 'response.created',
50
- headers: { 'x-codex-turn-state': 'handshake-token' },
55
+ type: 'response.metadata',
56
+ headers: { 'x-codex-turn-state': 'first-token' },
51
57
  });
52
58
  _captureTurnStateFromEvent(entry, {
53
- type: 'response.created',
59
+ type: 'response.metadata',
54
60
  headers: { 'x-codex-turn-state': 'later-token' },
55
61
  });
56
- assert.equal(entry.turnState, 'handshake-token');
62
+ assert.equal(entry.turnState, 'first-token');
57
63
  });
58
64
 
59
65
  // The logical request history always carries retained reasoning — a pooled
@@ -136,6 +142,48 @@ test('Codex delta parity falls back when a previous response item is absent', (t
136
142
  assert.deepEqual(delta.frame.input, body.input);
137
143
  });
138
144
 
145
+ test('input-prefix fallback reports only the first changed item type and hashes', (t) => {
146
+ const priorTransport = process.env.MIXDOG_OAI_TRANSPORT;
147
+ process.env.MIXDOG_OAI_TRANSPORT = 'ws-delta';
148
+ t.after(() => {
149
+ if (priorTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
150
+ else process.env.MIXDOG_OAI_TRANSPORT = priorTransport;
151
+ });
152
+ const previousInput = [{
153
+ type: 'message',
154
+ role: 'user',
155
+ content: [{ type: 'input_text', text: 'before secret' }],
156
+ }];
157
+ const body = {
158
+ model: 'gpt-5.6-sol',
159
+ input: [{
160
+ type: 'message',
161
+ role: 'user',
162
+ content: [{ type: 'input_text', text: 'after secret' }],
163
+ }],
164
+ };
165
+ const entry = {
166
+ lastRequestSansInput: _stableStringify(_sansInput({
167
+ model: body.model,
168
+ input: previousInput,
169
+ })),
170
+ lastRequestInput: previousInput,
171
+ lastResponseId: 'resp-1',
172
+ lastResponseItems: [],
173
+ turnState: 'sticky',
174
+ };
175
+ const delta = _computeDelta({ entry, body, traceProvider: 'openai-oauth' });
176
+ assert.equal(delta.reason, 'input_prefix_mismatch');
177
+ assert.equal(delta.requestInputMismatch.inputPrefixMismatchIndex, 0);
178
+ assert.equal(delta.requestInputMismatch.inputPrefixMismatchExpectedType, 'message');
179
+ assert.equal(delta.requestInputMismatch.inputPrefixMismatchActualType, 'message');
180
+ assert.notEqual(
181
+ delta.requestInputMismatch.inputPrefixMismatchExpectedHash,
182
+ delta.requestInputMismatch.inputPrefixMismatchActualHash,
183
+ );
184
+ assert.doesNotMatch(JSON.stringify(delta.requestInputMismatch), /before secret|after secret/);
185
+ });
186
+
139
187
  // Retained reasoning rides the logical history, and the delta builder is what
140
188
  // keeps it off the wire: it belongs to the anchored previous response, so the
141
189
  // frame carries only what is genuinely new. This is the whole reason the
@@ -188,3 +236,71 @@ test('the delta frame drops the anchored reasoning item from the wire tail', (t)
188
236
  assert.deepEqual(delta.frame.input, [toolOutput]);
189
237
  assert.equal(delta.strippedResponseItems, 2);
190
238
  });
239
+
240
+ test('one pool key serializes live owners and reconnects only after close', async (t) => {
241
+ class FakeSocket extends EventEmitter {
242
+ constructor() {
243
+ super();
244
+ this.readyState = 1;
245
+ }
246
+
247
+ close() {
248
+ if (this.readyState === 3) return;
249
+ this.readyState = 3;
250
+ this.emit('close');
251
+ }
252
+
253
+ terminate() {
254
+ this.close();
255
+ }
256
+
257
+ ref() {}
258
+ unref() {}
259
+ }
260
+
261
+ const handshakeArgs = [];
262
+ _clearWebSocketPoolForTest();
263
+ _setOpenSocketForTest(async (args) => {
264
+ handshakeArgs.push(args);
265
+ return { socket: new FakeSocket(), turnState: 'upgrade-token-must-be-ignored' };
266
+ });
267
+ t.after(() => {
268
+ _clearWebSocketPoolForTest();
269
+ _setOpenSocketForTest();
270
+ });
271
+
272
+ const args = {
273
+ auth: { type: 'openai-oauth', account_id: 'race-test' },
274
+ poolKey: 'race-pool',
275
+ cacheKey: 'race-cache',
276
+ codexHeaders: {},
277
+ };
278
+ const first = await acquireWebSocket(args);
279
+ assert.equal(first.entry.turnState, null);
280
+ let secondResolved = false;
281
+ const secondPromise = acquireWebSocket(args).then((value) => {
282
+ secondResolved = true;
283
+ return value;
284
+ });
285
+ await new Promise((resolve) => setImmediate(resolve));
286
+ assert.equal(secondResolved, false);
287
+ assert.equal(handshakeArgs.length, 1);
288
+
289
+ releaseWebSocket({ entry: first.entry, poolKey: args.poolKey, keep: true });
290
+ const second = await secondPromise;
291
+ assert.equal(second.entry, first.entry);
292
+ assert.equal(second.reused, true);
293
+ assert.ok(second.ownerWaitMs >= 0);
294
+ assert.equal(handshakeArgs.length, 1);
295
+
296
+ releaseWebSocket({ entry: second.entry, poolKey: args.poolKey, keep: false });
297
+ const replacement = await acquireWebSocket(args);
298
+ assert.notEqual(replacement.entry, first.entry);
299
+ assert.equal(handshakeArgs.length, 2);
300
+ releaseWebSocket({ entry: replacement.entry, poolKey: args.poolKey, keep: false });
301
+
302
+ assert.deepEqual(
303
+ handshakeArgs.map((call) => call.turnState),
304
+ [undefined, undefined],
305
+ );
306
+ });
@@ -196,6 +196,31 @@ function _normalizedResponseItemDiagnosticHash(item) {
196
196
  .digest('hex');
197
197
  }
198
198
 
199
+ export function _requestInputMismatchDiagnostics(currentInput, previousInput) {
200
+ const current = Array.isArray(currentInput) ? currentInput : [];
201
+ const previous = Array.isArray(previousInput) ? previousInput : [];
202
+ let index = Math.min(current.length, previous.length);
203
+ const limit = Math.min(current.length, previous.length);
204
+ for (let cursor = 0; cursor < limit; cursor += 1) {
205
+ if (_requestInputItemsMatch(current[cursor], previous[cursor])) continue;
206
+ index = cursor;
207
+ break;
208
+ }
209
+ const expected = previous[index];
210
+ const actual = current[index];
211
+ return {
212
+ inputPrefixMismatchIndex: index,
213
+ inputPrefixMismatchPreviousCount: previous.length,
214
+ inputPrefixMismatchCurrentCount: current.length,
215
+ inputPrefixMismatchExpectedType: expected?.type
216
+ || (expected?.role === 'assistant' ? 'message' : (expected ? 'unknown' : 'missing')),
217
+ inputPrefixMismatchExpectedHash: _normalizedResponseItemDiagnosticHash(expected),
218
+ inputPrefixMismatchActualType: actual?.type
219
+ || (actual?.role === 'assistant' ? 'message' : (actual ? 'unknown' : 'missing')),
220
+ inputPrefixMismatchActualHash: _normalizedResponseItemDiagnosticHash(actual),
221
+ };
222
+ }
223
+
199
224
  function _responseOutputMismatchDiagnostics(inputItem, responseItem, replayItemCount, responseItemCount) {
200
225
  return {
201
226
  response_output_mismatch_expected_type: responseItem?.type || 'unknown',
@@ -312,16 +337,11 @@ export function _buildResponseCreateFrame(body, { previousResponseId = null, inp
312
337
 
313
338
  export function _computeDelta({ entry, body, traceProvider }) {
314
339
  // DEFAULT: full-frame sends. codex's delta path is only cache-safe with
315
- // the x-codex-turn-state sticky-routing token. R11-R14 (2026-07-03)
316
- // measured zero turn-state EVENTS across 200+ calls despite
317
- // UA/version/beta-features/client_metadata parity, and delta at 18-28% warm
318
- // miss vs full-frame 0.0%. The event channel is still silent, but the
319
- // conclusion "the backend never issues it to us" was wrong: the token
320
- // arrives on the handshake 101 response instead (2026-08-21 bench: 81 of 88
321
- // calls carried it from iteration 1, before any response event existed), so
322
- // the pool captures it at upgrade and the frame metadata replays it.
323
- // Without the sticky token, previous_response_id requests hop cache nodes
324
- // and only the first prefix blocks hit.
340
+ // the x-codex-turn-state sticky-routing token. The token is request-scoped:
341
+ // response metadata establishes it, later response.create frames replay it
342
+ // for the same logical turn, and a new turn starts empty even when it reuses
343
+ // the physical connection. Handshake state is deliberately ignored because
344
+ // a connection can outlive the turn that created it.
325
345
  // Delta gating flows through the single transport-policy switch
326
346
  // (MIXDOG_OAI_TRANSPORT: ws-full | ws-delta | http-sse). Default ws-delta
327
347
  // selects the refs-compatible safe delta; 'ws-full'/'http-sse' force full
@@ -379,7 +399,12 @@ export function _computeDelta({ entry, body, traceProvider }) {
379
399
  const curInput = Array.isArray(body.input) ? body.input : [];
380
400
  const afterPreviousInput = _stripRequestPrefix(curInput, entry.lastRequestInput);
381
401
  if (!afterPreviousInput) {
382
- return { mode: 'full', reason: 'input_prefix_mismatch', frame: buildFrame(body) };
402
+ return {
403
+ mode: 'full',
404
+ reason: 'input_prefix_mismatch',
405
+ requestInputMismatch: _requestInputMismatchDiagnostics(curInput, entry.lastRequestInput),
406
+ frame: buildFrame(body),
407
+ };
383
408
  }
384
409
  const stripped = _stripResponseItemsFromHead(afterPreviousInput, entry.lastResponseItems);
385
410
  if (!stripped.ok) {