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
@@ -0,0 +1,984 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import {
8
+ cleanupBackgroundTasks,
9
+ getBackgroundTask,
10
+ startBackgroundTask,
11
+ } from '../runtime/shared/background-tasks.mjs';
12
+ import { _sessionSummary } from '../runtime/agent/orchestrator/session/store-summary-index.mjs';
13
+ import { restoreTranscriptItems } from '../tui/session/session-api-ext.mjs';
14
+ import { createStandaloneAgent } from './agent-tool.mjs';
15
+ import { createSessionService } from './session-service.mjs';
16
+
17
+ function persistedUserMessage(prompt, options = {}) {
18
+ const transcriptMeta = options.transcriptMeta && typeof options.transcriptMeta === 'object'
19
+ ? { ...options.transcriptMeta }
20
+ : null;
21
+ return {
22
+ role: 'user',
23
+ content: prompt,
24
+ ...(transcriptMeta ? { meta: { transcript: transcriptMeta } } : {}),
25
+ };
26
+ }
27
+
28
+ function fakeRuntimeFactory(events) {
29
+ return async (options = {}) => {
30
+ const listeners = new Set();
31
+ const state = {
32
+ sessionId: null,
33
+ items: [],
34
+ queued: [],
35
+ busy: false,
36
+ provider: options.provider,
37
+ model: options.model,
38
+ };
39
+ const emit = () => {
40
+ for (const listener of [...listeners]) listener();
41
+ };
42
+ return {
43
+ get provider() { return options.provider; },
44
+ get session() { return { provider: options.provider }; },
45
+ getState: () => state,
46
+ subscribe(listener) {
47
+ listeners.add(listener);
48
+ return () => listeners.delete(listener);
49
+ },
50
+ reserveSession(sessionId) {
51
+ state.sessionId = sessionId;
52
+ events.push(['reserve', sessionId, options]);
53
+ emit();
54
+ return true;
55
+ },
56
+ async submitAndWait(prompt, turnOptions) {
57
+ events.push(['turn', state.sessionId, prompt, turnOptions]);
58
+ state.busy = true;
59
+ state.items.push({ id: `user-${state.items.length}`, kind: 'user', text: prompt });
60
+ emit();
61
+ const content = `handoff:${prompt}`;
62
+ state.items.push({ id: `assistant-${state.items.length}`, kind: 'assistant', text: content });
63
+ state.busy = false;
64
+ emit();
65
+ return { status: 'done', result: { content } };
66
+ },
67
+ async abort(options) {
68
+ events.push(['abort', state.sessionId, options]);
69
+ state.busy = false;
70
+ emit();
71
+ return { aborted: true };
72
+ },
73
+ async closeCanonicalSession(reason) {
74
+ events.push(['close', state.sessionId, reason]);
75
+ state.busy = false;
76
+ emit();
77
+ return true;
78
+ },
79
+ async dispose(reason) {
80
+ events.push(['dispose', state.sessionId, reason]);
81
+ },
82
+ };
83
+ };
84
+ }
85
+
86
+ function deferred() {
87
+ let resolve;
88
+ const promise = new Promise((settle) => { resolve = settle; });
89
+ return { promise, resolve };
90
+ }
91
+
92
+ function cancellationRaceRuntimeFactory(events, controls) {
93
+ const createRuntime = fakeRuntimeFactory(events);
94
+ return async (options = {}) => {
95
+ const runtime = await createRuntime(options);
96
+ return {
97
+ ...runtime,
98
+ async submitAndWait(prompt, turnOptions) {
99
+ const state = runtime.getState();
100
+ events.push(['turn', state.sessionId, prompt, turnOptions]);
101
+ state.busy = true;
102
+ controls.turnPending = true;
103
+ controls.turnStarted.resolve(state.sessionId);
104
+ const detail = await controls.lateTurn.promise;
105
+ controls.turnPending = false;
106
+ state.busy = false;
107
+ return detail;
108
+ },
109
+ async closeCanonicalSession(reason) {
110
+ const state = runtime.getState();
111
+ events.push(['close', state.sessionId, reason]);
112
+ controls.closedWhileTurnPending = controls.turnPending;
113
+ state.busy = false;
114
+ return true;
115
+ },
116
+ };
117
+ };
118
+ }
119
+
120
+ function durableRuntimeFactory(events, records) {
121
+ return async (options = {}) => {
122
+ const listeners = new Set();
123
+ const state = {
124
+ sessionId: null,
125
+ items: [],
126
+ messages: [],
127
+ queued: [],
128
+ busy: false,
129
+ provider: options.provider,
130
+ model: options.model,
131
+ };
132
+ const emit = () => {
133
+ for (const listener of [...listeners]) listener();
134
+ };
135
+ const persist = (extra = {}) => {
136
+ if (!state.sessionId) return;
137
+ const previous = records.get(state.sessionId) || {};
138
+ records.set(state.sessionId, {
139
+ ...previous,
140
+ id: state.sessionId,
141
+ provider: options.provider || previous.provider || null,
142
+ model: options.model || previous.model || null,
143
+ effort: options.effort || previous.effort || null,
144
+ fast: options.fast === true || previous.fast === true,
145
+ modelParameters: options.modelParameters || previous.modelParameters || null,
146
+ ...(options.sessionProfile || {}),
147
+ messages: state.messages.map((message) => ({ ...message })),
148
+ messageCount: state.messages.length,
149
+ createdAt: previous.createdAt || Date.now(),
150
+ updatedAt: Date.now(),
151
+ status: state.busy ? 'running' : 'idle',
152
+ ...extra,
153
+ });
154
+ };
155
+ return {
156
+ get provider() { return options.provider; },
157
+ get session() { return { provider: options.provider }; },
158
+ getState: () => state,
159
+ subscribe(listener) {
160
+ listeners.add(listener);
161
+ return () => listeners.delete(listener);
162
+ },
163
+ reserveSession(sessionId) {
164
+ state.sessionId = sessionId;
165
+ events.push(['reserve', sessionId, options]);
166
+ persist();
167
+ emit();
168
+ return true;
169
+ },
170
+ resume(sessionId) {
171
+ const stored = records.get(sessionId);
172
+ if (!stored) return false;
173
+ state.sessionId = sessionId;
174
+ state.provider = stored.provider;
175
+ state.model = stored.model;
176
+ state.messages = Array.isArray(stored.messages)
177
+ ? stored.messages.map((message) => ({ ...message }))
178
+ : [];
179
+ state.items = state.messages.map((message, index) => ({
180
+ id: `${message.role}-${index}`,
181
+ kind: message.role,
182
+ text: message.content,
183
+ }));
184
+ events.push(['resume', sessionId, options]);
185
+ emit();
186
+ return true;
187
+ },
188
+ async submitAndWait(prompt, turnOptions) {
189
+ events.push(['turn', state.sessionId, prompt, turnOptions]);
190
+ state.busy = true;
191
+ state.messages.push(persistedUserMessage(prompt, turnOptions));
192
+ state.items.push({ id: `user-${state.items.length}`, kind: 'user', text: prompt });
193
+ persist();
194
+ emit();
195
+ const content = `handoff:${prompt}`;
196
+ state.messages.push({ role: 'assistant', content });
197
+ state.items.push({ id: `assistant-${state.items.length}`, kind: 'assistant', text: content });
198
+ state.busy = false;
199
+ persist();
200
+ emit();
201
+ return { status: 'done', result: { content } };
202
+ },
203
+ async submitAsync(prompt, submitOptions) {
204
+ events.push(['submit', state.sessionId, prompt, submitOptions]);
205
+ state.messages.push(persistedUserMessage(prompt, submitOptions));
206
+ state.items.push({ id: `user-${state.items.length}`, kind: 'user', text: prompt });
207
+ persist();
208
+ emit();
209
+ return true;
210
+ },
211
+ readModelMessages(start = 0) {
212
+ return {
213
+ messageCount: state.messages.length,
214
+ messages: state.messages.slice(start),
215
+ };
216
+ },
217
+ async abort(abortOptions) {
218
+ events.push(['abort', state.sessionId, abortOptions]);
219
+ state.busy = false;
220
+ persist({ status: 'cancelled' });
221
+ emit();
222
+ return { aborted: true };
223
+ },
224
+ async closeCanonicalSession(reason) {
225
+ events.push(['close', state.sessionId, reason]);
226
+ state.busy = false;
227
+ persist({
228
+ closed: true,
229
+ status: 'closed',
230
+ closedReason: reason,
231
+ });
232
+ emit();
233
+ return true;
234
+ },
235
+ async dispose(reason) {
236
+ events.push(['dispose', state.sessionId, reason]);
237
+ persist();
238
+ },
239
+ };
240
+ };
241
+ }
242
+
243
+ async function waitForAgentTask(agent, started, context) {
244
+ const taskId = /^agent task:\s*(\S+)/m.exec(started)?.[1];
245
+ assert.ok(taskId, started);
246
+ let output = '';
247
+ const deadline = Date.now() + 3_000;
248
+ while (Date.now() < deadline) {
249
+ output = await agent.execute({ type: 'read', task_id: taskId }, context);
250
+ if (/status: completed/.test(output)) return output;
251
+ await new Promise((resolve) => setTimeout(resolve, 10));
252
+ }
253
+ assert.match(output, /status: completed/);
254
+ return output;
255
+ }
256
+
257
+ test('canonical Agent children use ordinary session turns, retain Parent–Child links, and cascade cancel', async (t) => {
258
+ const events = [];
259
+ const service = createSessionService({
260
+ createSessionRuntime: fakeRuntimeFactory(events),
261
+ sessionExists: async () => true,
262
+ readStoredSession: async () => null,
263
+ listSessions: async () => [],
264
+ });
265
+ t.after(() => service.stop('test complete'));
266
+
267
+ const rootId = 'sess_root';
268
+ const first = await service.agentSurface.createChild({
269
+ tag: 'worker1',
270
+ spec: {
271
+ parentSessionId: rootId,
272
+ ownerSessionId: rootId,
273
+ agent: 'worker',
274
+ agentTag: 'worker1',
275
+ cwd: 'C:\\Project\\tree',
276
+ preset: {
277
+ id: 'worker-route',
278
+ provider: 'test-provider',
279
+ model: 'test-model',
280
+ effort: 'high',
281
+ },
282
+ },
283
+ });
284
+
285
+ const firstSession = first.session;
286
+ assert.equal(firstSession.parentSessionId, rootId);
287
+ assert.equal(firstSession.ownerSessionId, rootId);
288
+ assert.equal(firstSession.visibility, 'agent-only');
289
+ const firstCreate = events.find(([kind, id]) => kind === 'reserve' && id === firstSession.id);
290
+ assert.equal(firstCreate[2].sessionProfile.parentSessionId, rootId);
291
+ assert.equal(firstCreate[2].sessionProfile.ownerSessionId, rootId);
292
+ assert.equal(firstCreate[2].sessionProfile.visibility, 'agent-only');
293
+
294
+ const initial = await service.agentSurface.runTurn({
295
+ session: firstSession,
296
+ prompt: 'initial brief',
297
+ });
298
+ const followUp = await service.agentSurface.runTurn({
299
+ session: firstSession,
300
+ prompt: 'follow up',
301
+ });
302
+ assert.equal(initial.content, 'handoff:initial brief');
303
+ assert.equal(followUp.content, 'handoff:follow up');
304
+ const firstTurns = events.filter(([kind, id]) => kind === 'turn' && id === firstSession.id);
305
+ assert.equal(firstTurns.length, 2);
306
+ assert.deepEqual(firstTurns.map(([, , prompt]) => prompt), ['initial brief', 'follow up']);
307
+ assert.deepEqual(firstTurns[0][3].transcriptMeta, { sender: 'lead' });
308
+ assert.deepEqual(firstTurns[1][3].transcriptMeta, { sender: 'lead' });
309
+ assert.equal(firstTurns[0][3].mode, 'prompt');
310
+
311
+ const nested = await service.agentSurface.createChild({
312
+ tag: 'reviewer1',
313
+ spec: {
314
+ parentSessionId: firstSession.id,
315
+ agent: 'reviewer',
316
+ agentTag: 'reviewer1',
317
+ cwd: 'C:\\Project\\tree',
318
+ preset: { provider: 'test-provider', model: 'test-model' },
319
+ },
320
+ });
321
+ assert.equal(nested.session.parentSessionId, firstSession.id);
322
+ assert.equal(nested.session.ownerSessionId, rootId);
323
+ assert.equal(service.rootOwnerSessionId(nested.session.id), rootId);
324
+
325
+ const concurrent = await service.agentSurface.createChild({
326
+ tag: 'worker2',
327
+ spec: {
328
+ parentSessionId: rootId,
329
+ ownerSessionId: rootId,
330
+ agent: 'worker',
331
+ agentTag: 'worker2',
332
+ cwd: 'C:\\Project\\tree',
333
+ preset: { provider: 'test-provider', model: 'test-model' },
334
+ },
335
+ });
336
+ assert.deepEqual(await Promise.all([
337
+ service.agentManager.closeSession(concurrent.session.id, 'first close'),
338
+ service.agentManager.closeSession(concurrent.session.id, 'second close'),
339
+ ]), [true, true]);
340
+ assert.equal(events.filter(
341
+ ([kind, id]) => kind === 'close' && id === concurrent.session.id,
342
+ ).length, 1);
343
+
344
+ assert.equal(await service.agentManager.closeSession(firstSession.id, 'parent cancelled'), true);
345
+ const closedIds = events
346
+ .filter(([kind]) => kind === 'close')
347
+ .map(([, id]) => id);
348
+ assert.equal(closedIds.includes(firstSession.id), true);
349
+ assert.equal(closedIds.includes(nested.session.id), true);
350
+ assert.equal(service.agentDescriptor(firstSession.id).status, 'closed');
351
+ assert.equal(service.agentDescriptor(nested.session.id).status, 'closed');
352
+ });
353
+
354
+ test('turn abort does not hydrate the durable Agent catalog', async (t) => {
355
+ const events = [];
356
+ let listSessionsCalls = 0;
357
+ const service = createSessionService({
358
+ createSessionRuntime: fakeRuntimeFactory(events),
359
+ sessionExists: async () => true,
360
+ readStoredSession: async () => null,
361
+ listSessions: async () => {
362
+ listSessionsCalls += 1;
363
+ return [];
364
+ },
365
+ });
366
+ t.after(() => service.stop('test complete'));
367
+
368
+ const parent = await service.createSession({
369
+ sessionId: 'sess_turn_abort_no_scan',
370
+ cwd: 'C:\\Project\\tree',
371
+ provider: 'test-provider',
372
+ model: 'test-model',
373
+ });
374
+ const result = await service.abortSession({
375
+ sessionId: parent.sessionId,
376
+ });
377
+
378
+ assert.equal(result.aborted, true);
379
+ assert.equal(listSessionsCalls, 0);
380
+ assert.equal(events.filter(
381
+ ([kind, id]) => kind === 'abort' && id === parent.sessionId,
382
+ ).length, 1);
383
+ });
384
+
385
+ test('turn abort preserves background Agent descendants', async (t) => {
386
+ const events = [];
387
+ const delegatedWork = deferred();
388
+ let delegatedCancelCalls = 0;
389
+ const service = createSessionService({
390
+ createSessionRuntime: fakeRuntimeFactory(events),
391
+ sessionExists: async () => true,
392
+ readStoredSession: async () => null,
393
+ listSessions: async () => [],
394
+ });
395
+ t.after(() => {
396
+ cleanupBackgroundTasks({ surface: 'agent', force: true });
397
+ return service.stop('test complete');
398
+ });
399
+
400
+ const parent = await service.createSession({
401
+ sessionId: 'sess_turn_abort_parent',
402
+ cwd: 'C:\\Project\\tree',
403
+ provider: 'test-provider',
404
+ model: 'test-model',
405
+ });
406
+ const child = await service.agentSurface.createChild({
407
+ tag: 'background-worker',
408
+ spec: {
409
+ parentSessionId: parent.sessionId,
410
+ ownerSessionId: parent.sessionId,
411
+ agent: 'worker',
412
+ agentTag: 'background-worker',
413
+ cwd: 'C:\\Project\\tree',
414
+ preset: { provider: 'test-provider', model: 'test-model' },
415
+ },
416
+ });
417
+ const delegatedTask = startBackgroundTask({
418
+ taskId: 'agent_lead_abort_preserves_child',
419
+ surface: 'agent',
420
+ operation: 'spawn',
421
+ context: { callerSessionId: parent.sessionId },
422
+ cancel: () => {
423
+ delegatedCancelCalls += 1;
424
+ delegatedWork.resolve(null);
425
+ },
426
+ run: () => delegatedWork.promise,
427
+ });
428
+
429
+ const result = await service.abortSession({
430
+ sessionId: parent.sessionId,
431
+ });
432
+
433
+ assert.equal(result.aborted, true);
434
+ assert.equal(events.some(
435
+ ([kind, id]) => kind === 'close' && id === child.session.id,
436
+ ), false);
437
+ assert.equal(service.agentDescriptor(child.session.id).closed, false);
438
+ assert.equal(service.agentDescriptor(child.session.id).status, 'idle');
439
+ assert.equal(delegatedCancelCalls, 0);
440
+ assert.equal(delegatedTask.status, 'running');
441
+ delegatedWork.resolve(null);
442
+ await delegatedTask.promise;
443
+ });
444
+
445
+ test('Agent turn abort propagates to nested Agent background work without a durable scan', async (t) => {
446
+ const events = [];
447
+ let listSessionsCalls = 0;
448
+ const nestedWork = deferred();
449
+ let nestedCancelCalls = 0;
450
+ const service = createSessionService({
451
+ createSessionRuntime: fakeRuntimeFactory(events),
452
+ sessionExists: async () => true,
453
+ readStoredSession: async () => null,
454
+ listSessions: async () => {
455
+ listSessionsCalls += 1;
456
+ return [];
457
+ },
458
+ });
459
+ t.after(() => {
460
+ cleanupBackgroundTasks({ surface: 'agent', force: true });
461
+ return service.stop('test complete');
462
+ });
463
+
464
+ const parent = await service.createSession({
465
+ sessionId: 'sess_nested_abort_parent',
466
+ cwd: 'C:\\Project\\tree',
467
+ provider: 'test-provider',
468
+ model: 'test-model',
469
+ });
470
+ const child = await service.agentSurface.createChild({
471
+ tag: 'nested-parent-worker',
472
+ spec: {
473
+ parentSessionId: parent.sessionId,
474
+ ownerSessionId: parent.sessionId,
475
+ agent: 'worker',
476
+ agentTag: 'nested-parent-worker',
477
+ cwd: 'C:\\Project\\tree',
478
+ preset: { provider: 'test-provider', model: 'test-model' },
479
+ },
480
+ });
481
+ const callsBeforeAbort = listSessionsCalls;
482
+ const nestedTask = startBackgroundTask({
483
+ taskId: 'agent_nested_abort_child',
484
+ surface: 'agent',
485
+ operation: 'spawn',
486
+ context: { callerSessionId: child.session.id },
487
+ cancel: () => {
488
+ nestedCancelCalls += 1;
489
+ nestedWork.resolve(null);
490
+ },
491
+ run: () => nestedWork.promise,
492
+ });
493
+
494
+ const result = await service.abortSession({ sessionId: child.session.id });
495
+ await nestedTask.promise;
496
+
497
+ assert.equal(result.aborted, true);
498
+ assert.equal(listSessionsCalls, callsBeforeAbort);
499
+ assert.equal(nestedCancelCalls, 1);
500
+ assert.equal(nestedTask.status, 'cancelled');
501
+ assert.equal(events.filter(
502
+ ([kind, id]) => kind === 'abort' && id === child.session.id,
503
+ ).length, 1);
504
+ });
505
+
506
+ test('external desktop submit stamps User while canonical Agent turns stamp Lead', async (t) => {
507
+ const events = [];
508
+ const service = createSessionService({
509
+ createSessionRuntime: async (options = {}) => {
510
+ const runtime = await fakeRuntimeFactory(events)(options);
511
+ return {
512
+ ...runtime,
513
+ externalAction: true,
514
+ async submitAsync(prompt, submitOptions) {
515
+ events.push(['submit', runtime.getState().sessionId, prompt, submitOptions]);
516
+ return true;
517
+ },
518
+ };
519
+ },
520
+ sessionExists: async () => true,
521
+ readStoredSession: async () => null,
522
+ listSessions: async () => [],
523
+ });
524
+ t.after(() => service.stop('test complete'));
525
+
526
+ const created = await service.createSession({
527
+ sessionId: 'sess_desktop_user',
528
+ cwd: 'C:\\Project\\tree',
529
+ provider: 'test-provider',
530
+ model: 'test-model',
531
+ });
532
+ const submitted = await service.submitSession({
533
+ sessionId: created.sessionId,
534
+ prompt: 'please explain',
535
+ });
536
+ assert.equal(submitted.accepted, true);
537
+ const submits = events.filter(([kind, id]) => kind === 'submit' && id === created.sessionId);
538
+ assert.equal(submits.length, 1);
539
+ assert.equal(submits[0][3].transcriptMeta.sender, 'user');
540
+
541
+ const child = await service.agentSurface.createChild({
542
+ tag: 'worker-lead',
543
+ spec: {
544
+ parentSessionId: created.sessionId,
545
+ ownerSessionId: created.sessionId,
546
+ agent: 'worker',
547
+ agentTag: 'worker-lead',
548
+ cwd: 'C:\\Project\\tree',
549
+ preset: { provider: 'test-provider', model: 'test-model' },
550
+ },
551
+ });
552
+ await service.agentSurface.runTurn({
553
+ session: child.session,
554
+ prompt: 'review this change',
555
+ });
556
+ const turns = events.filter(([kind, id]) => kind === 'turn' && id === child.session.id);
557
+ assert.equal(turns.length, 1);
558
+ assert.deepEqual(turns[0][3].transcriptMeta, { sender: 'lead' });
559
+ assert.equal(child.session.parentSessionId, created.sessionId);
560
+ assert.equal(child.session.ownerSessionId, created.sessionId);
561
+ });
562
+
563
+ test('durable Agent and desktop transcripts restore Lead and User senders', async (t) => {
564
+ const events = [];
565
+ const records = new Map();
566
+ const createService = () => createSessionService({
567
+ createSessionRuntime: async (options = {}) => {
568
+ const runtime = await durableRuntimeFactory(events, records)(options);
569
+ runtime.externalAction = true;
570
+ return runtime;
571
+ },
572
+ sessionExists: async (sessionId) => records.has(sessionId),
573
+ readStoredSession: async (sessionId, options = {}) => {
574
+ const stored = records.get(sessionId);
575
+ if (!stored) return null;
576
+ return {
577
+ sessionId,
578
+ provider: stored.provider,
579
+ model: stored.model,
580
+ cwd: stored.cwd,
581
+ items: [],
582
+ ...(options.includeMessages === true ? { messages: stored.messages || [] } : {}),
583
+ };
584
+ },
585
+ listSessions: async () => [...records.values()],
586
+ });
587
+ let service = createService();
588
+ t.after(async () => {
589
+ await service?.stop('test complete');
590
+ });
591
+
592
+ const desktop = await service.createSession({
593
+ sessionId: 'sess_desktop_user',
594
+ cwd: 'C:\\Project\\tree',
595
+ provider: 'test-provider',
596
+ model: 'test-model',
597
+ });
598
+ const submitted = await service.submitSession({
599
+ sessionId: desktop.sessionId,
600
+ prompt: 'Please explain the result.',
601
+ });
602
+ assert.equal(submitted.accepted, true);
603
+
604
+ const child = await service.agentSurface.createChild({
605
+ tag: 'worker-lead',
606
+ spec: {
607
+ parentSessionId: desktop.sessionId,
608
+ ownerSessionId: desktop.sessionId,
609
+ agent: 'worker',
610
+ agentTag: 'worker-lead',
611
+ cwd: 'C:\\Project\\tree',
612
+ preset: { provider: 'test-provider', model: 'test-model' },
613
+ },
614
+ });
615
+ await service.agentSurface.runTurn({
616
+ session: child.session,
617
+ prompt: 'Review this change.',
618
+ });
619
+ assert.equal(child.session.parentSessionId, desktop.sessionId);
620
+ assert.equal(child.session.ownerSessionId, desktop.sessionId);
621
+
622
+ const liveAgent = await service.readSession({ sessionId: child.session.id, messageStart: 0 });
623
+ const liveDesktop = await service.readSession({ sessionId: desktop.sessionId, messageStart: 0 });
624
+ const liveAgentUser = liveAgent.messages.find((message) => message.role === 'user');
625
+ const liveDesktopUser = liveDesktop.messages.find((message) => message.role === 'user');
626
+ assert.equal(liveAgentUser.meta.transcript.sender, 'lead');
627
+ assert.equal(liveDesktopUser.meta.transcript.sender, 'user');
628
+
629
+ const agentSessionId = child.session.id;
630
+ const desktopSessionId = desktop.sessionId;
631
+ await service.stop('simulated daemon replacement');
632
+ service = createService();
633
+
634
+ const coldAgent = await service.readSession({ sessionId: agentSessionId, messageStart: 0 });
635
+ const coldDesktop = await service.readSession({ sessionId: desktopSessionId, messageStart: 0 });
636
+ const storedAgentUser = coldAgent.messages.find((message) => message.role === 'user');
637
+ const storedDesktopUser = coldDesktop.messages.find((message) => message.role === 'user');
638
+ assert.equal(storedAgentUser.content, 'Review this change.');
639
+ assert.equal(storedAgentUser.meta.transcript.sender, 'lead');
640
+ assert.equal(storedDesktopUser.content, 'Please explain the result.');
641
+ assert.equal(storedDesktopUser.meta.transcript.sender, 'user');
642
+
643
+ const restoredAgent = restoreTranscriptItems(coldAgent.messages, { sessionId: agentSessionId });
644
+ const restoredDesktop = restoreTranscriptItems(coldDesktop.messages, { sessionId: desktopSessionId });
645
+ assert.equal(restoredAgent.find((item) => item.kind === 'user')?.sender, 'lead');
646
+ assert.equal(restoredDesktop.find((item) => item.kind === 'user')?.sender, 'user');
647
+ });
648
+
649
+ test('live Agent cancel survives canonical close and a late cancelled turn rejection', {
650
+ timeout: 3_000,
651
+ }, async (t) => {
652
+ const dataDir = await mkdtemp(join(tmpdir(), 'mixdog-agent-cancel-race-'));
653
+ const events = [];
654
+ const controls = {
655
+ turnStarted: deferred(),
656
+ lateTurn: deferred(),
657
+ turnPending: false,
658
+ closedWhileTurnPending: false,
659
+ };
660
+ const config = {
661
+ default: 'main',
662
+ providers: {},
663
+ presets: [{
664
+ id: 'main',
665
+ name: 'Main',
666
+ provider: 'test-provider',
667
+ model: 'test-model',
668
+ tools: 'full',
669
+ }],
670
+ };
671
+ const service = createSessionService({
672
+ createSessionRuntime: cancellationRaceRuntimeFactory(events, controls),
673
+ sessionExists: async () => true,
674
+ readStoredSession: async () => null,
675
+ listSessions: async () => [],
676
+ });
677
+ const agent = createStandaloneAgent({
678
+ cfgMod: {
679
+ loadConfig: () => config,
680
+ getPluginData: () => dataDir,
681
+ resolveRuntimeSpec: (_preset, { lane, agentId }) => ({
682
+ lane,
683
+ scopeKey: `${lane}:${agentId}`,
684
+ }),
685
+ },
686
+ reg: {},
687
+ mgr: service.agentManager,
688
+ dataDir,
689
+ cwd: process.cwd(),
690
+ sessionSurface: service.agentSurface,
691
+ awaitKeychainPrewarm: async () => {},
692
+ isKeychainPrewarmReady: () => true,
693
+ notifySessionCompletion: () => true,
694
+ });
695
+ t.after(async () => {
696
+ cleanupBackgroundTasks({ surface: 'agent', force: true });
697
+ await service.stop('test complete');
698
+ await rm(dataDir, { recursive: true, force: true });
699
+ });
700
+
701
+ const context = {
702
+ callerSessionId: 'sess_cancel_race_root',
703
+ ownerSessionId: 'sess_cancel_race_root',
704
+ callerCwd: process.cwd(),
705
+ clientHostPid: process.pid,
706
+ };
707
+ const started = await agent.execute({
708
+ type: 'spawn',
709
+ tag: 'cancel-race-worker',
710
+ agent: 'worker',
711
+ prompt: 'hold until cancelled',
712
+ }, context);
713
+ const taskId = /^agent task:\s*(\S+)/m.exec(started)?.[1];
714
+ assert.ok(taskId, started);
715
+ const sessionId = await controls.turnStarted.promise;
716
+ const task = getBackgroundTask(taskId, { surface: 'agent' });
717
+ assert.ok(task);
718
+ assert.equal(task.status, 'running');
719
+
720
+ const cancelled = await agent.execute({ type: 'cancel', task_id: taskId }, context);
721
+ assert.match(cancelled, new RegExp(`agent task:\\s*${taskId}`));
722
+ assert.match(cancelled, new RegExp(sessionId));
723
+ assert.equal(controls.closedWhileTurnPending, true);
724
+ assert.equal(controls.turnPending, true);
725
+ assert.equal(events.filter(
726
+ ([kind, id]) => kind === 'close' && id === sessionId,
727
+ ).length, 1);
728
+ assert.equal(task.status, 'cancelled');
729
+
730
+ controls.lateTurn.resolve({ status: 'cancelled' });
731
+ await task.promise;
732
+ await new Promise((resolve) => setImmediate(resolve));
733
+
734
+ assert.equal(task.status, 'cancelled');
735
+ assert.notEqual(task.status, 'failed');
736
+ const stored = JSON.parse(await readFile(join(dataDir, 'agent-workers.json'), 'utf8'));
737
+ const worker = Object.values(stored.workers || {})
738
+ .find((row) => row.sessionId === sessionId);
739
+ assert.ok(worker);
740
+ assert.equal(worker.status, 'cancelled');
741
+ assert.equal(worker.stage, 'cancelled');
742
+ assert.notEqual(worker.status, 'error');
743
+ });
744
+
745
+ test('public Agent APIs rehydrate and reuse durable canonical ancestry after daemon replacement', async (t) => {
746
+ const dataDir = await mkdtemp(join(tmpdir(), 'mixdog-canonical-agent-'));
747
+ const events = [];
748
+ const notifications = [];
749
+ const records = new Map();
750
+ const listCalls = [];
751
+ const metadataReads = [];
752
+ const config = {
753
+ default: 'main',
754
+ providers: {},
755
+ presets: [{
756
+ id: 'main',
757
+ name: 'Main',
758
+ provider: 'test-provider',
759
+ model: 'test-model',
760
+ tools: 'full',
761
+ }],
762
+ };
763
+ const createService = () => createSessionService({
764
+ createSessionRuntime: durableRuntimeFactory(events, records),
765
+ sessionExists: async (sessionId) => records.has(sessionId),
766
+ readStoredSession: async (sessionId, options = {}) => {
767
+ const stored = records.get(sessionId);
768
+ if (!stored) return null;
769
+ if (options.metadataOnly === true) {
770
+ metadataReads.push(sessionId);
771
+ return {
772
+ id: sessionId,
773
+ sessionId,
774
+ owner: stored.owner,
775
+ agent: stored.agent,
776
+ parentSessionId: stored.parentSessionId,
777
+ ownerSessionId: stored.ownerSessionId,
778
+ visibility: stored.visibility,
779
+ };
780
+ }
781
+ return {
782
+ sessionId,
783
+ provider: stored.provider,
784
+ model: stored.model,
785
+ cwd: stored.cwd,
786
+ items: [],
787
+ ...(options.includeMessages === true ? { messages: stored.messages || [] } : {}),
788
+ };
789
+ },
790
+ listSessions: async (options = {}) => {
791
+ listCalls.push({ ...options });
792
+ return options.includeAgentOnly === true
793
+ ? [...records.values()].map((record) => {
794
+ const summary = _sessionSummary(record);
795
+ // Simulate a pre-parentSessionId v2 sidecar. Rehydration may read
796
+ // these exact Agent ids, but must never enumerate full sessions.
797
+ delete summary.parentSessionId;
798
+ return summary;
799
+ })
800
+ : [];
801
+ },
802
+ });
803
+ const createAgent = (service) => createStandaloneAgent({
804
+ cfgMod: {
805
+ loadConfig: () => config,
806
+ getPluginData: () => dataDir,
807
+ resolveRuntimeSpec: (_preset, { lane, agentId }) => ({
808
+ lane,
809
+ scopeKey: `${lane}:${agentId}`,
810
+ }),
811
+ },
812
+ reg: {},
813
+ mgr: service.agentManager,
814
+ dataDir,
815
+ cwd: process.cwd(),
816
+ sessionSurface: service.agentSurface,
817
+ awaitKeychainPrewarm: async () => {},
818
+ isKeychainPrewarmReady: () => true,
819
+ notifySessionCompletion(ownerSessionId, text, meta) {
820
+ notifications.push({ ownerSessionId, text, meta });
821
+ return true;
822
+ },
823
+ });
824
+ let service = createService();
825
+ let agent = createAgent(service);
826
+ assert.equal(agent.tools[0].annotations.agentHidden, false);
827
+ t.after(async () => {
828
+ cleanupBackgroundTasks({ surface: 'agent', force: true });
829
+ await service?.stop('test complete');
830
+ await rm(dataDir, { recursive: true, force: true });
831
+ });
832
+
833
+ const context = {
834
+ callerSessionId: 'sess_public_root',
835
+ ownerSessionId: 'sess_public_root',
836
+ callerCwd: process.cwd(),
837
+ clientHostPid: process.pid,
838
+ };
839
+ const started = await agent.execute({
840
+ type: 'spawn',
841
+ tag: 'canonical-worker',
842
+ agent: 'worker',
843
+ prompt: 'task brief',
844
+ }, context);
845
+ assert.match(started, /status: running/);
846
+ const status = await waitForAgentTask(agent, started, context);
847
+ assert.match(status, /status: completed/);
848
+ assert.match(status, /handoff:task brief/);
849
+ assert.doesNotMatch(status, /agent result/);
850
+
851
+ const sessionId = /^target:\s+\S+\s+(sess_\S+)/m.exec(status)?.[1];
852
+ assert.ok(sessionId);
853
+
854
+ const nestedContext = {
855
+ ...context,
856
+ callerSessionId: sessionId,
857
+ ownerSessionId: context.ownerSessionId,
858
+ };
859
+ const nestedStarted = await agent.execute({
860
+ type: 'spawn',
861
+ tag: 'canonical-reviewer',
862
+ agent: 'reviewer',
863
+ prompt: 'nested brief',
864
+ }, nestedContext);
865
+ const nestedStatus = await waitForAgentTask(agent, nestedStarted, nestedContext);
866
+ const nestedSessionId = /^target:\s+\S+\s+(sess_\S+)/m.exec(nestedStatus)?.[1];
867
+ assert.ok(nestedSessionId);
868
+ assert.notEqual(nestedSessionId, sessionId);
869
+ assert.equal(records.size, 2);
870
+
871
+ // Simulate a pre-visibility legacy Agent record while retaining its valid
872
+ // immediate-parent and root-owner metadata.
873
+ records.set(nestedSessionId, {
874
+ ...records.get(nestedSessionId),
875
+ visibility: null,
876
+ });
877
+
878
+ await service.stop('simulated daemon replacement');
879
+ cleanupBackgroundTasks({ surface: 'agent', force: true });
880
+ // A durable canonical session, not the auxiliary tag index, must be enough
881
+ // to recover same-tag reuse after a crash between the two writes.
882
+ await rm(join(dataDir, 'agent-workers.json'), { force: true });
883
+ service = createService();
884
+ agent = createAgent(service);
885
+
886
+ const coldRead = await agent.execute({ type: 'read', sessionId }, context);
887
+ assert.match(coldRead, /handoff:task brief/);
888
+
889
+ const sent = await agent.execute({
890
+ type: 'send',
891
+ tag: 'canonical-worker',
892
+ message: 'same child follow up',
893
+ }, context);
894
+ assert.match(sent, new RegExp(sessionId));
895
+ const follow = await waitForAgentTask(agent, sent, context);
896
+ assert.match(follow, /handoff:same child follow up/);
897
+
898
+ const rehydrated = service.agentDescriptor(sessionId);
899
+ const rehydratedNested = service.agentDescriptor(nestedSessionId);
900
+ assert.equal(rehydrated.id, sessionId);
901
+ assert.equal(rehydrated.parentSessionId, context.callerSessionId);
902
+ assert.equal(rehydrated.ownerSessionId, context.ownerSessionId);
903
+ assert.equal(rehydrated.visibility, 'agent-only');
904
+ assert.equal(rehydrated.provider, 'test-provider');
905
+ assert.equal(rehydrated.model, 'test-model');
906
+ assert.equal(rehydrated.status, 'idle');
907
+ assert.equal(rehydratedNested.parentSessionId, sessionId);
908
+ assert.equal(rehydratedNested.ownerSessionId, context.ownerSessionId);
909
+ assert.equal(rehydratedNested.visibility, 'agent-only');
910
+ assert.equal(service.rootOwnerSessionId(nestedSessionId), context.ownerSessionId);
911
+
912
+ const explicitSent = await agent.execute({
913
+ type: 'send',
914
+ sessionId,
915
+ message: 'explicit session follow up',
916
+ }, context);
917
+ assert.match(explicitSent, new RegExp(sessionId));
918
+ const explicitFollow = await waitForAgentTask(agent, explicitSent, context);
919
+ assert.match(explicitFollow, /handoff:explicit session follow up/);
920
+
921
+ cleanupBackgroundTasks({ surface: 'agent', force: true });
922
+ const publicStatus = await agent.execute({ type: 'status', sessionId }, context);
923
+ const publicRead = await agent.execute({ type: 'read', sessionId }, context);
924
+ assert.match(publicStatus, new RegExp(sessionId));
925
+ assert.match(publicStatus, /"status": "idle"/);
926
+ assert.match(publicRead, /handoff:explicit session follow up/);
927
+
928
+ const cancelled = await agent.execute({ type: 'cancel', sessionId }, context);
929
+ assert.match(cancelled, /agent close: ok/);
930
+ assert.match(cancelled, new RegExp(sessionId));
931
+ const closedIds = events
932
+ .filter(([kind]) => kind === 'close')
933
+ .map(([, id]) => id);
934
+ assert.equal(closedIds.includes(sessionId), true);
935
+ assert.equal(closedIds.includes(nestedSessionId), true);
936
+ assert.equal(service.agentDescriptor(sessionId).status, 'closed');
937
+ assert.equal(service.agentDescriptor(nestedSessionId).status, 'closed');
938
+ assert.equal(records.get(sessionId)?.closed, true);
939
+ assert.equal(records.get(nestedSessionId)?.closed, true);
940
+
941
+ await service.stop('second simulated daemon replacement');
942
+ cleanupBackgroundTasks({ surface: 'agent', force: true });
943
+ service = createService();
944
+ agent = createAgent(service);
945
+ const statusAfterClose = await agent.execute({ type: 'status', sessionId }, context);
946
+ const sendAfterClose = await agent.execute({
947
+ type: 'send',
948
+ sessionId,
949
+ message: 'must not resurrect',
950
+ }, context);
951
+ assert.match(statusAfterClose, /"status": "closed"/);
952
+ assert.match(sendAfterClose, /Error:.*(?:closed|not found)/i);
953
+ assert.equal(records.size, 2);
954
+ assert.equal(records.get(sessionId)?.closed, true);
955
+ assert.equal(records.get(nestedSessionId)?.closed, true);
956
+
957
+ const reservedIds = events
958
+ .filter(([kind]) => kind === 'reserve')
959
+ .map(([, id]) => id);
960
+ assert.deepEqual(new Set(reservedIds), new Set([sessionId, nestedSessionId]));
961
+ assert.equal(reservedIds.length, 2);
962
+ assert.equal(records.size, 2);
963
+ assert.equal(listCalls.some((options) => options.includeAgentOnly === true), true);
964
+ assert.equal(listCalls.every((options) => options.summaryOnly === true), true);
965
+ assert.equal(listCalls.some((options) => options.refreshFromStorage === true), false);
966
+ assert.equal(metadataReads.includes(sessionId), true);
967
+ assert.equal(metadataReads.includes(nestedSessionId), true);
968
+ const turnIds = events.filter(([kind]) => kind === 'turn').map(([, id]) => id);
969
+ assert.deepEqual(new Set(turnIds), new Set([sessionId, nestedSessionId]));
970
+ assert.ok(notifications.length >= 4);
971
+ assert.equal(
972
+ notifications.some((row) =>
973
+ row.ownerSessionId === sessionId && /handoff:nested brief/.test(row.text)),
974
+ true,
975
+ );
976
+ assert.equal(
977
+ notifications.some((row) =>
978
+ row.ownerSessionId === 'sess_public_root' && /handoff:nested brief/.test(row.text)),
979
+ false,
980
+ );
981
+ assert.equal(notifications.some((row) => /handoff:task brief/.test(row.text)), true);
982
+ assert.equal(notifications.some((row) => /handoff:same child follow up/.test(row.text)), true);
983
+ assert.equal(notifications.some((row) => /agent result/.test(row.text)), false);
984
+ });