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
@@ -0,0 +1,119 @@
1
+ const LEGACY_AGENT_ROUTE_SLOTS = Object.freeze({
2
+ explore: Object.freeze({ workflow: 'explorer', maintenance: 'explore' }),
3
+ maintainer: Object.freeze({ alias: 'maintenance', workflow: 'memory', maintenance: 'memory' }),
4
+ worker: Object.freeze({ workflow: 'agent' }),
5
+ });
6
+
7
+ const REDUNDANT_WORKFLOW_PRESET_IDS = new Set([
8
+ 'workflow-agent',
9
+ 'workflow-explorer',
10
+ 'workflow-memory',
11
+ ]);
12
+
13
+ function record(value) {
14
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
15
+ }
16
+
17
+ export function isCompleteAgentRoute(value) {
18
+ return !!value
19
+ && typeof value === 'object'
20
+ && !Array.isArray(value)
21
+ && !!String(value.provider || '').trim()
22
+ && !!String(value.model || '').trim();
23
+ }
24
+
25
+ function generatedPresetAgentId(preset) {
26
+ const id = String(preset?.id || '').trim().toLowerCase();
27
+ if (id === 'workflow-agent') return 'worker';
28
+ if (id === 'workflow-explorer') return 'explore';
29
+ if (id === 'workflow-memory') return 'maintainer';
30
+ if (!id.startsWith('workflow-agent-')) return '';
31
+ const suffix = id.slice('workflow-agent-'.length);
32
+ if (suffix === 'explorer' || suffix === 'explore') return 'explore';
33
+ if (suffix === 'maintenance' || suffix === 'memory' || suffix === 'maintainer') return 'maintainer';
34
+ return suffix;
35
+ }
36
+
37
+ function routeFromGeneratedPreset(preset) {
38
+ const provider = String(preset?.provider || '').trim();
39
+ const model = String(preset?.model || '').trim();
40
+ if (!provider || !model) return null;
41
+ const route = { provider, model };
42
+ const effort = String(preset?.effort || '').trim();
43
+ if (effort) route.effort = effort;
44
+ if (preset?.fast === true) route.fast = true;
45
+ return route;
46
+ }
47
+
48
+ export function configuredAgentRouteCandidates(config, agentId) {
49
+ const id = String(agentId || '').trim();
50
+ if (!id) return [];
51
+ const agents = record(config?.agents);
52
+ const legacy = LEGACY_AGENT_ROUTE_SLOTS[id] || null;
53
+ const candidates = [agents[id]];
54
+ if (legacy?.alias) candidates.push(agents[legacy.alias]);
55
+ if (legacy?.workflow) candidates.push(record(config?.workflowRoutes)[legacy.workflow]);
56
+ if (legacy?.maintenance) candidates.push(record(config?.maintenance)[legacy.maintenance]);
57
+ return candidates.filter(Boolean);
58
+ }
59
+
60
+ export function canonicalizeAgentRoutes(config = {}) {
61
+ const sourceAgents = record(config.agents);
62
+ const agents = Object.fromEntries(
63
+ Object.entries(sourceAgents).filter(([, route]) => isCompleteAgentRoute(route)),
64
+ );
65
+ for (const id of Object.keys(LEGACY_AGENT_ROUTE_SLOTS)) {
66
+ if (agents[id]) continue;
67
+ const candidate = configuredAgentRouteCandidates({ ...config, agents: sourceAgents }, id)
68
+ .find((route) => isCompleteAgentRoute(route));
69
+ if (candidate) agents[id] = candidate;
70
+ }
71
+ delete agents.maintenance;
72
+ // Older settings saves generated one preset per fixed/custom agent. Promote
73
+ // a preset only when no higher-priority canonical/legacy route exists, then
74
+ // the redundant preset can be removed without discarding user assignments.
75
+ for (const preset of Array.isArray(config?.presets) ? config.presets : []) {
76
+ const id = generatedPresetAgentId(preset);
77
+ if (!id || agents[id]) continue;
78
+ const route = routeFromGeneratedPreset(preset);
79
+ if (route) agents[id] = route;
80
+ }
81
+ return agents;
82
+ }
83
+
84
+ export function isRedundantGeneratedRoutePreset(preset, defaultPreset = null) {
85
+ const id = String(preset?.id || '').trim();
86
+ if (!id || id === String(defaultPreset || '').trim()) return false;
87
+ return REDUNDANT_WORKFLOW_PRESET_IDS.has(id) || id.startsWith('workflow-agent-');
88
+ }
89
+
90
+ export function canonicalizeAgentRouteStorage(config = {}) {
91
+ const {
92
+ workflowRoutes: _legacyWorkflowRoutes,
93
+ ...rest
94
+ } = config || {};
95
+ const maintenance = { ...record(config?.maintenance) };
96
+ delete maintenance.explore;
97
+ delete maintenance.memory;
98
+ const presets = Array.isArray(config?.presets)
99
+ ? config.presets.filter((preset) => !isRedundantGeneratedRoutePreset(preset, config?.default))
100
+ : [];
101
+ return {
102
+ ...rest,
103
+ agents: canonicalizeAgentRoutes(config),
104
+ maintenance,
105
+ presets,
106
+ };
107
+ }
108
+
109
+ export function agentRouteStorageNeedsMigration(config = {}) {
110
+ const agents = record(config?.agents);
111
+ const maintenance = record(config?.maintenance);
112
+ return Object.prototype.hasOwnProperty.call(config || {}, 'workflowRoutes')
113
+ || Object.prototype.hasOwnProperty.call(agents, 'maintenance')
114
+ || Object.prototype.hasOwnProperty.call(maintenance, 'explore')
115
+ || Object.prototype.hasOwnProperty.call(maintenance, 'memory')
116
+ || Object.values(agents).some((route) => !isCompleteAgentRoute(route))
117
+ || (Array.isArray(config?.presets)
118
+ && config.presets.some((preset) => isRedundantGeneratedRoutePreset(preset, config?.default)));
119
+ }
@@ -28,6 +28,55 @@ import { AsyncLocalStorage } from 'node:async_hooks';
28
28
 
29
29
  const _execFileAsync = promisify(execFile);
30
30
 
31
+ // Async atomic writes from many daemon sessions share one disk. Keep enough
32
+ // parallelism for SSD throughput, then use additive-increase/multiplicative-
33
+ // decrease when latency shows the filesystem is saturated. This gate never
34
+ // touches synchronous compatibility paths and never revokes in-flight writes.
35
+ const _ioCeiling = Math.max(1, Math.floor(Number(process.env.MIXDOG_FILE_IO_MAX_CONCURRENCY) || 16));
36
+ let _ioLimit = Math.min(4, _ioCeiling);
37
+ let _ioActive = 0;
38
+ let _ioFastStreak = 0;
39
+ const _ioQueue = [];
40
+
41
+ function _drainAdaptiveFileIo() {
42
+ while (_ioActive < _ioLimit && _ioQueue.length > 0) {
43
+ const item = _ioQueue.shift();
44
+ _ioActive += 1;
45
+ const startedAt = Date.now();
46
+ Promise.resolve()
47
+ .then(item.run)
48
+ .then(item.resolve, item.reject)
49
+ .finally(() => {
50
+ const elapsed = Date.now() - startedAt;
51
+ _ioActive = Math.max(0, _ioActive - 1);
52
+ if (elapsed >= 250) {
53
+ _ioLimit = Math.max(1, Math.ceil(_ioLimit / 2));
54
+ _ioFastStreak = 0;
55
+ } else if (elapsed <= 40 && _ioLimit < _ioCeiling) {
56
+ _ioFastStreak += 1;
57
+ if (_ioFastStreak >= _ioLimit * 4) {
58
+ _ioLimit += 1;
59
+ _ioFastStreak = 0;
60
+ }
61
+ } else {
62
+ _ioFastStreak = 0;
63
+ }
64
+ _drainAdaptiveFileIo();
65
+ });
66
+ }
67
+ }
68
+
69
+ function _runAdaptiveFileIo(run) {
70
+ return new Promise((resolve, reject) => {
71
+ _ioQueue.push({ run, resolve, reject });
72
+ _drainAdaptiveFileIo();
73
+ });
74
+ }
75
+
76
+ export function adaptiveFileIoSnapshot() {
77
+ return { active: _ioActive, queued: _ioQueue.length, limit: _ioLimit, ceiling: _ioCeiling };
78
+ }
79
+
31
80
  const RETRY_CODES = new Set(['EPERM', 'EACCES', 'EBUSY', 'EEXIST']);
32
81
  const LOCK_WAIT_CODES = new Set(['EEXIST', 'EPERM', 'EACCES', 'EBUSY']);
33
82
  const DEFAULT_BACKOFFS_MS = Object.freeze([25, 50, 100, 200, 400, 800, 1200, 1600]);
@@ -79,6 +128,28 @@ function _describeLockHolder(lockPath) {
79
128
  }
80
129
  }
81
130
 
131
+ // Lock-wait watchdog. A cross-process file lock is invisible in a profile: the
132
+ // only symptom is "everything stopped". One bounded line per path names the
133
+ // path and the wait, so the next stall is diagnosed from the log instead of a
134
+ // live repro.
135
+ const LOCK_WAIT_WARN_MS = 500;
136
+ const LOCK_WAIT_WARN_INTERVAL_MS = 10_000;
137
+ const _lockWaitWarnedAt = new Map();
138
+ function _reportLockWait(lockPath, waitedMs, mode) {
139
+ if (waitedMs < LOCK_WAIT_WARN_MS) return;
140
+ const now = Date.now();
141
+ if (now - (_lockWaitWarnedAt.get(lockPath) || 0) < LOCK_WAIT_WARN_INTERVAL_MS) return;
142
+ _lockWaitWarnedAt.set(lockPath, now);
143
+ if (_lockWaitWarnedAt.size > 64) {
144
+ for (const [path, at] of _lockWaitWarnedAt) {
145
+ if (now - at >= LOCK_WAIT_WARN_INTERVAL_MS * 6) _lockWaitWarnedAt.delete(path);
146
+ }
147
+ }
148
+ try {
149
+ process.stderr.write(`[atomic-file] ${mode} lock wait ${waitedMs}ms: ${lockPath}\n`);
150
+ } catch { /* diagnostics only */ }
151
+ }
152
+
82
153
  export function renameWithRetrySync(src, dst, opts = {}) {
83
154
  const backoffs = Array.isArray(opts.backoffs) && opts.backoffs.length > 0
84
155
  ? opts.backoffs
@@ -129,6 +200,7 @@ export function withFileLockSync(lockPath, fn, opts = {}) {
129
200
  }
130
201
  const deadline = Date.now() + timeoutMs;
131
202
  mkdirSync(dirname(lockPath), { recursive: true });
203
+ const waitStartedAt = Date.now();
132
204
  let attempt = 0;
133
205
  let lastErr = null;
134
206
  while (true) {
@@ -168,6 +240,7 @@ export function withFileLockSync(lockPath, fn, opts = {}) {
168
240
  // `<pid> <ts> <token>` is a superset of the old `<pid> <ts>`; a
169
241
  // tokenless (old-format) lock is still read as pid-authoritative.
170
242
  try { writeFileSync(fd, `${process.pid} ${Date.now()} ${_OWNER_TOKEN}\n`, 'utf8'); } catch {}
243
+ _reportLockWait(lockPath, Date.now() - waitStartedAt, 'sync');
171
244
  // For secret-bearing critical sections, the lock file sits beside
172
245
  // the secret in the same (shared-home) directory; clamp it owner-only
173
246
  // too. Fail-closed: an unenforceable ACL aborts before fn() runs.
@@ -362,6 +435,7 @@ async function _withOsFileLock(lockPath, fn, opts = {}) {
362
435
  const staleMs = Number.isFinite(opts.staleMs) ? opts.staleMs : 30000;
363
436
  const deadline = Date.now() + timeoutMs;
364
437
  mkdirSync(dirname(lockPath), { recursive: true });
438
+ const waitStartedAt = Date.now();
365
439
  let attempt = 0;
366
440
  let lastErr = null;
367
441
  while (true) {
@@ -393,6 +467,7 @@ async function _withOsFileLock(lockPath, fn, opts = {}) {
393
467
  continue;
394
468
  }
395
469
  try { writeFileSync(fd, `${process.pid} ${Date.now()} ${_OWNER_TOKEN}\n`, 'utf8'); } catch {}
470
+ _reportLockWait(lockPath, Date.now() - waitStartedAt, 'async');
396
471
  _osHeldPaths.add(lockPath);
397
472
  try {
398
473
  // Async ACL enforcement (promisified execFile) so a secret-bearing
@@ -738,10 +813,11 @@ export async function writeFileAtomicAsync(filePath, data, opts = {}) {
738
813
  throw err;
739
814
  }
740
815
  };
741
- if (opts.lock === true) {
742
- return withFileLock(`${filePath}.lock`, run, opts);
743
- }
744
- return run();
816
+ return _runAdaptiveFileIo(() => (
817
+ opts.lock === true
818
+ ? withFileLock(`${filePath}.lock`, run, opts)
819
+ : run()
820
+ ));
745
821
  }
746
822
 
747
823
  export function writeJsonAtomicAsync(filePath, value, opts = {}) {
@@ -7,8 +7,8 @@ export function channelNotificationModelContent(params = {}) {
7
7
  }
8
8
 
9
9
  export function shouldMirrorChannelNotificationToPending(meta = {}) {
10
- // CC parity (reference messageQueueManager.ts): every inbound channel/
11
- // schedule notification flows through the single queue — when no live
10
+ // Every inbound channel/schedule notification flows through the single
11
+ // queue — when no live
12
12
  // listener handled it (engine not subscribed yet, pane disposed, headless),
13
13
  // it is mirrored into the session pending queue for the next turn instead
14
14
  // of being dropped. Silent-to-agent stays excluded; the upstream content
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  import { spawn } from 'node:child_process';
4
+ import { randomUUID } from 'node:crypto';
4
5
  import { detachedSpawnOpts } from './spawn-flags.mjs';
5
6
 
6
7
  function positiveInt(value) {
@@ -8,13 +9,6 @@ function positiveInt(value) {
8
9
  return Number.isInteger(n) && n > 0 ? n : null;
9
10
  }
10
11
 
11
- const DEFAULT_MIN_FREE_MEMORY_MB = 1024;
12
-
13
- export function childGuardianMemoryFloorMb(env = process.env) {
14
- const n = Math.floor(Number(env.MIXDOG_MIN_FREE_MEMORY_MB));
15
- return Number.isFinite(n) && n >= 0 ? n : DEFAULT_MIN_FREE_MEMORY_MB;
16
- }
17
-
18
12
  export function childGuardianSpawnEnv(env = process.env) {
19
13
  return {
20
14
  PATH: env.PATH || '',
@@ -29,40 +23,25 @@ export function childGuardianSpawnEnv(env = process.env) {
29
23
  };
30
24
  }
31
25
 
32
- function guardianScript({
33
- parentPid,
34
- childPid,
35
- childGroupPid,
36
- platform,
37
- pollMs,
38
- orphanGraceMs,
39
- forceGraceMs,
40
- minFreeMemoryMb,
41
- receiptPath,
42
- }) {
26
+ function guardianBrokerScript() {
43
27
  return `
44
28
  const { spawnSync } = require('node:child_process');
45
- const { freemem } = require('node:os');
46
- const parentPid = ${JSON.stringify(parentPid)};
47
- const childPid = ${JSON.stringify(childPid)};
48
- const childGroupPid = ${JSON.stringify(childGroupPid || childPid)};
49
- const platform = ${JSON.stringify(platform)};
50
- const pollMs = ${JSON.stringify(pollMs)};
51
- const orphanGraceMs = ${JSON.stringify(orphanGraceMs)};
52
- const forceGraceMs = ${JSON.stringify(forceGraceMs)};
53
- const minFreeMemoryBytes = ${JSON.stringify(minFreeMemoryMb * 1024 * 1024)};
54
- const receiptPath = ${JSON.stringify(receiptPath || null)};
29
+ const platform = ${JSON.stringify(process.platform)};
30
+ const targets = new Map();
31
+ let input = '';
32
+ let everHadTarget = false;
33
+ let emptySince = 0;
55
34
  // Kill receipt: guardian kills are otherwise indistinguishable from a crashed
56
35
  // wrapper ("process exited without reporting an exit code"). Written BEFORE
57
36
  // the kill so the owner's next status refresh can attribute the death.
58
- function writeReceipt(reason, extra) {
59
- if (!receiptPath) return;
37
+ function writeReceipt(target, reason, extra) {
38
+ if (!target.receiptPath) return;
60
39
  try {
61
- require('node:fs').writeFileSync(receiptPath, JSON.stringify({
40
+ require('node:fs').writeFileSync(target.receiptPath, JSON.stringify({
62
41
  reason,
63
42
  at: new Date().toISOString(),
64
43
  guardianPid: process.pid,
65
- childPid,
44
+ childPid: target.childPid,
66
45
  ...extra,
67
46
  }));
68
47
  } catch {}
@@ -72,57 +51,186 @@ function alive(pid) {
72
51
  try { process.kill(pid, 0); return true; }
73
52
  catch (error) { return error && error.code === 'EPERM'; }
74
53
  }
75
- function killTarget(force) {
54
+ function killTarget(target, force) {
76
55
  if (platform === 'win32') {
77
56
  const systemRoot = process.env.SystemRoot || process.env.WINDIR || 'C:\\\\Windows';
78
57
  const taskkill = systemRoot + '\\\\System32\\\\taskkill.exe';
79
58
  if (force) {
80
- try { spawnSync(taskkill, ['/PID', String(childPid), '/T', '/F'], { stdio: 'ignore', windowsHide: true }); } catch {}
59
+ try { spawnSync(taskkill, ['/PID', String(target.childPid), '/T', '/F'], { stdio: 'ignore', windowsHide: true }); } catch {}
81
60
  } else {
82
- try { process.kill(childPid, 'SIGTERM'); } catch {}
83
- try { spawnSync(taskkill, ['/PID', String(childPid), '/T'], { stdio: 'ignore', windowsHide: true }); } catch {}
61
+ try { process.kill(target.childPid, 'SIGTERM'); } catch {}
62
+ try { spawnSync(taskkill, ['/PID', String(target.childPid), '/T'], { stdio: 'ignore', windowsHide: true }); } catch {}
84
63
  }
85
64
  return;
86
65
  }
87
66
  const signal = force ? 'SIGKILL' : 'SIGTERM';
88
- try { process.kill(-childGroupPid, signal); return; } catch {}
89
- try { process.kill(childPid, signal); } catch {}
67
+ try { process.kill(-target.childGroupPid, signal); return; } catch {}
68
+ try { process.kill(target.childPid, signal); } catch {}
69
+ }
70
+ function accept(message) {
71
+ if (!message || typeof message !== 'object') return;
72
+ const id = String(message.id || '');
73
+ if (!id) return;
74
+ if (message.type === 'remove') {
75
+ targets.delete(id);
76
+ return;
77
+ }
78
+ if (message.type !== 'add') return;
79
+ const childPid = Number(message.childPid);
80
+ const parentPid = Number(message.parentPid);
81
+ if (!Number.isInteger(childPid) || childPid <= 0
82
+ || !Number.isInteger(parentPid) || parentPid <= 0) return;
83
+ targets.set(id, {
84
+ id,
85
+ parentPid,
86
+ childPid,
87
+ childGroupPid: Number(message.childGroupPid) || childPid,
88
+ pollMs: Math.max(100, Number(message.pollMs) || 750),
89
+ orphanGraceMs: Math.max(100, Number(message.orphanGraceMs) || 3000),
90
+ forceGraceMs: Math.max(100, Number(message.forceGraceMs) || 3000),
91
+ receiptPath: typeof message.receiptPath === 'string' ? message.receiptPath : null,
92
+ orphanedAt: 0,
93
+ lastPolledAt: 0,
94
+ killing: false,
95
+ });
96
+ everHadTarget = true;
97
+ emptySince = 0;
90
98
  }
91
- let killing = false;
92
- let orphanedAt = 0;
99
+ process.stdin.setEncoding('utf8');
100
+ process.stdin.on('data', (chunk) => {
101
+ input += chunk;
102
+ let newline;
103
+ while ((newline = input.indexOf('\\n')) >= 0) {
104
+ const line = input.slice(0, newline);
105
+ input = input.slice(newline + 1);
106
+ try { accept(JSON.parse(line)); } catch {}
107
+ }
108
+ });
93
109
  const timer = setInterval(() => {
94
- if (!alive(childPid)) process.exit(0);
95
- // Admission protects only NEW work. A running shell can still consume the
96
- // host after it starts, so reclaim its entire tree before it starves the
97
- // owning Mixdog process. Force is intentional: at the memory floor there is
98
- // no safe grace window for a runaway allocator.
99
- if (!killing && minFreeMemoryBytes > 0) {
100
- let freeBytes = Number.POSITIVE_INFINITY;
101
- try { freeBytes = Number(freemem()); } catch {}
102
- if (Number.isFinite(freeBytes) && freeBytes < minFreeMemoryBytes) {
103
- killing = true;
104
- writeReceipt('host-memory-floor', { freeBytes, minFreeMemoryBytes });
105
- killTarget(true);
106
- process.exit(0);
110
+ const now = Date.now();
111
+ for (const [id, target] of targets) {
112
+ if (now - target.lastPolledAt < target.pollMs) continue;
113
+ target.lastPolledAt = now;
114
+ if (!alive(target.childPid)) {
115
+ targets.delete(id);
116
+ continue;
117
+ }
118
+ if (alive(target.parentPid)) {
119
+ target.orphanedAt = 0;
120
+ continue;
107
121
  }
122
+ if (!target.orphanedAt) target.orphanedAt = now;
123
+ if (target.killing || now - target.orphanedAt < target.orphanGraceMs) continue;
124
+ target.killing = true;
125
+ writeReceipt(target, 'parent-exit', {});
126
+ killTarget(target, false);
127
+ setTimeout(() => {
128
+ if (alive(target.childPid)) killTarget(target, true);
129
+ targets.delete(id);
130
+ }, target.forceGraceMs).unref?.();
108
131
  }
109
- // Orphaned when the parent PID dies. Matches mainstream CLIs: no console
110
- // probe — a hidden shell spawn (windowsHide) plus tree-kill on cleanup, so
111
- // orphan detection is pure parent-liveness.
112
- if (alive(parentPid)) {
113
- orphanedAt = 0;
114
- return;
132
+ // Pay for one Node supervisor only while it owns at least one live child.
133
+ // A short empty grace absorbs command-to-command reuse without keeping the
134
+ // broker resident for the whole daemon lifetime.
135
+ if (targets.size > 0) emptySince = 0;
136
+ else if (everHadTarget) {
137
+ if (!emptySince) emptySince = now;
138
+ else if (now - emptySince >= 1000) process.exit(0);
115
139
  }
116
- if (!orphanedAt) orphanedAt = Date.now();
117
- if (killing || Date.now() - orphanedAt < orphanGraceMs) return;
118
- killing = true;
119
- writeReceipt('parent-exit', {});
120
- killTarget(false);
121
- setTimeout(() => { if (alive(childPid)) killTarget(true); process.exit(0); }, forceGraceMs).unref?.();
122
- }, pollMs);
140
+ }, 100);
123
141
  `;
124
142
  }
125
143
 
144
+ let sharedBroker = null;
145
+ let brokerRestartTimer = null;
146
+ let brokerTargetSweepTimer = null;
147
+ const brokerTargets = new Map();
148
+
149
+ function sendBrokerMessage(message) {
150
+ try {
151
+ if (!sharedBroker?.stdin?.writable) return false;
152
+ sharedBroker.stdin.write(`${JSON.stringify(message)}\n`);
153
+ return true;
154
+ } catch {
155
+ return false;
156
+ }
157
+ }
158
+
159
+ function targetPidAlive(pid) {
160
+ try {
161
+ process.kill(pid, 0);
162
+ return true;
163
+ } catch (error) {
164
+ return error?.code === 'EPERM';
165
+ }
166
+ }
167
+
168
+ function stopBrokerTargetSweepIfIdle() {
169
+ if (brokerTargets.size > 0 || !brokerTargetSweepTimer) return;
170
+ clearInterval(brokerTargetSweepTimer);
171
+ brokerTargetSweepTimer = null;
172
+ }
173
+
174
+ function removeBrokerTarget(id, { notify = true } = {}) {
175
+ const target = brokerTargets.get(id);
176
+ if (!target) return false;
177
+ brokerTargets.delete(id);
178
+ try { target.onRemoved?.(); } catch {}
179
+ if (notify) sendBrokerMessage({ type: 'remove', id });
180
+ stopBrokerTargetSweepIfIdle();
181
+ return true;
182
+ }
183
+
184
+ function ensureBrokerTargetSweep() {
185
+ if (brokerTargetSweepTimer) return;
186
+ brokerTargetSweepTimer = setInterval(() => {
187
+ for (const [id, target] of brokerTargets) {
188
+ if (!targetPidAlive(target.childPid)) removeBrokerTarget(id);
189
+ }
190
+ stopBrokerTargetSweepIfIdle();
191
+ }, 250);
192
+ brokerTargetSweepTimer.unref?.();
193
+ }
194
+
195
+ function ensureSharedBroker() {
196
+ if (sharedBroker && sharedBroker.exitCode == null && sharedBroker.signalCode == null) {
197
+ return sharedBroker;
198
+ }
199
+ try {
200
+ const broker = spawn(process.execPath, [
201
+ '--no-warnings',
202
+ '--eval',
203
+ guardianBrokerScript(),
204
+ ], {
205
+ stdio: ['pipe', 'ignore', 'ignore'],
206
+ env: childGuardianSpawnEnv(),
207
+ ...detachedSpawnOpts,
208
+ });
209
+ sharedBroker = broker;
210
+ broker.unref?.();
211
+ broker.stdin?.unref?.();
212
+ broker.stdin?.on?.('error', () => {});
213
+ broker.once('exit', () => {
214
+ if (sharedBroker !== broker) return;
215
+ sharedBroker = null;
216
+ if (brokerTargets.size > 0 && !brokerRestartTimer) {
217
+ brokerRestartTimer = setTimeout(() => {
218
+ brokerRestartTimer = null;
219
+ const replacement = ensureSharedBroker();
220
+ if (!replacement) return;
221
+ for (const target of brokerTargets.values()) sendBrokerMessage(target);
222
+ }, 100);
223
+ brokerRestartTimer.unref?.();
224
+ }
225
+ });
226
+ for (const target of brokerTargets.values()) sendBrokerMessage(target);
227
+ return broker;
228
+ } catch {
229
+ sharedBroker = null;
230
+ return null;
231
+ }
232
+ }
233
+
126
234
  export function startChildGuardian({
127
235
  parentPid = process.pid,
128
236
  childPid,
@@ -132,53 +240,41 @@ export function startChildGuardian({
132
240
  graceMs = 3000,
133
241
  orphanGraceMs = graceMs,
134
242
  forceGraceMs = graceMs,
135
- protectHostMemory = false,
136
- minFreeMemoryMb = protectHostMemory ? childGuardianMemoryFloorMb() : 0,
137
243
  receiptPath = null,
138
244
  } = {}) {
139
245
  const parent = positiveInt(parentPid);
140
246
  const child = positiveInt(childPid);
141
247
  if (!parent || !child || parent === child) return null;
142
- const memoryFloorMb = Math.max(0, Math.floor(Number(minFreeMemoryMb) || 0));
143
-
144
- try {
145
- const guardian = spawn(process.execPath, [
146
- '--no-warnings',
147
- '--eval',
148
- guardianScript({
149
- parentPid: parent,
150
- childPid: child,
151
- childGroupPid: positiveInt(childGroupPid) || child,
152
- platform: process.platform,
153
- pollMs: Math.max(100, Math.floor(Number(pollMs) || 750)),
154
- orphanGraceMs: Math.max(100, Math.floor(Number(orphanGraceMs) || Number(graceMs) || 3000)),
155
- forceGraceMs: Math.max(100, Math.floor(Number(forceGraceMs) || Number(graceMs) || 3000)),
156
- minFreeMemoryMb: memoryFloorMb,
157
- receiptPath: typeof receiptPath === 'string' && receiptPath ? receiptPath : null,
158
- }),
159
- ], {
160
- stdio: 'ignore',
161
- env: childGuardianSpawnEnv(),
162
- ...detachedSpawnOpts,
163
- });
164
- guardian.unref?.();
165
- let stopped = false;
166
- return {
167
- pid: guardian.pid || null,
168
- label,
169
- childPid: child,
170
- stop() {
171
- if (stopped) return false;
172
- stopped = true;
173
- try {
174
- guardian.kill();
175
- return true;
176
- } catch {
177
- return false;
178
- }
179
- },
180
- };
181
- } catch {
248
+ const id = randomUUID();
249
+ let stopped = false;
250
+ const target = {
251
+ type: 'add',
252
+ id,
253
+ parentPid: parent,
254
+ childPid: child,
255
+ childGroupPid: positiveInt(childGroupPid) || child,
256
+ pollMs: Math.max(100, Math.floor(Number(pollMs) || 750)),
257
+ orphanGraceMs: Math.max(100, Math.floor(Number(orphanGraceMs) || Number(graceMs) || 3000)),
258
+ forceGraceMs: Math.max(100, Math.floor(Number(forceGraceMs) || Number(graceMs) || 3000)),
259
+ receiptPath: typeof receiptPath === 'string' && receiptPath ? receiptPath : null,
260
+ onRemoved: () => { stopped = true; },
261
+ };
262
+ brokerTargets.set(id, target);
263
+ ensureBrokerTargetSweep();
264
+ const broker = ensureSharedBroker();
265
+ if (!broker) {
266
+ removeBrokerTarget(id, { notify: false });
182
267
  return null;
183
268
  }
269
+ sendBrokerMessage(target);
270
+ return {
271
+ pid: broker.pid || null,
272
+ label,
273
+ childPid: child,
274
+ stop() {
275
+ if (stopped) return false;
276
+ stopped = true;
277
+ return removeBrokerTarget(id);
278
+ },
279
+ };
184
280
  }