mixdog 0.8.0 → 0.9.0

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 (285) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +344 -0
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1223 -95
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2325 -450
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +118 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
  114. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  116. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  117. package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
  118. package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
  119. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  120. package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
  121. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  123. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  124. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  125. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  126. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  128. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  129. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  130. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  132. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  133. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  139. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  140. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  141. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  142. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  143. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  144. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
  145. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  146. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  147. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  148. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  149. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  150. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  151. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  152. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  155. package/src/runtime/channels/index.mjs +152 -24
  156. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  157. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  158. package/src/runtime/channels/lib/tool-format.mjs +0 -2
  159. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  160. package/src/runtime/channels/lib/webhook.mjs +1 -1
  161. package/src/runtime/channels/tool-defs.mjs +29 -29
  162. package/src/runtime/memory/index.mjs +635 -107
  163. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  164. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  165. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  166. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  167. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  168. package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
  169. package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
  170. package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
  171. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  172. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  173. package/src/runtime/memory/lib/memory.mjs +88 -9
  174. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  175. package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  177. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  178. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  179. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  180. package/src/runtime/memory/tool-defs.mjs +46 -37
  181. package/src/runtime/search/index.mjs +102 -466
  182. package/src/runtime/search/lib/web-tools.mjs +45 -25
  183. package/src/runtime/search/tool-defs.mjs +16 -23
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/atomic-file.mjs +4 -3
  186. package/src/runtime/shared/background-tasks.mjs +122 -11
  187. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  188. package/src/runtime/shared/config.mjs +7 -4
  189. package/src/runtime/shared/err-text.mjs +131 -4
  190. package/src/runtime/shared/llm/cost.mjs +2 -2
  191. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  192. package/src/runtime/shared/llm/index.mjs +34 -11
  193. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  194. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  195. package/src/runtime/shared/singleton-owner.mjs +104 -0
  196. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  197. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  198. package/src/runtime/shared/tool-surface.mjs +624 -98
  199. package/src/runtime/shared/user-data-guard.mjs +0 -2
  200. package/src/standalone/agent-task-status.mjs +203 -0
  201. package/src/standalone/agent-task-status.test.mjs +76 -0
  202. package/src/standalone/agent-tool.mjs +1913 -0
  203. package/src/standalone/channel-worker.mjs +370 -14
  204. package/src/standalone/explore-tool.mjs +165 -70
  205. package/src/standalone/folder-dialog.mjs +314 -0
  206. package/src/standalone/hook-bus.mjs +898 -22
  207. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  208. package/src/standalone/projects.mjs +226 -0
  209. package/src/standalone/provider-admin.mjs +41 -24
  210. package/src/standalone/seeds.mjs +2 -69
  211. package/src/standalone/usage-dashboard.mjs +96 -8
  212. package/src/tui/App.jsx +4798 -2153
  213. package/src/tui/components/AnsiText.jsx +39 -28
  214. package/src/tui/components/ContextPanel.jsx +87 -29
  215. package/src/tui/components/Markdown.jsx +43 -77
  216. package/src/tui/components/MarkdownTable.jsx +9 -184
  217. package/src/tui/components/Message.jsx +28 -11
  218. package/src/tui/components/Picker.jsx +95 -56
  219. package/src/tui/components/PromptInput.jsx +367 -239
  220. package/src/tui/components/QueuedCommands.jsx +1 -1
  221. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  222. package/src/tui/components/Spinner.jsx +67 -38
  223. package/src/tui/components/StatusLine.jsx +606 -38
  224. package/src/tui/components/TextEntryPanel.jsx +128 -9
  225. package/src/tui/components/ToolExecution.jsx +617 -368
  226. package/src/tui/components/TurnDone.jsx +3 -3
  227. package/src/tui/components/UsagePanel.jsx +3 -5
  228. package/src/tui/components/tool-output-format.mjs +365 -0
  229. package/src/tui/components/tool-output-format.test.mjs +220 -0
  230. package/src/tui/dist/index.mjs +8915 -2418
  231. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  232. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  233. package/src/tui/engine.mjs +1455 -279
  234. package/src/tui/figures.mjs +21 -40
  235. package/src/tui/index.jsx +75 -31
  236. package/src/tui/input-editing.mjs +25 -0
  237. package/src/tui/markdown/format-token.mjs +511 -68
  238. package/src/tui/markdown/format-token.test.mjs +216 -0
  239. package/src/tui/markdown/render-ansi.mjs +94 -0
  240. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  241. package/src/tui/markdown/stream-fence.mjs +34 -0
  242. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  243. package/src/tui/markdown/table-layout.mjs +250 -0
  244. package/src/tui/paste-attachments.mjs +0 -7
  245. package/src/tui/spinner-verbs.mjs +1 -2
  246. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  247. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  248. package/src/tui/theme.mjs +746 -24
  249. package/src/tui/time-format.mjs +1 -1
  250. package/src/tui/transcript-tool-failures.mjs +67 -0
  251. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  252. package/src/ui/ansi.mjs +1 -2
  253. package/src/ui/markdown.mjs +85 -26
  254. package/src/ui/markdown.test.mjs +70 -0
  255. package/src/ui/model-display.mjs +121 -0
  256. package/src/ui/session-stats.mjs +44 -0
  257. package/src/ui/statusline-context-label.test.mjs +15 -0
  258. package/src/ui/statusline.mjs +386 -178
  259. package/src/ui/tool-card.mjs +3 -16
  260. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  261. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  262. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  263. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  264. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  265. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  266. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  267. package/src/workflows/default/WORKFLOW.md +12 -5
  268. package/src/workflows/default/workflow.json +0 -1
  269. package/src/workflows/solo/WORKFLOW.md +15 -0
  270. package/src/workflows/solo/workflow.json +7 -0
  271. package/vendor/ink/build/output.js +6 -1
  272. package/src/agents/scheduler-task.md +0 -3
  273. package/src/agents/web-researcher/AGENT.md +0 -3
  274. package/src/agents/web-researcher/agent.json +0 -6
  275. package/src/agents/webhook-handler.md +0 -3
  276. package/src/rules/bridge/00-common.md +0 -5
  277. package/src/rules/bridge/30-explorer.md +0 -4
  278. package/src/rules/lead/00-tool-lead.md +0 -5
  279. package/src/rules/shared/00-language.md +0 -3
  280. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  281. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  282. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  283. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  284. package/src/standalone/bridge-tool.mjs +0 -1414
  285. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -1,8 +1,11 @@
1
1
  import { execFile, fork, spawnSync } from 'node:child_process';
2
- import { appendFileSync, mkdirSync } from 'node:fs';
3
- import { dirname, join } from 'node:path';
2
+ import { appendFileSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
3
+ import http from 'node:http';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join, resolve } from 'node:path';
4
6
  import { fileURLToPath, pathToFileURL } from 'node:url';
5
7
  import { startChildGuardian } from '../runtime/shared/child-guardian.mjs';
8
+ import { claimSingletonOwner, readSingletonOwner, releaseSingletonOwner } from '../runtime/shared/singleton-owner.mjs';
6
9
 
7
10
  const CHANNEL_TOOLS = new Set([
8
11
  'reply',
@@ -29,6 +32,91 @@ function logLine(path, line) {
29
32
  }
30
33
  }
31
34
 
35
+ const CHANNEL_WORKER_EXIT_CLEANUPS = new Set();
36
+ let channelWorkerExitHookInstalled = false;
37
+
38
+ function registerChannelWorkerExitCleanup(cleanup) {
39
+ if (typeof cleanup !== 'function') return () => {};
40
+ CHANNEL_WORKER_EXIT_CLEANUPS.add(cleanup);
41
+ if (!channelWorkerExitHookInstalled) {
42
+ channelWorkerExitHookInstalled = true;
43
+ process.once('exit', () => {
44
+ for (const fn of Array.from(CHANNEL_WORKER_EXIT_CLEANUPS)) {
45
+ try { fn(); } catch {}
46
+ }
47
+ CHANNEL_WORKER_EXIT_CLEANUPS.clear();
48
+ });
49
+ }
50
+ return () => {
51
+ CHANNEL_WORKER_EXIT_CLEANUPS.delete(cleanup);
52
+ };
53
+ }
54
+
55
+ function runtimeRoot() {
56
+ return process.env.MIXDOG_RUNTIME_ROOT ? resolve(process.env.MIXDOG_RUNTIME_ROOT) : join(tmpdir(), 'mixdog');
57
+ }
58
+
59
+ function readJson(path) {
60
+ try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
61
+ }
62
+
63
+ function parsePort(value) {
64
+ const port = Number(value);
65
+ return Number.isInteger(port) && port > 0 && port < 65536 ? port : null;
66
+ }
67
+
68
+ function readActiveInstance() {
69
+ return readJson(join(runtimeRoot(), 'active-instance.json'));
70
+ }
71
+
72
+ function readOwnerSecret(instanceId) {
73
+ const parsed = readJson(join(runtimeRoot(), `owner-secret-${String(instanceId)}.json`));
74
+ return typeof parsed?.secret === 'string' ? parsed.secret : '';
75
+ }
76
+
77
+ function requestJson({ port, method = 'GET', path = '/', body = null, headers = {}, timeoutMs = 120_000 }) {
78
+ return new Promise((resolvePromise, reject) => {
79
+ const payload = body == null ? null : JSON.stringify(body);
80
+ const req = http.request({
81
+ hostname: '127.0.0.1',
82
+ port,
83
+ path,
84
+ method,
85
+ headers: {
86
+ ...headers,
87
+ ...(payload
88
+ ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }
89
+ : {}),
90
+ },
91
+ timeout: timeoutMs,
92
+ }, (res) => {
93
+ let data = '';
94
+ res.setEncoding('utf8');
95
+ res.on('data', chunk => { data += chunk; });
96
+ res.on('end', () => {
97
+ let parsed = null;
98
+ try { parsed = data ? JSON.parse(data) : null; } catch {}
99
+ if (res.statusCode && res.statusCode >= 400) {
100
+ reject(new Error(parsed?.error || data || `HTTP ${res.statusCode}`));
101
+ return;
102
+ }
103
+ resolvePromise(parsed ?? { raw: data });
104
+ });
105
+ });
106
+ req.on('error', reject);
107
+ req.on('timeout', () => {
108
+ req.destroy();
109
+ reject(new Error(`channel owner request timed out: ${method} ${path}`));
110
+ });
111
+ if (payload) req.write(payload);
112
+ req.end();
113
+ });
114
+ }
115
+
116
+ function textResult(text, isError = false) {
117
+ return { content: [{ type: 'text', text: String(text ?? '') }], ...(isError ? { isError: true } : {}) };
118
+ }
119
+
32
120
  export function createStandaloneChannelWorker({
33
121
  entry,
34
122
  rootDir,
@@ -48,13 +136,106 @@ export function createStandaloneChannelWorker({
48
136
  let inProcessMod = null;
49
137
  let inProcessStartPromise = null;
50
138
  let nextCallId = 1;
51
- let parentExitHookInstalled = false;
139
+ let parentExitCleanup = null;
52
140
  const pending = new Map();
53
141
  const ownedChildPids = new Set();
54
142
  const logPath = join(dataDir, 'channels-worker-standalone.log');
55
143
  const useProcessWorker = process.env.MIXDOG_CHANNEL_WORKER_PROCESS !== '0';
144
+ const singletonEnabled = process.env.MIXDOG_CHANNEL_SINGLETON !== '0';
145
+ const daemonEnabled = process.env.MIXDOG_CHANNEL_DAEMON !== '0';
146
+ const ownerPath = join(dataDir, 'channels-worker-owner.json');
147
+ const notifyBusPath = join(runtimeRoot(), 'channel-notifications.jsonl');
148
+ const clientDir = join(runtimeRoot(), 'channel-clients');
149
+ const clientPath = join(clientDir, `${process.pid}.json`);
150
+ let ownerClaim = null;
151
+ let ownerPid = process.pid;
152
+ let notifyBusTimer = null;
153
+ let notifyBusOffset = 0;
154
+ let clientHeartbeatTimer = null;
155
+ let clientHeartbeatExitCleanup = null;
156
+
157
+ function writeClientHeartbeat() {
158
+ try {
159
+ mkdirSync(clientDir, { recursive: true });
160
+ writeFileSync(clientPath, JSON.stringify({
161
+ pid: process.pid,
162
+ cwd,
163
+ updatedAt: Date.now(),
164
+ }));
165
+ } catch {}
166
+ }
167
+
168
+ function startClientHeartbeat() {
169
+ if (clientHeartbeatTimer) return;
170
+ writeClientHeartbeat();
171
+ clientHeartbeatTimer = setInterval(writeClientHeartbeat, 5000);
172
+ clientHeartbeatTimer.unref?.();
173
+ clientHeartbeatExitCleanup ||= registerChannelWorkerExitCleanup(stopClientHeartbeat);
174
+ }
175
+
176
+ function stopClientHeartbeat() {
177
+ if (clientHeartbeatExitCleanup) {
178
+ const unregister = clientHeartbeatExitCleanup;
179
+ clientHeartbeatExitCleanup = null;
180
+ unregister();
181
+ }
182
+ if (clientHeartbeatTimer) {
183
+ clearInterval(clientHeartbeatTimer);
184
+ clientHeartbeatTimer = null;
185
+ }
186
+ try { rmSync(clientPath, { force: true }); } catch {}
187
+ }
188
+
189
+ function startNotifyBus() {
190
+ if (!onNotify || notifyBusTimer) return;
191
+ try { notifyBusOffset = statSync(notifyBusPath).size; } catch { notifyBusOffset = 0; }
192
+ notifyBusTimer = setInterval(pollNotifyBus, 500);
193
+ notifyBusTimer.unref?.();
194
+ }
195
+
196
+ function stopNotifyBus() {
197
+ if (!notifyBusTimer) return;
198
+ clearInterval(notifyBusTimer);
199
+ notifyBusTimer = null;
200
+ }
201
+
202
+ function pollNotifyBus() {
203
+ let size = 0;
204
+ try { size = statSync(notifyBusPath).size; } catch { return; }
205
+ if (size < notifyBusOffset) notifyBusOffset = 0;
206
+ if (size === notifyBusOffset) return;
207
+ let chunk;
208
+ try {
209
+ const data = readFileSync(notifyBusPath);
210
+ chunk = data.subarray(notifyBusOffset, size).toString('utf8');
211
+ notifyBusOffset = size;
212
+ } catch {
213
+ return;
214
+ }
215
+ for (const line of chunk.split(/\r?\n/)) {
216
+ if (!line.trim()) continue;
217
+ try {
218
+ const msg = JSON.parse(line);
219
+ if (!msg || typeof msg !== 'object') continue;
220
+ onNotify({ type: 'notify', method: msg.method, params: msg.params, busId: msg.id, busPid: msg.pid });
221
+ } catch {}
222
+ }
223
+ }
224
+
225
+ startNotifyBus();
226
+ startClientHeartbeat();
56
227
 
57
228
  function status() {
229
+ const external = externalOwner();
230
+ if (external) {
231
+ return {
232
+ running: false,
233
+ pid: null,
234
+ pending: pending.size,
235
+ mode: 'external-owner',
236
+ ownerPid: Number(external.pid) || null,
237
+ };
238
+ }
58
239
  if (!useProcessWorker) {
59
240
  return {
60
241
  running: Boolean(inProcessMod),
@@ -71,6 +252,36 @@ export function createStandaloneChannelWorker({
71
252
  };
72
253
  }
73
254
 
255
+ function externalOwner() {
256
+ if (!singletonEnabled || ownerClaim?.owned) return null;
257
+ const current = readSingletonOwner(ownerPath);
258
+ if (current.alive && current.owner?.pid && Number(current.owner.pid) !== process.pid) return current.owner;
259
+ return null;
260
+ }
261
+
262
+ function ensureOwner() {
263
+ if (!singletonEnabled || ownerClaim?.owned) return true;
264
+ ownerClaim = claimSingletonOwner(ownerPath, {
265
+ kind: 'channel-worker-host',
266
+ pid: process.pid,
267
+ meta: { cwd },
268
+ });
269
+ if (!ownerClaim.owned) {
270
+ logLine(logPath, `runtime owned by pid ${ownerClaim.owner?.pid || 'unknown'}; skipping local channel worker`);
271
+ return false;
272
+ }
273
+ installParentExitHook();
274
+ return true;
275
+ }
276
+
277
+ function releaseOwner() {
278
+ if (!singletonEnabled || !ownerClaim?.owned) return;
279
+ releaseSingletonOwner(ownerPath, ownerPid);
280
+ ownerClaim = null;
281
+ ownerPid = process.pid;
282
+ uninstallParentExitHook();
283
+ }
284
+
74
285
  function rejectPending(error) {
75
286
  for (const [, item] of pending) {
76
287
  clearTimeout(item.timer);
@@ -81,6 +292,7 @@ export function createStandaloneChannelWorker({
81
292
 
82
293
  function start() {
83
294
  if (!useProcessWorker) return startInProcess();
295
+ if (!ensureOwner()) return Promise.resolve(status());
84
296
  if (stopPromise) {
85
297
  return stopPromise.then(() => start());
86
298
  }
@@ -94,13 +306,15 @@ export function createStandaloneChannelWorker({
94
306
  cwd,
95
307
  execArgv: ['--require', WORKER_PRELOAD],
96
308
  stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
309
+ detached: daemonEnabled,
97
310
  env: {
98
311
  ...process.env,
99
312
  MIXDOG_ROOT: rootDir,
100
313
  MIXDOG_DATA_DIR: dataDir,
101
314
  MIXDOG_STANDALONE: '1',
102
315
  MIXDOG_WORKER_MODE: '1',
103
- MIXDOG_CLI_OWNED: '1',
316
+ MIXDOG_CLI_OWNED: daemonEnabled ? '0' : '1',
317
+ MIXDOG_CHANNEL_DAEMON: daemonEnabled ? '1' : '0',
104
318
  MIXDOG_CHANNELS_AUTO_BOOT: '0',
105
319
  MIXDOG_CHANNEL_FLAG: '1',
106
320
  MIXDOG_QUIET_SESSION_LOG: process.env.MIXDOG_QUIET_SESSION_LOG ?? '1',
@@ -108,8 +322,19 @@ export function createStandaloneChannelWorker({
108
322
  windowsHide: true,
109
323
  });
110
324
  const spawnedPid = child.pid;
111
- startChildGuardian({ childPid: spawnedPid, label: 'channel-worker', orphanGraceMs: 8000, forceGraceMs: 3000 });
112
- if (spawnedPid) ownedChildPids.add(spawnedPid);
325
+ if (daemonEnabled && spawnedPid && singletonEnabled) {
326
+ releaseSingletonOwner(ownerPath, process.pid);
327
+ ownerClaim = claimSingletonOwner(ownerPath, {
328
+ kind: 'channel-worker-daemon',
329
+ pid: spawnedPid,
330
+ meta: { cwd, launcherPid: process.pid },
331
+ });
332
+ ownerPid = spawnedPid;
333
+ }
334
+ if (!daemonEnabled) {
335
+ startChildGuardian({ childPid: spawnedPid, label: 'channel-worker', orphanGraceMs: 8000, forceGraceMs: 3000 });
336
+ if (spawnedPid) ownedChildPids.add(spawnedPid);
337
+ }
113
338
  installParentExitHook();
114
339
 
115
340
  child.stderr?.on('data', (chunk) => {
@@ -135,12 +360,19 @@ export function createStandaloneChannelWorker({
135
360
  return;
136
361
  }
137
362
  if (msg.type === 'notify') {
138
- try { onNotify?.(msg); } catch {}
363
+ if (!daemonEnabled) {
364
+ try { onNotify?.(msg); } catch {}
365
+ }
139
366
  }
140
367
  });
141
368
 
142
369
  child.on('exit', (code, signal) => {
143
370
  if (spawnedPid) ownedChildPids.delete(spawnedPid);
371
+ if (daemonEnabled && spawnedPid) releaseSingletonOwner(ownerPath, spawnedPid);
372
+ if (daemonEnabled && ownerPid === spawnedPid) {
373
+ ownerClaim = null;
374
+ ownerPid = process.pid;
375
+ }
144
376
  const error = new Error(`channels runtime exited (${signal || (code ?? 'unknown')})`);
145
377
  if (readyReject) readyReject(error);
146
378
  readyResolve = null;
@@ -162,6 +394,7 @@ export function createStandaloneChannelWorker({
162
394
  }
163
395
 
164
396
  async function startInProcess() {
397
+ if (!ensureOwner()) return status();
165
398
  if (inProcessMod) return status();
166
399
  if (inProcessStartPromise) return inProcessStartPromise;
167
400
  inProcessStartPromise = (async () => {
@@ -184,6 +417,9 @@ export function createStandaloneChannelWorker({
184
417
 
185
418
  async function execute(name, args = {}, { timeoutMs = 120_000 } = {}) {
186
419
  if (!CHANNEL_TOOLS.has(name)) throw new Error(`unknown channel tool: ${name}`);
420
+ if (!ensureOwner()) {
421
+ return await executeViaOwnerHttp(name, args || {}, timeoutMs);
422
+ }
187
423
  await start();
188
424
  if (!useProcessWorker) {
189
425
  const call = inProcessMod?.handleToolCallWithBridgeRetry || inProcessMod?.handleToolCall;
@@ -217,6 +453,91 @@ export function createStandaloneChannelWorker({
217
453
  });
218
454
  }
219
455
 
456
+ async function executeViaOwnerHttp(name, args, timeoutMs) {
457
+ const active = readActiveInstance();
458
+ const port = parsePort(active?.httpPort);
459
+ const instanceId = active?.instanceId;
460
+ const token = readOwnerSecret(instanceId);
461
+ if (!port || !token) {
462
+ const owner = externalOwner();
463
+ throw new Error(`channels runtime is owned by pid ${owner?.pid || 'unknown'} but owner HTTP is unavailable`);
464
+ }
465
+ const headers = {
466
+ 'x-owner-token': token,
467
+ 'x-owner-instance': String(instanceId || ''),
468
+ };
469
+ const post = (path, body) => requestJson({ port, method: 'POST', path, body, headers, timeoutMs });
470
+ const get = (path) => requestJson({ port, method: 'GET', path, headers, timeoutMs });
471
+
472
+ switch (name) {
473
+ case 'reply': {
474
+ const result = await post('/send', {
475
+ chatId: args.chat_id,
476
+ text: args.text,
477
+ opts: {
478
+ replyTo: args.reply_to,
479
+ files: args.files ?? [],
480
+ embeds: args.embeds ?? [],
481
+ components: args.components ?? [],
482
+ },
483
+ });
484
+ const ids = Array.isArray(result?.sentIds) ? result.sentIds : [];
485
+ return textResult(ids.length === 1 ? `sent (id: ${ids[0]})` : `sent ${ids.length} parts (ids: ${ids.join(', ')})`);
486
+ }
487
+ case 'fetch': {
488
+ const channel = encodeURIComponent(String(args.channel || ''));
489
+ const limit = Math.max(1, Number(args.limit) || 20);
490
+ const result = await get(`/fetch?channel=${channel}&limit=${limit}`);
491
+ const msgs = Array.isArray(result?.messages) ? result.messages : [];
492
+ const text = msgs.length === 0 ? '(no messages)' : msgs.map((m) => {
493
+ const atts = Number(m.attachmentCount) > 0 ? ` +${m.attachmentCount}att` : '';
494
+ return `[${m.ts}] ${m.user}: ${m.text} (id: ${m.id}${atts})`;
495
+ }).join('\n');
496
+ return textResult(text);
497
+ }
498
+ case 'react':
499
+ await post('/react', { chatId: args.chat_id, messageId: args.message_id, emoji: args.emoji });
500
+ return textResult('reacted');
501
+ case 'edit_message': {
502
+ const result = await post('/edit', {
503
+ chatId: args.chat_id,
504
+ messageId: args.message_id,
505
+ text: args.text,
506
+ opts: { embeds: args.embeds ?? [], components: args.components ?? [] },
507
+ });
508
+ return textResult(`edited (id: ${result?.id ?? ''})`);
509
+ }
510
+ case 'download_attachment': {
511
+ const result = await post('/download', { chatId: args.chat_id, messageId: args.message_id });
512
+ const files = Array.isArray(result?.files) ? result.files : [];
513
+ if (files.length === 0) return textResult('message has no attachments');
514
+ return textResult(`downloaded ${files.length} attachment(s):\n${files.map((f) => ` ${f.path} (${f.name}, ${f.contentType}, ${(Number(f.size || 0) / 1024).toFixed(0)}KB)`).join('\n')}`);
515
+ }
516
+ case 'schedule_status': {
517
+ const result = await get('/schedule-status');
518
+ return result?.result ?? textResult('no schedules configured');
519
+ }
520
+ case 'trigger_schedule': {
521
+ const result = await post('/trigger-schedule', { name: args.name });
522
+ return textResult(result?.result == null ? '' : String(result.result));
523
+ }
524
+ case 'schedule_control': {
525
+ const result = await post('/schedule-control', { name: args.name, action: args.action, minutes: args.minutes });
526
+ return result?.result ?? textResult(`unknown action: ${args.action}`, true);
527
+ }
528
+ case 'activate_channel_bridge':
529
+ await post('/bridge/activate', { active: args.active === true });
530
+ return textResult(`channel bridge ${args.active ? 'activated' : 'deactivated'}`);
531
+ case 'inject_command':
532
+ await post('/inject', { content: `/${String(args.command || '').trim()}`, source: 'mixdog-agent', type: 'command' });
533
+ return textResult(`queued /${String(args.command || '').trim()} via channel owner`);
534
+ case 'reload_config':
535
+ return textResult('reload_config must run in the channel owner process', true);
536
+ default:
537
+ return textResult(`unknown channel tool: ${name}`, true);
538
+ }
539
+ }
540
+
220
541
  function forceKillTree(pid) {
221
542
  if (!pid) return;
222
543
  if (process.platform === 'win32') {
@@ -241,16 +562,26 @@ export function createStandaloneChannelWorker({
241
562
  }
242
563
 
243
564
  function installParentExitHook() {
244
- if (parentExitHookInstalled) return;
245
- parentExitHookInstalled = true;
246
- process.once('exit', () => {
247
- for (const pid of Array.from(ownedChildPids)) {
248
- forceKillTreeSync(pid);
565
+ if (parentExitCleanup) return;
566
+ parentExitCleanup = registerChannelWorkerExitCleanup(() => {
567
+ parentExitCleanup = null;
568
+ if (!daemonEnabled) {
569
+ for (const pid of Array.from(ownedChildPids)) {
570
+ forceKillTreeSync(pid);
571
+ }
572
+ releaseOwner();
249
573
  }
250
574
  ownedChildPids.clear();
251
575
  });
252
576
  }
253
577
 
578
+ function uninstallParentExitHook() {
579
+ if (!parentExitCleanup) return;
580
+ const unregister = parentExitCleanup;
581
+ parentExitCleanup = null;
582
+ unregister();
583
+ }
584
+
254
585
  function unrefChildHandles(target) {
255
586
  try { target?.unref?.(); } catch {}
256
587
  try { target?.stderr?.unref?.(); } catch {}
@@ -261,14 +592,20 @@ export function createStandaloneChannelWorker({
261
592
 
262
593
  function stop(reason = 'standalone shutdown', options = {}) {
263
594
  const waitForExit = options?.waitForExit !== false;
595
+ stopNotifyBus();
596
+ stopClientHeartbeat();
264
597
  if (stopPromise) return stopPromise;
265
598
  if (!useProcessWorker) {
266
- if (!inProcessMod && !inProcessStartPromise) return Promise.resolve(false);
599
+ if (!inProcessMod && !inProcessStartPromise) {
600
+ releaseOwner();
601
+ return Promise.resolve(false);
602
+ }
267
603
  stopPromise = Promise.resolve(inProcessStartPromise)
268
604
  .catch(() => null)
269
605
  .then(async () => {
270
606
  try { await inProcessMod?.stop?.(reason); } catch {}
271
607
  inProcessMod = null;
608
+ releaseOwner();
272
609
  return true;
273
610
  })
274
611
  .finally(() => {
@@ -276,10 +613,24 @@ export function createStandaloneChannelWorker({
276
613
  });
277
614
  return stopPromise;
278
615
  }
279
- if (!child) return Promise.resolve(false);
616
+ if (!child) {
617
+ stopNotifyBus();
618
+ releaseOwner();
619
+ return Promise.resolve(false);
620
+ }
280
621
  const target = child;
281
622
  const targetPid = target.pid;
282
623
  child = null;
624
+ if (daemonEnabled && options?.force !== true) {
625
+ rejectPending(new Error(`channels runtime detached (${reason})`));
626
+ if (targetPid) ownedChildPids.delete(targetPid);
627
+ try { target.disconnect?.(); } catch {}
628
+ unrefChildHandles(target);
629
+ ownerClaim = null;
630
+ ownerPid = process.pid;
631
+ uninstallParentExitHook();
632
+ return Promise.resolve(true);
633
+ }
283
634
  if (!waitForExit) {
284
635
  rejectPending(new Error(`channels runtime shutdown requested (${reason})`));
285
636
  if (targetPid) ownedChildPids.delete(targetPid);
@@ -291,6 +642,8 @@ export function createStandaloneChannelWorker({
291
642
  clearTimeout(sendTimer);
292
643
  stopPromise = null;
293
644
  unrefChildHandles(target);
645
+ releaseOwner();
646
+ uninstallParentExitHook();
294
647
  resolve(ok);
295
648
  };
296
649
  const sendTimer = setTimeout(() => finish(false), 250);
@@ -314,6 +667,9 @@ export function createStandaloneChannelWorker({
314
667
  clearTimeout(termTimer);
315
668
  clearTimeout(killTimer);
316
669
  stopPromise = null;
670
+ stopNotifyBus();
671
+ releaseOwner();
672
+ uninstallParentExitHook();
317
673
  resolve(ok);
318
674
  };
319
675
  const termTimer = setTimeout(() => {