mixdog 0.9.152 → 0.9.154

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 (300) hide show
  1. package/LICENSES/browser-import-NOTICE.txt +11 -0
  2. package/NOTICE.md +11 -0
  3. package/README.md +8 -10
  4. package/package.json +19 -3
  5. package/scripts/computer-task-cost-report.mjs +144 -0
  6. package/scripts/run-office-live-tests.mjs +32 -0
  7. package/src/agents/front-worker/AGENT.md +13 -0
  8. package/src/agents/front-worker/agent.json +6 -0
  9. package/src/agents/heavy-worker/AGENT.md +10 -11
  10. package/src/agents/heavy-worker/agent.json +1 -1
  11. package/src/agents/maintainer/AGENT.md +0 -2
  12. package/src/agents/reviewer/AGENT.md +9 -13
  13. package/src/agents/reviewer/agent.json +1 -1
  14. package/src/agents/security/AGENT.md +14 -0
  15. package/src/agents/security/agent.json +6 -0
  16. package/src/agents/worker/AGENT.md +8 -8
  17. package/src/agents/worker/agent.json +1 -1
  18. package/src/agents/writer/AGENT.md +14 -0
  19. package/src/agents/writer/agent.json +6 -0
  20. package/src/rules/agent/00-common.md +4 -7
  21. package/src/rules/lead/01-general.md +0 -3
  22. package/src/rules/shared/10-tool-workflow.md +10 -2
  23. package/src/rules/shared/60-verification.md +2 -0
  24. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +183 -26
  25. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
  26. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +109 -0
  27. package/src/runtime/agent/orchestrator/config.mjs +7 -2
  28. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +16 -3
  29. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +9 -1
  30. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -9
  31. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +18 -26
  32. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +25 -2
  33. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +103 -10
  34. package/src/runtime/agent/orchestrator/providers/openai-turn-state.mjs +68 -0
  35. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +36 -11
  36. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +140 -86
  37. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -13
  38. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +72 -4
  39. package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.mjs +61 -0
  40. package/src/runtime/agent/orchestrator/session/compact/handoff.mjs +0 -22
  41. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +11 -23
  43. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +28 -269
  44. package/src/runtime/agent/orchestrator/session/compact.mjs +0 -6
  45. package/src/runtime/agent/orchestrator/session/context-utils.mjs +86 -11
  46. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +17 -1
  47. package/src/runtime/agent/orchestrator/session/image-strip-recovery.mjs +12 -8
  48. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +110 -2
  49. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +3 -16
  50. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +80 -18
  51. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +6 -0
  52. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +3 -0
  53. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -0
  54. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +80 -21
  55. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +33 -72
  56. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +21 -2
  57. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +25 -51
  58. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +3 -0
  59. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -110
  60. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +39 -15
  61. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +14 -4
  62. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +41 -8
  63. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +129 -12
  64. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +2 -2
  65. package/src/runtime/agent/orchestrator/session/store/listing.mjs +8 -1
  66. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +15 -1
  67. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +36 -2
  68. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +157 -30
  69. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +57 -0
  70. package/src/runtime/agent/orchestrator/session/store.mjs +3 -1
  71. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +22 -0
  72. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +6 -6
  73. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +4 -0
  74. package/src/runtime/attachments/pdf-extract.mjs +3 -0
  75. package/src/runtime/attachments/pdfjs-runtime.mjs +13 -0
  76. package/src/runtime/browser-bridge/action-schema.mjs +251 -0
  77. package/src/runtime/browser-bridge/client.mjs +75 -26
  78. package/src/runtime/browser-bridge/tool-defs.mjs +132 -15
  79. package/src/runtime/computer-bridge/action-schema.mjs +653 -0
  80. package/src/runtime/computer-bridge/client.mjs +211 -28
  81. package/src/runtime/computer-bridge/tool-defs.mjs +25 -45
  82. package/src/runtime/office/assurance-benchmark.mjs +358 -0
  83. package/src/runtime/office/assurance.mjs +833 -0
  84. package/src/runtime/office/benchmark.mjs +496 -0
  85. package/src/runtime/office/capabilities.mjs +551 -0
  86. package/src/runtime/office/com-adapter.mjs +283 -0
  87. package/src/runtime/office/content-model.mjs +183 -0
  88. package/src/runtime/office/contract-benchmark.mjs +105 -0
  89. package/src/runtime/office/design-library.mjs +1562 -0
  90. package/src/runtime/office/design-system.mjs +1966 -0
  91. package/src/runtime/office/index.mjs +2413 -0
  92. package/src/runtime/office/journal.mjs +82 -0
  93. package/src/runtime/office/office-com-host.ps1 +3781 -0
  94. package/src/runtime/office/office-com-session-host.ps1 +855 -0
  95. package/src/runtime/office/ooxml-validator.mjs +271 -0
  96. package/src/runtime/office/pagination.mjs +132 -0
  97. package/src/runtime/office/pdf-adapter.mjs +591 -0
  98. package/src/runtime/office/pdf-analysis.mjs +372 -0
  99. package/src/runtime/office/pdf-render.mjs +167 -0
  100. package/src/runtime/office/pdf-security.mjs +63 -0
  101. package/src/runtime/office/polish-benchmark.mjs +190 -0
  102. package/src/runtime/office/portable-ooxml.mjs +1867 -0
  103. package/src/runtime/office/quality-live-benchmark.mjs +486 -0
  104. package/src/runtime/office/quality-pipeline.mjs +136 -0
  105. package/src/runtime/office/tabular.mjs +302 -0
  106. package/src/runtime/office/templates/mixdog-executive.pptx +0 -0
  107. package/src/runtime/office/templates/mixdog-executive.pptx.mixdog.json +190 -0
  108. package/src/runtime/office/tool-defs.mjs +113 -0
  109. package/src/runtime/office/visual-diff.mjs +89 -0
  110. package/src/runtime/office/xlsx-assertions.mjs +172 -0
  111. package/src/runtime/office/xlsx-contract.mjs +147 -0
  112. package/src/runtime/shared/agent-route-config.mjs +9 -0
  113. package/src/runtime/shared/tool-execution-contract.mjs +16 -0
  114. package/src/runtime/shared/tool-surface.mjs +9 -0
  115. package/src/session-runtime/context-status.mjs +9 -3
  116. package/src/session-runtime/deferred-tool-delta.mjs +104 -0
  117. package/src/session-runtime/goal-runtime.mjs +617 -197
  118. package/src/session-runtime/lifecycle-api.mjs +50 -8
  119. package/src/session-runtime/provider-request-snapshot.mjs +33 -9
  120. package/src/session-runtime/resource-api.mjs +31 -94
  121. package/src/session-runtime/runtime-core.mjs +78 -26
  122. package/src/session-runtime/session-lifecycle.mjs +34 -12
  123. package/src/session-runtime/session-title.mjs +2 -2
  124. package/src/session-runtime/session-turn-api.mjs +15 -14
  125. package/src/session-runtime/tool-catalog-data.mjs +1 -1
  126. package/src/session-runtime/tool-catalog.mjs +38 -99
  127. package/src/session-runtime/tool-defs.mjs +6 -1
  128. package/src/session-runtime/tool-surface.mjs +18 -8
  129. package/src/session-runtime/workflow-agents-api.mjs +8 -25
  130. package/src/session-runtime/workflow.mjs +8 -6
  131. package/src/standalone/agent-tool/helpers.mjs +2 -2
  132. package/src/standalone/agent-tool/job-views.mjs +3 -1
  133. package/src/standalone/agent-tool/notify.mjs +5 -1
  134. package/src/standalone/agent-tool/render.mjs +3 -0
  135. package/src/standalone/agent-tool/spawn-flow.mjs +61 -16
  136. package/src/standalone/agent-tool/tool-def.mjs +1 -1
  137. package/src/standalone/agent-tool/worker-index.mjs +123 -0
  138. package/src/standalone/agent-tool/worker-rows.mjs +2 -1
  139. package/src/standalone/agent-tool.mjs +57 -10
  140. package/src/standalone/daemon.mjs +105 -6
  141. package/src/standalone/session-runtime-host.mjs +61 -48
  142. package/src/standalone/session-runtime-inline-host.mjs +34 -127
  143. package/src/standalone/session-runtime-worker.mjs +20 -198
  144. package/src/standalone/session-service.mjs +632 -7
  145. package/src/tui/dist/index.mjs +24 -1
  146. package/src/tui/lib/voice-setup.mjs +4 -1
  147. package/src/tui/session/context-state.mjs +2 -13
  148. package/src/tui/session/goal-continuation.mjs +38 -7
  149. package/src/tui/session/session-api-ext.mjs +9 -0
  150. package/src/tui/session/session-api.mjs +15 -1
  151. package/src/tui/session/session-flow.mjs +25 -1
  152. package/src/tui/session/turn.mjs +88 -16
  153. package/src/tui/session-local.mjs +25 -3
  154. package/src/vendor/statusline/src/gateway/route-meta.mjs +31 -0
  155. package/scripts/generate-voice-runtime-manifest.test.mjs +0 -135
  156. package/scripts/native-binary-arch.test.mjs +0 -111
  157. package/scripts/patch-replay.test.mjs +0 -94
  158. package/src/headless-exec.test.mjs +0 -539
  159. package/src/runtime/agent/orchestrator/agent-trace-timing.test.mjs +0 -57
  160. package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +0 -153
  161. package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +0 -139
  162. package/src/runtime/agent/orchestrator/mcp/features.test.mjs +0 -66
  163. package/src/runtime/agent/orchestrator/mcp/security.test.mjs +0 -80
  164. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +0 -90
  165. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +0 -215
  166. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.test.mjs +0 -165
  167. package/src/runtime/agent/orchestrator/providers/media-parity.test.mjs +0 -142
  168. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +0 -153
  169. package/src/runtime/agent/orchestrator/providers/openai-codex-model.test.mjs +0 -24
  170. package/src/runtime/agent/orchestrator/providers/openai-ws-delta-parity.test.mjs +0 -190
  171. package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +0 -235
  172. package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +0 -719
  173. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.test.mjs +0 -40
  174. package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +0 -176
  175. package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +0 -161
  176. package/src/runtime/agent/orchestrator/session/eager-dispatch.test.mjs +0 -115
  177. package/src/runtime/agent/orchestrator/session/evidence-union.test.mjs +0 -228
  178. package/src/runtime/agent/orchestrator/session/image-strip-recovery.test.mjs +0 -245
  179. package/src/runtime/agent/orchestrator/session/lossless-tool-output.test.mjs +0 -125
  180. package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +0 -325
  181. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.test.mjs +0 -70
  182. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +0 -457
  183. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.test.mjs +0 -117
  184. package/src/runtime/agent/orchestrator/session/reduction-metrics.test.mjs +0 -87
  185. package/src/runtime/agent/orchestrator/session/store/serialize.test.mjs +0 -60
  186. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +0 -583
  187. package/src/runtime/agent/orchestrator/session/task-wait-control.test.mjs +0 -78
  188. package/src/runtime/agent/orchestrator/tools/builtin/absence-absorption.test.mjs +0 -43
  189. package/src/runtime/agent/orchestrator/tools/builtin/absolute-glob-expand.test.mjs +0 -63
  190. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +0 -14
  191. package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +0 -224
  192. package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +0 -224
  193. package/src/runtime/agent/orchestrator/tools/builtin/enoent-outside-project.test.mjs +0 -56
  194. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +0 -103
  195. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.test.mjs +0 -30
  196. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +0 -390
  197. package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.test.mjs +0 -83
  198. package/src/runtime/agent/orchestrator/tools/builtin/grep-output-budget.test.mjs +0 -31
  199. package/src/runtime/agent/orchestrator/tools/builtin/grep-single-file-rescue.test.mjs +0 -106
  200. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.test.mjs +0 -102
  201. package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +0 -152
  202. package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +0 -24
  203. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +0 -24
  204. package/src/runtime/agent/orchestrator/tools/builtin/noise-dir-visibility.test.mjs +0 -69
  205. package/src/runtime/agent/orchestrator/tools/builtin/read-glob-survey.test.mjs +0 -71
  206. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.test.mjs +0 -56
  207. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.test.mjs +0 -16
  208. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-top-k.test.mjs +0 -39
  209. package/src/runtime/agent/orchestrator/tools/builtin/write-symlink.test.mjs +0 -153
  210. package/src/runtime/agent/orchestrator/tools/code-graph/aggregate-anchor-relocation.test.mjs +0 -55
  211. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +0 -88
  212. package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +0 -72
  213. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +0 -54
  214. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.test.mjs +0 -20
  215. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +0 -55
  216. package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +0 -268
  217. package/src/runtime/agent/orchestrator/tools/env-scrub.test.mjs +0 -57
  218. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.test.mjs +0 -19
  219. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +0 -104
  220. package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +0 -661
  221. package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +0 -552
  222. package/src/runtime/agent/orchestrator/tools/patch/patch-symlink.test.mjs +0 -141
  223. package/src/runtime/agent/orchestrator/tools/patch/v4a-pure-move.test.mjs +0 -57
  224. package/src/runtime/agent/orchestrator/tools/patch/v4a-section-coalesce.test.mjs +0 -81
  225. package/src/runtime/agent/orchestrator/tools/spawn-binary-fetcher.test.mjs +0 -52
  226. package/src/runtime/attachments/store.test.mjs +0 -209
  227. package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +0 -17
  228. package/src/runtime/channels/lib/webhook/deliveries.test.mjs +0 -18
  229. package/src/runtime/channels/lib/webhook/relay-tunnel.test.mjs +0 -68
  230. package/src/runtime/media/adapters/codex-image.test.mjs +0 -22
  231. package/src/runtime/media/download.test.mjs +0 -52
  232. package/src/runtime/media/renditions.test.mjs +0 -99
  233. package/src/runtime/media/store.test.mjs +0 -159
  234. package/src/runtime/memory/lib/archive-security.test.mjs +0 -128
  235. package/src/runtime/memory/lib/embedding-model-config.test.mjs +0 -33
  236. package/src/runtime/memory/lib/http-wire.test.mjs +0 -15
  237. package/src/runtime/memory/lib/memory-cycle-packets.test.mjs +0 -49
  238. package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +0 -78
  239. package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +0 -50
  240. package/src/runtime/memory/lib/pg/process.test.mjs +0 -36
  241. package/src/runtime/memory/lib/trace-store.test.mjs +0 -57
  242. package/src/runtime/memory/lib/transcript-ingest.test.mjs +0 -52
  243. package/src/runtime/shared/background-tasks.test.mjs +0 -78
  244. package/src/runtime/shared/bounded-download.test.mjs +0 -41
  245. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -25
  246. package/src/runtime/shared/child-spawn-remote.test.mjs +0 -124
  247. package/src/runtime/shared/remote-intent.test.mjs +0 -40
  248. package/src/runtime/shared/resource-admission.test.mjs +0 -43
  249. package/src/runtime/shared/runtime-root.test.mjs +0 -34
  250. package/src/runtime/shared/schedule-time.test.mjs +0 -29
  251. package/src/runtime/shared/session-runtime-health.test.mjs +0 -83
  252. package/src/runtime/shared/skill-document.test.mjs +0 -81
  253. package/src/runtime/shared/tool-surface.test.mjs +0 -87
  254. package/src/runtime/shared/update-checker.test.mjs +0 -33
  255. package/src/runtime/shared/user-cwd.test.mjs +0 -43
  256. package/src/runtime/shared/webhook-session-run.test.mjs +0 -14
  257. package/src/session-runtime/agent-disable.test.mjs +0 -123
  258. package/src/session-runtime/cwd-plugins.test.mjs +0 -75
  259. package/src/session-runtime/cwd-tool-routing.test.mjs +0 -61
  260. package/src/session-runtime/global-extensions.test.mjs +0 -28
  261. package/src/session-runtime/goal-runtime.test.mjs +0 -108
  262. package/src/session-runtime/lifecycle-api.test.mjs +0 -21
  263. package/src/session-runtime/model-settings-persist.test.mjs +0 -105
  264. package/src/session-runtime/notification-bus.test.mjs +0 -148
  265. package/src/session-runtime/plugin-mcp-standard.test.mjs +0 -177
  266. package/src/session-runtime/prewarm.test.mjs +0 -52
  267. package/src/session-runtime/provider-request-snapshot.test.mjs +0 -37
  268. package/src/session-runtime/resource-api-global.test.mjs +0 -69
  269. package/src/session-runtime/session-lifecycle.test.mjs +0 -125
  270. package/src/session-runtime/session-title.test.mjs +0 -36
  271. package/src/session-runtime/skills-api.test.mjs +0 -99
  272. package/src/session-runtime/tool-policy-surface.test.mjs +0 -331
  273. package/src/standalone/agent-tool/helpers.test.mjs +0 -62
  274. package/src/standalone/agent-tool/notify.test.mjs +0 -37
  275. package/src/standalone/agent-tool/spawn-flow.test.mjs +0 -63
  276. package/src/standalone/channel-restart.test.mjs +0 -145
  277. package/src/standalone/channel-session-router.test.mjs +0 -37
  278. package/src/standalone/channel-worker-heartbeat.test.mjs +0 -30
  279. package/src/standalone/hook-bus/handlers.test.mjs +0 -88
  280. package/src/standalone/plugin-admin.test.mjs +0 -173
  281. package/src/standalone/session-runtime-agent-control-client.test.mjs +0 -71
  282. package/src/standalone/session-runtime-dispatch-cancel.test.mjs +0 -139
  283. package/src/standalone/session-runtime-host-factory.test.mjs +0 -31
  284. package/src/standalone/session-runtime-host-health.test.mjs +0 -160
  285. package/src/standalone/session-runtime-inline-host.test.mjs +0 -83
  286. package/src/standalone/session-runtime-provider-cooldown.test.mjs +0 -80
  287. package/src/standalone/session-runtime-shard-host.test.mjs +0 -569
  288. package/src/standalone/session-runtime-shard-router.test.mjs +0 -101
  289. package/src/tui/app/live-spinner-visibility.test.mjs +0 -44
  290. package/src/tui/app/panel-epoch.test.mjs +0 -1009
  291. package/src/tui/app/panel-handoff.test.mjs +0 -311
  292. package/src/tui/app/prompt-submit.test.mjs +0 -206
  293. package/src/tui/session/completion-card-restore.test.mjs +0 -111
  294. package/src/tui/session/goal-continuation.test.mjs +0 -84
  295. package/src/tui/session/oauth-flows.test.mjs +0 -134
  296. package/src/tui/session/queue-helpers.test.mjs +0 -46
  297. package/src/tui/session/session-action-surface.test.mjs +0 -108
  298. package/src/tui/session/task-wait-submit.test.mjs +0 -80
  299. package/src/tui/session/tool-result-status.test.mjs +0 -131
  300. package/src/tui/session/tui-steering-persist.test.mjs +0 -104
@@ -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 || []) {
@@ -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
 
@@ -892,6 +944,8 @@ let _drainComplete = false;
892
944
  // Force-closes pooled sockets and fences subsequent acquires.
893
945
  export function drainOpenaiWsPool(reason = 'shutdown', { immediate = false } = {}) {
894
946
  _drainComplete = true;
947
+ _clearAllPoolOwners();
948
+ clearAllCodexTurnStates();
895
949
  for (const arr of _wsPool.values()) {
896
950
  // _shutdownEntry tears down per-entry timers before the map is dropped
897
951
  // (otherwise the idle-close and liveness-ping intervals outlive the
@@ -35,6 +35,7 @@ import {
35
35
  import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
36
36
  import { createProviderReplay } from './lib/provider-replay.mjs';
37
37
  import { _wsErrLabel, WS_MAX_INCOMING_FRAME_BYTES } from './openai-ws-pool.mjs';
38
+ import { captureCodexTurnState } from './openai-turn-state.mjs';
38
39
  import {
39
40
  _sansInput,
40
41
  _stableStringify,
@@ -181,24 +182,19 @@ function _hasHeaderKey(headers, name) {
181
182
 
182
183
  export function _captureTurnStateFromEvent(entry, event) {
183
184
  if (!entry || entry.turnState || !event || typeof event !== 'object') return;
184
- // The token normally arrives on the WebSocket 101 handshake and is held
185
- // write-once per entry, which is what keeps sticky routing stable across
186
- // previous_response_id calls. This event path is the fallback for
187
- // deployments that surface the header on a response event instead; the
188
- // 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.
189
187
  const turnState = _headerString(event.headers, X_CODEX_TURN_STATE_HEADER)
190
188
  || _headerString(event.response?.headers, X_CODEX_TURN_STATE_HEADER)
191
189
  || _headerString(event.response?.metadata?.headers, X_CODEX_TURN_STATE_HEADER)
192
190
  || _headerString(event.metadata?.headers, X_CODEX_TURN_STATE_HEADER);
193
191
  if (turnState) {
194
- entry.turnState = turnState;
195
- // Attribute the captured token to the turn currently on the wire so
196
- // the per-turn drop guard (_withCodexWsClientMetadata) can retire it
197
- // once turn_id advances; null falls back to first-use attribution. An
198
- // empty turn_id (parity prewarm) is a valid owner — preserve '' rather
199
- // than collapsing it to null, so a prewarm-captured token is retired on
200
- // the next real turn instead of leaking onto it.
201
- 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);
202
198
  }
203
199
  }
204
200
 
@@ -27,7 +27,6 @@ import {
27
27
  _intraTurnSig,
28
28
  } from './loop/tool-classify.mjs';
29
29
  import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
30
- import { runRecallFastTrackCompact } from './loop/recall-fasttrack.mjs';
31
30
  import { executeTool, _scopedCacheOutcomeForCall, resolveLiveToolCwd } from './loop/tool-exec.mjs';
32
31
 
33
32
  // classifyResultKind is imported from result-classification.mjs at the top of
@@ -35,6 +34,7 @@ import { executeTool, _scopedCacheOutcomeForCall, resolveLiveToolCwd } from './l
35
34
  import { recordToolBatch } from '../tools/tool-batch-trace.mjs';
36
35
  import { projectProviderEvidence } from './evidence-union.mjs';
37
36
  import { prepareProviderPrefixGuard } from './provider-prefix-guard.mjs';
37
+ import { traceCacheBreak } from '../cache-break-trace.mjs';
38
38
 
39
39
 
40
40
  import { resolve as resolvePath, isAbsolute } from 'path';
@@ -147,6 +147,7 @@ const MAX_OUTPUT_EXHAUSTED_NOTICE = '[mixdog-runtime] Output remained truncated
147
147
  * tool. When aborted, throws SessionClosedError so the ask
148
148
  * wrapper can propagate a clean cancellation.
149
149
  * - `onStageChange(stage)` / `onStreamDelta()` — forwarded to provider.send for heartbeats
150
+ * - `liveProjection` — when true, Agent sessions keep provider onTextDelta / mid-turn text
150
151
  */
151
152
  // Stop reasons that signal the turn was cut short mid-synthesis (token cap,
152
153
  // provider pause). Empty content + one of these reasons means the worker
@@ -162,6 +163,19 @@ export function attachAssistantTranscriptMetadata(message, opts = {}) {
162
163
  return { ...message, meta: { ...meta, transcript } };
163
164
  }
164
165
 
166
+ // Agent mid-turn text is suppressed unless THIS send explicitly requested a
167
+ // live projection. The request is a send-opt (`liveProjection`) — never a
168
+ // durable session field — so a transient Agent pane cannot leak into later
169
+ // silent dispatches or stored session JSON. The standalone surface still
170
+ // flips `interactiveSessionSurface` on the in-memory session; honor that
171
+ // in-process hint without treating it as something dispatch should persist.
172
+ export function shouldSuppressAgentMidTurnText(sessionRef, opts = {}) {
173
+ if (!isAgentOwner(sessionRef)) return false;
174
+ if (opts?.liveProjection === true) return false;
175
+ if (sessionRef?.interactiveSessionSurface === true) return false;
176
+ return true;
177
+ }
178
+
165
179
  export async function agentLoop(provider, messages, model, tools, onToolCall, cwd, sendOpts) {
166
180
  let iterations = 0;
167
181
  let toolCallsTotal = 0;
@@ -203,6 +217,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
203
217
  };
204
218
  const sessionRef = opts.session || null;
205
219
  let _providerPrefixGuardState = sessionRef?._providerPrefixGuardState || null;
220
+ const _cacheBreakTraceKeys = new Set();
206
221
  // Provider tool snapshots are request-loop state, never durable session
207
222
  // state. Older builds persisted this field, which let a resumed session
208
223
  // keep advertising a retired schema even after session.tools was rebuilt.
@@ -222,7 +237,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
222
237
  // - buffered : opts.onAssistantText skipped (response.content below)
223
238
  // - history : tool-call turn content blanked before messages.push
224
239
  // Reasoning/thinking deltas, tool calls, and the final answer are kept.
225
- const suppressMidTurnText = isAgentOwner(sessionRef);
240
+ // Provider onTextDelta is restored only when live projection was
241
+ // explicitly requested for THIS send (`opts.liveProjection`). Absence
242
+ // of that flag preserves the silent Agent path even if a tracker
243
+ // wrapper was installed upstream.
244
+ const suppressMidTurnText = shouldSuppressAgentMidTurnText(sessionRef, opts);
226
245
  if (suppressMidTurnText) opts.onTextDelta = undefined;
227
246
  // Deferred mutation-body compaction: bodies left verbatim by a previous
228
247
  // turn's push (deferBodies below) collapse to markers now — the model has
@@ -264,6 +283,13 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
264
283
  const submissionIds = Array.isArray(merged.ids) ? merged.ids : [];
265
284
  const submittedAt = Number(merged.submittedAt);
266
285
  const injectedAt = Date.now();
286
+ const steeringTranscriptMeta = merged.transcriptMeta
287
+ && typeof merged.transcriptMeta === 'object'
288
+ ? {
289
+ at: Number.isFinite(submittedAt) && submittedAt > 0 ? submittedAt : injectedAt,
290
+ ...merged.transcriptMeta,
291
+ }
292
+ : null;
267
293
  if (Number.isFinite(submittedAt) && submittedAt > 0) {
268
294
  maxQueueWaitMs = Math.max(maxQueueWaitMs, injectedAt - submittedAt);
269
295
  }
@@ -277,6 +303,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
277
303
  meta: {
278
304
  source: 'steering',
279
305
  ...(submissionIds.length ? { submissionIds } : {}),
306
+ ...(steeringTranscriptMeta ? { transcript: steeringTranscriptMeta } : {}),
280
307
  },
281
308
  });
282
309
  const text = merged.text || steeringContentText(merged.content);
@@ -289,6 +316,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
289
316
  injectedAt,
290
317
  stage,
291
318
  ...(Array.isArray(merged.images) && merged.images.length ? { images: merged.images } : {}),
319
+ ...(steeringTranscriptMeta ? { transcriptMeta: steeringTranscriptMeta } : {}),
292
320
  });
293
321
  } catch {}
294
322
  }
@@ -621,6 +649,9 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
621
649
  pathAliases: false,
622
650
  });
623
651
  const _providerMessages = _evidenceProjection.messages;
652
+ const _prefixMutationSource = opts.cacheBreakIntent === 'transcript_rebuild'
653
+ ? 'transcript_rebuild'
654
+ : (_evidenceProjection.stats.changedToolResults > 0 ? 'evidence_union' : null);
624
655
  const _providerPrefixGuardCandidate = prepareProviderPrefixGuard(
625
656
  _providerPrefixGuardState,
626
657
  _providerMessages,
@@ -630,7 +661,28 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
630
661
  },
631
662
  {
632
663
  provider: sessionRef?.provider || provider?.name || null,
664
+ model: model || null,
633
665
  cacheBreakIntent: opts.cacheBreakIntent,
666
+ mutationSource: _prefixMutationSource,
667
+ onCacheBreak: (details) => {
668
+ const key = [
669
+ details.classification,
670
+ details.reason,
671
+ details.index,
672
+ details.previousHash,
673
+ details.nextHash,
674
+ details.previousRequestPrefixHash,
675
+ details.nextRequestPrefixHash,
676
+ ].join('|');
677
+ if (_cacheBreakTraceKeys.has(key)) return;
678
+ _cacheBreakTraceKeys.add(key);
679
+ traceCacheBreak({
680
+ sessionId,
681
+ iteration: nextIteration,
682
+ intentionalTransition: opts.cacheBreakIntent,
683
+ ...details,
684
+ });
685
+ },
634
686
  },
635
687
  );
636
688
  if (_evidenceProjection.stats.reusedRows > 0
@@ -701,8 +753,24 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
701
753
  response = _sendResult.response;
702
754
  _providerPrefixGuardState = _providerPrefixGuardCandidate;
703
755
  if (sessionRef) sessionRef._providerPrefixGuardState = _providerPrefixGuardState;
704
- if (_imageStripActive && Array.isArray(_pendingImageStripPersistMessages)) {
705
- messages.splice(0, messages.length, ..._pendingImageStripPersistMessages);
756
+ if (_imageStripActive) {
757
+ if (Array.isArray(_pendingImageStripPersistMessages)) {
758
+ messages.splice(0, messages.length, ..._pendingImageStripPersistMessages);
759
+ } else {
760
+ _providerPrefixGuardState = null;
761
+ if (sessionRef) delete sessionRef._providerPrefixGuardState;
762
+ traceCacheBreak({
763
+ sessionId,
764
+ iteration: nextIteration,
765
+ classification: 'intentional',
766
+ reason: 'image_strip_nonpersistent_rebaseline',
767
+ source: 'image_strip_retry',
768
+ provider: sessionRef?.provider || provider?.name || null,
769
+ model: model || null,
770
+ previousCount: _providerMessages.length,
771
+ nextCount: messages.length,
772
+ });
773
+ }
706
774
  _imageStripActive = false;
707
775
  _pendingImageStripPersistMessages = null;
708
776
  }
@@ -0,0 +1,61 @@
1
+ const SNAPSHOT_RE = /(?:^|\n)Snapshot:\s+(p\d+)-s\d+\b/i;
2
+
3
+ export const BROWSER_SNAPSHOT_SUPERSEDED =
4
+ '[Browser snapshot superseded — a newer snapshot for this page exists later in the conversation. '
5
+ + 'Use refs from the latest snapshot or call browser snapshot/observe again.]';
6
+
7
+ function contentText(content) {
8
+ if (typeof content === 'string') return content;
9
+ if (content && typeof content === 'object' && Array.isArray(content.content)) {
10
+ return contentText(content.content);
11
+ }
12
+ if (!Array.isArray(content)) return '';
13
+ return content
14
+ .filter((part) => part && typeof part === 'object' && part.type === 'text')
15
+ .map((part) => String(part.text || ''))
16
+ .join('\n');
17
+ }
18
+
19
+ export function supersedeBrowserSnapshots(messages) {
20
+ if (!Array.isArray(messages) || messages.length < 2) {
21
+ return { messages, replaced: 0, savedBytes: 0 };
22
+ }
23
+ const browserCallIds = new Set();
24
+ for (const message of messages) {
25
+ if (message?.role !== 'assistant' || !Array.isArray(message.toolCalls)) continue;
26
+ for (const call of message.toolCalls) {
27
+ const name = String(call?.name || call?.function?.name || '').toLowerCase();
28
+ if (name === 'browser' && call?.id) browserCallIds.add(call.id);
29
+ }
30
+ }
31
+ const locations = [];
32
+ const latestByPage = new Map();
33
+ messages.forEach((message, index) => {
34
+ if (message?.role !== 'tool') return;
35
+ if (browserCallIds.size > 0 && !browserCallIds.has(message.toolCallId)) return;
36
+ const text = contentText(message.content);
37
+ const match = SNAPSHOT_RE.exec(text);
38
+ if (!match) return;
39
+ const pageId = match[1].toLowerCase();
40
+ locations.push({ index, pageId, text });
41
+ latestByPage.set(pageId, index);
42
+ });
43
+ const stale = locations.filter((entry) => latestByPage.get(entry.pageId) !== entry.index);
44
+ if (!stale.length) return { messages, replaced: 0, savedBytes: 0 };
45
+ const projected = messages.slice();
46
+ let savedBytes = 0;
47
+ for (const entry of stale) {
48
+ const original = messages[entry.index];
49
+ const replacement = `${BROWSER_SNAPSHOT_SUPERSEDED} (${entry.pageId})`;
50
+ savedBytes += Math.max(0, Buffer.byteLength(entry.text) - Buffer.byteLength(replacement));
51
+ projected[entry.index] = {
52
+ ...original,
53
+ content: replacement,
54
+ meta: {
55
+ ...(original?.meta && typeof original.meta === 'object' ? original.meta : {}),
56
+ browserSnapshotSuperseded: true,
57
+ },
58
+ };
59
+ }
60
+ return { messages: projected, replaced: stale.length, savedBytes };
61
+ }