mixdog 0.9.95 → 0.9.97

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 (282) hide show
  1. package/LICENSES/Apache-2.0.txt +201 -0
  2. package/LICENSES/MIT.txt +56 -0
  3. package/LICENSES/codex-NOTICE.txt +6 -0
  4. package/NOTICE.md +46 -9
  5. package/README.md +0 -17
  6. package/package.json +16 -12
  7. package/scripts/.tmp-coverage-report.mjs +96 -0
  8. package/scripts/build-tui.mjs +17 -1
  9. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  10. package/scripts/fixtures/patch-replay-corpus.json +98 -0
  11. package/src/defaults/mixdog-config.template.json +5 -8
  12. package/src/defaults/skills/setup/SKILL.md +1 -1
  13. package/src/lib/keychain-cjs.cjs +1 -1
  14. package/src/lib/mixdog-debug.cjs +2 -3
  15. package/src/rules/agent/30-explorer.md +7 -4
  16. package/src/rules/lead/01-general.md +1 -2
  17. package/src/rules/lead/lead-tool.md +0 -3
  18. package/src/rules/shared/01-tool.md +21 -29
  19. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
  20. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
  23. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  24. package/src/runtime/agent/orchestrator/config.mjs +246 -67
  25. package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
  26. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
  27. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
  28. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
  29. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
  30. package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
  31. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
  33. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
  36. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
  37. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  39. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
  41. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
  42. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
  43. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +36 -10
  44. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -33
  46. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
  47. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
  48. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
  49. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +21 -29
  50. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
  51. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
  52. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  53. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
  54. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  55. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
  56. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
  57. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  58. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
  59. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
  60. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  61. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
  62. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  63. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  64. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
  65. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -33
  66. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
  67. package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
  70. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +116 -26
  71. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  72. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
  73. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
  74. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
  75. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  76. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
  77. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +12 -26
  78. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
  79. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
  80. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
  81. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -9
  82. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
  83. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
  84. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
  85. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
  87. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
  88. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
  89. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
  90. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +142 -26
  91. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  92. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
  93. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
  94. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  95. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
  96. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
  97. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
  99. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
  100. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
  101. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -11
  102. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
  104. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
  105. package/src/runtime/channels/lib/config.mjs +34 -34
  106. package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
  107. package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
  108. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  109. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  110. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  111. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  112. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  113. package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
  114. package/src/runtime/channels/lib/scheduler.mjs +2 -2
  115. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  116. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  117. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  118. package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
  119. package/src/runtime/channels/lib/worker-main.mjs +38 -38
  120. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  121. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  122. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  123. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/memory/index.mjs +76 -248
  126. package/src/runtime/memory/lib/agent-ipc.mjs +146 -81
  127. package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
  128. package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
  129. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +13 -34
  130. package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
  131. package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
  132. package/src/runtime/memory/lib/http-router.mjs +9 -6
  133. package/src/runtime/memory/lib/ko-morph.mjs +3 -20
  134. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
  135. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  136. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
  137. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
  138. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
  139. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  140. package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
  141. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  142. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  143. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  144. package/src/runtime/memory/lib/memory.mjs +1 -1
  145. package/src/runtime/memory/lib/pg/process.mjs +32 -9
  146. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  147. package/src/runtime/memory/tool-defs.mjs +1 -5
  148. package/src/runtime/search/index.mjs +2 -2
  149. package/src/runtime/search/tool-defs.mjs +2 -2
  150. package/src/runtime/shared/abort-race.mjs +70 -0
  151. package/src/runtime/shared/agent-route-config.mjs +119 -0
  152. package/src/runtime/shared/atomic-file.mjs +80 -4
  153. package/src/runtime/shared/channel-notification-routing.mjs +2 -2
  154. package/src/runtime/shared/child-guardian.mjs +204 -108
  155. package/src/runtime/shared/child-spawn-gate.mjs +139 -72
  156. package/src/runtime/shared/config.mjs +120 -47
  157. package/src/runtime/shared/llm/index.mjs +1 -1
  158. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  159. package/src/runtime/shared/resource-admission.mjs +150 -100
  160. package/src/runtime/shared/stream-progress.mjs +6 -0
  161. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  162. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  163. package/src/runtime/shared/tool-surface.mjs +91 -27
  164. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  165. package/src/runtime/shared/turn-snapshot.mjs +47 -6
  166. package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
  167. package/src/session-runtime/channel-config-api.mjs +5 -5
  168. package/src/session-runtime/config-helpers.mjs +39 -37
  169. package/src/session-runtime/config-lifecycle.mjs +27 -27
  170. package/src/session-runtime/lifecycle-api.mjs +22 -3
  171. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  172. package/src/session-runtime/model-route-api.mjs +7 -5
  173. package/src/session-runtime/prewarm.mjs +1 -1
  174. package/src/session-runtime/provider-auth-api.mjs +0 -7
  175. package/src/session-runtime/provider-models.mjs +1 -1
  176. package/src/session-runtime/remote-control.mjs +6 -7
  177. package/src/session-runtime/runtime-core.mjs +63 -39
  178. package/src/session-runtime/runtime-tunables.mjs +4 -4
  179. package/src/session-runtime/self-update.mjs +1 -1
  180. package/src/session-runtime/session-lifecycle.mjs +45 -17
  181. package/src/session-runtime/session-title.mjs +1 -1
  182. package/src/session-runtime/session-turn-api.mjs +96 -24
  183. package/src/session-runtime/settings-api.mjs +12 -24
  184. package/src/session-runtime/tool-defs.mjs +3 -3
  185. package/src/session-runtime/workflow-agents-api.mjs +55 -81
  186. package/src/session-runtime/workflow.mjs +14 -24
  187. package/src/standalone/agent-dispatch-broker.mjs +198 -0
  188. package/src/standalone/agent-tool/helpers.mjs +2 -2
  189. package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
  190. package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
  191. package/src/standalone/agent-tool/tool-def.mjs +0 -14
  192. package/src/standalone/channel-admin.mjs +53 -64
  193. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
  194. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
  195. package/src/standalone/channel-worker.mjs +124 -647
  196. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
  197. package/src/standalone/explore-tool.mjs +187 -70
  198. package/src/standalone/fair-call-scheduler.mjs +264 -0
  199. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  200. package/src/standalone/session-client.mjs +1134 -0
  201. package/src/standalone/session-protocol.mjs +192 -0
  202. package/src/standalone/session-runtime-pool.mjs +393 -0
  203. package/src/standalone/session-runtime-record.mjs +20 -0
  204. package/src/standalone/session-runtime-worker.mjs +203 -0
  205. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
  206. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
  207. package/src/standalone/session-wire.mjs +55 -0
  208. package/src/tui/App.jsx +34 -21
  209. package/src/tui/app/app-format.mjs +1 -1
  210. package/src/tui/app/app-view.jsx +5 -2
  211. package/src/tui/app/channel-pickers.mjs +41 -41
  212. package/src/tui/app/core-memory-picker.mjs +19 -19
  213. package/src/tui/app/doctor.mjs +5 -11
  214. package/src/tui/app/input-parsers.mjs +2 -2
  215. package/src/tui/app/message-selector.mjs +103 -0
  216. package/src/tui/app/onboarding-steps.mjs +3 -4
  217. package/src/tui/app/project-picker.mjs +80 -56
  218. package/src/tui/app/prompt-submit.mjs +48 -43
  219. package/src/tui/app/resume-picker.mjs +2 -2
  220. package/src/tui/app/route-pickers.mjs +3 -1
  221. package/src/tui/app/settings-picker.mjs +31 -33
  222. package/src/tui/app/transcript-window.mjs +38 -0
  223. package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
  224. package/src/tui/app/use-prompt-handlers.mjs +86 -21
  225. package/src/tui/app/use-prompt-hint.mjs +3 -2
  226. package/src/tui/app/use-prompt-queue-history.mjs +10 -3
  227. package/src/tui/app/use-transcript-scroll.mjs +10 -3
  228. package/src/tui/app/use-transcript-window.mjs +50 -17
  229. package/src/tui/components/PromptInput.jsx +71 -38
  230. package/src/tui/components/Spinner.jsx +1 -1
  231. package/src/tui/components/ToolExecution.jsx +1 -1
  232. package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
  233. package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
  234. package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
  235. package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
  236. package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
  237. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  238. package/src/tui/dist/index.mjs +1031 -1676
  239. package/src/tui/hooks/useSession.mjs +16 -0
  240. package/src/tui/index.jsx +12 -13
  241. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  242. package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
  243. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  244. package/src/tui/{engine → session}/context-state.mjs +3 -3
  245. package/src/tui/{engine → session}/labels.mjs +2 -2
  246. package/src/tui/{engine → session}/live-share.mjs +38 -4
  247. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  248. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  249. package/src/tui/session/oauth-flows.mjs +140 -0
  250. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  251. package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
  252. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  253. package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
  254. package/src/tui/{engine → session}/session-api.mjs +110 -93
  255. package/src/tui/{engine → session}/session-flow.mjs +8 -2
  256. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  257. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  258. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  259. package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
  260. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  261. package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
  262. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  263. package/src/tui/{engine → session}/turn.mjs +38 -176
  264. package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
  265. package/src/tui/session.mjs +22 -0
  266. package/src/tui/spinner-meta.mjs +4 -4
  267. package/src/tui/spinner-verbs.mjs +51 -3
  268. package/src/ui/statusline-segments.mjs +1 -1
  269. package/src/ui/statusline.mjs +1 -1
  270. package/vendor/ink/build/components/App.js +10 -1
  271. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  272. package/src/standalone/engine-daemon-client.mjs +0 -894
  273. package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
  274. package/src/standalone/engine-daemon-protocol.mjs +0 -33
  275. package/src/tui/engine/turn-watchdog.mjs +0 -92
  276. package/src/tui/engine.mjs +0 -22
  277. package/src/tui/hooks/useEngine.mjs +0 -16
  278. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  279. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  280. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  281. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  282. /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
@@ -1,25 +1,22 @@
1
- // Machine-global BACKEND daemon entry — the single mixdog backend process.
1
+ // Machine-global Mixdog daemon entry — one process for sessions, channels, and memory.
2
2
  //
3
- // One process per machine hosts the whole backend: the channels runtime
4
- // (worker-main), the memory runtime, and the session ENGINE pool. It exposes
5
- // two local front doors — channel-daemon-transport.mjs (pointer-routed channel
6
- // calls) and engine-daemon-transport.mjs (broadcast engine frames) — so the
3
+ // One process per machine hosts the whole service: the channels runtime
4
+ // (worker-main), the memory runtime, and the session runtime pool. It exposes
5
+ // two local front doors — channel transport (pointer-routed calls) and session
6
+ // transport (broadcast session frames) — so the
7
7
  // terminal TUI and the desktop app are views over ONE writer instead of each
8
- // booting its own engine and arbitrating ownership on disk.
8
+ // booting a separate service and arbitrating ownership on disk.
9
9
  // Spawned (or attached-to) by createStandaloneChannelWorker; ownership is a
10
10
  // pid-verified singleton lock (singleton-owner.mjs) — NOT the try-once
11
11
  // active-instance lock that starved under 6 contending workers. A stale daemon
12
12
  // (dead owner pid) is reclaimed by the next claim; a live peer that wins the
13
13
  // race makes this process exit(0) so the spawner attaches to the winner.
14
14
  //
15
- // Boot order matters: MIXDOG_CHANNEL_DAEMON must be set BEFORE importing the
16
- // channels runtime so worker-main skips its parent-IPC loop (runWorkerIpc) and
17
- // lets this entry own start()/stop() + the transport.
18
- process.env.MIXDOG_CHANNEL_DAEMON = '1';
15
+ // The unified host identity is set before the channels runtime is loaded so
16
+ // worker-main skips its parent-IPC loop and this entry owns start()/stop().
19
17
  process.env.MIXDOG_WORKER_MODE = process.env.MIXDOG_WORKER_MODE || '1';
20
- // This process IS the engine host: its own createEngineSession() must build
21
- // REAL engines, never a proxy back into itself.
22
- process.env.MIXDOG_ENGINE_DAEMON_HOST = '1';
18
+ // This process owns session runtimes and must never proxy back into itself.
19
+ process.env.MIXDOG_DAEMON_HOST = '1';
23
20
 
24
21
  // V8 compile cache: the daemon is a standalone child entry (not via cli.mjs);
25
22
  // caching compiled bytecode across restarts removes the channels+memory
@@ -31,21 +28,46 @@ try {
31
28
 
32
29
  import os from 'node:os';
33
30
  import path from 'node:path';
34
- import { mkdirSync } from 'node:fs';
31
+ import { mkdirSync, rmSync } from 'node:fs';
35
32
  import { appendFile, mkdir, open, stat, writeFile } from 'node:fs/promises';
36
33
  import { fileURLToPath } from 'node:url';
37
34
  import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
38
35
  import { inspect } from 'node:util';
36
+ import { writeJsonAtomicSync } from '../runtime/shared/atomic-file.mjs';
37
+ import { ensureProcessListenerHeadroom } from '../runtime/shared/process-listener-headroom.mjs';
39
38
  import { claimSingletonOwner, releaseSingletonOwner } from '../runtime/shared/singleton-owner.mjs';
40
39
  import { PLUGIN_LOG_MAX_BYTES, PLUGIN_LOG_KEEP_BYTES } from '../lib/mixdog-debug.cjs';
41
40
  import { setChannelNotifySink } from '../runtime/channels/lib/parent-bridge.mjs';
42
41
  import { setOwnerContext } from '../runtime/channels/lib/runtime-paths.mjs';
43
42
  import { safeIpcSend } from '../runtime/shared/safe-ipc-send.mjs';
44
- import { createChannelDaemonTransport } from './channel-daemon-transport.mjs';
45
- import { installEngineDaemonLocalBridge } from './engine-daemon-local-bridge.mjs';
46
- import { createEngineDaemonTransport } from './engine-daemon-transport.mjs';
47
- import { createEngineDaemonService } from './engine-daemon-service.mjs';
48
- import { createStandaloneMemoryRuntime } from './memory-runtime-proxy.mjs';
43
+ import { resourceAdmission } from '../runtime/shared/resource-admission.mjs';
44
+ import { snapshot as childSpawnSnapshot } from '../runtime/shared/child-spawn-gate.mjs';
45
+ import { toolWorkloadSnapshot } from '../runtime/shared/tool-workload-gates.mjs';
46
+ import { providerAdmissionScheduler } from '../runtime/agent/orchestrator/providers/admission-scheduler.mjs';
47
+ import { getMcpAdmissionSnapshot } from '../runtime/agent/orchestrator/mcp/client.mjs';
48
+ import { loadConfig as loadAgentConfig } from '../runtime/agent/orchestrator/config.mjs';
49
+ import { initProviders } from '../runtime/agent/orchestrator/providers/registry.mjs';
50
+ import { makeAgentDispatch } from '../runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs';
51
+ import {
52
+ closeProviderStreamJsonPool,
53
+ providerStreamJsonSnapshot,
54
+ } from '../runtime/agent/orchestrator/providers/stream-json-pool.mjs';
55
+ import { createAgentDispatchBroker } from './agent-dispatch-broker.mjs';
56
+ import { createChannelTransport } from './channel-transport.mjs';
57
+ import { createSessionTransport } from './session-transport.mjs';
58
+ import { createSessionService } from './session-service.mjs';
59
+ import { createSessionProtocolClient } from './session-protocol.mjs';
60
+ import { createSessionRuntimePool } from './session-runtime-pool.mjs';
61
+ import { getStandaloneMemoryRuntime } from './memory-runtime-proxy.mjs';
62
+ import {
63
+ compareRuntimeVersions,
64
+ SESSION_CAPABILITY_FINGERPRINT,
65
+ SESSION_PROTOCOL,
66
+ SESSION_REVISION,
67
+ runtimeVersion,
68
+ } from './session-wire.mjs';
69
+
70
+ ensureProcessListenerHeadroom(64);
49
71
 
50
72
  function runtimeRoot() {
51
73
  return process.env.MIXDOG_RUNTIME_ROOT
@@ -54,28 +76,21 @@ function runtimeRoot() {
54
76
  }
55
77
 
56
78
  const RUNTIME_ROOT = runtimeRoot();
57
- const DATA_DIR = process.env.MIXDOG_DATA_DIR ? path.resolve(process.env.MIXDOG_DATA_DIR) : RUNTIME_ROOT;
79
+ const DATA_DIR = process.env.MIXDOG_DATA_DIR
80
+ ? path.resolve(process.env.MIXDOG_DATA_DIR)
81
+ : path.join(process.env.MIXDOG_HOME || path.join(os.homedir(), '.mixdog'), 'data');
82
+ process.env.MIXDOG_DATA_DIR = DATA_DIR;
83
+ process.env.MIXDOG_SERVER_PID = String(process.pid);
58
84
  const CWD = process.cwd();
59
- const DISCOVERY_PATH = path.join(RUNTIME_ROOT, 'channel-daemon.json');
60
- // Engine views discover the SAME process through their own endpoint: one
61
- // daemon, two front doors (channel routing is pointer-targeted, engine frames
62
- // are broadcast — they must not share a router).
63
- const ENGINE_DISCOVERY_PATH = path.join(RUNTIME_ROOT, 'engine-daemon.json');
64
- // Owner-election lock, separate from the channels seat/bridge state. Reused
65
- // pid-verified claim primitive with real claim-lock retry inside.
66
- const OWNER_PATH = path.join(DATA_DIR, 'channel-daemon-owner.json');
67
- // Memory runtime is folded into the daemon: the ONE machine-global process is
68
- // responsible for starting BOTH the channels runtime and the memory runtime.
69
- // The memory proxy is spawn-or-attach + singleton (memory-runtime-owner lock)
70
- // and advertises memory_port to active-instance.json exactly as before, so
71
- // external readers / memory-client.mjs discovery is UNCHANGED.
85
+ const DAEMON_DISCOVERY_PATH = path.join(RUNTIME_ROOT, 'daemon.json');
86
+ // Owner-election lock, separate from the channels seat/bridge state.
87
+ const OWNER_PATH = path.join(DATA_DIR, 'daemon-owner.json');
72
88
  const MEMORY_ENTRY = fileURLToPath(new URL('../runtime/memory/index.mjs', import.meta.url));
73
-
74
89
  // The spawning TUI mirrors our stderr into this file ONLY until it sees our
75
90
  // 'ready' message; after that its pipe consumer dies on parent exit and later
76
91
  // lines would be lost. So once ready we append here ourselves (fileLogging on),
77
92
  // keyed to the SAME ready event the spawner detaches on — no loss, no dup.
78
- const LOG_PATH = path.join(DATA_DIR, 'channels-worker-standalone.log');
93
+ const LOG_PATH = path.join(DATA_DIR, 'daemon.log');
79
94
  let fileLogging = false;
80
95
  // This process outlives every spawner, so it has to bound its OWN log: the
81
96
  // spawner's boot-time rotate never runs again while the daemon lives, and the
@@ -87,6 +102,7 @@ let logQueueBytes = 0;
87
102
  let logDropped = 0;
88
103
  let logFlushTimer = null;
89
104
  let logWriter = Promise.resolve();
105
+ let logFileBytes = null;
90
106
 
91
107
  function boundedLogText(value) {
92
108
  const text = String(value ?? '');
@@ -95,15 +111,18 @@ function boundedLogText(value) {
95
111
  }
96
112
 
97
113
  async function rotateDaemonLogIfNeeded(incomingBytes) {
98
- let info;
99
- try { info = await stat(LOG_PATH); } catch { return; }
100
- if (info.size + incomingBytes <= PLUGIN_LOG_MAX_BYTES) return;
101
- const keep = Math.min(info.size, PLUGIN_LOG_KEEP_BYTES);
114
+ if (logFileBytes === null) {
115
+ try { logFileBytes = (await stat(LOG_PATH)).size; }
116
+ catch { logFileBytes = 0; }
117
+ }
118
+ if (logFileBytes + incomingBytes <= PLUGIN_LOG_MAX_BYTES) return;
119
+ const keep = Math.min(logFileBytes, PLUGIN_LOG_KEEP_BYTES);
102
120
  const tail = Buffer.allocUnsafe(keep);
103
121
  const handle = await open(LOG_PATH, 'r');
104
122
  try {
105
- const { bytesRead } = await handle.read(tail, 0, keep, Math.max(0, info.size - keep));
123
+ const { bytesRead } = await handle.read(tail, 0, keep, Math.max(0, logFileBytes - keep));
106
124
  await writeFile(LOG_PATH, tail.subarray(0, bytesRead));
125
+ logFileBytes = bytesRead;
107
126
  } finally {
108
127
  await handle.close().catch(() => {});
109
128
  }
@@ -117,7 +136,7 @@ function takeLogBatch() {
117
136
  logQueueBytes = 0;
118
137
  logDropped = 0;
119
138
  if (dropped > 0) {
120
- rows.unshift(`[${new Date().toISOString()}] [backend-daemon] dropped ${dropped} log line(s) under backpressure\n`);
139
+ rows.unshift(`[${new Date().toISOString()}] [daemon] dropped ${dropped} log line(s) under backpressure\n`);
121
140
  }
122
141
  return rows.join('');
123
142
  }
@@ -132,6 +151,7 @@ function queueLogFlush(delayMs = 10) {
132
151
  await mkdir(path.dirname(LOG_PATH), { recursive: true });
133
152
  await rotateDaemonLogIfNeeded(Buffer.byteLength(batch));
134
153
  await appendFile(LOG_PATH, batch, 'utf8');
154
+ logFileBytes = (logFileBytes || 0) + Buffer.byteLength(batch);
135
155
  }).catch(() => {});
136
156
  if (logQueue.length > 0 || logDropped > 0) queueLogFlush();
137
157
  }, delayMs);
@@ -162,12 +182,13 @@ async function flushDaemonLogs() {
162
182
  await mkdir(path.dirname(LOG_PATH), { recursive: true });
163
183
  await rotateDaemonLogIfNeeded(Buffer.byteLength(batch));
164
184
  await appendFile(LOG_PATH, batch, 'utf8');
185
+ logFileBytes = (logFileBytes || 0) + Buffer.byteLength(batch);
165
186
  }).catch(() => {});
166
187
  }
167
188
  await logWriter;
168
189
  }
169
190
  function log(line) {
170
- const text = `[backend-daemon] ${line}`;
191
+ const text = `[daemon] ${line}`;
171
192
  // Exactly ONE sink per line: before ready the spawner mirrors our stderr into
172
193
  // the log, so write stderr only; after ready we own the file, so write the
173
194
  // file only — never both (no duplicate around the ready handoff).
@@ -213,13 +234,15 @@ function installDaemonLogRedirect() {
213
234
 
214
235
  let channels = null;
215
236
  let transport = null;
216
- let engineTransport = null;
217
- let engineService = null;
218
- let localEngineBridge = null;
219
- let uninstallLocalEngineBridge = null;
237
+ let sessionTransport = null;
238
+ let sessionService = null;
239
+ let sessionRuntimePool = null;
240
+ let localSessionBridge = null;
220
241
  let memoryRuntime = null;
242
+ let agentDispatchBroker = null;
221
243
  let shuttingDown = false;
222
244
  let shutdownRecheckTimer = null;
245
+ let replacementRequested = null;
223
246
  const eventLoopDelay = monitorEventLoopDelay({ resolution: 20 });
224
247
  eventLoopDelay.enable();
225
248
  let eventLoopLagTimer = null;
@@ -242,45 +265,68 @@ async function shutdown(reason, code = 0) {
242
265
  }
243
266
  log(`shutting down (${reason})`);
244
267
  try { setChannelNotifySink(null); } catch {}
245
- try { await engineService?.stop?.(reason); } catch (e) { log(`engine.stop failed: ${e?.message || e}`); }
246
- try { await localEngineBridge?.close?.(reason); } catch (e) { log(`local engine bridge.close failed: ${e?.message || e}`); }
247
- try { uninstallLocalEngineBridge?.(); } catch {}
248
- uninstallLocalEngineBridge = null;
249
- try { await engineTransport?.stop?.(); } catch (e) { log(`engine transport.stop failed: ${e?.message || e}`); }
268
+ try { await sessionService?.stop?.(reason); } catch (e) { log(`session service stop failed: ${e?.message || e}`); }
269
+ try { await sessionRuntimePool?.close?.(reason); } catch (e) { log(`session shard pool stop failed: ${e?.message || e}`); }
270
+ try { await localSessionBridge?.close?.(reason); } catch (e) { log(`local session bridge.close failed: ${e?.message || e}`); }
271
+ try { await sessionTransport?.stop?.(); } catch (e) { log(`session transport stop failed: ${e?.message || e}`); }
250
272
  try { await channels?.stop?.(); } catch (e) { log(`channels.stop failed: ${e?.message || e}`); }
251
- // Detach the memory client (never hard-kills the shared memory daemon).
273
+ // The daemon only releases its client registration. The isolated memory
274
+ // process shuts down after its last live client disappears.
252
275
  try { await memoryRuntime?.stop?.(); } catch (e) { log(`memory.stop failed: ${e?.message || e}`); }
276
+ try { agentDispatchBroker?.close?.(reason); } catch (e) { log(`agent broker stop failed: ${e?.message || e}`); }
277
+ try { await closeProviderStreamJsonPool(reason); } catch (e) { log(`stream parser stop failed: ${e?.message || e}`); }
253
278
  try { await transport?.stop?.(); } catch (e) { log(`transport.stop failed: ${e?.message || e}`); }
254
279
  if (eventLoopLagTimer) { clearInterval(eventLoopLagTimer); eventLoopLagTimer = null; }
255
280
  eventLoopDelay.disable();
281
+ for (const discoveryPath of [DAEMON_DISCOVERY_PATH]) {
282
+ try { rmSync(discoveryPath, { force: true }); } catch {}
283
+ }
256
284
  try { releaseSingletonOwner(OWNER_PATH, process.pid); } catch {}
257
285
  await flushDaemonLogs();
258
286
  process.exit(code);
259
287
  }
260
288
 
261
- /** One process, two front doors (channels + engines): an idle side must never
289
+ /** One process, two front doors (channels + sessions): an idle side must never
262
290
  * evict a busy one, so every self-shutdown trigger checks BOTH registries. */
263
291
  function maybeSelfShutdown(reason) {
264
292
  const channelClients = transport?.clientCount ?? 0;
265
- const engineClients = engineTransport?.clientCount ?? 0;
266
- const remoteClients = engineService?.externalClientCount ?? 0;
267
- if (channelClients > 0 || engineClients > 0 || remoteClients > 0) {
293
+ const sessionClients = sessionTransport?.clientCount ?? 0;
294
+ const remoteClients = sessionService?.externalClientCount ?? 0;
295
+ const replacing = Boolean(replacementRequested);
296
+ if (channelClients > 0 || sessionClients > 0 || (!replacing && remoteClients > 0)) {
268
297
  if (shutdownRecheckTimer) {
269
298
  clearTimeout(shutdownRecheckTimer);
270
299
  shutdownRecheckTimer = null;
271
300
  }
272
- log(`shutdown deferred (${reason}): channels=${channelClients} engines=${engineClients} remote=${remoteClients}`);
301
+ log(`shutdown deferred (${reason}): channels=${channelClients} sessionClients=${sessionClients} remote=${remoteClients}`);
302
+ return;
303
+ }
304
+ const activeCalls = (transport?.activeCount ?? 0) + (sessionTransport?.activeCount ?? 0);
305
+ const queuedCalls = (transport?.queuedCount ?? 0) + (sessionTransport?.queuedCount ?? 0);
306
+ if (activeCalls > 0 || queuedCalls > 0) {
307
+ log(`shutdown deferred (${reason}): activeCalls=${activeCalls} queuedCalls=${queuedCalls}`);
308
+ if (!shutdownRecheckTimer) {
309
+ shutdownRecheckTimer = setTimeout(() => {
310
+ shutdownRecheckTimer = null;
311
+ maybeSelfShutdown('service calls settled');
312
+ }, 250);
313
+ shutdownRecheckTimer.unref?.();
314
+ }
273
315
  return;
274
316
  }
275
317
  // A turn in flight outlives every view — closing the app or the terminal is
276
318
  // not a reason to abandon work the daemon is still running.
277
- const busyEngines = engineService?.busyCount ?? 0;
278
- if (busyEngines > 0) {
279
- log(`shutdown deferred (${reason}): ${busyEngines} engine(s) still working`);
319
+ const busySessions = sessionService?.busyCount ?? 0;
320
+ const busyMemoryAgents = agentDispatchBroker?.snapshot?.().inFlight ?? 0;
321
+ if (busySessions > 0 || busyMemoryAgents > 0) {
322
+ log(
323
+ `shutdown deferred (${reason}): busySessions=${busySessions}`
324
+ + ` memoryAgents=${busyMemoryAgents}`,
325
+ );
280
326
  if (!shutdownRecheckTimer) {
281
327
  shutdownRecheckTimer = setTimeout(() => {
282
328
  shutdownRecheckTimer = null;
283
- maybeSelfShutdown('busy engines settled');
329
+ maybeSelfShutdown('busy service work settled');
284
330
  }, 1_000);
285
331
  shutdownRecheckTimer.unref?.();
286
332
  }
@@ -289,6 +335,30 @@ function maybeSelfShutdown(reason) {
289
335
  void shutdown(reason);
290
336
  }
291
337
 
338
+ function requestDaemonReplacement({ protocol, revision, version } = {}) {
339
+ const requested = {
340
+ protocol: Number(protocol),
341
+ revision: Math.max(0, Number(revision) || 0),
342
+ version: String(version || '0.0.0'),
343
+ };
344
+ if (requested.protocol !== SESSION_PROTOCOL) return false;
345
+ const revisionOrder = requested.revision - SESSION_REVISION;
346
+ const versionOrder = compareRuntimeVersions(requested.version, runtimeVersion());
347
+ if (revisionOrder < 0 || (revisionOrder === 0 && versionOrder <= 0)) return false;
348
+ if (
349
+ replacementRequested
350
+ && requested.revision === replacementRequested.revision
351
+ && requested.version === replacementRequested.version
352
+ ) return true;
353
+ replacementRequested = requested;
354
+ const reason = `daemon replacement by revision/build ${requested.revision}/${requested.version}`;
355
+ log(`${reason} requested — draining clients without interrupting live work`);
356
+ sessionTransport?.beginDrain?.(reason);
357
+ transport?.beginDrain?.(reason);
358
+ maybeSelfShutdown(reason);
359
+ return true;
360
+ }
361
+
292
362
  async function main() {
293
363
  const startedAt = performance.now();
294
364
  // 0o700 matters where the runtime root is a SHARED temp dir: the discovery
@@ -299,12 +369,28 @@ async function main() {
299
369
  // Pid-verified singleton claim (claimSingletonOwner reclaims a dead-pid owner
300
370
  // file and refuses only a LIVE peer). Loser exits so the spawner attaches to
301
371
  // the winner instead of running a second daemon.
302
- const claim = claimSingletonOwner(OWNER_PATH, { kind: 'channel-runtime-daemon', pid: process.pid, meta: { cwd: CWD } });
372
+ const claim = claimSingletonOwner(OWNER_PATH, {
373
+ kind: 'mixdog-daemon',
374
+ pid: process.pid,
375
+ meta: {
376
+ cwd: CWD,
377
+ protocol: SESSION_PROTOCOL,
378
+ revision: SESSION_REVISION,
379
+ version: runtimeVersion(),
380
+ },
381
+ });
303
382
  if (!claim.owned) {
304
383
  log(`live peer holds owner lock (pid=${claim.owner?.pid}) — exiting for attach`);
305
384
  process.exit(0);
306
385
  }
307
386
  process.on('exit', () => { try { releaseSingletonOwner(OWNER_PATH, process.pid); } catch {} });
387
+ agentDispatchBroker = createAgentDispatchBroker({
388
+ loadConfig: loadAgentConfig,
389
+ initProviders,
390
+ makeAgentDispatch,
391
+ log,
392
+ onActivityChanged: () => { maybeSelfShutdown('memory agent activity changed'); },
393
+ });
308
394
  process.on('mixdog:turn-timing', (row = {}) => {
309
395
  const ms = (value) => Number.isFinite(value) ? Math.round(value) : -1;
310
396
  log(
@@ -318,7 +404,7 @@ async function main() {
318
404
 
319
405
  // The channels runtime is imported AFTER the daemon env is set so worker-main
320
406
  // skips runWorkerIpc; that import also triggers its boot side effects
321
- // (config/backend). It is now deferred past the ready handshake: an engine
407
+ // (config/service). It is now deferred past the ready handshake: a session
322
408
  // view attaching to this same process must not wait out the channels graph,
323
409
  // and every channels call awaits this promise anyway.
324
410
  let channelsReady = null;
@@ -333,9 +419,9 @@ async function main() {
333
419
  }
334
420
 
335
421
  // Channels bring automation with them (schedules, webhook listener + tunnel,
336
- // optional messaging backend). A daemon spawned by a TUI starts them exactly
337
- // as before; a daemon spawned for ENGINE views stays dormant until a channels
338
- // client actually registers, so an app-only backend runs no tunnels.
422
+ // optional messaging service). A daemon spawned by a TUI starts them exactly
423
+ // as before; a daemon spawned for session views stays dormant until a channels
424
+ // client actually registers, so an app-only service runs no tunnels.
339
425
  let channelsStarted = false;
340
426
  function startChannels() {
341
427
  if (channelsStarted) return;
@@ -355,79 +441,77 @@ async function main() {
355
441
  }
356
442
  return module.handleToolCallWithBridgeRetry(name, args || {});
357
443
  };
358
- transport = createChannelDaemonTransport({
444
+ transport = createChannelTransport({
359
445
  handleCall,
360
- // If a manual owner disappears without sending OFF, deactivate the backend.
446
+ agentBroker: agentDispatchBroker,
447
+ remoteStatePath: path.join(RUNTIME_ROOT, 'channel-remote-state.json'),
448
+ remoteIntentPath: path.join(RUNTIME_ROOT, 'channel-remote-intent.json'),
449
+ // If a manual owner disappears without sending OFF, deactivate the service.
361
450
  // No survivor is selected or rebound.
362
451
  dispatchControl: (name, args, ctx) => handleCall(name, args, ctx),
363
- discoveryPath: DISCOVERY_PATH,
364
452
  log,
365
453
  // Self-shutdown when the last attached TUI leaves (reuses the SSE/client
366
- // registry as the liveness signal — mirrors the memory daemon grace).
454
+ // registry as the liveness signal).
367
455
  onClientsEmpty: () => { maybeSelfShutdown('no live channel clients'); },
368
456
  // First channels client in: bring the channels runtime up (see startChannels).
369
457
  onClientRegistered: () => { startChannels(); },
370
458
  });
371
459
  setChannelNotifySink((method, params) => transport.notify(method, params));
372
460
  const { port, token } = await transport.start();
461
+ // Memory-cycle agent dispatch is rare and initializes on first use. Eagerly
462
+ // loading its provider graph here consumed the control loop before any
463
+ // memory cycle requested it.
373
464
 
374
- // ── Engine front door ───────────────────────────────────────────────────────
375
- // Session engines live in THIS process too, so the terminal TUI and the
376
- // desktop app share ONE writer instead of arbitrating ownership on disk. The
377
- // engine module is the whole runtime graph, so it is imported when the first
378
- // engine client registers — early enough to overlap user think time, while a
379
- // channels-only spawn still never pays for it.
380
- const localEngineClients = new Map();
381
- let nextLocalEngineClient = 0;
382
- localEngineBridge = {
465
+ // ── Session front door ──────────────────────────────────────────────────────
466
+ // Session runtimes live in child-process shards. The daemon owns routing and
467
+ // fan-out only, so one provider/parser turn cannot stall health or input for
468
+ // every other session.
469
+ const localSessionClients = new Map();
470
+ let nextLocalSessionClient = 0;
471
+ localSessionBridge = {
383
472
  attach({ onFrame = () => {}, onFatal = () => {} } = {}) {
384
- const clientToken = `daemon_local_${process.pid}_${++nextLocalEngineClient}`;
473
+ const clientToken = `daemon_local_${process.pid}_${++nextLocalSessionClient}`;
385
474
  let closed = false;
386
- localEngineClients.set(clientToken, { onFrame, onFatal });
387
- return {
475
+ localSessionClients.set(clientToken, { onFrame, onFatal });
476
+ return createSessionProtocolClient({
388
477
  call(name, args = {}, options = {}) {
389
- if (closed) throw new Error('engine daemon local bridge is closed');
390
- return engineService.handleCall(name, args, {
478
+ if (closed) throw new Error('daemon-local session client is closed');
479
+ return sessionService.handleCall(name, args, {
391
480
  clientToken,
392
481
  ...(options?.callId ? { callId: String(options.callId) } : {}),
393
482
  });
394
483
  },
395
- async close(reason = 'local engine view closed') {
484
+ async close(reason = 'local session view closed') {
396
485
  if (closed) return;
397
486
  closed = true;
398
- localEngineClients.delete(clientToken);
399
- try { engineService.releaseClient(clientToken); } catch {}
487
+ localSessionClients.delete(clientToken);
488
+ try { sessionService.releaseClient(clientToken); } catch {}
400
489
  log(`${reason} (${clientToken})`);
401
490
  },
402
- };
491
+ });
403
492
  },
404
493
  publish(frame, targetTokens = null) {
405
494
  const targets = targetTokens ? new Set(targetTokens) : null;
406
- for (const [clientToken, client] of localEngineClients) {
495
+ for (const [clientToken, client] of localSessionClients) {
407
496
  if (targets && !targets.has(clientToken)) continue;
408
497
  try { client.onFrame(frame); } catch {}
409
498
  }
410
499
  },
411
500
  async close(reason = 'daemon shutdown') {
412
- for (const [clientToken, client] of localEngineClients) {
501
+ for (const [clientToken, client] of localSessionClients) {
413
502
  try { client.onFatal(reason); } catch {}
414
- try { engineService.releaseClient(clientToken); } catch {}
503
+ try { sessionService.releaseClient(clientToken); } catch {}
415
504
  }
416
- localEngineClients.clear();
505
+ localSessionClients.clear();
417
506
  },
418
507
  };
419
- uninstallLocalEngineBridge = installEngineDaemonLocalBridge(localEngineBridge);
420
- const engineDaemonClientModule = () => import('./engine-daemon-client.mjs');
421
508
  const desktopRuntime = {
422
- async createRemoteEngineSession(options) {
423
- return (await engineDaemonClientModule()).createRemoteEngineSession(options);
424
- },
425
- async callDaemonSession(options) {
426
- return (await engineDaemonClientModule()).callDaemonSession(options);
509
+ async attachSessionClient(options = {}) {
510
+ if (!localSessionBridge) throw new Error('daemon-local session client is unavailable');
511
+ return localSessionBridge.attach(options);
427
512
  },
428
513
  loadProjects: () => import('./projects.mjs'),
429
514
  loadSessionStore: () => import('../runtime/agent/orchestrator/session/store-summary-reader.mjs'),
430
- loadStatuslineSegments: () => import('../ui/statusline-segments.mjs'),
431
515
  loadConfig: () => import('../runtime/shared/config.mjs'),
432
516
  loadCommitCompletion: () => import(
433
517
  '../runtime/agent/orchestrator/agent-runtime/commit-message-completion.mjs'
@@ -437,68 +521,96 @@ async function main() {
437
521
  return graph.executeCodeGraphTool(name, args, cwd);
438
522
  },
439
523
  };
440
- let engineModulePromise = null;
441
- let engineRuntimePrewarmStarted = false;
442
- function loadEngineModule() {
443
- if (!engineModulePromise) {
444
- engineModulePromise = import('../tui/engine-local-session.mjs').catch((error) => {
445
- engineModulePromise = null;
446
- throw error;
447
- });
448
- }
449
- return engineModulePromise;
450
- }
451
- function prewarmEngineRuntime() {
452
- if (engineRuntimePrewarmStarted) return;
453
- engineRuntimePrewarmStarted = true;
454
- void loadEngineModule()
455
- .then((engineModule) => {
456
- engineModule.preloadSessionRuntimeModule?.();
457
- engineModule.preloadKeychainSecrets?.();
458
- log('engine runtime/keychain prewarm started');
459
- })
524
+ let sessionRuntimePrewarmStarted = false;
525
+ sessionRuntimePool = createSessionRuntimePool({ cwd: CWD, log });
526
+ function prewarmSessionRuntime() {
527
+ if (sessionRuntimePrewarmStarted) return;
528
+ sessionRuntimePrewarmStarted = true;
529
+ void sessionRuntimePool.prewarm()
530
+ .then(() => log('session shard runtime/agent-loop/keychain prewarm started'))
460
531
  .catch((error) => {
461
- engineRuntimePrewarmStarted = false;
462
- log(`engine runtime/keychain prewarm failed (non-fatal): ${error?.message || error}`);
532
+ sessionRuntimePrewarmStarted = false;
533
+ log(`session runtime/keychain prewarm failed (non-fatal): ${error?.message || error}`);
463
534
  });
464
535
  }
465
- engineService = createEngineDaemonService({
466
- createEngine: async (options) => {
467
- const engineModule = await loadEngineModule();
468
- return engineModule.createLocalEngineSession(options);
536
+ sessionService = createSessionService({
537
+ createSessionRuntime: (options) => sessionRuntimePool.create(options),
538
+ sessionExists: async (sessionId) => {
539
+ const store = await desktopRuntime.loadSessionStore();
540
+ return store.storedSessionExists?.(sessionId) === true;
541
+ },
542
+ listSessions: async (options = {}) => {
543
+ const store = await desktopRuntime.loadSessionStore();
544
+ return store.listStoredSessionSummaries({
545
+ refreshFromStorage: options.refreshFromStorage === true,
546
+ });
469
547
  },
470
548
  desktopRuntime,
471
549
  onFrame: (frame, targetTokens) => {
472
- localEngineBridge?.publish(frame, targetTokens);
473
- engineTransport?.broadcast(frame, targetTokens);
550
+ localSessionBridge?.publish(frame, targetTokens);
551
+ sessionTransport?.broadcast(frame, targetTokens);
474
552
  },
475
553
  onExternalClientsChanged: () => { maybeSelfShutdown('remote clients changed'); },
476
554
  log,
477
555
  });
478
- engineTransport = createEngineDaemonTransport({
479
- // ctx carries the CLIENT token: the engine pool refcounts views across
480
- // processes with it, so a terminal exiting cannot destroy the engine a
556
+ sessionTransport = createSessionTransport({
557
+ // ctx carries the CLIENT token: the session pool refcounts views across
558
+ // processes with it, so a terminal exiting cannot destroy the session a
481
559
  // desktop window is still streaming.
482
- handleCall: (name, args, ctx) => engineService.handleCall(name, args, ctx),
483
- discoveryPath: ENGINE_DISCOVERY_PATH,
560
+ handleCall: (name, args, ctx) => sessionService.handleCall(name, args, ctx),
484
561
  log,
485
562
  // `busy` is what stops a newer install from draining a daemon that is
486
563
  // mid-turn: work outlives views AND installs.
487
564
  getStatus: () => ({
488
- engines: engineService.size,
489
- busy: engineService.busyCount,
565
+ sessions: sessionService.size,
566
+ busy: sessionService.busyCount,
567
+ sessionPool: sessionService.status,
568
+ sessionShards: sessionRuntimePool.status,
569
+ workload: {
570
+ resources: resourceAdmission.snapshot(),
571
+ childSpawns: childSpawnSnapshot(),
572
+ toolIo: toolWorkloadSnapshot(),
573
+ mcp: getMcpAdmissionSnapshot(),
574
+ providers: providerAdmissionScheduler.snapshot(),
575
+ streamParsing: providerStreamJsonSnapshot(),
576
+ },
577
+ memory: {
578
+ ...(() => {
579
+ const usage = process.memoryUsage();
580
+ return {
581
+ rssBytes: usage.rss,
582
+ heapTotalBytes: usage.heapTotal,
583
+ heapUsedBytes: usage.heapUsed,
584
+ externalBytes: usage.external,
585
+ arrayBufferBytes: usage.arrayBuffers,
586
+ };
587
+ })(),
588
+ },
490
589
  ...eventLoopStatus(),
491
590
  }),
492
- onClientsEmpty: () => { maybeSelfShutdown('no live engine views'); },
493
- onClientRegistered: () => { prewarmEngineRuntime(); },
494
- onClientDropped: (token) => { try { engineService.releaseClient(token); } catch {} },
591
+ onClientsEmpty: () => { maybeSelfShutdown('no live session clients'); },
592
+ onClientRegistered: () => { prewarmSessionRuntime(); },
593
+ onClientDropped: (token) => { try { sessionService.releaseClient(token); } catch {} },
594
+ onUpgradeRequested: requestDaemonReplacement,
495
595
  });
496
- const engineEndpoint = await engineTransport.start();
497
- log(`engine front door on 127.0.0.1:${engineEndpoint.port}`);
596
+ const sessionEndpoint = await sessionTransport.start();
597
+ writeJsonAtomicSync(DAEMON_DISCOVERY_PATH, {
598
+ protocol: SESSION_PROTOCOL,
599
+ revision: SESSION_REVISION,
600
+ version: runtimeVersion(),
601
+ capabilityFingerprint: SESSION_CAPABILITY_FINGERPRINT,
602
+ pid: process.pid,
603
+ startedAt: Date.now(),
604
+ endpoints: {
605
+ channel: { port, token },
606
+ session: { port: sessionEndpoint.port, token: sessionEndpoint.token },
607
+ },
608
+ }, { compact: true });
609
+ log(`session front door on 127.0.0.1:${sessionEndpoint.port}`);
498
610
 
499
- // Ready handshake for the spawner FIRST (mirrors the memory daemon's ready
500
- // port). Transport is already listening; signal ready before the heavy
501
- // Discord connect so the spawner's ready wait never blocks on backend I/O.
611
+ // Ready handshake for the spawner first. Transport is already listening;
612
+ // signal ready before the heavy
613
+ // Discord connect so the spawner's ready wait never blocks on service I/O.
502
614
  // Take over file logging from the spawner at the ready boundary. No rotate
503
615
  // here: the spawner already bounds the file at its own boot (channel-worker
504
616
  // rotateBoundedLog), and rotating now would race other processes' buffered
@@ -533,18 +645,16 @@ async function main() {
533
645
 
534
646
  // Boot messaging only for an explicit manual remote request. Automation may
535
647
  // spawn the shared daemon with no remote intent; keep schedules/webhooks live
536
- // without connecting the channel backend until a manual Remote ON arrives.
537
- // Legacy `auto` intent is deliberately ignored.
648
+ // without connecting the channel service until a manual Remote ON arrives.
538
649
  // A TUI-spawned daemon keeps the historical eager start (its channels client
539
- // is already on the way). An engine-spawned one waits for a real channels
650
+ // is already on the way). A session-spawned one waits for a real channels
540
651
  // client — see the transport's onClientRegistered hook.
541
- if (process.env.MIXDOG_DAEMON_SPAWNED_FOR !== 'engine') startChannels();
652
+ if (process.env.MIXDOG_DAEMON_SPAWNED_FOR !== 'session') startChannels();
542
653
 
543
- // Fold memory startup in: eagerly ensure the memory runtime is up under the
544
- // daemon's lifecycle (spawn-or-attach singleton). Fire-and-forget — memory
545
- // boot is heavy (DB/embeddings) and must NOT delay the ready handshake below
546
- // (the spawner's ready wait would time out); the proxy publishes memory_port
547
- // to active-instance.json when ready and memory-client buffers until then.
654
+ // Memory owns a separate process/event loop. Initialization stays
655
+ // asynchronous because DB/embedding startup must not delay the front-door
656
+ // ready handshake. The proxy retains it for this daemon pid and its
657
+ // client-grace lifecycle reaps it only after the daemon is gone.
548
658
  // Isolated test roots opt out (MIXDOG_DAEMON_SKIP_MEMORY=1): spinning a
549
659
  // throwaway Postgres cluster per test run is pure cost, and a hard-killed
550
660
  // daemon would orphan it.
@@ -553,8 +663,14 @@ async function main() {
553
663
  return;
554
664
  }
555
665
  try {
556
- memoryRuntime = createStandaloneMemoryRuntime({ entry: MEMORY_ENTRY, dataDir: DATA_DIR, cwd: CWD });
557
- void memoryRuntime.start().catch((e) => log(`memory.start failed (non-fatal): ${e?.message || e}`));
666
+ memoryRuntime = getStandaloneMemoryRuntime({
667
+ entry: MEMORY_ENTRY,
668
+ dataDir: DATA_DIR,
669
+ cwd: CWD,
670
+ });
671
+ void memoryRuntime.init()
672
+ .then(() => log('memory runtime ready in isolated process'))
673
+ .catch((e) => log(`memory.start failed (non-fatal): ${e?.message || e}`));
558
674
  } catch (e) { log(`memory.start setup failed (non-fatal): ${e?.message || e}`); }
559
675
  }
560
676