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
@@ -59,7 +59,11 @@ import { createChannelSessionRouter } from './channel-session-router.mjs';
59
59
  import { createSessionTransport } from './session-transport.mjs';
60
60
  import { createSessionService } from './session-service.mjs';
61
61
  import { createSessionProtocolClient } from './session-protocol.mjs';
62
- import { createSessionRuntimeHost } from './session-runtime-host.mjs';
62
+ import {
63
+ listStoredActiveGoalSessionIds,
64
+ readStoredGoalSnapshot,
65
+ } from '../session-runtime/goal-runtime.mjs';
66
+ import { createDaemonSessionRuntimeHost } from './session-runtime-host-factory.mjs';
63
67
  import { getStandaloneMemoryRuntime } from './memory-runtime-proxy.mjs';
64
68
  import {
65
69
  compareRuntimeVersions,
@@ -244,10 +248,10 @@ const eventLoopDelay = monitorEventLoopDelay({ resolution: 20 });
244
248
  eventLoopDelay.enable();
245
249
  let eventLoopLagTimer = null;
246
250
 
247
- function startMemoryRuntimeEarly() {
248
- // Start the isolated memory process immediately after singleton ownership is
249
- // established. Do not await it: daemon front-door readiness remains
250
- // independent while PG/embedding cold-start overlaps all other boot work.
251
+ function registerMemoryRuntimeLazy() {
252
+ // Register the shared proxy immediately so daemon shutdown owns its
253
+ // lifecycle, but keep the isolated process off the startup path. A real
254
+ // memory call or the post-connect idle warmup starts the exact singleton.
251
255
  if (process.env.MIXDOG_DAEMON_SKIP_MEMORY === '1') {
252
256
  log('memory runtime skipped (MIXDOG_DAEMON_SKIP_MEMORY=1)');
253
257
  return;
@@ -259,9 +263,7 @@ function startMemoryRuntimeEarly() {
259
263
  dataDir: DATA_DIR,
260
264
  cwd: CWD,
261
265
  });
262
- void memoryRuntime.init()
263
- .then(() => log('memory runtime ready in isolated process'))
264
- .catch((e) => log(`memory.start failed (non-fatal): ${e?.message || e}`));
266
+ log('memory runtime registered for lazy start');
265
267
  } catch (e) { log(`memory.start setup failed (non-fatal): ${e?.message || e}`); }
266
268
  }
267
269
 
@@ -414,11 +416,10 @@ async function main() {
414
416
  process.exit(0);
415
417
  }
416
418
  process.on('exit', () => { try { releaseSingletonOwner(OWNER_PATH, process.pid); } catch {} });
417
- startMemoryRuntimeEarly();
419
+ registerMemoryRuntimeLazy();
418
420
  agentDispatchBroker = createAgentDispatchBroker({
419
- // Memory-cycle agents execute inside the recyclable session runtime
420
- // worker: this permanent daemon never loads the orchestrator graph for
421
- // them, so its commit stays flat across cycles.
421
+ // Memory-cycle agents use the same lazy provider/orchestrator graph as
422
+ // session actors; it stays unloaded until a real cycle requests it.
422
423
  dispatchAgent: (payload, options) => {
423
424
  if (!sessionRuntimeHost) throw new Error('session runtime host is not ready');
424
425
  return sessionRuntimeHost.agentDispatch(payload, options);
@@ -580,10 +581,64 @@ async function main() {
580
581
  };
581
582
  let sessionRuntimePrewarmStarted = false;
582
583
  let sessionRuntimePrewarmPromise = null;
583
- sessionRuntimeHost = createSessionRuntimeHost({
584
+ let canonicalAgentToolPromise = null;
585
+ function canonicalAgentTool() {
586
+ if (!sessionService) {
587
+ return Promise.reject(new Error('canonical session service is not ready'));
588
+ }
589
+ canonicalAgentToolPromise ??= Promise.all([
590
+ import('./agent-tool.mjs'),
591
+ import('../runtime/agent/orchestrator/config.mjs'),
592
+ import('../runtime/agent/orchestrator/providers/registry.mjs'),
593
+ ]).then(([agentModule, cfgMod, reg]) => agentModule.createStandaloneAgent({
594
+ cfgMod,
595
+ reg,
596
+ mgr: sessionService.agentManager,
597
+ dataDir: cfgMod.getPluginData(),
598
+ cwd: CWD,
599
+ awaitKeychainPrewarm: async () => {},
600
+ isKeychainPrewarmReady: () => true,
601
+ sessionSurface: sessionService.agentSurface,
602
+ notifySessionCompletion(ownerSessionId, text, meta = {}) {
603
+ return sessionRuntimeHost?.notifySessionCompletion?.(ownerSessionId, text, meta) === true;
604
+ },
605
+ })).catch((error) => {
606
+ canonicalAgentToolPromise = null;
607
+ throw error;
608
+ });
609
+ return canonicalAgentToolPromise;
610
+ }
611
+ async function executeCanonicalAgentControl(args = {}, context = {}) {
612
+ const tool = await canonicalAgentTool();
613
+ const parentSessionId = String(context?.callerSessionId || '').trim();
614
+ await sessionService.rehydrateAgentSessions();
615
+ const scopedContext = {
616
+ ...context,
617
+ callerSessionId: parentSessionId || null,
618
+ ownerSessionId: sessionService.rootOwnerSessionId(parentSessionId),
619
+ };
620
+ if (String(args?.type || '') === '__close_all') {
621
+ await tool.closeAll?.(
622
+ String(args?.reason || 'agent owner closed'),
623
+ { callerSessionId: parentSessionId || null },
624
+ );
625
+ await sessionService.cancelAgentDescendants(
626
+ parentSessionId,
627
+ String(args?.reason || 'agent owner closed'),
628
+ );
629
+ return 'agent close all: ok';
630
+ }
631
+ return await tool.execute(args, scopedContext);
632
+ }
633
+ sessionRuntimeHost = createDaemonSessionRuntimeHost({
584
634
  cwd: CWD,
585
635
  log,
636
+ executeAgentControl: executeCanonicalAgentControl,
586
637
  });
638
+ // Codex-style runtime: daemon routing and independent async session actors
639
+ // share one V8 isolate and module graph. CPU-heavy work stays in bounded
640
+ // native helpers/worker pools instead of duplicating the whole runtime.
641
+ log(`session runtime mode=${sessionRuntimeHost.status.mode}`);
587
642
  function prewarmSessionRuntime() {
588
643
  if (sessionRuntimePrewarmPromise) return sessionRuntimePrewarmPromise;
589
644
  sessionRuntimePrewarmStarted = true;
@@ -592,7 +647,7 @@ async function main() {
592
647
  .catch((error) => log(`image pipeline prewarm failed (non-fatal): ${error?.message || error}`));
593
648
  sessionRuntimePrewarmPromise = sessionRuntimeHost.prewarm()
594
649
  .then(() => {
595
- log('session runtime worker/agent-loop/keychain/memory prewarm ready');
650
+ log('session runtime/agent-loop/keychain prewarm ready');
596
651
  return true;
597
652
  })
598
653
  .catch((error) => {
@@ -601,7 +656,7 @@ async function main() {
601
656
  log(`session runtime/keychain prewarm failed (non-fatal): ${error?.message || error}`);
602
657
  return false;
603
658
  });
604
- return sessionRuntimePrewarmPromise;
659
+ return sessionRuntimePrewarmPromise;
605
660
  }
606
661
  sessionService = createSessionService({
607
662
  createSessionRuntime: (options) => sessionRuntimeHost.create(options),
@@ -617,7 +672,31 @@ async function main() {
617
672
  if (typeof store.readStoredSessionTranscript !== 'function') return null;
618
673
  return await store.readStoredSessionTranscript(sessionId, options) ?? null;
619
674
  },
675
+ readStoredGoal: async (sessionId) => readStoredGoalSnapshot({
676
+ dataDir: DATA_DIR,
677
+ sessionId,
678
+ }),
679
+ listStoredActiveGoalSessionIds: async () => listStoredActiveGoalSessionIds({
680
+ dataDir: DATA_DIR,
681
+ }),
620
682
  listSessions: async (options = {}) => {
683
+ if (options.includeAgentOnly === true) {
684
+ // Agent discovery is metadata-only. Exact session reads/subscriptions
685
+ // keep using the canonical session id after the user opens a worker.
686
+ const store = await import('../runtime/agent/orchestrator/session/store.mjs');
687
+ const summaries = store.listStoredSessionSummaries({
688
+ refreshFromStorage: options.refreshFromStorage === true,
689
+ });
690
+ const viewStore = await desktopRuntime.loadSessionStore();
691
+ const links = viewStore.listStoredAgentWorkerLinks?.() || [];
692
+ if (!links.length) return summaries;
693
+ const linksById = new Map(links.map((link) => [link.sessionId, link]));
694
+ return summaries.map((row) => {
695
+ if (row?.parentSessionId) return row;
696
+ const link = linksById.get(row?.id);
697
+ return link ? { ...row, ...link, id: row.id } : row;
698
+ });
699
+ }
621
700
  const store = await desktopRuntime.loadSessionStore();
622
701
  return store.listStoredSessionSummaries({
623
702
  refreshFromStorage: options.refreshFromStorage === true,
@@ -715,11 +794,30 @@ async function main() {
715
794
  safeIpcSend(process, { type: 'ready', port, token });
716
795
  }
717
796
  log(`ready port=${port} pid=${process.pid} in ${(performance.now() - startedAt).toFixed(0)}ms`);
797
+ void prewarmSessionRuntime()
798
+ .then(() => sessionService.recoverActiveGoals())
799
+ .then(({ found, resumed, skipped, failed }) => {
800
+ log(`active Goal recovery found=${found} resumed=${resumed} skipped=${skipped} failed=${failed}`);
801
+ })
802
+ .catch((error) => {
803
+ log(`active Goal recovery failed: ${error?.message || error}`);
804
+ });
718
805
  eventLoopLagTimer = setInterval(() => {
719
806
  const status = eventLoopStatus();
720
807
  if (status.eventLoopP99Ms >= 250) {
721
808
  log(`event-loop lag p95=${status.eventLoopP95Ms}ms p99=${status.eventLoopP99Ms}ms max=${status.eventLoopMaxMs}ms`);
722
809
  }
810
+ // Legacy external runtime hosts may still report shard-local lag. The
811
+ // production in-process host is already covered by daemon loop telemetry.
812
+ const shards = sessionRuntimeHost?.status?.shards || [];
813
+ const lagging = shards.filter((shard) => Number(shard?.lag?.p99Ms) >= 250 || shard?.degraded);
814
+ if (lagging.length > 0) {
815
+ log(`session runtime shard lag ${lagging.map((shard) => (
816
+ `#${shard.index}${shard.degraded ? '(quarantined)' : ''}`
817
+ + ` p95=${shard.lag?.p95Ms ?? -1}ms p99=${shard.lag?.p99Ms ?? -1}ms`
818
+ + ` max=${shard.lag?.maxMs ?? -1}ms runtimes=${shard.runtimes}`
819
+ )).join(' ')}`);
820
+ }
723
821
  eventLoopDelay.reset();
724
822
  }, 30_000);
725
823
  eventLoopLagTimer.unref?.();
@@ -81,6 +81,7 @@ function pluginHookConfigEntries(dataDir) {
81
81
  }
82
82
  const entries = [];
83
83
  for (const plugin of plugins || []) {
84
+ if (plugin?.enabled === false) continue;
84
85
  const root = String(plugin?.root || '').trim();
85
86
  if (!root || !existsSync(root)) continue;
86
87
  const id = cleanHookId(plugin.id || plugin.name || plugin.title || root.split(/[\\/]/).pop());
@@ -242,6 +242,7 @@ function enrichEntry(entry) {
242
242
  const name = clean(manifest.name) || clean(manifest.id) || clean(entry.name) || displayNameFromUrl(entry.source);
243
243
  return {
244
244
  ...entry,
245
+ enabled: entry.enabled !== false,
245
246
  name,
246
247
  title: clean(manifest.title) || clean(manifest.displayName) || clean(entry.title) || name,
247
248
  version: clean(manifest.version) || clean(entry.version) || null,
@@ -280,6 +281,7 @@ export function addPlugin(sourceInput, { dataDir = resolvePluginData(), name } =
280
281
  title: clean(manifest.title) || clean(manifest.displayName) || clean(name) || displayNameFromUrl(normalized.displaySource),
281
282
  version: clean(manifest.version) || null,
282
283
  description: clean(manifest.description),
284
+ enabled: true,
283
285
  installedAt: nowIso(),
284
286
  updatedAt: nowIso(),
285
287
  };
@@ -338,6 +340,21 @@ export function updatePlugin(idOrName, { dataDir = resolvePluginData() } = {}) {
338
340
  });
339
341
  }
340
342
 
343
+ export function setPluginEnabled(idOrName, enabled, { dataDir = resolvePluginData() } = {}) {
344
+ const key = clean(idOrName);
345
+ if (!key) throw new Error('plugin id/name is required');
346
+ return mutateRegistry(dataDir, (registry) => {
347
+ const index = pluginIndex(registry, key);
348
+ if (index < 0) throw new Error(`plugin not registered: ${key}`);
349
+ const next = {
350
+ ...registry.plugins[index],
351
+ enabled: enabled !== false,
352
+ };
353
+ registry.plugins[index] = next;
354
+ return enrichEntry(next);
355
+ });
356
+ }
357
+
341
358
  export function removePlugin(idOrName, { dataDir = resolvePluginData() } = {}) {
342
359
  const key = clean(idOrName);
343
360
  if (!key) throw new Error('plugin id/name is required');
@@ -18,7 +18,9 @@ import {
18
18
  _publishManagedPluginRoot,
19
19
  addPlugin,
20
20
  listRegisteredPlugins,
21
+ setPluginEnabled,
21
22
  } from './plugin-admin.mjs';
23
+ import { hookConfigEntries } from './hook-bus/config.mjs';
22
24
 
23
25
  test('plugin registry persists atomically with owner-only permissions', () => {
24
26
  const root = mkdtempSync(join(tmpdir(), 'mixdog-plugin-registry-'));
@@ -33,6 +35,11 @@ test('plugin registry persists atomically with owner-only permissions', () => {
33
35
  const registry = JSON.parse(readFileSync(registryPath, 'utf8'));
34
36
  assert.equal(registry.plugins.length, 1);
35
37
  assert.equal(listRegisteredPlugins({ dataDir })[0].name, 'local-test');
38
+ assert.equal(listRegisteredPlugins({ dataDir })[0].enabled, true);
39
+ assert.equal(hookConfigEntries(dataDir, root).some((entry) => entry.sourceType === 'plugin'), true);
40
+ setPluginEnabled('local-test', false, { dataDir });
41
+ assert.equal(listRegisteredPlugins({ dataDir })[0].enabled, false);
42
+ assert.equal(hookConfigEntries(dataDir, root).some((entry) => entry.sourceType === 'plugin'), false);
36
43
  if (process.platform !== 'win32') {
37
44
  assert.equal(statSync(registryPath).mode & 0o777, 0o600);
38
45
  }
@@ -44,6 +44,7 @@ import {
44
44
 
45
45
  const API_PROVIDERS = Object.freeze([
46
46
  Object.freeze({ id: 'opencode-go', name: 'OpenCode Go API', env: 'OPENCODE_API_KEY', url: 'https://opencode.ai' }),
47
+ Object.freeze({ id: 'openrouter', name: 'OpenRouter', env: 'OPENROUTER_API_KEY', url: 'https://openrouter.ai/keys' }),
47
48
  Object.freeze({ id: 'openai', name: 'OpenAI API', env: 'OPENAI_API_KEY', url: 'https://platform.openai.com/api-keys' }),
48
49
  Object.freeze({ id: 'anthropic', name: 'Anthropic API', env: 'ANTHROPIC_API_KEY', url: 'https://console.anthropic.com/settings/keys' }),
49
50
  Object.freeze({ id: 'gemini', name: 'Gemini API', env: 'GEMINI_API_KEY', url: 'https://aistudio.google.com/apikey' }),
@@ -15,6 +15,7 @@ export const SESSION_READ_ACTIONS = Object.freeze([
15
15
  'getCompactionSettings',
16
16
  'getDisabledSkills',
17
17
  'getMediaJob',
18
+ 'getMcpServerConfig',
18
19
  'getOnboardingStatus',
19
20
  'getOAuthProviderLoginStatus',
20
21
  'getOutputStyle',
@@ -79,6 +80,7 @@ export const SESSION_CONFIGURE_ACTIONS = Object.freeze([
79
80
  'deleteWorkflow',
80
81
  'enablePluginMcp',
81
82
  'forgetProviderAuth',
83
+ 'goalControl',
82
84
  'inheritFrom',
83
85
  'loginOAuthProvider',
84
86
  'loginOpenCodeGoUsage',
@@ -107,10 +109,12 @@ export const SESSION_CONFIGURE_ACTIONS = Object.freeze([
107
109
  'runScheduleNow',
108
110
  'runUpdateNow',
109
111
  'saveAgentDefinition',
112
+ 'saveMcpServer',
110
113
  'saveOpenAIUsageSessionKey',
111
114
  'saveOpenCodeGoUsageAuth',
112
115
  'saveProviderApiKey',
113
116
  'saveSchedule',
117
+ 'saveSkill',
114
118
  'saveWebhook',
115
119
  'saveWorkflowPack',
116
120
  'selectTools',
@@ -129,6 +133,7 @@ export const SESSION_CONFIGURE_ACTIONS = Object.freeze([
129
133
  'setMemoryToolsEnabled',
130
134
  'setModel',
131
135
  'setOutputStyle',
136
+ 'setPluginEnabled',
132
137
  'setProfile',
133
138
  'setProgressHint',
134
139
  'setRecapEnabled',
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ // Runtime-shard client for distributed Agent control.
4
+ //
5
+ // A Lead runtime and every Subagent it creates used to share one worker
6
+ // process even after top-level session sharding. Agent control now crosses the
7
+ // existing shard IPC link: the daemon chooses a target shard, while the target
8
+ // keeps the ordinary persistent Agent tool/tag/session implementation.
9
+ import { safeIpcSend } from '../runtime/shared/safe-ipc-send.mjs';
10
+ import { isSessionRuntimeWorkerProcess } from '../runtime/shared/child-spawn-remote.mjs';
11
+
12
+ const pending = new Map();
13
+ let sequence = 0;
14
+ let listening = false;
15
+
16
+ function remoteError(body) {
17
+ const error = new Error(String(body?.message || 'remote agent control failed'));
18
+ if (body?.name) error.name = String(body.name);
19
+ if (body?.stack) error.stack = String(body.stack);
20
+ if (body?.code) error.code = String(body.code);
21
+ return error;
22
+ }
23
+
24
+ function ensureListener() {
25
+ if (listening) return;
26
+ listening = true;
27
+ process.on('message', (message) => {
28
+ if (!message || message.type !== 'agent-control-result') return;
29
+ const controlId = String(message.controlId || '');
30
+ const request = pending.get(controlId);
31
+ if (!request) return;
32
+ pending.delete(controlId);
33
+ request.cleanup();
34
+ if (message.ok === false) request.reject(remoteError(message.error));
35
+ else request.resolve(message.value);
36
+ });
37
+ process.on('disconnect', () => {
38
+ for (const [controlId, request] of pending) {
39
+ pending.delete(controlId);
40
+ request.cleanup();
41
+ request.reject(new Error('runtime shard disconnected during agent control'));
42
+ }
43
+ });
44
+ }
45
+
46
+ export function remoteAgentControlEnabled(env = process.env) {
47
+ return isSessionRuntimeWorkerProcess(env)
48
+ && typeof process.send === 'function'
49
+ && process.connected === true;
50
+ }
51
+
52
+ export function executeRemoteAgentControl(args = {}, context = {}) {
53
+ if (!remoteAgentControlEnabled()) {
54
+ return Promise.reject(new Error('remote agent control is unavailable'));
55
+ }
56
+ ensureListener();
57
+ const controlId = `agent-control-${process.pid}-${++sequence}`;
58
+ const signal = context?.signal || null;
59
+ if (signal?.aborted) {
60
+ return Promise.reject(signal.reason instanceof Error
61
+ ? signal.reason
62
+ : new Error(String(signal.reason || 'agent control canceled')));
63
+ }
64
+ return new Promise((resolve, reject) => {
65
+ let timer = null;
66
+ let onAbort = null;
67
+ const cleanup = () => {
68
+ if (timer) clearTimeout(timer);
69
+ timer = null;
70
+ if (onAbort && signal) {
71
+ try { signal.removeEventListener('abort', onAbort); } catch {}
72
+ }
73
+ onAbort = null;
74
+ };
75
+ pending.set(controlId, { resolve, reject, cleanup });
76
+ if (signal) {
77
+ onAbort = () => {
78
+ safeIpcSend(process, {
79
+ type: 'agent-control-cancel',
80
+ controlId,
81
+ reason: String(signal.reason?.message || signal.reason || 'agent control canceled'),
82
+ }, { onError: () => {} });
83
+ const request = pending.get(controlId);
84
+ if (!request) return;
85
+ pending.delete(controlId);
86
+ cleanup();
87
+ reject(signal.reason instanceof Error
88
+ ? signal.reason
89
+ : new Error(String(signal.reason || 'agent control canceled')));
90
+ };
91
+ signal.addEventListener('abort', onAbort, { once: true });
92
+ }
93
+ timer = setTimeout(() => {
94
+ const request = pending.get(controlId);
95
+ if (!request) return;
96
+ pending.delete(controlId);
97
+ cleanup();
98
+ reject(new Error('remote agent control timed out'));
99
+ }, 180_000);
100
+ timer.unref?.();
101
+ const sent = safeIpcSend(process, {
102
+ type: 'agent-control',
103
+ controlId,
104
+ args: args && typeof args === 'object' ? args : {},
105
+ context: {
106
+ callerCwd: typeof context?.callerCwd === 'string' ? context.callerCwd : null,
107
+ invocationSource: typeof context?.invocationSource === 'string'
108
+ ? context.invocationSource
109
+ : null,
110
+ callerSessionId: typeof context?.callerSessionId === 'string'
111
+ ? context.callerSessionId
112
+ : null,
113
+ clientHostPid: Number(context?.clientHostPid) || null,
114
+ },
115
+ }, {
116
+ onError: (error) => {
117
+ const request = pending.get(controlId);
118
+ if (!request) return;
119
+ pending.delete(controlId);
120
+ cleanup();
121
+ reject(error);
122
+ },
123
+ });
124
+ if (!sent) {
125
+ const request = pending.get(controlId);
126
+ if (request) {
127
+ pending.delete(controlId);
128
+ cleanup();
129
+ reject(new Error('runtime shard IPC is unavailable'));
130
+ }
131
+ }
132
+ });
133
+ }
@@ -0,0 +1,71 @@
1
+ import assert from 'node:assert/strict';
2
+ import { fork } from 'node:child_process';
3
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import test from 'node:test';
7
+
8
+ const CLIENT_URL = new URL('./session-runtime-agent-control-client.mjs', import.meta.url).href;
9
+
10
+ test('runtime shard Agent client sends serializable context and receives the routed result', async () => {
11
+ const dir = await mkdtemp(join(tmpdir(), 'mixdog-agent-control-client-'));
12
+ const entry = join(dir, 'client.mjs');
13
+ await writeFile(entry, `
14
+ process.env.MIXDOG_SESSION_RUNTIME_WORKER_PID = String(process.pid);
15
+ const client = await import(${JSON.stringify(CLIENT_URL)});
16
+ const value = await client.executeRemoteAgentControl(
17
+ { type: 'spawn', tag: 'routed', agent: 'worker', prompt: 'run' },
18
+ {
19
+ callerCwd: 'C:/Project/test',
20
+ invocationSource: 'model-tool',
21
+ callerSessionId: 'sess_lead',
22
+ clientHostPid: 4242,
23
+ },
24
+ );
25
+ process.send({ type: 'report', value });
26
+ `, 'utf8');
27
+
28
+ try {
29
+ const observed = await new Promise((resolve, reject) => {
30
+ const child = fork(entry, [], {
31
+ stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
32
+ env: { ...process.env },
33
+ });
34
+ const timer = setTimeout(() => {
35
+ try { child.kill(); } catch {}
36
+ reject(new Error('agent control client test timed out'));
37
+ }, 10_000);
38
+ child.on('message', (message) => {
39
+ if (message?.type === 'agent-control') {
40
+ assert.deepEqual(message.args, {
41
+ type: 'spawn',
42
+ tag: 'routed',
43
+ agent: 'worker',
44
+ prompt: 'run',
45
+ });
46
+ assert.deepEqual(message.context, {
47
+ callerCwd: 'C:/Project/test',
48
+ invocationSource: 'model-tool',
49
+ callerSessionId: 'sess_lead',
50
+ clientHostPid: 4242,
51
+ });
52
+ child.send({
53
+ type: 'agent-control-result',
54
+ controlId: message.controlId,
55
+ ok: true,
56
+ value: 'agent task: task_routed',
57
+ });
58
+ return;
59
+ }
60
+ if (message?.type !== 'report') return;
61
+ clearTimeout(timer);
62
+ try { child.kill(); } catch {}
63
+ resolve(message.value);
64
+ });
65
+ child.on('error', reject);
66
+ });
67
+ assert.equal(observed, 'agent task: task_routed');
68
+ } finally {
69
+ await rm(dir, { recursive: true, force: true });
70
+ }
71
+ });
@@ -0,0 +1,139 @@
1
+ // Cold-start cancellation contract, exercised against the REAL runtime worker
2
+ // child (no stub): a cancel that arrives before or during the dispatch's
3
+ // module/provider cold start must never be answered "not running" and then let
4
+ // the dispatch run anyway.
5
+ import assert from 'node:assert/strict';
6
+ import { fork } from 'node:child_process';
7
+ import { mkdtemp, rm } from 'node:fs/promises';
8
+ import { tmpdir } from 'node:os';
9
+ import { dirname, join } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import test from 'node:test';
12
+
13
+ const WORKER_ENTRY = fileURLToPath(new URL('./session-runtime-worker.mjs', import.meta.url));
14
+ const REPO_ROOT = join(dirname(WORKER_ENTRY), '..', '..');
15
+
16
+ function startWorker(runtimeRoot) {
17
+ const child = fork(WORKER_ENTRY, [], {
18
+ cwd: REPO_ROOT,
19
+ execArgv: [],
20
+ stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
21
+ env: {
22
+ ...process.env,
23
+ MIXDOG_RUNTIME_ROOT: runtimeRoot,
24
+ MIXDOG_DATA_DIR: runtimeRoot,
25
+ MIXDOG_SESSION_RUNTIME_SHARD: '0',
26
+ MIXDOG_SESSION_RUNTIME_SHARD_COUNT: '2',
27
+ },
28
+ });
29
+ const responses = new Map();
30
+ const waiters = new Map();
31
+ child.on('message', (message) => {
32
+ if (message?.type !== 'response') return;
33
+ const id = String(message.requestId || '');
34
+ const waiter = waiters.get(id);
35
+ if (waiter) {
36
+ waiters.delete(id);
37
+ waiter(message);
38
+ return;
39
+ }
40
+ responses.set(id, message);
41
+ });
42
+ const send = (payload) => child.send(payload);
43
+ const wait = (requestId, timeoutMs = 30_000) => new Promise((resolve, reject) => {
44
+ const settled = responses.get(requestId);
45
+ if (settled) {
46
+ responses.delete(requestId);
47
+ resolve(settled);
48
+ return;
49
+ }
50
+ const timer = setTimeout(
51
+ () => reject(new Error(`timed out waiting for ${requestId}`)),
52
+ timeoutMs,
53
+ );
54
+ waiters.set(requestId, (message) => { clearTimeout(timer); resolve(message); });
55
+ });
56
+ return { child, send, wait };
57
+ }
58
+
59
+ async function stopWorker(worker) {
60
+ try {
61
+ worker.send({ type: 'shutdown', requestId: 'shutdown-1', reason: 'test complete' });
62
+ await worker.wait('shutdown-1', 8_000).catch(() => {});
63
+ await new Promise((resolve) => setTimeout(resolve, 250));
64
+ } finally {
65
+ try { worker.child.kill(); } catch {}
66
+ }
67
+ }
68
+
69
+ test('a cancel that arrives before the dispatch is retained and aborts it', async () => {
70
+ const runtimeRoot = await mkdtemp(join(tmpdir(), 'mixdog-dispatch-cancel-'));
71
+ const worker = startWorker(runtimeRoot);
72
+ try {
73
+ worker.send({
74
+ type: 'agent-dispatch-cancel',
75
+ requestId: 'cancel-early',
76
+ dispatchId: 'dispatch-cold',
77
+ reason: 'user aborted during cold start',
78
+ });
79
+ const cancel = await worker.wait('cancel-early', 15_000);
80
+ assert.equal(cancel.ok, true);
81
+ // Never "cancelled: false": an unknown id means not-registered-YET.
82
+ assert.equal(cancel.value.cancelled, true);
83
+ assert.equal(cancel.value.retained, true);
84
+
85
+ const startedAt = Date.now();
86
+ worker.send({
87
+ type: 'agent-dispatch',
88
+ requestId: 'dispatch-cold-1',
89
+ dispatchId: 'dispatch-cold',
90
+ agent: 'memory',
91
+ params: { prompt: 'must never run' },
92
+ });
93
+ const dispatch = await worker.wait('dispatch-cold-1', 20_000);
94
+ assert.equal(dispatch.ok, false);
95
+ assert.match(String(dispatch.error?.message || ''), /user aborted during cold start/);
96
+ // The abort is adopted at registration — before the orchestrator/provider
97
+ // cold start, which alone takes seconds in this worker.
98
+ assert.ok(
99
+ Date.now() - startedAt < 3_000,
100
+ 'aborted dispatch never entered module/provider setup',
101
+ );
102
+ } finally {
103
+ await stopWorker(worker);
104
+ await rm(runtimeRoot, { recursive: true, force: true });
105
+ }
106
+ });
107
+
108
+ test('a cancel racing a cold-start dispatch is never answered "not running"', async () => {
109
+ const runtimeRoot = await mkdtemp(join(tmpdir(), 'mixdog-dispatch-cancel-warm-'));
110
+ const worker = startWorker(runtimeRoot);
111
+ try {
112
+ worker.send({
113
+ type: 'agent-dispatch',
114
+ requestId: 'dispatch-warm-1',
115
+ dispatchId: 'dispatch-warm',
116
+ agent: 'memory',
117
+ params: { prompt: 'cancelled while importing' },
118
+ });
119
+ // The cold start compiles the orchestrator graph and blocks this loop, so
120
+ // the cancel frame can only be handled once that window closes. Whichever
121
+ // side wins, the abort must be honoured or retained — never dropped with
122
+ // `cancelled: false`, which is what let a canceled dispatch keep running.
123
+ worker.send({
124
+ type: 'agent-dispatch-cancel',
125
+ requestId: 'cancel-warm',
126
+ dispatchId: 'dispatch-warm',
127
+ reason: 'aborted mid cold start',
128
+ });
129
+ const cancel = await worker.wait('cancel-warm', 60_000);
130
+ assert.equal(cancel.ok, true);
131
+ assert.equal(cancel.value.cancelled, true);
132
+
133
+ const dispatch = await worker.wait('dispatch-warm-1', 60_000);
134
+ assert.equal(dispatch.ok, false);
135
+ } finally {
136
+ await stopWorker(worker);
137
+ await rm(runtimeRoot, { recursive: true, force: true });
138
+ }
139
+ });
@@ -0,0 +1,17 @@
1
+ import { createInlineSessionRuntimeHost } from './session-runtime-inline-host.mjs';
2
+ import { createSessionRuntimeHost } from './session-runtime-host.mjs';
3
+
4
+ export function resolveSessionRuntimeMode(value = process.env.MIXDOG_SESSION_RUNTIME_MODE) {
5
+ const mode = String(value || 'inline').trim().toLowerCase();
6
+ if (mode === 'inline') return 'inline';
7
+ if (mode === 'process') return 'process';
8
+ throw new Error(`unsupported MIXDOG_SESSION_RUNTIME_MODE=${JSON.stringify(value)}`);
9
+ }
10
+
11
+ export function createDaemonSessionRuntimeHost(options = {}, {
12
+ createInline = createInlineSessionRuntimeHost,
13
+ createProcess = createSessionRuntimeHost,
14
+ mode = resolveSessionRuntimeMode(),
15
+ } = {}) {
16
+ return mode === 'process' ? createProcess(options) : createInline(options);
17
+ }