mixdog 0.7.18 → 0.8.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 (844) hide show
  1. package/README.md +37 -331
  2. package/package.json +67 -99
  3. package/scripts/boot-smoke.mjs +94 -0
  4. package/scripts/build-tui.mjs +52 -0
  5. package/scripts/compact-smoke.mjs +199 -0
  6. package/scripts/lead-workflow-smoke.mjs +598 -0
  7. package/scripts/live-worker-smoke.mjs +239 -0
  8. package/scripts/output-style-smoke.mjs +101 -0
  9. package/scripts/smoke-loop-report.mjs +221 -0
  10. package/scripts/smoke-loop.mjs +201 -0
  11. package/scripts/smoke.mjs +113 -0
  12. package/scripts/tool-failures.mjs +143 -0
  13. package/scripts/tool-smoke.mjs +456 -0
  14. package/src/agents/debugger/AGENT.md +3 -0
  15. package/src/agents/debugger/agent.json +6 -0
  16. package/src/agents/explore/AGENT.md +4 -0
  17. package/src/agents/explore/agent.json +6 -0
  18. package/src/agents/heavy-worker/AGENT.md +3 -0
  19. package/src/agents/heavy-worker/agent.json +6 -0
  20. package/src/agents/maintainer/AGENT.md +3 -0
  21. package/src/agents/maintainer/agent.json +6 -0
  22. package/src/agents/reviewer/AGENT.md +3 -0
  23. package/src/agents/reviewer/agent.json +6 -0
  24. package/src/agents/scheduler-task.md +3 -0
  25. package/src/agents/web-researcher/AGENT.md +3 -0
  26. package/src/agents/web-researcher/agent.json +6 -0
  27. package/src/agents/webhook-handler.md +3 -0
  28. package/src/agents/worker/AGENT.md +3 -0
  29. package/src/agents/worker/agent.json +6 -0
  30. package/src/app.mjs +90 -0
  31. package/src/cli.mjs +11 -0
  32. package/src/defaults/hidden-roles.json +72 -0
  33. package/src/defaults/mixdog-config.template.json +15 -0
  34. package/src/hooks/lib/permission-evaluator.cjs +488 -0
  35. package/src/hooks/lib/settings-loader.cjs +112 -0
  36. package/src/lib/keychain-cjs.cjs +332 -0
  37. package/src/lib/plugin-paths.cjs +28 -0
  38. package/src/lib/rules-builder.cjs +315 -0
  39. package/src/mixdog-session-runtime.mjs +3704 -0
  40. package/src/output-styles/default.md +38 -0
  41. package/src/output-styles/extreme-simple.md +17 -0
  42. package/src/output-styles/simple.md +17 -0
  43. package/src/repl.mjs +322 -0
  44. package/src/rules/bridge/00-common.md +5 -0
  45. package/src/rules/bridge/20-skip-protocol.md +11 -0
  46. package/src/rules/bridge/30-explorer.md +4 -0
  47. package/src/rules/bridge/40-cycle1-agent.md +28 -0
  48. package/src/rules/bridge/41-cycle2-agent.md +59 -0
  49. package/src/rules/lead/00-tool-lead.md +5 -0
  50. package/src/rules/lead/01-general.md +5 -0
  51. package/src/rules/lead/02-channels.md +3 -0
  52. package/src/rules/lead/04-workflow.md +12 -0
  53. package/src/rules/shared/00-language.md +3 -0
  54. package/src/rules/shared/01-tool.md +3 -0
  55. package/src/runtime/agent/orchestrator/bridge-trace.mjs +814 -0
  56. package/src/runtime/agent/orchestrator/cache-mtime.mjs +60 -0
  57. package/src/runtime/agent/orchestrator/config.mjs +446 -0
  58. package/src/runtime/agent/orchestrator/context/collect.mjs +796 -0
  59. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +417 -0
  60. package/src/runtime/agent/orchestrator/internal-roles.mjs +188 -0
  61. package/src/runtime/agent/orchestrator/internal-tools.mjs +88 -0
  62. package/src/runtime/agent/orchestrator/mcp/client.mjs +345 -0
  63. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2104 -0
  64. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +784 -0
  65. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +341 -0
  66. package/src/runtime/agent/orchestrator/providers/gemini.mjs +1679 -0
  67. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +959 -0
  68. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +213 -0
  69. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +38 -0
  70. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +471 -0
  71. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +615 -0
  72. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +808 -0
  73. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +1719 -0
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2587 -0
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1953 -0
  76. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +136 -0
  77. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +317 -0
  78. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +109 -0
  79. package/src/runtime/agent/orchestrator/providers/registry.mjs +247 -0
  80. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +332 -0
  81. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/providers/trace-utils.mjs +50 -0
  83. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
  84. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +318 -0
  85. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +367 -0
  86. package/src/runtime/agent/orchestrator/session/compact.mjs +882 -0
  87. package/src/runtime/agent/orchestrator/session/context-utils.mjs +233 -0
  88. package/src/runtime/agent/orchestrator/session/loop.mjs +2320 -0
  89. package/src/runtime/agent/orchestrator/session/manager.mjs +2960 -0
  90. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  91. package/src/runtime/agent/orchestrator/session/store.mjs +663 -0
  92. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +166 -0
  93. package/src/runtime/agent/orchestrator/smart-bridge/bridge-llm.mjs +339 -0
  94. package/src/runtime/agent/orchestrator/smart-bridge/cache-strategy.mjs +419 -0
  95. package/src/runtime/agent/orchestrator/stall-policy.mjs +227 -0
  96. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +235 -0
  97. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +723 -0
  98. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +389 -0
  99. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +637 -0
  100. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +165 -0
  101. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +104 -0
  102. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +194 -0
  103. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +596 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +110 -0
  105. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +153 -0
  106. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
  107. package/src/runtime/agent/orchestrator/tools/builtin/read-open.mjs +189 -0
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +731 -0
  109. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +168 -0
  110. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
  111. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +465 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +160 -0
  113. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +982 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +1087 -0
  115. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +231 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +223 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin.mjs +478 -0
  118. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
  119. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4102 -0
  120. package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +323 -0
  121. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +154 -0
  122. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +26 -0
  123. package/src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs +143 -0
  124. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +18 -0
  125. package/src/runtime/agent/orchestrator/tools/patch.mjs +2772 -0
  126. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +114 -0
  127. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +880 -0
  128. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +312 -0
  129. package/src/runtime/channels/backends/discord.mjs +781 -0
  130. package/src/runtime/channels/data/voice-runtime-manifest.json +138 -0
  131. package/src/runtime/channels/index.mjs +3309 -0
  132. package/src/runtime/channels/lib/config.mjs +285 -0
  133. package/src/runtime/channels/lib/drop-trace.mjs +71 -0
  134. package/src/runtime/channels/lib/event-pipeline.mjs +81 -0
  135. package/src/runtime/channels/lib/holidays.mjs +138 -0
  136. package/src/runtime/channels/lib/hook-pipe-server.mjs +671 -0
  137. package/src/runtime/channels/lib/output-forwarder.mjs +765 -0
  138. package/src/runtime/channels/lib/runtime-paths.mjs +497 -0
  139. package/src/runtime/channels/lib/scheduler.mjs +710 -0
  140. package/src/runtime/channels/lib/session-discovery.mjs +102 -0
  141. package/src/runtime/channels/lib/state-file.mjs +68 -0
  142. package/src/runtime/channels/lib/status-snapshot.mjs +224 -0
  143. package/src/runtime/channels/lib/tool-format.mjs +124 -0
  144. package/src/runtime/channels/lib/transcript-discovery.mjs +195 -0
  145. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +734 -0
  146. package/src/runtime/channels/lib/webhook.mjs +1288 -0
  147. package/src/runtime/channels/tool-defs.mjs +177 -0
  148. package/src/runtime/lib/keychain-cjs.cjs +289 -0
  149. package/src/runtime/memory/data/runtime-manifest.json +40 -0
  150. package/src/runtime/memory/index.mjs +3600 -0
  151. package/src/runtime/memory/lib/core-memory-store.mjs +336 -0
  152. package/src/runtime/memory/lib/embedding-provider.mjs +275 -0
  153. package/src/runtime/memory/lib/embedding-worker.mjs +331 -0
  154. package/src/runtime/memory/lib/memory-cycle-requests.mjs +276 -0
  155. package/src/runtime/memory/lib/memory-cycle1.mjs +783 -0
  156. package/src/runtime/memory/lib/memory-cycle2.mjs +1389 -0
  157. package/src/runtime/memory/lib/memory-cycle3.mjs +646 -0
  158. package/src/runtime/memory/lib/memory-embed.mjs +300 -0
  159. package/src/runtime/memory/lib/memory-ops-policy.mjs +149 -0
  160. package/src/runtime/memory/lib/memory-recall-store.mjs +644 -0
  161. package/src/runtime/memory/lib/memory.mjs +418 -0
  162. package/src/runtime/memory/lib/pg/adapter.mjs +314 -0
  163. package/src/runtime/memory/lib/pg/process.mjs +366 -0
  164. package/src/runtime/memory/lib/pg/supervisor.mjs +495 -0
  165. package/src/runtime/memory/lib/runtime-fetcher.mjs +464 -0
  166. package/src/runtime/memory/lib/trace-store.mjs +734 -0
  167. package/src/runtime/memory/tool-defs.mjs +79 -0
  168. package/src/runtime/search/index.mjs +925 -0
  169. package/src/runtime/search/lib/config.mjs +61 -0
  170. package/src/runtime/search/lib/web-tools.mjs +1278 -0
  171. package/src/runtime/search/tool-defs.mjs +64 -0
  172. package/src/runtime/shared/atomic-file.mjs +435 -0
  173. package/src/runtime/shared/background-tasks.mjs +376 -0
  174. package/src/runtime/shared/child-guardian.mjs +98 -0
  175. package/src/runtime/shared/config.mjs +393 -0
  176. package/src/runtime/shared/err-text.mjs +121 -0
  177. package/src/runtime/shared/launcher-control.mjs +259 -0
  178. package/src/runtime/shared/llm/http-agent.mjs +129 -0
  179. package/src/runtime/shared/open-url.mjs +37 -0
  180. package/src/runtime/shared/plugin-paths.mjs +25 -0
  181. package/src/runtime/shared/process-shutdown.mjs +147 -0
  182. package/src/runtime/shared/schedules-store.mjs +70 -0
  183. package/src/runtime/shared/tool-execution-contract.mjs +104 -0
  184. package/src/runtime/shared/tool-surface.mjs +950 -0
  185. package/src/runtime/shared/user-cwd.mjs +221 -0
  186. package/src/runtime/shared/user-data-guard.mjs +232 -0
  187. package/src/runtime/shared/workspace-router.mjs +259 -0
  188. package/src/standalone/bridge-tool.mjs +1414 -0
  189. package/src/standalone/channel-admin.mjs +366 -0
  190. package/src/standalone/channel-worker-preload.cjs +3 -0
  191. package/src/standalone/channel-worker.mjs +353 -0
  192. package/src/standalone/explore-tool.mjs +233 -0
  193. package/src/standalone/hook-bus.mjs +246 -0
  194. package/src/standalone/plugin-admin.mjs +247 -0
  195. package/src/standalone/provider-admin.mjs +338 -0
  196. package/src/standalone/seeds.mjs +94 -0
  197. package/src/standalone/usage-dashboard.mjs +510 -0
  198. package/src/tui/App.jsx +5438 -0
  199. package/src/tui/components/AnsiText.jsx +199 -0
  200. package/src/tui/components/ContextPanel.jsx +217 -0
  201. package/src/tui/components/Markdown.jsx +205 -0
  202. package/src/tui/components/MarkdownTable.jsx +204 -0
  203. package/src/tui/components/Message.jsx +103 -0
  204. package/src/tui/components/Picker.jsx +317 -0
  205. package/src/tui/components/PromptInput.jsx +584 -0
  206. package/src/tui/components/QueuedCommands.jsx +47 -0
  207. package/src/tui/components/SlashCommandPalette.jsx +114 -0
  208. package/src/tui/components/Spinner.jsx +317 -0
  209. package/src/tui/components/StatusLine.jsx +87 -0
  210. package/src/tui/components/TextEntryPanel.jsx +323 -0
  211. package/src/tui/components/ToolExecution.jsx +772 -0
  212. package/src/tui/components/TurnDone.jsx +78 -0
  213. package/src/tui/components/UsagePanel.jsx +331 -0
  214. package/src/tui/dist/index.mjs +12359 -0
  215. package/src/tui/engine.mjs +2410 -0
  216. package/src/tui/figures.mjs +50 -0
  217. package/src/tui/hooks/useEngine.mjs +16 -0
  218. package/src/tui/index.jsx +254 -0
  219. package/src/tui/input-editing.mjs +242 -0
  220. package/src/tui/markdown/format-token.mjs +194 -0
  221. package/src/tui/paste-attachments.mjs +198 -0
  222. package/src/tui/runtime/shared/process-shutdown.mjs +1 -0
  223. package/src/tui/spinner-verbs.mjs +45 -0
  224. package/src/tui/theme.mjs +67 -0
  225. package/src/tui/time-format.mjs +53 -0
  226. package/src/ui/ansi.mjs +115 -0
  227. package/src/ui/markdown.mjs +195 -0
  228. package/src/ui/statusline.mjs +730 -0
  229. package/src/ui/tool-card.mjs +101 -0
  230. package/src/vendor/statusline/bin/statusline-lib.mjs +805 -0
  231. package/src/vendor/statusline/bin/statusline-route.mjs +596 -0
  232. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +285 -0
  233. package/src/vendor/statusline/src/gateway/claude-current.mjs +320 -0
  234. package/src/vendor/statusline/src/gateway/route-meta.mjs +753 -0
  235. package/src/vendor/statusline/src/gateway/session-routes.mjs +244 -0
  236. package/src/workflows/default/WORKFLOW.md +7 -0
  237. package/src/workflows/default/workflow.json +14 -0
  238. package/vendor/ink/build/ansi-tokenizer.d.ts +38 -0
  239. package/vendor/ink/build/ansi-tokenizer.js +316 -0
  240. package/vendor/ink/build/ansi-tokenizer.js.map +1 -0
  241. package/vendor/ink/build/colorize.d.ts +3 -0
  242. package/vendor/ink/build/colorize.js +48 -0
  243. package/vendor/ink/build/colorize.js.map +1 -0
  244. package/vendor/ink/build/components/AccessibilityContext.d.ts +3 -0
  245. package/vendor/ink/build/components/AccessibilityContext.js +5 -0
  246. package/vendor/ink/build/components/AccessibilityContext.js.map +1 -0
  247. package/vendor/ink/build/components/AnimationContext.d.ts +9 -0
  248. package/vendor/ink/build/components/AnimationContext.js +13 -0
  249. package/vendor/ink/build/components/AnimationContext.js.map +1 -0
  250. package/vendor/ink/build/components/App.d.ts +24 -0
  251. package/vendor/ink/build/components/App.js +554 -0
  252. package/vendor/ink/build/components/App.js.map +1 -0
  253. package/vendor/ink/build/components/AppContext.d.ts +80 -0
  254. package/vendor/ink/build/components/AppContext.js +25 -0
  255. package/vendor/ink/build/components/AppContext.js.map +1 -0
  256. package/vendor/ink/build/components/BackgroundContext.d.ts +4 -0
  257. package/vendor/ink/build/components/BackgroundContext.js +3 -0
  258. package/vendor/ink/build/components/BackgroundContext.js.map +1 -0
  259. package/vendor/ink/build/components/Box.d.ts +130 -0
  260. package/vendor/ink/build/components/Box.js +34 -0
  261. package/vendor/ink/build/components/Box.js.map +1 -0
  262. package/vendor/ink/build/components/CursorContext.d.ts +11 -0
  263. package/vendor/ink/build/components/CursorContext.js +8 -0
  264. package/vendor/ink/build/components/CursorContext.js.map +1 -0
  265. package/vendor/ink/build/components/ErrorBoundary.d.ts +18 -0
  266. package/vendor/ink/build/components/ErrorBoundary.js +23 -0
  267. package/vendor/ink/build/components/ErrorBoundary.js.map +1 -0
  268. package/vendor/ink/build/components/ErrorOverview.d.ts +6 -0
  269. package/vendor/ink/build/components/ErrorOverview.js +90 -0
  270. package/vendor/ink/build/components/ErrorOverview.js.map +1 -0
  271. package/vendor/ink/build/components/FocusContext.d.ts +16 -0
  272. package/vendor/ink/build/components/FocusContext.js +17 -0
  273. package/vendor/ink/build/components/FocusContext.js.map +1 -0
  274. package/vendor/ink/build/components/Newline.d.ts +13 -0
  275. package/vendor/ink/build/components/Newline.js +8 -0
  276. package/vendor/ink/build/components/Newline.js.map +1 -0
  277. package/vendor/ink/build/components/Spacer.d.ts +7 -0
  278. package/vendor/ink/build/components/Spacer.js +11 -0
  279. package/vendor/ink/build/components/Spacer.js.map +1 -0
  280. package/vendor/ink/build/components/Static.d.ts +24 -0
  281. package/vendor/ink/build/components/Static.js +28 -0
  282. package/vendor/ink/build/components/Static.js.map +1 -0
  283. package/vendor/ink/build/components/StderrContext.d.ts +15 -0
  284. package/vendor/ink/build/components/StderrContext.js +13 -0
  285. package/vendor/ink/build/components/StderrContext.js.map +1 -0
  286. package/vendor/ink/build/components/StdinContext.d.ts +28 -0
  287. package/vendor/ink/build/components/StdinContext.js +20 -0
  288. package/vendor/ink/build/components/StdinContext.js.map +1 -0
  289. package/vendor/ink/build/components/StdoutContext.d.ts +15 -0
  290. package/vendor/ink/build/components/StdoutContext.js +13 -0
  291. package/vendor/ink/build/components/StdoutContext.js.map +1 -0
  292. package/vendor/ink/build/components/Text.d.ts +55 -0
  293. package/vendor/ink/build/components/Text.js +50 -0
  294. package/vendor/ink/build/components/Text.js.map +1 -0
  295. package/vendor/ink/build/components/Transform.d.ts +16 -0
  296. package/vendor/ink/build/components/Transform.js +15 -0
  297. package/vendor/ink/build/components/Transform.js.map +1 -0
  298. package/vendor/ink/build/cursor-helpers.d.ts +39 -0
  299. package/vendor/ink/build/cursor-helpers.js +62 -0
  300. package/vendor/ink/build/cursor-helpers.js.map +1 -0
  301. package/vendor/ink/build/devtools-window-polyfill.d.ts +1 -0
  302. package/vendor/ink/build/devtools-window-polyfill.js +68 -0
  303. package/vendor/ink/build/devtools-window-polyfill.js.map +1 -0
  304. package/vendor/ink/build/devtools.d.ts +1 -0
  305. package/vendor/ink/build/devtools.js +36 -0
  306. package/vendor/ink/build/devtools.js.map +1 -0
  307. package/vendor/ink/build/dom.d.ts +62 -0
  308. package/vendor/ink/build/dom.js +143 -0
  309. package/vendor/ink/build/dom.js.map +1 -0
  310. package/vendor/ink/build/get-max-width.d.ts +3 -0
  311. package/vendor/ink/build/get-max-width.js +10 -0
  312. package/vendor/ink/build/get-max-width.js.map +1 -0
  313. package/vendor/ink/build/hooks/use-animation.d.ts +49 -0
  314. package/vendor/ink/build/hooks/use-animation.js +87 -0
  315. package/vendor/ink/build/hooks/use-animation.js.map +1 -0
  316. package/vendor/ink/build/hooks/use-app.d.ts +5 -0
  317. package/vendor/ink/build/hooks/use-app.js +8 -0
  318. package/vendor/ink/build/hooks/use-app.js.map +1 -0
  319. package/vendor/ink/build/hooks/use-box-metrics.d.ts +59 -0
  320. package/vendor/ink/build/hooks/use-box-metrics.js +81 -0
  321. package/vendor/ink/build/hooks/use-box-metrics.js.map +1 -0
  322. package/vendor/ink/build/hooks/use-cursor.d.ts +12 -0
  323. package/vendor/ink/build/hooks/use-cursor.js +29 -0
  324. package/vendor/ink/build/hooks/use-cursor.js.map +1 -0
  325. package/vendor/ink/build/hooks/use-focus-manager.d.ts +43 -0
  326. package/vendor/ink/build/hooks/use-focus-manager.js +18 -0
  327. package/vendor/ink/build/hooks/use-focus-manager.js.map +1 -0
  328. package/vendor/ink/build/hooks/use-focus.d.ts +30 -0
  329. package/vendor/ink/build/hooks/use-focus.js +43 -0
  330. package/vendor/ink/build/hooks/use-focus.js.map +1 -0
  331. package/vendor/ink/build/hooks/use-input.d.ts +132 -0
  332. package/vendor/ink/build/hooks/use-input.js +126 -0
  333. package/vendor/ink/build/hooks/use-input.js.map +1 -0
  334. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.d.ts +6 -0
  335. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.js +12 -0
  336. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.js.map +1 -0
  337. package/vendor/ink/build/hooks/use-paste.d.ts +35 -0
  338. package/vendor/ink/build/hooks/use-paste.js +62 -0
  339. package/vendor/ink/build/hooks/use-paste.js.map +1 -0
  340. package/vendor/ink/build/hooks/use-stderr.d.ts +5 -0
  341. package/vendor/ink/build/hooks/use-stderr.js +8 -0
  342. package/vendor/ink/build/hooks/use-stderr.js.map +1 -0
  343. package/vendor/ink/build/hooks/use-stdin.d.ts +7 -0
  344. package/vendor/ink/build/hooks/use-stdin.js +9 -0
  345. package/vendor/ink/build/hooks/use-stdin.js.map +1 -0
  346. package/vendor/ink/build/hooks/use-stdout.d.ts +5 -0
  347. package/vendor/ink/build/hooks/use-stdout.js +8 -0
  348. package/vendor/ink/build/hooks/use-stdout.js.map +1 -0
  349. package/vendor/ink/build/hooks/use-window-size.d.ts +18 -0
  350. package/vendor/ink/build/hooks/use-window-size.js +22 -0
  351. package/vendor/ink/build/hooks/use-window-size.js.map +1 -0
  352. package/vendor/ink/build/index.d.ts +42 -0
  353. package/vendor/ink/build/index.js +24 -0
  354. package/vendor/ink/build/index.js.map +1 -0
  355. package/vendor/ink/build/ink.d.ts +146 -0
  356. package/vendor/ink/build/ink.js +1022 -0
  357. package/vendor/ink/build/ink.js.map +1 -0
  358. package/vendor/ink/build/input-parser.d.ts +10 -0
  359. package/vendor/ink/build/input-parser.js +194 -0
  360. package/vendor/ink/build/input-parser.js.map +1 -0
  361. package/vendor/ink/build/instances.d.ts +3 -0
  362. package/vendor/ink/build/instances.js +8 -0
  363. package/vendor/ink/build/instances.js.map +1 -0
  364. package/vendor/ink/build/kitty-keyboard.d.ts +23 -0
  365. package/vendor/ink/build/kitty-keyboard.js +32 -0
  366. package/vendor/ink/build/kitty-keyboard.js.map +1 -0
  367. package/vendor/ink/build/log-update.d.ts +20 -0
  368. package/vendor/ink/build/log-update.js +261 -0
  369. package/vendor/ink/build/log-update.js.map +1 -0
  370. package/vendor/ink/build/measure-element.d.ts +20 -0
  371. package/vendor/ink/build/measure-element.js +13 -0
  372. package/vendor/ink/build/measure-element.js.map +1 -0
  373. package/vendor/ink/build/measure-text.d.ts +6 -0
  374. package/vendor/ink/build/measure-text.js +21 -0
  375. package/vendor/ink/build/measure-text.js.map +1 -0
  376. package/vendor/ink/build/output.d.ts +35 -0
  377. package/vendor/ink/build/output.js +328 -0
  378. package/vendor/ink/build/output.js.map +1 -0
  379. package/vendor/ink/build/parse-keypress.d.ts +20 -0
  380. package/vendor/ink/build/parse-keypress.js +495 -0
  381. package/vendor/ink/build/parse-keypress.js.map +1 -0
  382. package/vendor/ink/build/reconciler.d.ts +4 -0
  383. package/vendor/ink/build/reconciler.js +306 -0
  384. package/vendor/ink/build/reconciler.js.map +1 -0
  385. package/vendor/ink/build/render-background.d.ts +4 -0
  386. package/vendor/ink/build/render-background.js +25 -0
  387. package/vendor/ink/build/render-background.js.map +1 -0
  388. package/vendor/ink/build/render-border.d.ts +4 -0
  389. package/vendor/ink/build/render-border.js +84 -0
  390. package/vendor/ink/build/render-border.js.map +1 -0
  391. package/vendor/ink/build/render-node-to-output.d.ts +14 -0
  392. package/vendor/ink/build/render-node-to-output.js +162 -0
  393. package/vendor/ink/build/render-node-to-output.js.map +1 -0
  394. package/vendor/ink/build/render-to-string.d.ts +38 -0
  395. package/vendor/ink/build/render-to-string.js +116 -0
  396. package/vendor/ink/build/render-to-string.js.map +1 -0
  397. package/vendor/ink/build/render.d.ts +176 -0
  398. package/vendor/ink/build/render.js +71 -0
  399. package/vendor/ink/build/render.js.map +1 -0
  400. package/vendor/ink/build/renderer.d.ts +8 -0
  401. package/vendor/ink/build/renderer.js +64 -0
  402. package/vendor/ink/build/renderer.js.map +1 -0
  403. package/vendor/ink/build/sanitize-ansi.d.ts +2 -0
  404. package/vendor/ink/build/sanitize-ansi.js +27 -0
  405. package/vendor/ink/build/sanitize-ansi.js.map +1 -0
  406. package/vendor/ink/build/squash-text-nodes.d.ts +3 -0
  407. package/vendor/ink/build/squash-text-nodes.js +36 -0
  408. package/vendor/ink/build/squash-text-nodes.js.map +1 -0
  409. package/vendor/ink/build/styles.d.ts +302 -0
  410. package/vendor/ink/build/styles.js +303 -0
  411. package/vendor/ink/build/styles.js.map +1 -0
  412. package/vendor/ink/build/utils.d.ts +9 -0
  413. package/vendor/ink/build/utils.js +19 -0
  414. package/vendor/ink/build/utils.js.map +1 -0
  415. package/vendor/ink/build/wrap-text.d.ts +3 -0
  416. package/vendor/ink/build/wrap-text.js +38 -0
  417. package/vendor/ink/build/wrap-text.js.map +1 -0
  418. package/vendor/ink/build/write-synchronized.d.ts +4 -0
  419. package/vendor/ink/build/write-synchronized.js +9 -0
  420. package/vendor/ink/build/write-synchronized.js.map +1 -0
  421. package/vendor/ink/license +10 -0
  422. package/vendor/ink/package.json +137 -0
  423. package/.claude-plugin/marketplace.json +0 -34
  424. package/.claude-plugin/plugin.json +0 -20
  425. package/.gitattributes +0 -34
  426. package/.mcp.json +0 -14
  427. package/ARCHITECTURE.md +0 -77
  428. package/CHANGELOG.md +0 -30
  429. package/CONTRIBUTING.md +0 -45
  430. package/DATA-FLOW.md +0 -79
  431. package/LICENSE +0 -21
  432. package/SECURITY.md +0 -138
  433. package/UNINSTALL.md +0 -115
  434. package/agents/maintenance.md +0 -5
  435. package/agents/memory-classification.md +0 -30
  436. package/agents/scheduler-task.md +0 -18
  437. package/agents/webhook-handler.md +0 -27
  438. package/agents/worker.md +0 -24
  439. package/bin/bridge +0 -133
  440. package/bin/statusline-launcher.mjs +0 -82
  441. package/bin/statusline-lib.mjs +0 -581
  442. package/bin/statusline-route.mjs +0 -273
  443. package/bin/statusline.mjs +0 -638
  444. package/bun.lock +0 -927
  445. package/commands/config.md +0 -16
  446. package/commands/doctor.md +0 -13
  447. package/commands/model.md +0 -61
  448. package/commands/setup.md +0 -17
  449. package/defaults/hidden-roles.json +0 -68
  450. package/defaults/memory-chunk-prompt.md +0 -63
  451. package/defaults/mixdog-config.template.json +0 -27
  452. package/defaults/user-workflow.json +0 -8
  453. package/defaults/user-workflow.md +0 -17
  454. package/hooks/hooks.json +0 -73
  455. package/hooks/lib/active-instance.cjs +0 -77
  456. package/hooks/lib/permission-evaluator.cjs +0 -411
  457. package/hooks/lib/permission-route.cjs +0 -63
  458. package/hooks/lib/settings-loader.cjs +0 -117
  459. package/hooks/post-tool-use.cjs +0 -84
  460. package/hooks/pre-mcp-sandbox.cjs +0 -158
  461. package/hooks/pre-tool-subagent.cjs +0 -258
  462. package/hooks/session-start.cjs +0 -1493
  463. package/hooks/shim-launcher.cjs +0 -65
  464. package/hooks/turn-timer.cjs +0 -82
  465. package/lib/claude-md-writer.cjs +0 -386
  466. package/lib/keychain-cjs.cjs +0 -290
  467. package/lib/plugin-paths.cjs +0 -69
  468. package/lib/rules-builder.cjs +0 -241
  469. package/native/README.md +0 -117
  470. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  471. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  472. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  473. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  474. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  475. package/prompts/code-review.txt +0 -16
  476. package/prompts/security-audit.txt +0 -17
  477. package/rules/bridge/00-common.md +0 -39
  478. package/rules/bridge/20-skip-protocol.md +0 -18
  479. package/rules/bridge/30-explorer.md +0 -33
  480. package/rules/bridge/40-cycle1-agent.md +0 -52
  481. package/rules/bridge/41-cycle2-agent.md +0 -62
  482. package/rules/lead/00-tool-lead.md +0 -61
  483. package/rules/lead/01-general.md +0 -26
  484. package/rules/lead/02-channels.md +0 -49
  485. package/rules/lead/03-team.md +0 -27
  486. package/rules/lead/04-workflow.md +0 -20
  487. package/rules/shared/00-language.md +0 -14
  488. package/rules/shared/01-tool.md +0 -138
  489. package/scripts/bootstrap.mjs +0 -130
  490. package/scripts/bridge-unify-smoke.mjs +0 -308
  491. package/scripts/build-runtime-linux.sh +0 -348
  492. package/scripts/build-runtime-macos.sh +0 -217
  493. package/scripts/build-runtime-windows.ps1 +0 -242
  494. package/scripts/builtin-utils-smoke.mjs +0 -398
  495. package/scripts/bump.mjs +0 -80
  496. package/scripts/check-json.mjs +0 -45
  497. package/scripts/check-syntax-changed.mjs +0 -102
  498. package/scripts/check-syntax.mjs +0 -58
  499. package/scripts/code-graph-batch.test.mjs +0 -33
  500. package/scripts/config-preserve-smoke.mjs +0 -180
  501. package/scripts/doctor.mjs +0 -489
  502. package/scripts/edit-normalize-fuzz.mjs +0 -130
  503. package/scripts/edit-normalize-smoke.mjs +0 -401
  504. package/scripts/edit-operation-smoke.mjs +0 -369
  505. package/scripts/edit2-smoke.mjs +0 -63
  506. package/scripts/ensure-deps.mjs +0 -259
  507. package/scripts/fuzzy-e2e.mjs +0 -28
  508. package/scripts/fuzzy-smoke.mjs +0 -26
  509. package/scripts/gateway-model.mjs +0 -596
  510. package/scripts/generate-runtime-manifest.mjs +0 -166
  511. package/scripts/guard-smoke.mjs +0 -66
  512. package/scripts/hidden-role-schema-smoke.mjs +0 -162
  513. package/scripts/hook-routing-smoke.mjs +0 -29
  514. package/scripts/inject-input.ps1 +0 -204
  515. package/scripts/io-complex-smoke.mjs +0 -667
  516. package/scripts/io-explore-bench.mjs +0 -424
  517. package/scripts/io-guardrails-smoke.mjs +0 -205
  518. package/scripts/io-mini-bench-baseline.json +0 -11
  519. package/scripts/io-mini-bench.mjs +0 -216
  520. package/scripts/io-route-harness.mjs +0 -933
  521. package/scripts/io-telemetry-report.mjs +0 -691
  522. package/scripts/lib/gateway-inventory.mjs +0 -178
  523. package/scripts/lib/gateway-settings.mjs +0 -78
  524. package/scripts/mutation-bench.mjs +0 -564
  525. package/scripts/mutation-io-smoke.mjs +0 -1097
  526. package/scripts/native-patch-bridge-smoke.mjs +0 -288
  527. package/scripts/native-patch-smoke.mjs +0 -304
  528. package/scripts/openai-oauth-catalog-smoke.mjs +0 -53
  529. package/scripts/patch-interior-context-smoke.mjs +0 -49
  530. package/scripts/patch-newline-utf8-smoke.mjs +0 -157
  531. package/scripts/perf-hook-smoke.mjs +0 -71
  532. package/scripts/permission-eval-smoke.mjs +0 -443
  533. package/scripts/prep-patch.mjs +0 -53
  534. package/scripts/prep-shim.mjs +0 -96
  535. package/scripts/provider-cache-smoke.mjs +0 -687
  536. package/scripts/report-runtime-health.mjs +0 -132
  537. package/scripts/resolve-bun.mjs +0 -60
  538. package/scripts/run-mcp.mjs +0 -1473
  539. package/scripts/salvage-v4a-shatter.test.mjs +0 -58
  540. package/scripts/scoped-cache-io-smoke.mjs +0 -103
  541. package/scripts/shell-policy-round3-smoke.mjs +0 -46
  542. package/scripts/smoke-runtime-negative.ps1 +0 -100
  543. package/scripts/smoke-runtime-negative.sh +0 -95
  544. package/scripts/stall-policy-smoke.mjs +0 -50
  545. package/scripts/start-memory-worker.mjs +0 -23
  546. package/scripts/statusline-launcher-smoke.mjs +0 -235
  547. package/scripts/stress-atomic-write.mjs +0 -1028
  548. package/scripts/test-fault-inject.mjs +0 -164
  549. package/scripts/test-large-file.mjs +0 -174
  550. package/scripts/tool-edge-smoke.mjs +0 -209
  551. package/scripts/uninstall.mjs +0 -238
  552. package/scripts/webhook-selfheal-smoke.mjs +0 -27
  553. package/scripts/write-overwrite-guard-smoke.mjs +0 -56
  554. package/server-main.mjs +0 -3350
  555. package/server.mjs +0 -468
  556. package/setup/config-merge.mjs +0 -246
  557. package/setup/install.mjs +0 -574
  558. package/setup/launch-core.mjs +0 -617
  559. package/setup/launch.mjs +0 -101
  560. package/setup/locate-claude.mjs +0 -56
  561. package/setup/mixdog-cli.mjs +0 -122
  562. package/setup/setup-server.mjs +0 -3305
  563. package/setup/setup.html +0 -3740
  564. package/setup/tui.mjs +0 -325
  565. package/skills/retro-skill-proposer/SKILL.md +0 -92
  566. package/skills/schedule-add/SKILL.md +0 -77
  567. package/skills/setup/SKILL.md +0 -356
  568. package/skills/webhook-add/SKILL.md +0 -81
  569. package/src/agent/bridge-stall-watchdog.mjs +0 -337
  570. package/src/agent/index.mjs +0 -2229
  571. package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +0 -1010
  572. package/src/agent/orchestrator/bridge-retry.mjs +0 -220
  573. package/src/agent/orchestrator/bridge-trace.mjs +0 -601
  574. package/src/agent/orchestrator/cache-mtime.mjs +0 -58
  575. package/src/agent/orchestrator/config.mjs +0 -405
  576. package/src/agent/orchestrator/context/collect.mjs +0 -651
  577. package/src/agent/orchestrator/dispatch-persist.mjs +0 -549
  578. package/src/agent/orchestrator/drain-registry.mjs +0 -50
  579. package/src/agent/orchestrator/explore-validator.mjs +0 -8
  580. package/src/agent/orchestrator/internal-roles.mjs +0 -118
  581. package/src/agent/orchestrator/internal-tools.mjs +0 -88
  582. package/src/agent/orchestrator/jobs.mjs +0 -116
  583. package/src/agent/orchestrator/mcp/client.mjs +0 -364
  584. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +0 -1884
  585. package/src/agent/orchestrator/providers/anthropic.mjs +0 -598
  586. package/src/agent/orchestrator/providers/gemini.mjs +0 -1530
  587. package/src/agent/orchestrator/providers/grok-oauth.mjs +0 -779
  588. package/src/agent/orchestrator/providers/model-catalog.mjs +0 -374
  589. package/src/agent/orchestrator/providers/openai-compat-stream.mjs +0 -366
  590. package/src/agent/orchestrator/providers/openai-compat.mjs +0 -1511
  591. package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -1891
  592. package/src/agent/orchestrator/providers/openai-oauth.mjs +0 -1456
  593. package/src/agent/orchestrator/providers/openai-ws.mjs +0 -127
  594. package/src/agent/orchestrator/providers/registry.mjs +0 -192
  595. package/src/agent/orchestrator/providers/retry-classifier.mjs +0 -325
  596. package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +0 -142
  597. package/src/agent/orchestrator/session/cache/read-cache.mjs +0 -319
  598. package/src/agent/orchestrator/session/cache/scoped-cache.mjs +0 -361
  599. package/src/agent/orchestrator/session/loop.mjs +0 -1619
  600. package/src/agent/orchestrator/session/manager.mjs +0 -1991
  601. package/src/agent/orchestrator/session/result-classification.mjs +0 -65
  602. package/src/agent/orchestrator/session/store.mjs +0 -632
  603. package/src/agent/orchestrator/session/stream-watchdog.mjs +0 -130
  604. package/src/agent/orchestrator/session/tool-result-offload.mjs +0 -166
  605. package/src/agent/orchestrator/session/trim.mjs +0 -491
  606. package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +0 -115
  607. package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +0 -331
  608. package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +0 -150
  609. package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +0 -228
  610. package/src/agent/orchestrator/smart-bridge/index.mjs +0 -215
  611. package/src/agent/orchestrator/smart-bridge/profiles.mjs +0 -37
  612. package/src/agent/orchestrator/smart-bridge/registry.mjs +0 -348
  613. package/src/agent/orchestrator/stall-policy.mjs +0 -201
  614. package/src/agent/orchestrator/tool-loop-guard.mjs +0 -75
  615. package/src/agent/orchestrator/tools/bash-session.mjs +0 -722
  616. package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +0 -511
  617. package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +0 -480
  618. package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +0 -256
  619. package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +0 -58
  620. package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +0 -240
  621. package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +0 -113
  622. package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +0 -74
  623. package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +0 -242
  624. package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +0 -211
  625. package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +0 -1364
  626. package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +0 -126
  627. package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +0 -141
  628. package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +0 -194
  629. package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +0 -60
  630. package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +0 -168
  631. package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +0 -173
  632. package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +0 -48
  633. package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -99
  634. package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +0 -193
  635. package/src/agent/orchestrator/tools/builtin/list-tool.mjs +0 -597
  636. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  637. package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +0 -300
  638. package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +0 -152
  639. package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +0 -118
  640. package/src/agent/orchestrator/tools/builtin/read-open.mjs +0 -190
  641. package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +0 -728
  642. package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +0 -173
  643. package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +0 -602
  644. package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +0 -196
  645. package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +0 -422
  646. package/src/agent/orchestrator/tools/builtin/search-builders.mjs +0 -158
  647. package/src/agent/orchestrator/tools/builtin/search-tool.mjs +0 -869
  648. package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +0 -962
  649. package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +0 -223
  650. package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +0 -206
  651. package/src/agent/orchestrator/tools/builtin/write-tool.mjs +0 -401
  652. package/src/agent/orchestrator/tools/builtin.mjs +0 -503
  653. package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +0 -24
  654. package/src/agent/orchestrator/tools/code-graph.mjs +0 -4095
  655. package/src/agent/orchestrator/tools/cwd-tool.mjs +0 -298
  656. package/src/agent/orchestrator/tools/destructive-warning.mjs +0 -323
  657. package/src/agent/orchestrator/tools/edit-normalize.mjs +0 -603
  658. package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +0 -154
  659. package/src/agent/orchestrator/tools/graph-manifest.json +0 -26
  660. package/src/agent/orchestrator/tools/host-input.mjs +0 -204
  661. package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +0 -143
  662. package/src/agent/orchestrator/tools/patch-manifest.json +0 -26
  663. package/src/agent/orchestrator/tools/patch-tool-defs.mjs +0 -20
  664. package/src/agent/orchestrator/tools/patch.mjs +0 -2754
  665. package/src/agent/orchestrator/tools/progress-message.mjs +0 -118
  666. package/src/agent/orchestrator/tools/shell-command.mjs +0 -865
  667. package/src/agent/orchestrator/tools/shell-policy-imports.mjs +0 -7
  668. package/src/agent/orchestrator/tools/shell-snapshot.mjs +0 -313
  669. package/src/agent/orchestrator/workflow-store.mjs +0 -93
  670. package/src/agent/tool-defs.mjs +0 -110
  671. package/src/channels/backends/discord.mjs +0 -784
  672. package/src/channels/data/voice-runtime-manifest.json +0 -138
  673. package/src/channels/index.mjs +0 -3268
  674. package/src/channels/lib/config.mjs +0 -292
  675. package/src/channels/lib/drop-trace.mjs +0 -71
  676. package/src/channels/lib/event-pipeline.mjs +0 -81
  677. package/src/channels/lib/holidays.mjs +0 -138
  678. package/src/channels/lib/hook-pipe-server.mjs +0 -822
  679. package/src/channels/lib/output-forwarder.mjs +0 -765
  680. package/src/channels/lib/runtime-paths.mjs +0 -552
  681. package/src/channels/lib/scheduler.mjs +0 -723
  682. package/src/channels/lib/session-discovery.mjs +0 -103
  683. package/src/channels/lib/state-file.mjs +0 -68
  684. package/src/channels/lib/status-snapshot.mjs +0 -219
  685. package/src/channels/lib/tool-format.mjs +0 -140
  686. package/src/channels/lib/transcript-discovery.mjs +0 -195
  687. package/src/channels/lib/voice-runtime-fetcher.mjs +0 -734
  688. package/src/channels/lib/webhook.mjs +0 -1318
  689. package/src/channels/tool-defs.mjs +0 -170
  690. package/src/daemon/host.mjs +0 -118
  691. package/src/daemon/mcp-transport.mjs +0 -47
  692. package/src/daemon/session.mjs +0 -100
  693. package/src/daemon/thin-client.mjs +0 -71
  694. package/src/daemon/transport.mjs +0 -163
  695. package/src/gateway/claude-current.mjs +0 -255
  696. package/src/gateway/oauth-usage.mjs +0 -598
  697. package/src/gateway/route-meta.mjs +0 -629
  698. package/src/gateway/server.mjs +0 -713
  699. package/src/memory/data/runtime-manifest.json +0 -40
  700. package/src/memory/index.mjs +0 -3332
  701. package/src/memory/lib/core-memory-store.mjs +0 -330
  702. package/src/memory/lib/embedding-provider.mjs +0 -269
  703. package/src/memory/lib/embedding-worker.mjs +0 -323
  704. package/src/memory/lib/memory-cycle1.mjs +0 -645
  705. package/src/memory/lib/memory-cycle2.mjs +0 -1284
  706. package/src/memory/lib/memory-cycle3.mjs +0 -540
  707. package/src/memory/lib/memory-embed.mjs +0 -299
  708. package/src/memory/lib/memory-ops-policy.mjs +0 -190
  709. package/src/memory/lib/memory-recall-store.mjs +0 -638
  710. package/src/memory/lib/memory.mjs +0 -412
  711. package/src/memory/lib/pg/adapter.mjs +0 -308
  712. package/src/memory/lib/pg/process.mjs +0 -360
  713. package/src/memory/lib/pg/supervisor.mjs +0 -396
  714. package/src/memory/lib/runtime-fetcher.mjs +0 -458
  715. package/src/memory/lib/trace-store.mjs +0 -728
  716. package/src/memory/tool-defs.mjs +0 -79
  717. package/src/search/index.mjs +0 -1173
  718. package/src/search/lib/backends/anthropic-oauth.mjs +0 -98
  719. package/src/search/lib/backends/exa.mjs +0 -50
  720. package/src/search/lib/backends/firecrawl.mjs +0 -61
  721. package/src/search/lib/backends/gemini-api.mjs +0 -83
  722. package/src/search/lib/backends/grok-oauth.mjs +0 -86
  723. package/src/search/lib/backends/index.mjs +0 -150
  724. package/src/search/lib/backends/openai-api.mjs +0 -144
  725. package/src/search/lib/backends/openai-oauth.mjs +0 -102
  726. package/src/search/lib/backends/openai-web-search.mjs +0 -76
  727. package/src/search/lib/backends/tavily.mjs +0 -55
  728. package/src/search/lib/backends/xai-api.mjs +0 -113
  729. package/src/search/lib/config.mjs +0 -192
  730. package/src/search/lib/provider-usage.mjs +0 -67
  731. package/src/search/lib/providers.mjs +0 -47
  732. package/src/search/lib/search-intent.mjs +0 -109
  733. package/src/search/lib/setup-handler.mjs +0 -261
  734. package/src/search/lib/web-tools.mjs +0 -1219
  735. package/src/search/tool-defs.mjs +0 -83
  736. package/src/setup/defender-exclusion.mjs +0 -183
  737. package/src/shared/atomic-file.mjs +0 -436
  738. package/src/shared/config.mjs +0 -372
  739. package/src/shared/daemon-recycle.mjs +0 -108
  740. package/src/shared/disable-claude-builtins.mjs +0 -91
  741. package/src/shared/err-text.mjs +0 -12
  742. package/src/shared/llm/http-agent.mjs +0 -123
  743. package/src/shared/open-url.mjs +0 -62
  744. package/src/shared/plugin-paths.mjs +0 -58
  745. package/src/shared/schedules-store.mjs +0 -70
  746. package/src/shared/seed.mjs +0 -161
  747. package/src/shared/user-cwd.mjs +0 -225
  748. package/src/shared/user-data-guard.mjs +0 -244
  749. package/src/status/aggregator.mjs +0 -584
  750. package/src/status/server.mjs +0 -413
  751. package/tools.json +0 -1671
  752. /package/{defaults → src/defaults}/cycle3-review-prompt.md +0 -0
  753. /package/{defaults → src/defaults}/memory-promote-prompt.md +0 -0
  754. /package/{hooks → src/hooks}/lib/permission-rules.cjs +0 -0
  755. /package/{lib → src/lib}/config-cjs.cjs +0 -0
  756. /package/{lib → src/lib}/hook-pipe-path.cjs +0 -0
  757. /package/{lib → src/lib}/mixdog-debug.cjs +0 -0
  758. /package/{lib → src/lib}/text-utils.cjs +0 -0
  759. /package/{rules → src/rules}/bridge/42-cycle3-agent.md +0 -0
  760. /package/src/{agent → runtime/agent}/orchestrator/activity-bus.mjs +0 -0
  761. /package/src/{agent → runtime/agent}/orchestrator/providers/anthropic-betas.mjs +0 -0
  762. /package/src/{agent → runtime/agent}/orchestrator/session/abort-lookup.mjs +0 -0
  763. /package/src/{agent → runtime/agent}/orchestrator/session/cache/post-edit-marks.mjs +0 -0
  764. /package/src/{agent → runtime/agent}/orchestrator/session/cache/scoped-cache-outcome.mjs +0 -0
  765. /package/src/{agent → runtime/agent}/orchestrator/session/cache/util.mjs +0 -0
  766. /package/src/{agent → runtime/agent}/orchestrator/session/read-dedup.mjs +0 -0
  767. /package/src/{agent → runtime/agent}/orchestrator/session/save-session-worker.mjs +0 -0
  768. /package/src/{agent → runtime/agent}/orchestrator/smart-bridge/session-builder.mjs +0 -0
  769. /package/src/{agent → runtime/agent}/orchestrator/tools/bash-policy-scan.mjs +0 -0
  770. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/advisory-lock.mjs +0 -0
  771. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/atomic-write.mjs +0 -0
  772. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/binary-file.mjs +0 -0
  773. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/cache-layers.mjs +0 -0
  774. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/cwd-utils.mjs +0 -0
  775. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/device-paths.mjs +0 -0
  776. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -0
  777. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/diff-utils.mjs +0 -0
  778. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/fs-reachability.mjs +0 -0
  779. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/grep-formatting.mjs +0 -0
  780. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/hash-utils.mjs +0 -0
  781. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/list-formatting.mjs +0 -0
  782. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/open-config-tool.mjs +0 -0
  783. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/path-locks.mjs +0 -0
  784. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/path-utils.mjs +0 -0
  785. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-args.mjs +0 -0
  786. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-batch.mjs +0 -0
  787. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-constants.mjs +0 -0
  788. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-image-resize.mjs +0 -0
  789. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-image.mjs +0 -0
  790. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-lines.mjs +0 -0
  791. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-mode-tool.mjs +0 -0
  792. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-range-index.mjs +0 -0
  793. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-ranges.mjs +0 -0
  794. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-special-files.mjs +0 -0
  795. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-tool.mjs +0 -0
  796. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-windows.mjs +0 -0
  797. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/shell-analysis.mjs +0 -0
  798. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/shell-output.mjs +0 -0
  799. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/snapshot-helpers.mjs +0 -0
  800. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/snapshot-validation.mjs +0 -0
  801. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/text-stats.mjs +0 -0
  802. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/windows-roots.mjs +0 -0
  803. /package/src/{agent → runtime/agent}/orchestrator/tools/code-graph-prewarm-worker.mjs +0 -0
  804. /package/src/{agent → runtime/agent}/orchestrator/tools/env-scrub.mjs +0 -0
  805. /package/src/{agent → runtime/agent}/orchestrator/tools/mutation-content-cache.mjs +0 -0
  806. /package/src/{agent → runtime/agent}/orchestrator/tools/mutation-planner.mjs +0 -0
  807. /package/src/{agent → runtime/agent}/orchestrator/tools/next-call-utils.mjs +0 -0
  808. /package/src/{agent → runtime/agent}/orchestrator/tools/result-compression.mjs +0 -0
  809. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-exec-policy.mjs +0 -0
  810. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-policy-danger-target.mjs +0 -0
  811. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-policy.mjs +0 -0
  812. /package/src/{channels → runtime/channels}/lib/cli-worker-host.mjs +0 -0
  813. /package/src/{channels → runtime/channels}/lib/config-lock.mjs +0 -0
  814. /package/src/{channels → runtime/channels}/lib/event-queue.mjs +0 -0
  815. /package/src/{channels → runtime/channels}/lib/executor.mjs +0 -0
  816. /package/src/{channels → runtime/channels}/lib/format.mjs +0 -0
  817. /package/src/{channels → runtime/channels}/lib/interaction-workflows.mjs +0 -0
  818. /package/src/{channels → runtime/channels}/lib/memory-client.mjs +0 -0
  819. /package/src/{channels → runtime/channels}/lib/session-control.mjs +0 -0
  820. /package/src/{channels → runtime/channels}/lib/settings.mjs +0 -0
  821. /package/src/{channels → runtime/channels}/lib/whisper-server.mjs +0 -0
  822. /package/src/{memory → runtime/memory}/lib/agent-ipc.mjs +0 -0
  823. /package/src/{memory → runtime/memory}/lib/bridge-trace-queries.mjs +0 -0
  824. /package/src/{memory → runtime/memory}/lib/llm-worker-host.mjs +0 -0
  825. /package/src/{memory → runtime/memory}/lib/memory-cycle.mjs +0 -0
  826. /package/src/{memory → runtime/memory}/lib/memory-extraction.mjs +0 -0
  827. /package/src/{memory → runtime/memory}/lib/memory-maintenance-store.mjs +0 -0
  828. /package/src/{memory → runtime/memory}/lib/memory-recall-id-patch.mjs +0 -0
  829. /package/src/{memory → runtime/memory}/lib/memory-recall-read-query.mjs +0 -0
  830. /package/src/{memory → runtime/memory}/lib/memory-recall-scope-filter.mjs +0 -0
  831. /package/src/{memory → runtime/memory}/lib/memory-retrievers.mjs +0 -0
  832. /package/src/{memory → runtime/memory}/lib/memory-score.mjs +0 -0
  833. /package/src/{memory → runtime/memory}/lib/memory-text-utils.mjs +0 -0
  834. /package/src/{memory → runtime/memory}/lib/model-profile.mjs +0 -0
  835. /package/src/{memory → runtime/memory}/lib/project-id-resolver.mjs +0 -0
  836. /package/src/{search → runtime/search}/lib/cache.mjs +0 -0
  837. /package/src/{search → runtime/search}/lib/formatter.mjs +0 -0
  838. /package/src/{search → runtime/search}/lib/state.mjs +0 -0
  839. /package/src/{shared → runtime/shared}/abort-controller.mjs +0 -0
  840. /package/src/{shared → runtime/shared}/llm/cost.mjs +0 -0
  841. /package/src/{shared → runtime/shared}/llm/index.mjs +0 -0
  842. /package/src/{shared → runtime/shared}/llm/pid-cleanup.mjs +0 -0
  843. /package/src/{shared → runtime/shared}/llm/usage-log.mjs +0 -0
  844. /package/src/{shared → runtime/shared}/wsl.mjs +0 -0
@@ -1,3268 +0,0 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
- import {
4
- ListToolsRequestSchema,
5
- CallToolRequestSchema
6
- } from "@modelcontextprotocol/sdk/types.js";
7
- import { spawn, execSync, spawnSync } from "child_process";
8
- import * as crypto from "crypto";
9
- import * as fs from "fs";
10
- import * as http from "http";
11
- import * as os from "os";
12
- import * as path from "path";
13
- import { pathToFileURL } from "url";
14
- import { createRequire } from "module";
15
- const _require = createRequire(import.meta.url);
16
- import { loadConfig, createBackend, loadProfileConfig, DATA_DIR } from "./lib/config.mjs";
17
- import { resolveVoiceRuntime } from "./lib/voice-runtime-fetcher.mjs";
18
- import { ensureReady, transcribe, stopVoiceWhisperServer } from "./lib/whisper-server.mjs";
19
- import { loadConfig as loadAgentConfig } from "../agent/orchestrator/config.mjs";
20
- import { captureOriginalUserCwd, readLastSessionCwd } from "../shared/user-cwd.mjs";
21
- import { initProviders } from "../agent/orchestrator/providers/registry.mjs";
22
- import { Scheduler } from "./lib/scheduler.mjs";
23
- import { startSnapshotWriter, stopSnapshotWriter, recordFetchedMessages } from "./lib/status-snapshot.mjs";
24
- import { hasPending as dispatchHasPending } from "../agent/orchestrator/dispatch-persist.mjs";
25
- import { setListener as setActivityBusListener } from "../agent/orchestrator/activity-bus.mjs";
26
- import { stripSoftWarns } from "../agent/orchestrator/tool-loop-guard.mjs";
27
- import { invalidatePrefetchCache } from "../agent/orchestrator/session/cache/prefetch-cache.mjs";
28
- import { WebhookServer } from "./lib/webhook.mjs";
29
- import { EventPipeline } from "./lib/event-pipeline.mjs";
30
- import { startCliWorker } from "./lib/cli-worker-host.mjs";
31
- import {
32
- OutputForwarder,
33
- discoverSessionBoundTranscript,
34
- findLatestTranscriptByMtime
35
- } from "./lib/output-forwarder.mjs";
36
- import { controlClaudeSession } from "./lib/session-control.mjs";
37
- import { JsonStateFile, ensureDir, removeFileIfExists, writeTextFile } from "./lib/state-file.mjs";
38
- import {
39
- buildModalRequestSpec,
40
- PendingInteractionStore
41
- } from "./lib/interaction-workflows.mjs";
42
- import {
43
- ensureRuntimeDirs,
44
- makeInstanceId,
45
- getTurnEndPath,
46
- getStatusPath,
47
- getPermissionResultPath,
48
- getChannelOwnerPath,
49
- getActiveOwnerPid,
50
- getTerminalLeadPid,
51
- readActiveInstance,
52
- refreshActiveInstance,
53
- cleanupStaleRuntimeFiles,
54
- cleanupInstanceRuntimeFiles,
55
- releaseOwnedChannelLocks,
56
- clearActiveInstance,
57
- killAllPreviousServers,
58
- writeServerPid,
59
- clearServerPid,
60
- RUNTIME_ROOT
61
- } from "./lib/runtime-paths.mjs";
62
- import { PLUGIN_ROOT, getDiscordToken } from "./lib/config.mjs";
63
- const memoryClientModulePath = pathToFileURL(path.join(PLUGIN_ROOT, "src/channels/lib/memory-client.mjs")).href;
64
- const {
65
- appendEntry: memoryAppendEntry,
66
- ingestTranscript: memoryIngestTranscript,
67
- } = await import(memoryClientModulePath);
68
- const searchModulePath = pathToFileURL(path.join(PLUGIN_ROOT, "src/search/index.mjs")).href;
69
- const DEFAULT_PLUGIN_VERSION = "0.0.1";
70
- function localTimestamp() {
71
- return (/* @__PURE__ */ new Date()).toLocaleString("sv-SE", { hour12: false });
72
- }
73
- function readPluginVersion() {
74
- try {
75
- const manifestPath = path.join(PLUGIN_ROOT, ".claude-plugin", "plugin.json");
76
- const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
77
- return manifest.version || DEFAULT_PLUGIN_VERSION;
78
- } catch {
79
- return DEFAULT_PLUGIN_VERSION;
80
- }
81
- }
82
- const PLUGIN_VERSION = readPluginVersion();
83
- let crashLogging = false;
84
- let _channelsDegraded = false;
85
- let _stderrBroken = false;
86
- function isChannelsDegraded() { return _channelsDegraded; }
87
-
88
- function moduleEnabled(name) {
89
- try {
90
- const raw = JSON.parse(fs.readFileSync(path.join(DATA_DIR, "mixdog-config.json"), "utf8"));
91
- const entry = raw?.modules?.[name];
92
- return !(entry && typeof entry === "object" && entry.enabled === false);
93
- } catch {
94
- return true;
95
- }
96
- }
97
-
98
- function normalizeInternalToolResult(result) {
99
- if (!result || result.isError !== true) return result;
100
- const text = Array.isArray(result.content)
101
- ? result.content.map((part) => part?.type === "text" ? part.text || "" : JSON.stringify(part)).join("\n").trim()
102
- : String(result.raw || result.error || "").trim();
103
- return {
104
- ...result,
105
- content: [{ type: "text", text: `Error: ${text || "tool failed"}` }],
106
- isError: true,
107
- };
108
- }
109
-
110
- // stderr can break when the parent stdio pipe closes. Node then emits an
111
- // async 'error' on process.stderr, which sync try/catch around write() does
112
- // not catch — without a listener, that error becomes uncaughtException and
113
- // re-enters logCrash, looping until the disk fills. Register a suppressor
114
- // once at load time and stop writing to stderr after the first EPIPE so the
115
- // loop cannot start.
116
- try {
117
- process.stderr.on('error', (e) => {
118
- if (e && (e.code === 'EPIPE' || /EPIPE/.test(String(e.message || '')))) {
119
- _stderrBroken = true;
120
- _channelsDegraded = true;
121
- }
122
- });
123
- } catch {}
124
-
125
- // Crash log guards: dedup repeated identical errors (a single broken handler
126
- // can fire thousands of times per minute) and rotate at a 10 MB cap so the
127
- // file cannot grow unbounded. One .old generation is kept; older rolls drop.
128
- const CRASH_LOG_MAX_BYTES = 10 * 1024 * 1024;
129
- let _lastCrashSig = "";
130
- let _crashRepeatCount = 0;
131
-
132
- function _writeCrashLine(crashLog, line) {
133
- try {
134
- let size = 0;
135
- try { size = fs.statSync(crashLog).size; } catch {}
136
- if (size + line.length > CRASH_LOG_MAX_BYTES) {
137
- try { fs.renameSync(crashLog, crashLog + ".old"); } catch {}
138
- }
139
- fs.appendFileSync(crashLog, line);
140
- } catch {}
141
- }
142
-
143
- function logCrash(label, err) {
144
- if (crashLogging) return;
145
- crashLogging = true;
146
- const msg = `[${localTimestamp()}] mixdog: ${label}: ${err}
147
- ${err instanceof Error ? err.stack : ""}
148
- `;
149
- if (!_stderrBroken) {
150
- try { process.stderr.write(msg); } catch (e) {
151
- if (e && (e.code === 'EPIPE' || /EPIPE/.test(String(e.message || '')))) {
152
- _stderrBroken = true;
153
- }
154
- }
155
- }
156
- const sig = `${label}|${err && err.message ? err.message : String(err)}`;
157
- const crashLog = path.join(DATA_DIR, "crash.log");
158
- if (sig === _lastCrashSig) {
159
- // Same error repeating — count it but skip the disk write. The next
160
- // distinct error (or EPIPE branch below) flushes the suppressed total.
161
- _crashRepeatCount += 1;
162
- } else {
163
- if (_crashRepeatCount > 0) {
164
- _writeCrashLine(crashLog, `[${localTimestamp()}] mixdog: previous error repeated ${_crashRepeatCount} more time(s)\n`);
165
- _crashRepeatCount = 0;
166
- }
167
- _lastCrashSig = sig;
168
- _writeCrashLine(crashLog, msg);
169
- }
170
- if (err instanceof Error && err.message.includes("EPIPE")) {
171
- _channelsDegraded = true;
172
- _stderrBroken = true;
173
- }
174
- crashLogging = false;
175
- }
176
- process.on("unhandledRejection", (err) => logCrash("unhandled rejection", err));
177
- process.on("uncaughtException", (err) => logCrash("uncaught exception", err));
178
- if (process.env.MIXDOG_CHANNELS_NO_CONNECT) {
179
- process.exit(0);
180
- }
181
- const _isWorkerMode = process.env.MIXDOG_WORKER_MODE === '1'
182
- const _bootLogEarly = path.join(
183
- process.env.CLAUDE_PLUGIN_DATA || path.join(os.tmpdir(), "mixdog"),
184
- "boot.log"
185
- );
186
- const {
187
- isMixdogDebugEnabled: isMixdogDebug,
188
- pruneStalePluginDataLogSiblings,
189
- appendSessionStartCriticalLog,
190
- DEFAULT_STALE_LOG_SIBLING_MAX,
191
- } = _require("../../lib/mixdog-debug.cjs");
192
- // One-shot log rotation at worker boot (10 MB threshold, .1 suffix overwrite).
193
- if (isMixdogDebug()) {
194
- try { if (fs.statSync(_bootLogEarly).size > 10 * 1024 * 1024) fs.renameSync(_bootLogEarly, _bootLogEarly + '.1') } catch {}
195
- fs.appendFileSync(_bootLogEarly, `[${localTimestamp()}] bootstrap start pid=${process.pid}
196
- `);
197
- }
198
- const _bootLog = path.join(DATA_DIR, "boot.log");
199
- let config = loadConfig();
200
- let backend = createBackend(config);
201
- const INSTANCE_ID = makeInstanceId();
202
- const TERMINAL_LEAD_PID = getTerminalLeadPid();
203
- // ── drop-trace instrumentation ──────────────────────────────────────────────
204
- const _dropTraceLog = path.join(DATA_DIR, "drop-trace.log");
205
- const DROP_TRACE_ENABLED =
206
- process.env.MIXDOG_DROP_TRACE === "1" ||
207
- process.env.MIXDOG_DROP_TRACE === "true" ||
208
- process.env.MIXDOG_DEBUG_CHANNELS === "1" ||
209
- process.env.MIXDOG_DEBUG_CHANNELS === "true";
210
- // One-shot rotation for drop-trace.log at worker boot.
211
- if (DROP_TRACE_ENABLED) {
212
- try { if (fs.statSync(_dropTraceLog).size > 10 * 1024 * 1024) fs.renameSync(_dropTraceLog, _dropTraceLog + '.1') } catch {}
213
- }
214
- // Rotate additional worker logs (10 MB threshold).
215
- for (const _rotLog of ["channels-worker.log", "schedule.log", "event.log", "memory-worker.log", "mcp-debug.log", "webhook.log", "pg.log", "session-start.log"]) {
216
- const _rotPath = path.join(DATA_DIR, _rotLog);
217
- try { if (fs.statSync(_rotPath).size > 10 * 1024 * 1024) fs.renameSync(_rotPath, _rotPath + ".1") } catch {}
218
- }
219
- // GC per-worker scoped sibling logs (`<name>-worker.<leadPid>.<workerPid>.log`).
220
- // Master logs above rotate live; scoped siblings are opened once per worker
221
- // process and never reopened, so age-based removal is the only reliable
222
- // cleanup signal. 7-day TTL keeps recent crash forensics while bounding leak.
223
- const _STALE_WORKER_LOG_TTL_MS = 7 * 24 * 60 * 60 * 1000;
224
- try {
225
- const _now = Date.now();
226
- for (const _f of fs.readdirSync(DATA_DIR)) {
227
- if (!/^(channels|memory)-worker\.\d+\.\d+\.log$/.test(_f)
228
- && !/^mcp-debug\.\d+\.\d+\.log$/.test(_f)
229
- && !/^supervisor\.\d+\.log$/.test(_f)) continue;
230
- const _p = path.join(DATA_DIR, _f);
231
- try { if (_now - fs.statSync(_p).mtimeMs > _STALE_WORKER_LOG_TTL_MS) fs.unlinkSync(_p); } catch {}
232
- }
233
- } catch {}
234
- // GC stale ephemeral session files. closeSession plants a closed=true
235
- // tombstone, but bench / smoke / probe drivers historically created sessions
236
- // without ever calling closeSession, leaving 175-byte placeholders behind.
237
- // 7-day TTL is safe because live bridge sessions touch their JSON file on
238
- // every ask iteration, so any file older than 7 days is provably abandoned.
239
- const _SESSIONS_DIR = path.join(DATA_DIR, 'sessions');
240
- const _STALE_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
241
- try {
242
- const _now = Date.now();
243
- for (const _f of fs.readdirSync(_SESSIONS_DIR)) {
244
- if (!_f.endsWith('.json')) continue;
245
- const _p = path.join(_SESSIONS_DIR, _f);
246
- try { if (_now - fs.statSync(_p).mtimeMs > _STALE_SESSION_TTL_MS) fs.unlinkSync(_p); } catch {}
247
- }
248
- } catch {}
249
- // Count-based cap: drop oldest *.log siblings when plugin-data accumulates
250
- // hundreds of per-process files (doctor warns above 300).
251
- try {
252
- pruneStalePluginDataLogSiblings(DATA_DIR, DEFAULT_STALE_LOG_SIBLING_MAX);
253
- } catch {}
254
-
255
- // ── Buffered drop-trace writer (channels/index) ──────────────────────────────
256
- // Flushes every 1 s OR when buffer reaches 64 KB — whichever fires first.
257
- // Drains on process exit so no log lines are lost.
258
- let _dtIdxBuf = "";
259
- let _dtIdxBytes = 0;
260
- let _dtIdxFlushTimer = null;
261
- let _dtIdxStream = null;
262
- function _dtIdxGetStream() {
263
- if (!_dtIdxStream) _dtIdxStream = fs.createWriteStream(_dropTraceLog, { flags: "a" });
264
- return _dtIdxStream;
265
- }
266
- async function _dtIdxFlush() {
267
- if (_dtIdxFlushTimer) { clearTimeout(_dtIdxFlushTimer); _dtIdxFlushTimer = null; }
268
- if (!_dtIdxBuf) return;
269
- const stream = _dtIdxGetStream();
270
- const buf = _dtIdxBuf;
271
- _dtIdxBuf = "";
272
- _dtIdxBytes = 0;
273
- try {
274
- const ok = stream.write(buf);
275
- if (!ok) { const { once } = await import("node:events"); await once(stream, "drain").catch(() => {}); }
276
- } catch {}
277
- }
278
- function _dtIdxScheduleFlush() {
279
- if (_dtIdxFlushTimer) return;
280
- _dtIdxFlushTimer = setTimeout(() => { void _dtIdxFlush(); }, 1000);
281
- if (_dtIdxFlushTimer.unref) _dtIdxFlushTimer.unref();
282
- }
283
- function _dtIdxAppend(line) {
284
- _dtIdxBuf += line;
285
- _dtIdxBytes += Buffer.byteLength(line);
286
- if (_dtIdxBytes >= 65536) { void _dtIdxFlush(); return; }
287
- _dtIdxScheduleFlush();
288
- }
289
- process.on("exit", () => { void _dtIdxFlush(); });
290
- // SIGTERM: flush the drop-trace buffer, but do NOT exit here. In worker
291
- // mode the graceful `_channelsShutdownHandler` below owns shutdown
292
- // (stop() → cleanup → process.exit). In non-worker mode no SIGTERM
293
- // handler was previously installed beyond this one; defer to default
294
- // termination so process.on('exit') hooks still run.
295
- process.on("SIGTERM", () => {
296
- void _dtIdxFlush();
297
- if (!_isWorkerMode) process.exit(0);
298
- });
299
-
300
- function preview(text) {
301
- if (!text) return "";
302
- const s = String(text).replace(/\n/g, "\\n");
303
- return s.length > 120 ? s.slice(0, 120) + "…" : s;
304
- }
305
- function dropTrace(event, fields) {
306
- if (!DROP_TRACE_ENABLED) return;
307
- try {
308
- const ts = (/* @__PURE__ */ new Date()).toISOString();
309
- const loc = `[${ts}][pid=${process.pid}] ${event}`;
310
- const kv = fields ? " " + Object.entries(fields).map(([k, v]) => `${k}=${v}`).join(" ") : "";
311
- _dtIdxAppend(loc + kv + "\n");
312
- } catch {}
313
- }
314
- // ────────────────────────────────────────────────────────────────────────────
315
- ensureRuntimeDirs();
316
- cleanupStaleRuntimeFiles();
317
- if (!_isWorkerMode) {
318
- killAllPreviousServers();
319
- writeServerPid();
320
- // Publish owner identity immediately so the SessionStart shim's
321
- // owner_lead_alive() sees a live owner and uses the full connect budget
322
- // instead of the 5s no-owner grace (fixes missing recap/core on restart).
323
- // backendReady intentionally omitted — readiness stays gated until connect.
324
- refreshActiveInstance(INSTANCE_ID);
325
- startCliWorker();
326
- }
327
- const INSTRUCTIONS = "";
328
-
329
- // ── Parent notification helper ───────────────────────────────────────
330
- // This worker has no MCP transport of its own. All notifications flow
331
- // through IPC to the parent (server.mjs), which owns the single connected
332
- // MCP `Server` instance. The parent's IPC message handler translates
333
- // `{type:'notify', method, params}` into `server.notification({method, params})`.
334
- //
335
- // Before v0.6.7 the worker had its own orphan `Server` instance that was
336
- // never `connect()`ed to any transport, so `.notification()` silently
337
- // threw 'Not connected' inside the SDK and every call was dropped by an
338
- // outer `.catch(() => {})`. That regression is what this path replaces.
339
- function sendNotifyToParent(method, params) {
340
- if (!process.send) {
341
- try { process.stderr.write(`mixdog channels: notify dropped (no IPC): ${method}\n`); } catch {}
342
- return;
343
- }
344
- // CC channel schema requires meta: Record<string,string> (channelNotification.ts).
345
- // Coerce every meta value to string so a non-string (e.g. a Discord
346
- // interaction.type number) can't fail zod and silently drop the notify.
347
- // silent_to_agent stays boolean — an internal routing flag the daemon
348
- // router / agentNotify consume (=== true) before the CC zod boundary.
349
- let outParams = params;
350
- if (method === 'notifications/claude/channel' && params && params.meta) {
351
- const m = {};
352
- for (const [k, v] of Object.entries(params.meta)) {
353
- if (v === undefined || v === null) continue;
354
- m[k] = k === 'silent_to_agent' ? (v === true || v === 'true') : String(v);
355
- }
356
- outParams = { ...params, meta: m };
357
- }
358
- try {
359
- process.send({ type: 'notify', method, params: outParams });
360
- } catch (err) {
361
- try { process.stderr.write(`mixdog channels: notify IPC send failed: ${err && err.message || err}\n`); } catch {}
362
- }
363
- }
364
-
365
- const recapState = { state: 'idle', running: false, startedAt: null, lastCompletedAt: null, updatedAt: null, errorMessage: null };
366
- function sendRecapStateToParent() {
367
- if (!process.send) return;
368
- try {
369
- process.send({ type: 'recap_status', recap: { ...recapState } });
370
- } catch (err) {
371
- try { process.stderr.write(`mixdog channels: recap status IPC send failed: ${err && err.message || err}\n`); } catch {}
372
- }
373
- }
374
-
375
- // ── Memory worker bridge (worker → parent → memory) ─────────────────
376
- // The channels worker does not own the memory worker handle. To trigger
377
- // memory tool actions (e.g. cycle1) we send `memory_call_request` to the
378
- // parent, which routes through callWorker('memory', ...) and ships the
379
- // result back as `memory_call_response`. The response listener is
380
- // integrated into the main IPC handler below (not a second listener).
381
- const _memoryCallPending = new Map();
382
- let _memoryCallSeq = 0;
383
-
384
- function callMemoryAction(action, args, timeoutMs) {
385
- return new Promise((resolve, reject) => {
386
- if (!process.send) return reject(new Error('not a worker process'));
387
- const callId = `mc_${INSTANCE_ID}_${++_memoryCallSeq}_${Math.random().toString(36).slice(2, 8)}`;
388
- const timer = setTimeout(() => {
389
- _memoryCallPending.delete(callId);
390
- reject(new Error(`memory_call ${action} timed out after ${timeoutMs}ms`));
391
- }, timeoutMs);
392
- _memoryCallPending.set(callId, {
393
- resolve: (v) => { clearTimeout(timer); resolve(v); },
394
- reject: (e) => { clearTimeout(timer); reject(e); },
395
- });
396
- try {
397
- process.send({ type: 'memory_call_request', callId, action, args: args || {} });
398
- } catch (e) {
399
- _memoryCallPending.delete(callId);
400
- clearTimeout(timer);
401
- reject(e);
402
- }
403
- });
404
- }
405
- function resolveChannelLabel(channelsConfig, label) {
406
- if (!label || !channelsConfig) return label;
407
- const entry = channelsConfig[label];
408
- if (entry?.channelId) return entry.channelId;
409
- return label;
410
- }
411
- let channelBridgeActive = false;
412
- function writeBridgeState(active) {
413
- try {
414
- const stateFile = path.join(os.tmpdir(), "mixdog", "bridge-state.json");
415
- fs.mkdirSync(path.dirname(stateFile), { recursive: true });
416
- fs.writeFileSync(stateFile, JSON.stringify({ active, ts: Date.now() }));
417
- } catch {
418
- }
419
- }
420
- function isChannelBridgeActive() {
421
- return channelBridgeActive;
422
- }
423
- let typingChannelId = null;
424
- const pendingSetup = new PendingInteractionStore();
425
- function startServerTyping(channelId) {
426
- if (typingChannelId && typingChannelId !== channelId) {
427
- backend.stopTyping(typingChannelId);
428
- }
429
- typingChannelId = channelId;
430
- backend.startTyping(channelId);
431
- }
432
- function stopServerTyping() {
433
- if (typingChannelId) {
434
- backend.stopTyping(typingChannelId);
435
- typingChannelId = null;
436
- }
437
- }
438
- const TURN_END_FILE = getTurnEndPath(INSTANCE_ID);
439
- const TURN_END_BASENAME = path.basename(TURN_END_FILE);
440
- const TURN_END_DIR = path.dirname(TURN_END_FILE);
441
- let turnEndWatcher = null;
442
- if (!_isWorkerMode) {
443
- removeFileIfExists(TURN_END_FILE);
444
- turnEndWatcher = fs.watch(TURN_END_DIR, async (_event, filename) => {
445
- if (filename !== TURN_END_BASENAME) return;
446
- try {
447
- const stat = fs.statSync(TURN_END_FILE);
448
- if (stat.size > 0) {
449
- stopServerTyping();
450
- await forwarder.forwardFinalText();
451
- removeFileIfExists(TURN_END_FILE);
452
- }
453
- } catch {
454
- }
455
- });
456
- }
457
- const STATUS_FILE = getStatusPath(INSTANCE_ID);
458
- const statusState = new JsonStateFile(STATUS_FILE, {});
459
- statusState.ensure();
460
- function sessionIdFromTranscriptPath(transcriptPath) {
461
- const base = path.basename(transcriptPath);
462
- return base.endsWith(".jsonl") ? base.slice(0, -6) : "";
463
- }
464
- function getPersistedTranscriptPath() {
465
- const state = statusState.read();
466
- if (typeof state.transcriptPath === "string" && state.transcriptPath) return state.transcriptPath;
467
- return readActiveInstance()?.transcriptPath ?? "";
468
- }
469
- function pickUsableTranscriptPath(bound, previousPath) {
470
- if (bound?.exists) return bound.transcriptPath;
471
- if (!previousPath) return "";
472
- if (!bound?.sessionId) return previousPath;
473
- return sessionIdFromTranscriptPath(previousPath) === bound.sessionId ? previousPath : "";
474
- }
475
- const forwarder = new OutputForwarder({
476
- send: async (ch, text) => {
477
- if (!channelBridgeActive) {
478
- throw new Error("send() called while channel bridge is inactive");
479
- }
480
- await backend.sendMessage(ch, text);
481
- },
482
- recordAssistantTurn: async () => {
483
- },
484
- react: (ch, mid, emoji) => {
485
- if (!channelBridgeActive) return Promise.resolve();
486
- return backend.react(ch, mid, emoji);
487
- },
488
- removeReaction: (ch, mid, emoji) => {
489
- if (!channelBridgeActive) return Promise.resolve();
490
- return backend.removeReaction(ch, mid, emoji);
491
- }
492
- }, statusState);
493
- forwarder.setOnIdle(() => {
494
- stopServerTyping();
495
- void forwarder.forwardFinalText();
496
- });
497
- // Wire the forwarder ownership probe unconditionally. wireEventQueueHandlers()
498
- // also sets this, but that path only runs when the event pipeline starts
499
- // (webhook enabled or event rules present). Without an event pipeline the
500
- // forwarder's ownerGetter stayed null and _isOwner() failed open, letting a
501
- // non-owner / proxy process forward transcript output (duplicate Discord
502
- // sends). The closure reads bridgeRuntimeConnected/proxyMode at call time.
503
- forwarder.setOwnerGetter(() => bridgeRuntimeConnected && !proxyMode);
504
- function applyTranscriptBinding(channelId, transcriptPath, options = {}) {
505
- if (!transcriptPath) return;
506
- forwarder.setContext(channelId, transcriptPath, { replayFromStart: options.replayFromStart, catchUpFromPersisted: options.catchUpFromPersisted });
507
- const boundTranscriptPath = forwarder.transcriptPath || transcriptPath;
508
- forwarder.startWatch();
509
- void memoryIngestTranscript(boundTranscriptPath, { cwd: options.cwd });
510
- refreshActiveInstance(INSTANCE_ID, { channelId, transcriptPath: boundTranscriptPath });
511
- if (options.persistStatus !== false) {
512
- statusState.update((state) => {
513
- state.channelId = channelId;
514
- state.transcriptPath = boundTranscriptPath;
515
- state.lastFileSize = forwarder.lastFileSize;
516
- state.sentCount = forwarder.sentCount;
517
- state.lastSentHash = forwarder.lastHash;
518
- state.lastSentTime = 0;
519
- state.sessionIdle = false;
520
- state.sessionCwd = options.cwd ?? null;
521
- });
522
- }
523
- }
524
- async function rebindTranscriptContext(channelId, options = {}) {
525
- const previousPath = options.previousPath ?? "";
526
- const mode = options.mode ?? "same";
527
- const explicitTranscriptPath = typeof options.transcriptPath === "string" ? options.transcriptPath.trim() : "";
528
- if (explicitTranscriptPath) {
529
- let explicitExists = false;
530
- try {
531
- explicitExists = fs.statSync(explicitTranscriptPath).isFile();
532
- } catch {
533
- explicitExists = false;
534
- }
535
- if (explicitExists) {
536
- applyTranscriptBinding(channelId, explicitTranscriptPath, {
537
- replayFromStart: Boolean(options.catchUp),
538
- catchUpFromPersisted: options.catchUpFromPersisted,
539
- persistStatus: options.persistStatus
540
- });
541
- if (options.catchUp || options.catchUpFromPersisted) {
542
- await forwarder.forwardNewText();
543
- }
544
- return explicitTranscriptPath;
545
- }
546
- }
547
- let sawPendingTranscript = false;
548
- let pendingSessionId = "";
549
- for (let attempt = 0; attempt < 30; attempt += 1) {
550
- const bound = discoverSessionBoundTranscript();
551
- if (bound?.exists) {
552
- const acceptable = mode === "same" || !previousPath || bound.transcriptPath !== previousPath;
553
- if (acceptable) {
554
- const replayFromStart = Boolean(
555
- options.catchUp && !previousPath && sawPendingTranscript && pendingSessionId === bound.sessionId
556
- );
557
- applyTranscriptBinding(channelId, bound.transcriptPath, {
558
- replayFromStart,
559
- catchUpFromPersisted: options.catchUpFromPersisted,
560
- persistStatus: options.persistStatus,
561
- cwd: bound.sessionCwd,
562
- });
563
- if (replayFromStart || options.catchUpFromPersisted) {
564
- await forwarder.forwardNewText();
565
- }
566
- return bound.transcriptPath;
567
- }
568
- } else if (bound?.sessionId) {
569
- sawPendingTranscript = true;
570
- pendingSessionId = bound.sessionId;
571
- }
572
- await new Promise((resolve) => setTimeout(resolve, 150));
573
- }
574
- if (previousPath) {
575
- applyTranscriptBinding(channelId, previousPath, { catchUpFromPersisted: true, cwd: statusState.read().sessionCwd });
576
- await forwarder.forwardNewText();
577
- process.stderr.write(`mixdog: rebind fallback: bound previous transcript ${previousPath}\n`);
578
- return previousPath;
579
- }
580
- process.stderr.write(`mixdog: rebind failed: no transcript found and no previous path to fall back to\n`);
581
- return "";
582
- }
583
- const scheduler = new Scheduler(
584
- config.nonInteractive ?? [],
585
- config.interactive ?? [],
586
- // channelsConfig kept for channel-label resolution (resolveChannel)
587
- // only — quiet/schedules now come from the top-level config.
588
- config.channelsConfig,
589
- // 0.1.62: top-level normalized config carries quiet/schedules.
590
- config
591
- );
592
- // Register the pending-dispatch probe so the scheduler treats an in-flight
593
- // bridge dispatch as "active" regardless of user-inbound silence.
594
- scheduler.setPendingCheck(() => {
595
- try {
596
- return dispatchHasPending(process.env.CLAUDE_PLUGIN_DATA);
597
- } catch {
598
- return false;
599
- }
600
- });
601
- // Bridge the orchestrator-side activity notifier into the scheduler so
602
- // events like `addPending` can bump lastActivity without importing the
603
- // scheduler instance directly (avoids module cycles).
604
- setActivityBusListener(() => scheduler.noteActivity());
605
- let webhookServer = null;
606
- let eventPipeline = null;
607
- let bridgeRuntimeConnected = false;
608
- let bridgeRuntimeStarting = false;
609
- // Stop-requested signal: set by stopOwnedRuntime() when it runs during the
610
- // startOwnedRuntime() in-flight window (bridgeRuntimeStarting=true). Checked
611
- // by startOwnedRuntime() right after backend.connect() resolves so the
612
- // in-flight start does not revive owner state after the stop already tore
613
- // the partial-start state down.
614
- let _ownedRuntimeStopRequested = false;
615
- let bridgeOwnershipRefreshInFlight = null;
616
- let bridgeOwnershipTimer = null;
617
- let lastOwnershipNote = "";
618
- const ACTIVE_OWNER_STALE_MS = 1e4;
619
- // Owner heartbeat: keep active-instance.json fresh so other sessions cannot
620
- // steal the seat after 10 s of channel-action silence. unref'd interval —
621
- // never blocks process exit. Single JSON atomic write, no measurable load.
622
- const OWNER_HEARTBEAT_INTERVAL_MS = 5e3;
623
- let ownerHeartbeatTimer = null;
624
- // Owner gating here is multi-process runtime coordination: only the active
625
- // bindingReady gates all send paths until the boot-time refreshBridgeOwnership
626
- // ({ restoreBinding: true }) call completes. Without this, scheduler/webhook
627
- // emissions fired within the first ~few hundred ms after restart drop because
628
- // the Discord backend binding has not yet been established.
629
- let bindingReadyStatus = "pending";
630
- // Channel-flag detection result, stored at module scope so the worker-mode
631
- // ready IPC can forward it to the daemon for caching across respawns.
632
- let _channelFlagDetected = false;
633
- let _bindingReadyResolve;
634
- const bindingReady = new Promise((r) => { _bindingReadyResolve = r; });
635
- dropTrace("bindingReady.create", { status: bindingReadyStatus });
636
- // owner runs webhook/event ticks. It is not webhook HTTP authentication.
637
- let proxyMode = false;
638
- let ownerHttpPort = 0;
639
- let ownerHttpServer = null;
640
- const PROXY_PORT_MIN = 3460;
641
- const PROXY_PORT_MAX = 3467;
642
- // Per-owner-process auth secret. Generated once at HTTP server start and
643
- // published into runtime/owner-secret-<instanceId>.json with 0o600 perms so
644
- // only the owner UID can read it back. requireOwnerToken below checks THIS
645
- // secret (not the public-by-/ping instanceId) so any local caller that
646
- // scrapes /ping cannot forge owner-side calls. The file is keyed on the
647
- // owner's INSTANCE_ID — the SAME identifier published into active-instance
648
- // as `instanceId` and validated by requireOwnerToken's x-owner-instance
649
- // header check — so proxy readers can resolve the path off readActiveInstance()
650
- // without depending on getActiveOwnerPid(), which prefers ownerLeadPid/
651
- // terminalLeadPid/supervisor_pid and would diverge from process.pid in
652
- // supervisor-backed sessions.
653
- let OWNER_SECRET = "";
654
- function getOwnerSecretPath(instanceId) {
655
- return path.join(RUNTIME_ROOT, `owner-secret-${String(instanceId)}.json`);
656
- }
657
- function publishOwnerSecret(secret) {
658
- const file = getOwnerSecretPath(INSTANCE_ID);
659
- try { ensureDir(RUNTIME_ROOT); } catch {}
660
- // Best-effort restrictive write: O_CREAT|O_TRUNC|O_WRONLY with mode 0o600.
661
- // On Windows mode bits are largely ignored, but the file still lives in
662
- // the per-user tmp dir; an attacker without the same UID cannot read it.
663
- try { fs.unlinkSync(file); } catch {}
664
- const fd = fs.openSync(file, fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY, 0o600);
665
- try {
666
- fs.writeSync(fd, JSON.stringify({ instanceId: INSTANCE_ID, pid: process.pid, secret, updatedAt: Date.now() }));
667
- } finally {
668
- try { fs.closeSync(fd); } catch {}
669
- }
670
- try { fs.chmodSync(file, 0o600); } catch {}
671
- }
672
- function clearOwnerSecret() {
673
- try { fs.unlinkSync(getOwnerSecretPath(INSTANCE_ID)); } catch {}
674
- }
675
- function readOwnerSecretFor(ownerInstanceId) {
676
- if (!ownerInstanceId) return "";
677
- try {
678
- const raw = fs.readFileSync(getOwnerSecretPath(ownerInstanceId), "utf8");
679
- const parsed = JSON.parse(raw);
680
- return typeof parsed?.secret === "string" ? parsed.secret : "";
681
- } catch {
682
- return "";
683
- }
684
- }
685
- async function proxyRequest(endpoint, method, body) {
686
- return new Promise((resolve) => {
687
- const url = new URL(`http://127.0.0.1:${ownerHttpPort}${endpoint}`);
688
- // Auth: read the owner's per-process secret from the restricted
689
- // owner-secret file (0o600). The instanceId header is kept only as a
690
- // secondary diagnostic — requireOwnerToken on the owner side checks
691
- // the secret, not the instanceId.
692
- const active = readActiveInstance();
693
- const ownerInstanceId = active?.instanceId || INSTANCE_ID;
694
- // Key the secret-file lookup on the owner's published instanceId — the
695
- // SAME identifier the owner used when writing owner-secret-<instanceId>.json
696
- // (publishOwnerSecret above) and what requireOwnerToken's x-owner-instance
697
- // header check compares against. Do NOT route this through
698
- // getActiveOwnerPid(active): that helper prefers ownerLeadPid /
699
- // terminalLeadPid / supervisor_pid, which in a supervisor-backed session
700
- // diverge from the owner-HTTP process.pid (== owner's INSTANCE_ID),
701
- // causing the proxy to read owner-secret-<supervisorPid>.json while the
702
- // owner wrote owner-secret-<process.pid>.json → empty secret → 401.
703
- const ownerSecret = readOwnerSecretFor(ownerInstanceId);
704
- if (!ownerSecret) {
705
- resolve({ ok: false, error: "owner secret unavailable (file missing or unreadable)" });
706
- return;
707
- }
708
- const reqOpts = {
709
- hostname: "127.0.0.1",
710
- port: ownerHttpPort,
711
- path: url.pathname + url.search,
712
- method,
713
- headers: {
714
- "Content-Type": "application/json",
715
- "x-owner-token": ownerSecret,
716
- "x-owner-instance": ownerInstanceId,
717
- },
718
- timeout: 3e4
719
- };
720
- const req = http.request(reqOpts, (res) => {
721
- let data = "";
722
- res.on("data", (chunk) => {
723
- data += chunk;
724
- });
725
- res.on("end", () => {
726
- try {
727
- const parsed = JSON.parse(data);
728
- resolve({ ok: res.statusCode === 200, data: parsed, error: parsed.error });
729
- } catch {
730
- resolve({ ok: false, error: `invalid response from owner: ${data.slice(0, 200)}` });
731
- }
732
- });
733
- });
734
- req.on("error", (err) => {
735
- resolve({ ok: false, error: `proxy request failed: ${err.message}` });
736
- });
737
- req.on("timeout", () => {
738
- req.destroy();
739
- resolve({ ok: false, error: "proxy request timed out" });
740
- });
741
- if (body) req.write(JSON.stringify(body));
742
- req.end();
743
- });
744
- }
745
- async function pingOwner(port) {
746
- return new Promise((resolve) => {
747
- const req = http.request({
748
- hostname: "127.0.0.1",
749
- port,
750
- path: "/ping",
751
- method: "GET",
752
- timeout: 3e3
753
- }, (res) => {
754
- res.resume();
755
- resolve(res.statusCode === 200);
756
- });
757
- req.on("error", () => resolve(false));
758
- req.on("timeout", () => {
759
- req.destroy();
760
- resolve(false);
761
- });
762
- req.end();
763
- });
764
- }
765
- function tryListenPort(server, port) {
766
- return new Promise((resolve) => {
767
- server.once("error", () => resolve(false));
768
- server.listen(port, "127.0.0.1", () => resolve(true));
769
- });
770
- }
771
- // Owner-token auth gate. Compares x-owner-token against the per-process
772
- // OWNER_SECRET generated at startOwnerHttpServer time. The secret is NOT
773
- // returned by /ping (only the public instanceId is) so a local caller that
774
- // scrapes /ping still cannot forge owner-side calls. Constant-time compare
775
- // to avoid trivial timing leakage on the local socket. Optional secondary
776
- // instanceId check via x-owner-instance: when present it must match this
777
- // process's INSTANCE_ID, catching stale clients targeting an old owner.
778
- function requireOwnerToken(req, res) {
779
- const token = req.headers["x-owner-token"];
780
- if (!OWNER_SECRET || typeof token !== "string" || token.length !== OWNER_SECRET.length) {
781
- res.writeHead(401, { "Content-Type": "application/json" });
782
- res.end(JSON.stringify({ error: "unauthorized: x-owner-token required" }));
783
- return false;
784
- }
785
- let ok = false;
786
- try {
787
- ok = crypto.timingSafeEqual(Buffer.from(token), Buffer.from(OWNER_SECRET));
788
- } catch {
789
- ok = false;
790
- }
791
- if (!ok) {
792
- res.writeHead(401, { "Content-Type": "application/json" });
793
- res.end(JSON.stringify({ error: "unauthorized: x-owner-token required" }));
794
- return false;
795
- }
796
- const instanceHeader = req.headers["x-owner-instance"];
797
- if (instanceHeader && instanceHeader !== INSTANCE_ID) {
798
- res.writeHead(401, { "Content-Type": "application/json" });
799
- res.end(JSON.stringify({ error: "unauthorized: instance mismatch" }));
800
- return false;
801
- }
802
- return true;
803
- }
804
- // Per-route handler table. Each handler matches the original switch-case
805
- // behavior byte-for-byte (auth checks, status codes, response shapes); the
806
- // outer dispatch loop just looks up the entry instead of running a long
807
- // switch. `methods` mirrors any pre-existing 405 guard.
808
- const OWNER_ROUTES = {
809
- "/ping": async (req, res /*, body, url*/) => {
810
- res.writeHead(200);
811
- res.end(JSON.stringify({ ok: true, instanceId: INSTANCE_ID, pid: process.pid }));
812
- },
813
- "/send": async (req, res, body) => {
814
- if (!requireOwnerToken(req, res)) return;
815
- // Pre/post-send activity bumps keep idle gating consistent across
816
- // slow network / attachment / rate-limited sends; double bump is
817
- // harmless.
818
- scheduler.noteActivity();
819
- const sendResult = await backend.sendMessage(body.chatId, body.text, body.opts);
820
- scheduler.noteActivity();
821
- res.writeHead(200);
822
- res.end(JSON.stringify({ sentIds: sendResult.sentIds }));
823
- },
824
- "/react": async (req, res, body) => {
825
- if (!requireOwnerToken(req, res)) return;
826
- await backend.react(body.chatId, body.messageId, body.emoji);
827
- res.writeHead(200);
828
- res.end(JSON.stringify({ ok: true }));
829
- },
830
- "/edit": async (req, res, body) => {
831
- if (!requireOwnerToken(req, res)) return;
832
- const editId = await backend.editMessage(body.chatId, body.messageId, body.text, body.opts);
833
- res.writeHead(200);
834
- res.end(JSON.stringify({ id: editId }));
835
- },
836
- "/fetch": async (req, res, body, url) => {
837
- if (!requireOwnerToken(req, res)) return;
838
- const channelId = url.searchParams.get("channel") ?? "";
839
- const limit = parseInt(url.searchParams.get("limit") ?? "20", 10);
840
- const msgs = await backend.fetchMessages(channelId, limit);
841
- recordFetchedMessages(channelId, labelForChannelId(channelId), msgs);
842
- res.writeHead(200);
843
- res.end(JSON.stringify({ messages: msgs }));
844
- },
845
- "/download": async (req, res, body) => {
846
- if (!requireOwnerToken(req, res)) return;
847
- const files = await backend.downloadAttachment(body.chatId, body.messageId);
848
- res.writeHead(200);
849
- res.end(JSON.stringify({ files }));
850
- },
851
- "/typing/start": async (req, res, body) => {
852
- if (!requireOwnerToken(req, res)) return;
853
- backend.startTyping(body.channelId);
854
- res.writeHead(200);
855
- res.end(JSON.stringify({ ok: true }));
856
- },
857
- "/typing/stop": async (req, res, body) => {
858
- if (!requireOwnerToken(req, res)) return;
859
- backend.stopTyping(body.channelId);
860
- res.writeHead(200);
861
- res.end(JSON.stringify({ ok: true }));
862
- },
863
- "/inject": async (req, res, body) => {
864
- // Require owner-token header to prevent unauthenticated local injection.
865
- if (!requireOwnerToken(req, res)) return;
866
- const content = body.content;
867
- if (!content) {
868
- res.writeHead(400);
869
- res.end(JSON.stringify({ error: "content required" }));
870
- return;
871
- }
872
- const source = body.source || "mixdog-agent";
873
- const injMeta = { user: source, user_id: "system", ts: (/* @__PURE__ */ new Date()).toISOString() };
874
- if (body.instruction) injMeta.instruction = body.instruction;
875
- if (body.type) injMeta.type = body.type;
876
- sendNotifyToParent("notifications/claude/channel", { content, meta: injMeta });
877
- res.writeHead(200);
878
- res.end(JSON.stringify({ ok: true }));
879
- },
880
- "/trigger-schedule": async (req, res, body) => {
881
- // Native fallback for `mcp__trigger_schedule` so out-of-band
882
- // verification works when the MCP stdio bridge is down (Claude Code
883
- // disconnected, supervisor restart pending, etc.). Same authz as
884
- // /inject — x-owner-token must equal INSTANCE_ID.
885
- if (req.method !== "POST") { res.writeHead(405); res.end(JSON.stringify({ error: "POST required" })); return; }
886
- if (!requireOwnerToken(req, res)) return;
887
- const triggerName = body.name;
888
- if (!triggerName) {
889
- res.writeHead(400);
890
- res.end(JSON.stringify({ error: "name required" }));
891
- return;
892
- }
893
- try {
894
- const r = await scheduler.triggerManual(triggerName);
895
- res.writeHead(200);
896
- res.end(JSON.stringify({ ok: true, result: r ?? null }));
897
- } catch (e) {
898
- res.writeHead(500);
899
- res.end(JSON.stringify({ error: e?.message || String(e) }));
900
- }
901
- },
902
- "/schedule-status": async (req, res) => {
903
- // Owner-side schedule_status so standby/proxy sessions read the LIVE
904
- // scheduler instead of their own stale local state. Mirrors the MCP
905
- // schedule_status handler's formatting (kept byte-identical via the
906
- // shared scheduleStatusResult() helper).
907
- if (!requireOwnerToken(req, res)) return;
908
- try {
909
- const r = scheduleStatusResult();
910
- res.writeHead(200);
911
- res.end(JSON.stringify({ ok: true, result: r }));
912
- } catch (e) {
913
- res.writeHead(500);
914
- res.end(JSON.stringify({ error: e?.message || String(e) }));
915
- }
916
- },
917
- "/schedule-control": async (req, res, body) => {
918
- // Owner-side schedule_control so standby/proxy sessions mutate the LIVE
919
- // scheduler (defer/skip_today) instead of their own stale local state.
920
- // Validation lives here because the proxy side's scheduler.nonInteractive/
921
- // interactive lists are not authoritative.
922
- if (req.method !== "POST") { res.writeHead(405); res.end(JSON.stringify({ error: "POST required" })); return; }
923
- if (!requireOwnerToken(req, res)) return;
924
- try {
925
- const r = scheduleControlResult(body || {});
926
- res.writeHead(200);
927
- res.end(JSON.stringify({ ok: true, result: r }));
928
- } catch (e) {
929
- res.writeHead(500);
930
- res.end(JSON.stringify({ error: e?.message || String(e) }));
931
- }
932
- },
933
- "/bridge": async (req, res, body) => {
934
- if (req.method !== "POST") { res.writeHead(405); res.end(JSON.stringify({ error: "POST required" })); return; }
935
- if (!requireOwnerToken(req, res)) return;
936
- const bridgeFile = body.file;
937
- const bridgePrompt = body.prompt;
938
- const bridgeRef = body.ref;
939
- const bridgeRole = body.role;
940
- const bridgeContext = body.context;
941
- let bridgePromptFinal = bridgePrompt;
942
- if (!bridgePromptFinal && bridgeFile) {
943
- try { bridgePromptFinal = fs.readFileSync(bridgeFile, "utf-8").trim(); } catch (e) {
944
- res.writeHead(400); res.end(JSON.stringify({ error: `Cannot read file: ${e.message}` })); return;
945
- }
946
- }
947
- if (!bridgePromptFinal && !bridgeRef) { res.writeHead(400); res.end(JSON.stringify({ error: "prompt, file, or ref required" })); return; }
948
- try {
949
- const agentMod = await import(pathToFileURL(path.join(path.dirname(import.meta.url.replace("file:///", "").replace(/\//g, path.sep)), "..", "agent", "index.mjs")).href);
950
- if (agentMod.init) await agentMod.init();
951
- const toolArgs = {};
952
- if (bridgePromptFinal) toolArgs.prompt = bridgePromptFinal;
953
- if (bridgeRef) toolArgs.ref = bridgeRef;
954
- if (bridgeRole) toolArgs.role = bridgeRole;
955
- if (bridgeContext) toolArgs.context = bridgeContext;
956
- const notifyFn = (text, extraMeta) => {
957
- sendNotifyToParent("notifications/claude/channel", {
958
- content: text,
959
- meta: {
960
- user: "mixdog-agent",
961
- user_id: "system",
962
- ts: new Date().toISOString(),
963
- ...(extraMeta || {})
964
- }
965
- });
966
- };
967
- const BRIDGE_HTTP_TIMEOUT_MS = 10 * 60 * 1000; // 10 min
968
- const bridgeAbort = new AbortController();
969
- const bridgeTimer = setTimeout(() => bridgeAbort.abort(new Error("bridge HTTP timeout")), BRIDGE_HTTP_TIMEOUT_MS);
970
- const onReqClose = () => bridgeAbort.abort(new Error("client disconnected"));
971
- req.on("close", onReqClose);
972
- let result;
973
- try {
974
- result = await Promise.race([
975
- agentMod.handleToolCall("bridge", toolArgs, { notifyFn, requestSignal: bridgeAbort.signal }),
976
- new Promise((_, reject) => bridgeAbort.signal.addEventListener("abort", () => reject(bridgeAbort.signal.reason), { once: true })),
977
- ]);
978
- } finally {
979
- clearTimeout(bridgeTimer);
980
- req.removeListener("close", onReqClose);
981
- }
982
- res.writeHead(200);
983
- res.end(JSON.stringify(result));
984
- } catch (e) {
985
- res.writeHead(500); res.end(JSON.stringify({ error: e.message })); return;
986
- }
987
- },
988
- "/bridge/activate": async (req, res, body) => {
989
- if (!requireOwnerToken(req, res)) return;
990
- const active = Boolean(body.active);
991
- const wasActive = channelBridgeActive;
992
- channelBridgeActive = active;
993
- writeBridgeState(active);
994
- if (!active && wasActive) {
995
- // Mirror the MCP activate_channel_bridge deactivate path: tear down
996
- // owner-side runtime (Discord/scheduler/webhook/event/owner-HTTP/
997
- // heartbeat) so a deactivated bridge doesn't keep running and this
998
- // owner can't later proxyMode against its own port.
999
- stopServerTyping();
1000
- try { await stopOwnedRuntime("bridge deactivated"); } catch (e) {
1001
- process.stderr.write(`mixdog: stopOwnedRuntime on deactivate failed: ${e?.message || e}\n`);
1002
- }
1003
- }
1004
- res.writeHead(200);
1005
- res.end(JSON.stringify({ ok: true, active: channelBridgeActive }));
1006
- },
1007
- "/mcp": async (req, res, body) => {
1008
- if (req.method === "POST") {
1009
- // Require owner-token header to prevent unauthenticated local MCP dispatch.
1010
- if (!requireOwnerToken(req, res)) return;
1011
- const httpMcp = createHttpMcpServer();
1012
- const httpTransport = new StreamableHTTPServerTransport({
1013
- sessionIdGenerator: void 0,
1014
- enableJsonResponse: true
1015
- });
1016
- res.on("close", () => {
1017
- httpTransport.close();
1018
- void httpMcp.close();
1019
- });
1020
- await httpMcp.connect(httpTransport);
1021
- await httpTransport.handleRequest(req, res, body);
1022
- } else {
1023
- res.writeHead(405);
1024
- res.end(JSON.stringify({ error: "Method not allowed" }));
1025
- }
1026
- },
1027
- "/recap/reset": async (req, res /*, body*/) => {
1028
- if (req.method !== "POST") { res.writeHead(405); res.end(JSON.stringify({ error: "POST required" })); return; }
1029
- if (!requireOwnerToken(req, res)) return;
1030
- // Called by hooks/session-start.cjs on `/clear` (matcher startup|clear).
1031
- // The session-start hook runs in a separate cjs process with no IPC
1032
- // handle to this forked channels child, so it can't drop recap
1033
- // status directly. Reset to an `empty` baseline so the statusline
1034
- // doesn't carry the prior session's `injected`/`error` recap badge
1035
- // into the cleared session.
1036
- const now = Date.now();
1037
- recapState.state = 'empty';
1038
- recapState.running = false;
1039
- recapState.startedAt = null;
1040
- recapState.lastCompletedAt = now;
1041
- recapState.updatedAt = now;
1042
- recapState.errorMessage = null;
1043
- sendRecapStateToParent();
1044
- res.writeHead(200);
1045
- res.end(JSON.stringify({ ok: true }));
1046
- },
1047
- "/cycle1": async (req, res, body) => {
1048
- if (req.method !== "POST") { res.writeHead(405); res.end(JSON.stringify({ ok: false, reason: "method-not-allowed", error: "POST required" })); return; }
1049
- if (!requireOwnerToken(req, res)) return;
1050
- const tCycleEntry = Date.now();
1051
- const timeoutMs = Number(body?.timeout_ms) > 0 ? Math.min(60000, Number(body.timeout_ms)) : 15000;
1052
- // IPC timer must outlive the worker-side deadline so a graceful
1053
- // {timedOutWaiting:true} resolve has time to traverse IPC before
1054
- // the channel timer rejects with memory-timeout. Without the
1055
- // buffer, the worker resolves at deadline-0ms and the local
1056
- // setTimeout fires at deadline+0ms in the same tick — race won by
1057
- // whichever scheduler ordering wins, turning intended 200 flags
1058
- // into 503 responses.
1059
- const ipcTimeoutMs = timeoutMs + 2000;
1060
- try {
1061
- // Carry the caller deadline through to the memory worker so a
1062
- // pending cycle1 in-flight is awaited under the same budget.
1063
- // Without this, when the previous cycle1's LLM call lives past
1064
- // 60s, every later SessionStart slot stacks another full 60s
1065
- // wait behind the same zombie promise.
1066
- const result = await callMemoryAction(
1067
- 'cycle1',
1068
- { ...(body?.args || {}), _callerDeadlineMs: timeoutMs },
1069
- ipcTimeoutMs,
1070
- );
1071
- // A successful IPC round-trip can still carry a nested MCP error
1072
- // envelope ({ isError: true }) when the memory worker served the
1073
- // call but the action failed — e.g. a promoted fork-proxy whose
1074
- // local `db` is still null. Surfacing that as outer { ok: true }
1075
- // masks the failure and makes session-start log a phantom success.
1076
- // Return a transient 503 so the hook's 503-retry path (which gates
1077
- // only on statusCode===503) re-polls instead of trusting it.
1078
- if (result && typeof result === 'object' && result.isError === true) {
1079
- const nestedText = Array.isArray(result.content)
1080
- ? result.content.map(c => (c && c.text) || '').join(' ').trim()
1081
- : '';
1082
- try { process.stderr.write(`[cycle1-time] route ms=${Date.now() - tCycleEntry} nestedError=1\n`); } catch {}
1083
- res.writeHead(503);
1084
- res.end(JSON.stringify({ ok: false, reason: 'memory-not-ready', error: nestedText || 'memory cycle1 returned isError' }));
1085
- } else {
1086
- try { process.stderr.write(`[cycle1-time] route ms=${Date.now() - tCycleEntry}\n`); } catch {}
1087
- res.writeHead(200);
1088
- res.end(JSON.stringify({ ok: true, result }));
1089
- }
1090
- } catch (e) {
1091
- // Classify transient/unavailable failures so the session-start hook
1092
- // (and other 503-retry callers) can distinguish boot-time races from
1093
- // IPC-layer faults and timeouts. All four reasons stay on 503 to
1094
- // preserve the hook retry contract (hooks/session-start.cjs:516
1095
- // gates only on statusCode===503); only the `reason` label changes.
1096
- //
1097
- // Source → reason mapping (upstream messages from server.mjs
1098
- // callWorker at 457-490 and local callMemoryAction at 169-187):
1099
- // server.mjs:470 "not ready (still booting)" → memory-not-ready
1100
- // server.mjs:464/467 "not available (...)" → worker-unavailable
1101
- // server.mjs:435 "exited unexpectedly" → worker-unavailable
1102
- // local "not a worker process" guard → worker-unavailable
1103
- // server.mjs:483 "IPC channel full or closed" → ipc-error
1104
- // server.mjs:488 "send failed: ..." → ipc-error
1105
- // server.mjs:475 "worker ... call ... timed out" → memory-timeout
1106
- // local "memory_call <action> timed out after Nms" → memory-timeout
1107
- const msg = e?.message || String(e);
1108
- let reason;
1109
- if (/worker memory not ready/i.test(msg)) {
1110
- reason = 'memory-not-ready';
1111
- } else if (/worker memory (IPC channel|send failed)/i.test(msg)) {
1112
- reason = 'ipc-error';
1113
- } else if (/timed out/i.test(msg)) {
1114
- reason = 'memory-timeout';
1115
- } else if (msg.includes('restart cap exceeded') || msg.includes('degraded')) {
1116
- // Permanent degraded state: restart cap hit or boot-time init failure.
1117
- // Use a distinct reason so callers can fail-fast without retrying.
1118
- // NOTE: checked before 'not available' — the error message
1119
- // "worker memory not available (restart cap exceeded)" contains both
1120
- // substrings and must land in 'memory-degraded', not 'worker-unavailable'.
1121
- reason = 'memory-degraded';
1122
- } else if (msg.includes('worker memory not available') || msg.includes('worker memory exited unexpectedly') || msg.includes('not a worker process')) {
1123
- reason = 'worker-unavailable';
1124
- }
1125
- const transient = Boolean(reason);
1126
- res.writeHead(transient ? 503 : 500);
1127
- res.end(JSON.stringify({ ok: false, reason, error: msg }));
1128
- }
1129
- },
1130
- "/rebind": async (req, res, body) => {
1131
- if (!requireOwnerToken(req, res)) return;
1132
- const channelId = statusState.read().channelId;
1133
- if (!channelId) {
1134
- res.writeHead(200);
1135
- res.end(JSON.stringify({ rebound: false, reason: "no channelId" }));
1136
- return;
1137
- }
1138
- const previousPath = getPersistedTranscriptPath();
1139
- const explicitTranscriptPath = typeof body?.transcriptPath === "string" ? body.transcriptPath.trim() : "";
1140
- const bound = await rebindTranscriptContext(channelId, {
1141
- previousPath,
1142
- persistStatus: true,
1143
- catchUp: true,
1144
- ...(explicitTranscriptPath ? { transcriptPath: explicitTranscriptPath } : {})
1145
- });
1146
- const reboundChanged = Boolean(bound) && bound !== previousPath;
1147
- res.writeHead(200);
1148
- res.end(JSON.stringify({ rebound: reboundChanged, path: bound || null }));
1149
- },
1150
- };
1151
- const BACKEND_DEPENDENT_PATHS = new Set([
1152
- "/send",
1153
- "/react",
1154
- "/edit",
1155
- "/fetch",
1156
- "/download",
1157
- "/typing/start",
1158
- "/typing/stop",
1159
- "/mcp"
1160
- ]);
1161
- async function ownerRequestHandler(req, res) {
1162
- res.setHeader("Content-Type", "application/json");
1163
- let body = {};
1164
- if (req.method === "POST") {
1165
- const chunks = [];
1166
- for await (const chunk of req) chunks.push(chunk);
1167
- try {
1168
- const rawBody = Buffer.concat(chunks).toString();
1169
- body = rawBody.trim() ? JSON.parse(rawBody) : {};
1170
- } catch {
1171
- res.writeHead(400);
1172
- res.end(JSON.stringify({ error: "invalid JSON body" }));
1173
- return;
1174
- }
1175
- }
1176
- try {
1177
- const url = new URL(req.url ?? "/", `http://127.0.0.1`);
1178
- if (BACKEND_DEPENDENT_PATHS.has(url.pathname) && !bridgeRuntimeConnected) {
1179
- res.writeHead(503);
1180
- res.end(JSON.stringify({ ok: false, reason: "backend-not-ready" }));
1181
- return;
1182
- }
1183
- const handler = OWNER_ROUTES[url.pathname];
1184
- if (handler) {
1185
- await handler(req, res, body, url);
1186
- return;
1187
- }
1188
- res.writeHead(404);
1189
- res.end(JSON.stringify({ error: "not found" }));
1190
- } catch (err) {
1191
- const msg = err instanceof Error ? err.message : String(err);
1192
- res.writeHead(500);
1193
- res.end(JSON.stringify({ error: msg }));
1194
- }
1195
- }
1196
- async function startOwnerHttpServer() {
1197
- if (ownerHttpServer) return ownerHttpServer.address().port;
1198
- // Generate a fresh cryptographic owner-secret BEFORE the listener accepts
1199
- // traffic so requireOwnerToken always has a real secret to compare. Stored
1200
- // in a 0o600 sidecar file (owner-secret-<pid>.json) under RUNTIME_ROOT so
1201
- // only the same UID + same active owner pid can read it back. /ping does
1202
- // NOT return this value — only the public instanceId.
1203
- if (!OWNER_SECRET) {
1204
- OWNER_SECRET = crypto.randomBytes(32).toString("hex");
1205
- try { publishOwnerSecret(OWNER_SECRET); }
1206
- catch (e) {
1207
- process.stderr.write(`mixdog: failed to publish owner secret: ${e?.message || e}\n`);
1208
- }
1209
- }
1210
- const server = http.createServer(ownerRequestHandler);
1211
- for (let port = PROXY_PORT_MIN; port <= PROXY_PORT_MAX; port++) {
1212
- if (await tryListenPort(server, port)) {
1213
- ownerHttpServer = server;
1214
- process.stderr.write(`mixdog: owner HTTP server listening on 127.0.0.1:${port}
1215
- `);
1216
- return port;
1217
- }
1218
- server.removeAllListeners("error");
1219
- }
1220
- throw new Error(`no available port in range ${PROXY_PORT_MIN}-${PROXY_PORT_MAX}`);
1221
- }
1222
- function stopOwnerHttpServer() {
1223
- if (!ownerHttpServer) return;
1224
- ownerHttpServer.close();
1225
- ownerHttpServer = null;
1226
- // Drop the per-process secret + sidecar file. A future startOwnerHttpServer()
1227
- // call regenerates a fresh one, so a stale standby that read the old secret
1228
- // before the restart cannot authenticate against the new owner.
1229
- OWNER_SECRET = "";
1230
- try { clearOwnerSecret(); } catch {}
1231
- globalThis.__mixdogBeaconRealHandler = null;
1232
- globalThis.__mixdogBeacon = null;
1233
- }
1234
- function logOwnership(note) {
1235
- if (lastOwnershipNote === note) return;
1236
- lastOwnershipNote = note;
1237
- process.stderr.write(`[ownership] ${note}
1238
- `);
1239
- }
1240
- function currentOwnerState() {
1241
- const active = readActiveInstance();
1242
- return {
1243
- active,
1244
- owned: active?.instanceId === INSTANCE_ID || getActiveOwnerPid(active) === TERMINAL_LEAD_PID
1245
- };
1246
- }
1247
- function getBridgeOwnershipSnapshot() {
1248
- return currentOwnerState();
1249
- }
1250
- // MIXDOG_PIN_OWNER=1 in the owning process writes `pinned:true` into
1251
- // active-instance.json. Pinned owners ignore the 10 s stale window — they
1252
- // only relinquish ownership when their OS process actually dies. Set per
1253
- // session (env var on the Claude Code shell) to lock that Lead as the
1254
- // schedule/webhook receiver across multi-session use.
1255
- function canStealOwnership(active) {
1256
- if (!active) return true;
1257
- if (active.instanceId === INSTANCE_ID || getActiveOwnerPid(active) === TERMINAL_LEAD_PID) return true;
1258
- if (active.pinned) {
1259
- const pinnedPid = getActiveOwnerPid(active);
1260
- if (!pinnedPid) return true;
1261
- try { process.kill(pinnedPid, 0); return false; }
1262
- catch { return true; }
1263
- }
1264
- if (Date.now() - active.updatedAt > ACTIVE_OWNER_STALE_MS) return true;
1265
- const ownerPid = getActiveOwnerPid(active);
1266
- try {
1267
- if (!ownerPid) throw new Error("missing owner pid");
1268
- process.kill(ownerPid, 0);
1269
- return false;
1270
- } catch {
1271
- return true;
1272
- }
1273
- }
1274
- function claimBridgeOwnership(reason) {
1275
- refreshActiveInstance(INSTANCE_ID);
1276
- logOwnership(`claimed owner (${reason})`);
1277
- }
1278
- function noteStartupHandoff(previous) {
1279
- if (!previous) return;
1280
- if (previous.instanceId === INSTANCE_ID) return;
1281
- if (getActiveOwnerPid(previous) === TERMINAL_LEAD_PID) return;
1282
- logOwnership(`startup handoff from ${previous.instanceId}`);
1283
- }
1284
- async function bindPersistedTranscriptIfAny() {
1285
- // Resolve channelId first from persisted status; fall back to the most
1286
- // recent status-*.json snapshot, then to the configured main channel when
1287
- // the bridge is active. No exists-gate here — once we have a channelId,
1288
- // hand off to rebindTranscriptContext(), which owns the 30-attempt retry
1289
- // for transcripts that are not yet on disk at boot/activate time.
1290
- let currentStatus = statusState.read();
1291
- if (!currentStatus.channelId) {
1292
- try {
1293
- const files = fs.readdirSync(RUNTIME_ROOT).filter((f) => f.startsWith("status-") && f.endsWith(".json")).map((f) => {
1294
- const full = path.join(RUNTIME_ROOT, f);
1295
- return { path: full, mtime: fs.statSync(full).mtimeMs };
1296
- }).sort((a, b) => b.mtime - a.mtime);
1297
- for (const { path: fp } of files) {
1298
- try {
1299
- const data = JSON.parse(fs.readFileSync(fp, "utf8"));
1300
- if (data.channelId) {
1301
- statusState.update((state) => {
1302
- Object.assign(state, data);
1303
- });
1304
- currentStatus = statusState.read();
1305
- process.stderr.write(`mixdog: restored status from ${fp}
1306
- `);
1307
- break;
1308
- }
1309
- } catch {
1310
- }
1311
- }
1312
- } catch {
1313
- }
1314
- }
1315
- if (!currentStatus.channelId && channelBridgeActive) {
1316
- const chCfg = config.channelsConfig;
1317
- const mainLabel = config.mainChannel ?? "main";
1318
- const mainEntry = chCfg?.[mainLabel];
1319
- const mainId = mainEntry?.channelId;
1320
- if (mainId) {
1321
- statusState.update((state) => {
1322
- state.channelId = mainId;
1323
- });
1324
- currentStatus = statusState.read();
1325
- process.stderr.write(`mixdog: auto-bound to main channel ${mainId}
1326
- `);
1327
- }
1328
- }
1329
- if (!currentStatus.channelId) return;
1330
- const bound = await rebindTranscriptContext(currentStatus.channelId, {
1331
- previousPath: getPersistedTranscriptPath(),
1332
- persistStatus: true,
1333
- catchUpFromPersisted: true
1334
- });
1335
- if (bound) {
1336
- process.stderr.write(`mixdog: initial transcript bind: ${bound}
1337
- `);
1338
- }
1339
- }
1340
- function shouldStartEventPipelineRuntime() {
1341
- return config.webhook?.enabled === true || (Array.isArray(config.events?.rules) && config.events.rules.length > 0);
1342
- }
1343
- function ensureEventPipelineRuntime() {
1344
- if (!eventPipeline) {
1345
- eventPipeline = new EventPipeline(config.events, config.channelsConfig);
1346
- wireEventQueueHandlers(eventPipeline.getQueue());
1347
- }
1348
- return eventPipeline;
1349
- }
1350
- function ensureWebhookServerRuntime() {
1351
- if (!webhookServer) {
1352
- // Pass top-level normalized config so the webhook gate reads the new
1353
- // top-level `quiet` subtree (and `webhook.respectQuiet`) introduced in
1354
- // 0.1.62. See applyDefaults() in lib/config.mjs.
1355
- webhookServer = new WebhookServer(config.webhook, { quiet: config.quiet ?? null });
1356
- }
1357
- wireWebhookHandlers();
1358
- return webhookServer;
1359
- }
1360
- function stopWebhookAndEventRuntime() {
1361
- if (webhookServer) {
1362
- webhookServer.stop();
1363
- webhookServer = null;
1364
- }
1365
- if (eventPipeline) {
1366
- eventPipeline.stop();
1367
- eventPipeline = null;
1368
- }
1369
- }
1370
- function syncOwnedWebhookAndEventRuntime({ reload = false } = {}) {
1371
- if (shouldStartEventPipelineRuntime()) {
1372
- const pipeline = ensureEventPipelineRuntime();
1373
- if (reload) {
1374
- pipeline.reloadConfig(config.events, config.channelsConfig);
1375
- wireEventQueueHandlers(pipeline.getQueue());
1376
- }
1377
- pipeline.start();
1378
- } else if (eventPipeline) {
1379
- eventPipeline.stop();
1380
- eventPipeline = null;
1381
- }
1382
-
1383
- if (config.webhook?.enabled === true) {
1384
- const server = ensureWebhookServerRuntime();
1385
- if (reload) {
1386
- // server.reloadConfig is async (it awaits the current server's
1387
- // close() before re-listening). Chain start() onto its resolution
1388
- // so we don't race the bound port — calling start() synchronously
1389
- // here would re-listen before close() finishes and surface
1390
- // EADDRINUSE on the same port.
1391
- server.reloadConfig(config.webhook, { quiet: config.quiet ?? null }, {
1392
- autoStart: false
1393
- }).then(() => {
1394
- // A stopWebhookAndEventRuntime() / deactivate landing during the async
1395
- // close()+reload window nulls out webhookServer (and webhook.enabled may
1396
- // have flipped off). Without this guard the resolved continuation would
1397
- // re-listen and resurrect an orphan listener that no teardown tracks.
1398
- if (webhookServer !== server || config.webhook?.enabled !== true) {
1399
- try { server.stop(); } catch {}
1400
- return;
1401
- }
1402
- wireWebhookHandlers();
1403
- server.start();
1404
- }).catch((err) => {
1405
- process.stderr.write(`mixdog channels: webhook reload failed: ${err instanceof Error ? err.message : String(err)}\n`);
1406
- });
1407
- } else {
1408
- server.start();
1409
- }
1410
- } else if (webhookServer) {
1411
- webhookServer.stop();
1412
- webhookServer = null;
1413
- }
1414
- }
1415
- async function startOwnedRuntime(options = {}) {
1416
- if (bridgeRuntimeConnected) return;
1417
- if (bridgeRuntimeStarting) return;
1418
- if (!channelBridgeActive) return;
1419
- bridgeRuntimeStarting = true;
1420
- _ownedRuntimeStopRequested = false;
1421
- // Advertise active-instance.json BEFORE backend connect so peers can
1422
- // discover this owner (httpPort) immediately. backendReady=false marks
1423
- // the partial state until backend.connect() succeeds.
1424
- let httpPort;
1425
- try {
1426
- httpPort = await startOwnerHttpServer();
1427
- } catch (e) {
1428
- process.stderr.write(`mixdog: HTTP server start failed (non-fatal): ${e instanceof Error ? e.message : String(e)}
1429
- `);
1430
- }
1431
- refreshActiveInstance(INSTANCE_ID, { ...httpPort ? { httpPort } : {}, backendReady: false });
1432
- startOwnerHeartbeat();
1433
- // Re-check after each post-connect await so a stopOwnedRuntime() landing
1434
- // mid-start cannot be overridden by the resuming start (scheduler/snapshot/
1435
- // webhook/binding launches below would revive owner state after stop).
1436
- // Idempotent: stop's sync teardown already ran; re-running disconnect +
1437
- // teardown is safe and covers both the pre-connected window (stop could
1438
- // not disconnect an in-flight backend) and the post-connected window
1439
- // (stop did disconnect; redo to be defensive).
1440
- const bailIfStopRequested = async () => {
1441
- if (!_ownedRuntimeStopRequested) return false;
1442
- try { await backend.disconnect(); } catch {}
1443
- try { stopOwnerHttpServer(); } catch {}
1444
- try { stopOwnerHeartbeat(); } catch {}
1445
- try { releaseOwnedChannelLocks(INSTANCE_ID); } catch {}
1446
- try { clearActiveInstance(INSTANCE_ID); } catch {}
1447
- bridgeRuntimeConnected = false;
1448
- _ownedRuntimeStopRequested = false;
1449
- return true;
1450
- };
1451
- // Await backend.connect() so callers (and bindingReady) only resolve after
1452
- // the Discord binding is real. Previously this was fire-and-forget and
1453
- // refreshBridgeOwnership returned immediately, letting bindingReady fire
1454
- // before backend listeners were attached.
1455
- try {
1456
- await backend.connect();
1457
- if (await bailIfStopRequested()) return;
1458
- bridgeRuntimeConnected = true;
1459
- refreshActiveInstance(INSTANCE_ID, { ...httpPort ? { httpPort } : {}, backendReady: true });
1460
- proxyMode = false;
1461
- // initProviders must complete before scheduler.start() — otherwise the
1462
- // scheduler's first fire can land before the registry is populated and
1463
- // return `Provider "<name>" not found or not enabled`. The previous
1464
- // fire-and-forget call let scheduler.start() race ahead of init.
1465
- try {
1466
- const agentCfg = loadAgentConfig();
1467
- await initProviders(agentCfg.providers || {});
1468
- } catch (e) {
1469
- process.stderr.write(`mixdog: initProviders failed (non-fatal): ${e instanceof Error ? e.message : String(e)}\n`);
1470
- }
1471
- if (await bailIfStopRequested()) return;
1472
- scheduler.start();
1473
- startSnapshotWriter(scheduler);
1474
- syncOwnedWebhookAndEventRuntime();
1475
- if (options.restoreBinding !== false) bindPersistedTranscriptIfAny().catch((e) => {
1476
- process.stderr.write(`mixdog: bindPersistedTranscriptIfAny failed (non-fatal): ${e instanceof Error ? e.message : String(e)}\n`);
1477
- });
1478
- process.stderr.write(`mixdog: running with ${backend.name} backend\n`);
1479
- logOwnership(`active owner lead=${TERMINAL_LEAD_PID} pid=${process.pid}`);
1480
- } catch (e) {
1481
- process.stderr.write(`mixdog: backend connect failed (non-fatal, cycle1/MCP still up): ${e instanceof Error ? e.message : String(e)}\n`);
1482
- // Roll back partial owner-side state advertised before connect() ran:
1483
- // HTTP server, heartbeat, and active-instance entry. Without this cleanup
1484
- // stopOwnedRuntime() at shutdown will short-circuit on !bridgeRuntimeConnected
1485
- // and leave the port bound + active-instance.json stale.
1486
- try { stopOwnerHttpServer(); } catch {}
1487
- try { stopOwnerHeartbeat(); } catch {}
1488
- try { releaseOwnedChannelLocks(INSTANCE_ID); } catch {}
1489
- try { clearActiveInstance(INSTANCE_ID); } catch {}
1490
- } finally {
1491
- bridgeRuntimeStarting = false;
1492
- }
1493
- }
1494
- async function stopOwnedRuntime(reason) {
1495
- // startOwnedRuntime() advertises owner HTTP/heartbeat/active-instance and
1496
- // claims channel locks BEFORE awaiting backend.connect(). If shutdown lands
1497
- // during that window (bridgeRuntimeStarting=true, bridgeRuntimeConnected
1498
- // still false) we still need to tear that partial state down — otherwise
1499
- // the port stays bound and active-instance.json stays stale.
1500
- if (!bridgeRuntimeConnected && !bridgeRuntimeStarting) return;
1501
- // If a start is in flight (bridgeRuntimeStarting=true), signal the in-flight
1502
- // startOwnedRuntime() to abort right after its backend.connect() resolves.
1503
- // Without this the in-flight start re-marks connected and re-launches
1504
- // scheduler/webhook/heartbeat after we tear them down here.
1505
- if (bridgeRuntimeStarting) _ownedRuntimeStopRequested = true;
1506
- const wasConnected = bridgeRuntimeConnected;
1507
- stopServerTyping();
1508
- // Release the transcript fs.watch handle plus the forwarder's debounce/retry
1509
- // timers on standby. Without this the watcher keeps firing scheduleWatchFlush
1510
- // and the drain/retry timers stay live after ownership is dropped, leaking a
1511
- // file handle + timers for the rest of the process lifetime.
1512
- try { forwarder.stopWatch(); } catch {}
1513
- stopOwnerHttpServer();
1514
- stopOwnerHeartbeat();
1515
- scheduler.stop();
1516
- stopSnapshotWriter();
1517
- stopWebhookAndEventRuntime();
1518
- releaseOwnedChannelLocks(INSTANCE_ID);
1519
- clearActiveInstance(INSTANCE_ID);
1520
- try {
1521
- // Only disconnect the backend when connect() actually completed; calling
1522
- // disconnect() mid-connect races the connect promise.
1523
- if (wasConnected) await backend.disconnect();
1524
- } finally {
1525
- bridgeRuntimeConnected = false;
1526
- logOwnership(`standby: ${reason}`);
1527
- }
1528
- }
1529
- function refreshBridgeOwnershipSafe(options = {}) {
1530
- refreshBridgeOwnership(options).catch(err => process.stderr.write(`[channels] refreshBridgeOwnership rejected: ${err?.message || err}\n`));
1531
- }
1532
- function startOwnerHeartbeat() {
1533
- if (ownerHeartbeatTimer) return;
1534
- ownerHeartbeatTimer = setInterval(() => {
1535
- try { refreshActiveInstance(INSTANCE_ID); }
1536
- catch (e) {
1537
- process.stderr.write(`[ownership] heartbeat refresh failed: ${e instanceof Error ? e.message : String(e)}\n`);
1538
- }
1539
- }, OWNER_HEARTBEAT_INTERVAL_MS);
1540
- ownerHeartbeatTimer.unref?.();
1541
- }
1542
- function stopOwnerHeartbeat() {
1543
- if (!ownerHeartbeatTimer) return;
1544
- clearInterval(ownerHeartbeatTimer);
1545
- ownerHeartbeatTimer = null;
1546
- }
1547
- async function refreshBridgeOwnership(options = {}) {
1548
- // Coalesce concurrent callers onto the in-flight refresh so backend tool
1549
- // calls landing during normal login wait for the same connect attempt
1550
- // instead of returning early and observing spurious auto-connect failure.
1551
- if (bridgeOwnershipRefreshInFlight) return bridgeOwnershipRefreshInFlight;
1552
- bridgeOwnershipRefreshInFlight = (async () => {
1553
- if (!channelBridgeActive) {
1554
- const { active: active2 } = currentOwnerState();
1555
- if (active2?.httpPort && !proxyMode) {
1556
- const alive = await pingOwner(active2.httpPort);
1557
- if (alive) {
1558
- proxyMode = true;
1559
- ownerHttpPort = active2.httpPort;
1560
- logOwnership(`non-channel session \u2014 proxy mode via ${active2.instanceId}`);
1561
- }
1562
- }
1563
- return;
1564
- }
1565
- const { active, owned } = currentOwnerState();
1566
- const activeHttpPort = Number(active?.httpPort) || 0;
1567
- let activeHttpChecked = false;
1568
- let activeHttpAlive = false;
1569
- const checkActiveHttp = async () => {
1570
- if (!activeHttpPort) return false;
1571
- if (!activeHttpChecked) {
1572
- activeHttpAlive = await pingOwner(activeHttpPort);
1573
- activeHttpChecked = true;
1574
- }
1575
- return activeHttpAlive;
1576
- };
1577
- const enterProxyMode = (note) => {
1578
- proxyMode = true;
1579
- ownerHttpPort = activeHttpPort;
1580
- if (note) logOwnership(note);
1581
- };
1582
- if (proxyMode && !owned && activeHttpPort) {
1583
- const alive = await checkActiveHttp();
1584
- if (!alive) {
1585
- process.stderr.write(`[ownership] owner ping failed, attempting takeover
1586
- `);
1587
- proxyMode = false;
1588
- ownerHttpPort = 0;
1589
- claimBridgeOwnership(`owner ${active.instanceId} unreachable`);
1590
- const next2 = currentOwnerState();
1591
- if (next2.owned) {
1592
- refreshActiveInstance(INSTANCE_ID);
1593
- await startOwnedRuntime(options);
1594
- }
1595
- return;
1596
- }
1597
- // Active owner is alive but may have rebound to a new port since the
1598
- // previous refresh (owner restart on a different PROXY_PORT). Sync
1599
- // ownerHttpPort so subsequent proxyRequest() hits the new port instead
1600
- // of the stale value cached at proxy-mode entry.
1601
- if (ownerHttpPort !== activeHttpPort) {
1602
- ownerHttpPort = activeHttpPort;
1603
- logOwnership(`proxy mode via owner ${active.instanceId} port ${activeHttpPort}`);
1604
- }
1605
- return;
1606
- }
1607
- if (!owned && activeHttpPort) {
1608
- const alive = await checkActiveHttp();
1609
- if (alive) {
1610
- enterProxyMode(`proxy mode via owner ${active.instanceId} port ${activeHttpPort}`);
1611
- return;
1612
- }
1613
- const updatedAt = Number(active?.updatedAt);
1614
- const activeAgeMs = Number.isFinite(updatedAt) ? Date.now() - updatedAt : Number.POSITIVE_INFINITY;
1615
- if (active?.backendReady === true || activeAgeMs > ACTIVE_OWNER_STALE_MS) {
1616
- logOwnership(`owner ${active.instanceId} port ${activeHttpPort} unreachable`);
1617
- claimBridgeOwnership(`owner ${active.instanceId} unreachable`);
1618
- }
1619
- }
1620
- if (!owned && canStealOwnership(active)) {
1621
- claimBridgeOwnership(active ? `takeover from ${active.instanceId}` : "startup");
1622
- }
1623
- const next = currentOwnerState();
1624
- if (next.owned) {
1625
- refreshActiveInstance(INSTANCE_ID);
1626
- await startOwnedRuntime(options);
1627
- return;
1628
- }
1629
- if (bridgeRuntimeConnected) {
1630
- const reason = next.active?.instanceId ? `newer server ${next.active.instanceId}` : "no active owner";
1631
- await stopOwnedRuntime(reason);
1632
- return;
1633
- }
1634
- if (next.active?.httpPort && !proxyMode) {
1635
- const alive = await pingOwner(next.active.httpPort);
1636
- if (alive) {
1637
- proxyMode = true;
1638
- ownerHttpPort = next.active.httpPort;
1639
- logOwnership(`proxy mode via owner ${next.active.instanceId} port ${next.active.httpPort}`);
1640
- return;
1641
- }
1642
- }
1643
- if (next.active?.instanceId) {
1644
- logOwnership(`standby under owner ${next.active.instanceId}`);
1645
- }
1646
- })();
1647
- try {
1648
- return await bridgeOwnershipRefreshInFlight;
1649
- } finally {
1650
- bridgeOwnershipRefreshInFlight = null;
1651
- }
1652
- }
1653
-
1654
- // ── inject_command helpers ─────────────────────────────────────────
1655
- // Resolve the host Claude Code console PID. The mcp child knows its supervisor
1656
- // PID via MIXDOG_SUPERVISOR_PID env; the supervisor's parent is the CC console.
1657
- // Cached after first resolution because the relation cannot change for the
1658
- // lifetime of this process.
1659
- let _ccPidCache = null;
1660
- function _resolveClaudeCodePid() {
1661
- if (_ccPidCache) return _ccPidCache;
1662
- const supPid = Number(process.env.MIXDOG_SUPERVISOR_PID);
1663
- if (!Number.isFinite(supPid) || supPid <= 0) {
1664
- throw new Error("MIXDOG_SUPERVISOR_PID env not set; cannot resolve CC PID");
1665
- }
1666
- if (process.platform !== "win32") {
1667
- throw new Error("inject_command CC PID resolution is Windows-only");
1668
- }
1669
- const r = spawnSync("powershell", ["-NoProfile", "-Command",
1670
- `(Get-CimInstance Win32_Process -Filter "ProcessId=${supPid}").ParentProcessId`,
1671
- ], { encoding: "utf8", windowsHide: true });
1672
- const ppid = Number.parseInt((r.stdout || "").trim(), 10);
1673
- if (!Number.isFinite(ppid) || ppid <= 0) {
1674
- throw new Error(`cannot resolve supervisor (pid=${supPid}) parent process`);
1675
- }
1676
- _ccPidCache = ppid;
1677
- return ppid;
1678
- }
1679
- function _injectScriptPath() {
1680
- const root = process.env.CLAUDE_PLUGIN_ROOT;
1681
- if (!root) throw new Error("CLAUDE_PLUGIN_ROOT env not set");
1682
- const p = path.join(root, "scripts", "inject-input.ps1");
1683
- if (!fs.existsSync(p)) throw new Error(`inject-input.ps1 missing at ${p}`);
1684
- return p;
1685
- }
1686
-
1687
- async function reloadRuntimeConfig() {
1688
- const previousBackend = backend;
1689
- const previousBackendName = previousBackend?.name || "";
1690
- config = loadConfig();
1691
- scheduler.reloadConfig(
1692
- config.nonInteractive ?? [],
1693
- config.interactive ?? [],
1694
- // channelsConfig: channel-label resolution only.
1695
- config.channelsConfig,
1696
- // 0.1.62: top-level normalized config (quiet/schedules).
1697
- config,
1698
- { restart: bridgeRuntimeConnected }
1699
- );
1700
- const nextBackend = createBackend(config);
1701
- const backendChanged = (nextBackend?.name || "") !== previousBackendName;
1702
- if (backendChanged) {
1703
- const shouldRestart = bridgeRuntimeConnected || bridgeRuntimeStarting;
1704
- if (shouldRestart) await stopOwnedRuntime("backend config changed");
1705
- backend = nextBackend;
1706
- if (shouldRestart) refreshBridgeOwnershipSafe({ restoreBinding: false });
1707
- } else if (nextBackend !== previousBackend) {
1708
- try { await nextBackend.disconnect?.(); } catch {}
1709
- }
1710
- if (bridgeRuntimeConnected) {
1711
- syncOwnedWebhookAndEventRuntime({ reload: true });
1712
- } else {
1713
- stopWebhookAndEventRuntime();
1714
- }
1715
- }
1716
- function injectAndRecord(channelId, name, content, options) {
1717
- // Strip soft-warn marker blocks (Tool-loop / Repeated-input / legacy
1718
- // Repeated-tool / Mixed-tool / Tool-budget / Same-file multi-chunk /
1719
- // Bash file-lookup / Iteration / 0-match advisory) from anywhere in the
1720
- // outbound body. Markers are
1721
- // intentionally prepended onto tool RESULTS upstream (tool-loop-guard.mjs
1722
- // build*Warn) so the model
1723
- // self-corrects, but bridge roles commonly echo them and we don't want them
1724
- // surfacing in Discord / Lead channel push.
1725
- if (typeof content === 'string') content = stripSoftWarns(content);
1726
- // Skip-protocol guard: bridge workers (webhook-handler / scheduler-task)
1727
- // prefix `[meta:silent]` on the first line to opt out
1728
- // of Lead inject for genuine no-op results (label-only events, dedup,
1729
- // "nothing to report"). The body still goes to Discord for audit; only
1730
- // the Lead-context inject is suppressed. See rules/bridge/20-skip-protocol.md.
1731
- if (typeof content === 'string') {
1732
- const m = content.match(/^\s*\[meta:silent\][^\n]*\n?([\s\S]*)$/);
1733
- if (m) {
1734
- content = m[1];
1735
- options = { ...(options || {}), silent_to_agent: true };
1736
- }
1737
- }
1738
- const ts = new Date().toISOString();
1739
- const now = new Date();
1740
- const timeLabel = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")} `;
1741
- const sourceLabel = options?.type ? `${timeLabel}: ${options.type}` : timeLabel;
1742
- const meta = { chat_id: channelId, user: sourceLabel, user_id: "system", ts };
1743
- if (options?.instruction) meta.instruction = options.instruction;
1744
- if (options?.type) meta.type = options.type;
1745
- // `silent_to_agent` — lifecycle status pings (worker/iter/started echoes)
1746
- // surface on Discord but should NOT land in Lead's context window. When
1747
- // set, skip the parent-notify hop but keep the Discord-forward + event-log
1748
- // record. The meta flag is also propagated downstream so consumers that
1749
- // still see the notification (e.g. Lead itself if emission changes later)
1750
- // can recognise and drop it. Default is false → legacy behaviour preserved.
1751
- if (options?.silent_to_agent) meta.silent_to_agent = true;
1752
- const silent = options?.silent_to_agent === true;
1753
- if (!silent) {
1754
- sendNotifyToParent("notifications/claude/channel", { content, meta });
1755
- } else {
1756
- forwardLifecycleToDiscord(channelId, content);
1757
- }
1758
- }
1759
-
1760
- // Best-effort direct Discord emission for silent-to-agent lifecycle pings.
1761
- // Only used when the parent-notify hop is skipped, so the user still sees
1762
- // the status on Discord even though Lead will never echo it through the
1763
- // transcript-tail forwarder. Falls back to a no-op when no channel is
1764
- // resolvable — lifecycle pings are non-critical.
1765
- function forwardLifecycleToDiscord(channelId, content) {
1766
- try {
1767
- // Skip rather than guess: lifecycle callers pass the channelId they own;
1768
- // falling back to statusState.channelId can route to a stale/unrelated
1769
- // channel when the caller did not supply one intentionally.
1770
- const target = channelId || null;
1771
- dropTrace("send.lifecycle.entry", { channelId: target || "(none)", bindingReadyStatus, backendPresent: !!backend?.sendMessage, preview: preview(content) });
1772
- if (!target || !backend?.sendMessage) return;
1773
- void bindingReady.then(() =>
1774
- backend.sendMessage(target, content)
1775
- .then(() => dropTrace("send.lifecycle.ok", { channelId: target }))
1776
- .catch((err) => dropTrace("send.lifecycle.err", { channelId: target, err: String(err) }))
1777
- ).catch(() => {});
1778
- } catch { /* best-effort */ }
1779
- }
1780
- scheduler.setInjectHandler((channelId, name, content, options) => {
1781
- injectAndRecord(channelId, name, content, options);
1782
- });
1783
- scheduler.setSendHandler(async (channelId, text) => {
1784
- // Skip protocol: a scheduler-task emitting `[meta:silent]` has nothing to
1785
- // report — suppress the channel send entirely (no noise). Mirrors the
1786
- // webhook delegate drop and injectAndRecord's silent handling.
1787
- if (typeof text === "string" && /^\s*\[meta:silent\]/.test(text)) {
1788
- dropTrace("send.scheduler.silent", { channelId });
1789
- return;
1790
- }
1791
- dropTrace("send.scheduler.entry", { channelId, preview: preview(text) });
1792
- await bindingReady;
1793
- dropTrace("send.scheduler.ready", { channelId });
1794
- try {
1795
- await backend.sendMessage(channelId, text);
1796
- dropTrace("send.scheduler.ok", { channelId });
1797
- } catch (err) {
1798
- dropTrace("send.scheduler.err", { channelId, err: String(err) });
1799
- throw err;
1800
- }
1801
- });
1802
- function wireWebhookHandlers() {
1803
- if (!webhookServer) return;
1804
- webhookServer.setEventPipeline(eventPipeline);
1805
- webhookServer.setBridgeDispatch(async ({ role, preset, prompt, cwd, context }) => {
1806
- // Delegate-mode webhook → bridge orchestrator. Each bridge progress /
1807
- // final event is forwarded to the Lead via the same channel-notify
1808
- // path used by schedule & event-queue (injectAndRecord). Silent
1809
- // lifecycle pings keep routing only to Discord.
1810
- const agentMod = await import("../agent/index.mjs");
1811
- const channelId = resolveWebhookChannelId(context?.channel);
1812
- const endpoint = context?.endpoint || "unknown";
1813
- const event = context?.event || null;
1814
- const deliveryId = context?.deliveryId || null;
1815
- const label = `webhook:${endpoint}`;
1816
- const instruction = `Webhook review from role=${role} on endpoint "${endpoint}"`
1817
- + (event ? ` (event=${event})` : "")
1818
- + (deliveryId ? ` (delivery=${deliveryId})` : "")
1819
- + ". Relay the finding to the user naturally — summarize clearly, call out any issues, and note what needs a decision.";
1820
- const notifyFn = (text, meta = {}) => {
1821
- if (!text) return;
1822
- // Webhook skip protocol: when the bridge worker emits a `[meta:silent]`
1823
- // marker (optionally behind model/role tag prefixes), the event is a
1824
- // no-op (label-only, dedup, "nothing to report"). Drop the message
1825
- // entirely — neither Lead inject nor Discord forward — instead of the
1826
- // partial `silent_to_agent` semantics that still audit to Discord.
1827
- const raw = String(text);
1828
- if (/^\s*(?:\[[^\]\n]+\]\s*)*\[meta:silent\]/.test(raw)) return;
1829
- // Deterministic findings-count drop. Code-review handlers emit a
1830
- // structured `[[findings:N]]` token (N = number of issues). The RELAY —
1831
- // not the worker's prose — decides: N==0 => clean review, drop entirely
1832
- // (no Lead inject, no Discord forward). Token absent => fail-safe forward
1833
- // so a real finding is never silently dropped if the worker omits it.
1834
- const fc = raw.match(/\[\[findings:(\d+)\]\]/i);
1835
- if (fc && Number(fc[1]) === 0) return;
1836
- // Lifecycle pings (started / iter echoes, marked silent_to_agent) are
1837
- // channel noise for an automated webhook review — drop them entirely so
1838
- // a skip stays fully silent and only the final answer reaches the
1839
- // channel. The final [meta:silent] skip result is already dropped above.
1840
- if (meta?.silent_to_agent === true) return;
1841
- // Strip the verdict token before surfacing (findings present, N>0).
1842
- const surfaced = raw.replace(/\[\[findings:\d+\]\]/gi, "").replace(/[^\S\n]{2,}/g, " ").trim();
1843
- injectAndRecord(channelId, label, surfaced || raw, {
1844
- type: "webhook",
1845
- instruction,
1846
- });
1847
- };
1848
- // Per-terminal cwd under the daemon's single channels worker. A webhook
1849
- // result is injected to ownerConn() — the connection whose session.leadPid
1850
- // equals active-instance ownerLeadPid — so the worker must run in THAT
1851
- // owner terminal's cwd. Read the sentinel keyed by ownerLeadPid; cwd-tool
1852
- // writes session-cwd-<leadPid>.txt per connection, so write and read meet
1853
- // on the same leadPid key no matter which terminal holds the owner seat.
1854
- // Falls back to the session entry position; never the plugin CACHE root.
1855
- const ownerPid = getActiveOwnerPid(readActiveInstance());
1856
- const ownerCwd = (ownerPid && readLastSessionCwd(ownerPid)) || captureOriginalUserCwd();
1857
- return agentMod.handleToolCall(
1858
- "bridge",
1859
- { role, preset, prompt, cwd: cwd || ownerCwd },
1860
- { notifyFn },
1861
- );
1862
- });
1863
- }
1864
- function resolveWebhookChannelId(channelLabel) {
1865
- // Fail closed: route only to channels explicitly present in config —
1866
- // the endpoint's owner-configured `channel`, else the `main` channel.
1867
- // Runtime / persisted-status fallbacks are never used (they could route
1868
- // a delivery to an arbitrary or stale channel). The endpoint channel is
1869
- // owner-authored config, not attacker payload, so honoring it is safe.
1870
- const channels = config?.channelsConfig || {};
1871
- if (channelLabel && channels[channelLabel]?.channelId) return channels[channelLabel].channelId;
1872
- return channels.main?.channelId || "";
1873
- }
1874
- function wireEventQueueHandlers(eventQueue) {
1875
- if (!eventQueue) return;
1876
- eventQueue.setInjectHandler((channelId, name, content, options) => {
1877
- injectAndRecord(channelId, name, content, options);
1878
- });
1879
- // Defensive ownership probe: the queue tick should only run in the active
1880
- // owner process. Standby / proxy instances see bridgeRuntimeConnected=false
1881
- // or proxyMode=true and will skip the tick even if an errant start() slipped
1882
- // through.
1883
- eventQueue.setOwnerGetter(() => bridgeRuntimeConnected && !proxyMode);
1884
- forwarder.setOwnerGetter(() => bridgeRuntimeConnected && !proxyMode);
1885
- }
1886
- function editDiscordMessage(channelId, messageId, label) {
1887
- // Behavior-preserving: route through the backend abstraction (which uses
1888
- // discord.js under the hood) instead of issuing a raw REST PATCH. Errors
1889
- // are swallowed to stderr to match the prior fire-and-forget shape — the
1890
- // call site never awaited the HTTPS request either.
1891
- if (!getDiscordToken()) return;
1892
- const text = `\u{1F510} **Permission Request** \u2014 ${label}`;
1893
- void backend.editMessage(channelId, messageId, text, { components: [] }).catch((err) => {
1894
- process.stderr.write(`mixdog: editDiscordMessage failed: ${err}
1895
- `);
1896
- });
1897
- }
1898
- backend.onModalRequest = async (rawInteraction) => {
1899
- if (!bridgeRuntimeConnected || !getBridgeOwnershipSnapshot().owned) {
1900
- refreshBridgeOwnershipSafe();
1901
- return;
1902
- }
1903
- const { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder } = await import("discord.js");
1904
- const customId = rawInteraction.customId;
1905
- const channelId = rawInteraction.channelId ?? "";
1906
- pendingSetup.rememberMessage(rawInteraction.user.id, channelId, rawInteraction.message?.id);
1907
- const modalSpec = buildModalRequestSpec(
1908
- customId,
1909
- pendingSetup.get(rawInteraction.user.id, channelId),
1910
- loadProfileConfig()
1911
- );
1912
- if (!modalSpec) return;
1913
- const modal = new ModalBuilder().setCustomId(modalSpec.customId).setTitle(modalSpec.title);
1914
- const rows = modalSpec.fields.map(
1915
- (field) => new ActionRowBuilder().addComponents((() => {
1916
- const input = new TextInputBuilder().setCustomId(field.id).setLabel(field.label).setStyle(TextInputStyle.Short).setRequired(field.required);
1917
- if (field.value) input.setValue(field.value);
1918
- return input;
1919
- })())
1920
- );
1921
- modal.addComponents(...rows);
1922
- await rawInteraction.showModal(modal);
1923
- };
1924
- const pendingPermRequests = new Map();
1925
- const TOOL_EXEC_CONSUMER_MARKER = path.join(RUNTIME_ROOT, '.tool-exec-consumer');
1926
- function refreshToolExecConsumerMarker() {
1927
- try {
1928
- if (pendingPermRequests.size > 0) {
1929
- fs.writeFileSync(TOOL_EXEC_CONSUMER_MARKER, String(Date.now()));
1930
- } else {
1931
- try { fs.unlinkSync(TOOL_EXEC_CONSUMER_MARKER); } catch {}
1932
- }
1933
- } catch {}
1934
- }
1935
- // Watch for terminal-approved tool executions. The PostToolUse hook writes a
1936
- // signal file per tool call; when we see one, find the oldest pending perm
1937
- // request with a matching tool name and mark its Discord message as
1938
- // "Allowed (terminal)" so users don't see stale active buttons.
1939
- try {
1940
- try { if (!fs.existsSync(RUNTIME_ROOT)) fs.mkdirSync(RUNTIME_ROOT, { recursive: true }); } catch {}
1941
- const SIGNAL_RE = /^tool-exec-\d+-[0-9a-f]+\.signal$/;
1942
- fs.watch(RUNTIME_ROOT, { persistent: false }, (eventType, filename) => {
1943
- if (!filename || !SIGNAL_RE.test(filename)) return;
1944
- setTimeout(() => {
1945
- try {
1946
- const signalPath = path.join(RUNTIME_ROOT, filename);
1947
- let raw;
1948
- try { raw = fs.readFileSync(signalPath, 'utf8'); } catch { return; }
1949
- let payload;
1950
- try { payload = JSON.parse(raw); } catch { return; }
1951
- const toolName = payload?.toolName;
1952
- if (!toolName) return;
1953
- const sigFilePath = payload?.filePath || '';
1954
- let oldestKey = null;
1955
- let oldestEntry = null;
1956
- for (const [k, v] of pendingPermRequests) {
1957
- if (v.toolName !== toolName) continue;
1958
- // Bind on filePath too. If both sides are empty (non-file tools
1959
- // like Bash), toolName alone is the match. Otherwise both must
1960
- // equal — prevents two concurrent Edit/Write requests from
1961
- // cross-approving each other.
1962
- const vFilePath = v.filePath || '';
1963
- if (vFilePath || sigFilePath) {
1964
- if (vFilePath !== sigFilePath) continue;
1965
- }
1966
- if (!oldestEntry || v.createdAt < oldestEntry.createdAt) {
1967
- oldestKey = k;
1968
- oldestEntry = v;
1969
- }
1970
- }
1971
- // No matching pending request — leave the signal on disk so a
1972
- // bridge role hook (or other consumer) gets a chance to claim it.
1973
- if (!oldestKey || !oldestEntry) return;
1974
- if (oldestEntry.channelId && oldestEntry.messageId) {
1975
- try {
1976
- editDiscordMessage(oldestEntry.channelId, oldestEntry.messageId, 'Allowed (terminal)');
1977
- } catch (err) {
1978
- try { process.stderr.write(`mixdog channels: tool-exec signal edit failed: ${err && err.message || err}\n`); } catch {}
1979
- }
1980
- }
1981
- pendingPermRequests.delete(oldestKey);
1982
- refreshToolExecConsumerMarker();
1983
- // Only unlink once we've confirmed the match and handled it.
1984
- try { fs.unlinkSync(signalPath); } catch {}
1985
- } catch (err) {
1986
- try { process.stderr.write(`mixdog channels: tool-exec signal handler error: ${err && err.message || err}\n`); } catch {}
1987
- }
1988
- }, 50);
1989
- });
1990
- // Stale-signal sweeper: any signal file older than 60s is removed so
1991
- // unclaimed files don't accumulate on disk. Runs every 30s.
1992
- setInterval(() => {
1993
- try {
1994
- const now = Date.now();
1995
- const entries = fs.readdirSync(RUNTIME_ROOT);
1996
- for (const name of entries) {
1997
- if (!SIGNAL_RE.test(name)) continue;
1998
- const p = path.join(RUNTIME_ROOT, name);
1999
- try {
2000
- const st = fs.statSync(p);
2001
- if (now - st.mtimeMs > 60_000) {
2002
- try { fs.unlinkSync(p); } catch {}
2003
- }
2004
- } catch {}
2005
- }
2006
- } catch {}
2007
- }, 30_000)?.unref?.();
2008
- } catch (err) {
2009
- try { process.stderr.write(`mixdog channels: tool-exec signal watcher setup failed: ${err && err.message || err}\n`); } catch {}
2010
- }
2011
-
2012
- backend.onInteraction = (interaction) => {
2013
- // Channel-route permission reply. Custom_id format: perm-ch-{request_id}-{allow|session|deny}.
2014
- // request_id is the 5-letter short ID CC generates via shortRequestId().
2015
- // Emit notifications/claude/channel/permission back to Claude Code; the race
2016
- // logic in interactiveHandler.ts resolves the pending request and dismisses
2017
- // every other racer (local dialog, bridge, hook, classifier).
2018
- if (interaction.customId?.startsWith("perm-ch-")) {
2019
- const match = interaction.customId.match(/^perm-ch-([a-km-z]{5})-(allow|session|deny)$/);
2020
- if (!match) return;
2021
- const [, requestId, action] = match;
2022
- const access = config.access;
2023
- if (access?.allowFrom?.length > 0 && !access.allowFrom.includes(interaction.userId)) {
2024
- process.stderr.write(`mixdog: perm-ch button rejected — user ${interaction.userId} not in allowFrom\n`);
2025
- return;
2026
- }
2027
- const pending = pendingPermRequests.get(requestId);
2028
- pendingPermRequests.delete(requestId);
2029
- refreshToolExecConsumerMarker();
2030
- const params = { request_id: requestId };
2031
- if (action === 'deny') {
2032
- params.behavior = 'deny';
2033
- } else if (action === 'session') {
2034
- params.behavior = 'allow';
2035
- const toolName = pending?.toolName;
2036
- if (toolName) {
2037
- params.updatedPermissions = [{ type: 'addRules', rules: [{ toolName }], behavior: 'allow', destination: 'session' }];
2038
- }
2039
- } else {
2040
- params.behavior = 'allow';
2041
- }
2042
- sendNotifyToParent('notifications/claude/channel/permission', params);
2043
- const labels = { allow: 'Approved', session: 'Session Approved', deny: 'Denied' };
2044
- if (interaction.message?.id && interaction.channelId) {
2045
- editDiscordMessage(interaction.channelId, interaction.message.id, labels[action] || action);
2046
- }
2047
- return;
2048
- }
2049
- if (interaction.customId?.startsWith("perm-")) {
2050
- const match = interaction.customId.match(/^perm-([0-9a-f]{32})-(allow|session|deny)$/);
2051
- if (!match) return;
2052
- const [, uuid, action] = match;
2053
- const access = config.access;
2054
- if (!access) {
2055
- const _permDropLine = `[${localTimestamp()}] perm interaction dropped: no access config\n`;
2056
- if (isMixdogDebug()) {
2057
- fs.appendFileSync(_bootLog, _permDropLine);
2058
- } else {
2059
- appendSessionStartCriticalLog(DATA_DIR, `[channels] ${_permDropLine}`);
2060
- }
2061
- return;
2062
- }
2063
- if (access.allowFrom?.length > 0 && !access.allowFrom.includes(interaction.userId)) {
2064
- process.stderr.write(`mixdog: perm button rejected \u2014 user ${interaction.userId} not in allowFrom
2065
- `);
2066
- return;
2067
- }
2068
- const resultPaths = [getPermissionResultPath(INSTANCE_ID, uuid)];
2069
- const leadInstanceId = String(TERMINAL_LEAD_PID);
2070
- if (leadInstanceId && leadInstanceId !== INSTANCE_ID) {
2071
- resultPaths.push(getPermissionResultPath(leadInstanceId, uuid));
2072
- }
2073
- for (const resultPath of resultPaths) {
2074
- try {
2075
- fs.writeFileSync(resultPath, action, { flag: "wx" });
2076
- } catch (e) {
2077
- if (e.code !== "EEXIST") {
2078
- process.stderr.write(`mixdog: writePermissionResult failed: ${e.message}\n`);
2079
- }
2080
- }
2081
- }
2082
- const labels = { allow: "Approved", session: "Session Approved", deny: "Denied" };
2083
- if (interaction.message?.id && interaction.channelId) {
2084
- editDiscordMessage(interaction.channelId, interaction.message.id, labels[action] || action);
2085
- }
2086
- return;
2087
- }
2088
- if (!bridgeRuntimeConnected || !getBridgeOwnershipSnapshot().owned) {
2089
- refreshBridgeOwnershipSafe();
2090
- return;
2091
- }
2092
- scheduler.noteActivity();
2093
- if (interaction.customId === "stop_task") {
2094
- controlClaudeSession(INSTANCE_ID, { type: "interrupt" })
2095
- .catch(err => process.stderr.write(`[channels] controlClaudeSession rejected: ${err?.message || err}\n`));
2096
- writeTextFile(TURN_END_FILE, String(Date.now()));
2097
- return;
2098
- }
2099
- sendNotifyToParent("notifications/claude/channel", {
2100
- content: `[interaction] ${interaction.type}: ${interaction.customId}${interaction.values ? " values=" + interaction.values.join(",") : ""}`,
2101
- meta: {
2102
- chat_id: interaction.channelId,
2103
- user: `interaction:${interaction.type}`,
2104
- user_id: interaction.userId,
2105
- ts: (/* @__PURE__ */ new Date()).toISOString(),
2106
- interaction_type: interaction.type,
2107
- custom_id: interaction.customId,
2108
- ...interaction.values ? { values: interaction.values.join(",") } : {},
2109
- ...interaction.message ? { message_id: interaction.message.id } : {}
2110
- }
2111
- });
2112
- };
2113
- function isVoiceAttachment(contentType) {
2114
- if (typeof contentType !== 'string') return false;
2115
- const ct = contentType.toLowerCase();
2116
- return ct.startsWith("audio/") || ct.startsWith("application/ogg");
2117
- }
2118
- function runCmd(cmd, args, capture = false) {
2119
- return new Promise((resolve, reject) => {
2120
- const proc = spawn(cmd, args, {
2121
- stdio: capture ? ["ignore", "pipe", "ignore"] : "ignore",
2122
- windowsHide: true
2123
- });
2124
- let out = "";
2125
- if (capture && proc.stdout) proc.stdout.on("data", (d) => {
2126
- out += d;
2127
- });
2128
- proc.on("close", (code) => code === 0 ? resolve(out) : reject(new Error(`${cmd} exit ${code}`)));
2129
- proc.on("error", reject);
2130
- });
2131
- }
2132
- let resolvedWhisperLanguage = null;
2133
- function normalizeWhisperLanguage(value) {
2134
- const raw = String(value ?? "").trim().toLowerCase();
2135
- if (!raw || raw === "auto") return null;
2136
- if (raw.startsWith("ko")) return "ko";
2137
- if (raw.startsWith("ja")) return "ja";
2138
- if (raw.startsWith("en")) return "en";
2139
- if (raw.startsWith("zh")) return "zh";
2140
- if (raw.startsWith("de")) return "de";
2141
- if (raw.startsWith("fr")) return "fr";
2142
- if (raw.startsWith("es")) return "es";
2143
- if (raw.startsWith("it")) return "it";
2144
- if (raw.startsWith("pt")) return "pt";
2145
- if (raw.startsWith("ru")) return "ru";
2146
- return raw;
2147
- }
2148
- function detectDeviceLanguage() {
2149
- if (resolvedWhisperLanguage) return resolvedWhisperLanguage;
2150
- const candidates = [
2151
- process.env.MIXDOG_CHANNELS_WHISPER_LANGUAGE,
2152
- process.env.LC_ALL,
2153
- process.env.LC_MESSAGES,
2154
- process.env.LANG,
2155
- Intl.DateTimeFormat().resolvedOptions().locale
2156
- ];
2157
- for (const candidate of candidates) {
2158
- const normalized = normalizeWhisperLanguage(candidate);
2159
- if (normalized) {
2160
- resolvedWhisperLanguage = normalized;
2161
- return normalized;
2162
- }
2163
- }
2164
- resolvedWhisperLanguage = "auto";
2165
- return resolvedWhisperLanguage;
2166
- }
2167
- // ── voice.transcription concurrency queue (max=1 by default, config-driven) ──
2168
- const _voiceTranscriptionQueue = (() => {
2169
- let running = 0;
2170
- const pending = [];
2171
- function drain() {
2172
- const limit = config.voice?.transcription?.maxConcurrency ?? 1;
2173
- while (running < limit && pending.length > 0) {
2174
- const { fn, resolve, reject } = pending.shift();
2175
- running++;
2176
- fn().then(resolve, reject).finally(() => { running--; drain(); });
2177
- }
2178
- }
2179
- return function enqueue(fn) {
2180
- return new Promise((resolve, reject) => { pending.push({ fn, resolve, reject }); drain(); });
2181
- };
2182
- })();
2183
-
2184
- // ── wav + transcript cache keyed by attachment id ──
2185
- const _voiceWavCache = new Map(); // attachmentId → wavPath
2186
- const _voiceTranscriptCache = new Map(); // attachmentId → transcript string
2187
- const _voiceInflight = new Map(); // attachmentId → Promise<string|null>
2188
- const _voiceFfmpegInflight = new Map(); // attachmentId|wavPath → Promise<void> single-flight ffmpeg
2189
-
2190
- async function _probeAudioDurationSec(filePath) {
2191
- try {
2192
- const ffprobePath = (() => { try { return _require('ffprobe-static').path; } catch { return 'ffprobe'; } })();
2193
- return await new Promise((resolve, reject) => {
2194
- const args = ['-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', filePath];
2195
- let out = '';
2196
- const proc = spawn(ffprobePath, args, { windowsHide: true });
2197
- proc.stdout.on('data', (d) => { out += d; });
2198
- proc.on('close', (code) => { code === 0 ? resolve(parseFloat(out.trim()) || null) : reject(new Error(`ffprobe exit ${code}`)); });
2199
- proc.on('error', reject);
2200
- });
2201
- } catch {
2202
- return null;
2203
- }
2204
- }
2205
-
2206
- async function transcribeVoice(audioPath, { attachmentId } = {}) {
2207
- // ── size gate (config: voice.transcription.maxFileSizeMB) ──
2208
- const maxSizeBytes = (config.voice?.transcription?.maxFileSizeMB ?? 0) * 1024 * 1024;
2209
- if (maxSizeBytes > 0) {
2210
- try {
2211
- const stat = await fs.promises.stat(audioPath);
2212
- if (stat.size > maxSizeBytes) {
2213
- process.stderr.write(`mixdog: voice.transcription skipped — file too large (${(stat.size / 1024 / 1024).toFixed(1)} MB > ${config.voice.transcription.maxFileSizeMB} MB): ${audioPath}\n`);
2214
- return null;
2215
- }
2216
- } catch { /* stat failure: proceed */ }
2217
- }
2218
- // ── duration gate (config: voice.transcription.maxDurationSec) ──
2219
- const maxDurationSec = config.voice?.transcription?.maxDurationSec ?? 0;
2220
- if (maxDurationSec > 0) {
2221
- const dur = await _probeAudioDurationSec(audioPath);
2222
- if (dur !== null && dur > maxDurationSec) {
2223
- process.stderr.write(`mixdog: voice.transcription skipped — audio too long (${dur.toFixed(1)}s > ${maxDurationSec}s): ${audioPath}\n`);
2224
- return null;
2225
- }
2226
- }
2227
- // ── transcript cache hit ──
2228
- if (attachmentId && _voiceTranscriptCache.has(attachmentId)) {
2229
- process.stderr.write(`mixdog: voice.transcription cache hit (${attachmentId})\n`);
2230
- return _voiceTranscriptCache.get(attachmentId);
2231
- }
2232
- if (attachmentId && _voiceInflight.has(attachmentId)) {
2233
- return _voiceInflight.get(attachmentId);
2234
- }
2235
- const p = _voiceTranscriptionQueue(() => _doTranscribeVoice(audioPath, attachmentId));
2236
- if (attachmentId) {
2237
- _voiceInflight.set(attachmentId, p);
2238
- p.catch((err) => {
2239
- try { process.stderr.write(`mixdog: voice.transcription inflight rejection: ${err?.stack || err}\n`); } catch {}
2240
- }).finally(() => _voiceInflight.delete(attachmentId));
2241
- }
2242
- return p;
2243
- }
2244
-
2245
- async function _doTranscribeVoice(audioPath, attachmentId) {
2246
- try {
2247
- const runtime = resolveVoiceRuntime(DATA_DIR);
2248
- if (!runtime?.installed) {
2249
- const missing = [runtime?.binary ? null : 'binary', runtime?.model ? null : 'model', runtime?.ffmpeg ? null : 'ffmpeg'].filter(Boolean).join(' + ');
2250
- throw new Error(`voice runtime not installed (missing: ${missing}) — open the setup wizard and click "Install voice"`);
2251
- }
2252
- const whisperCmd = runtime.whisperCmd;
2253
- const modelPath = runtime.modelPath;
2254
- const ffmpegPath = runtime.ffmpegPath;
2255
- const lang = normalizeWhisperLanguage(config.voice?.language) ?? detectDeviceLanguage();
2256
- const _cpuCount = (() => { try { return os.cpus().length; } catch { return 2; } })();
2257
- const threadCount = config.voice?.transcription?.threadCount ?? Math.max(1, Math.ceil(_cpuCount / 4));
2258
- // ── wav cache keyed by attachment id ──
2259
- let wavPath;
2260
- if (attachmentId && _voiceWavCache.has(attachmentId)) {
2261
- wavPath = _voiceWavCache.get(attachmentId);
2262
- if (!fs.existsSync(wavPath)) {
2263
- _voiceWavCache.delete(attachmentId);
2264
- wavPath = undefined;
2265
- } else {
2266
- process.stderr.write(`mixdog: voice.transcription wav cache hit (${attachmentId})\n`);
2267
- }
2268
- }
2269
- if (!wavPath) {
2270
- wavPath = audioPath.replace(/\.[^.]+$/, ".wav");
2271
- const sampleRate = config.voice?.transcription?.sampleRate ?? 16000;
2272
- const channels = config.voice?.transcription?.channels ?? 1;
2273
- // Single-flight: parallel callers for the same key share one ffmpeg spawn.
2274
- const _ffmpegKey = attachmentId || wavPath;
2275
- if (_voiceFfmpegInflight.has(_ffmpegKey)) {
2276
- await _voiceFfmpegInflight.get(_ffmpegKey);
2277
- } else {
2278
- const _ffmpegPromise = runCmd(ffmpegPath, ["-i", audioPath, "-ar", String(sampleRate), "-ac", String(channels), "-threads", String(threadCount), "-y", wavPath]);
2279
- _voiceFfmpegInflight.set(_ffmpegKey, _ffmpegPromise);
2280
- try {
2281
- await _ffmpegPromise;
2282
- if (attachmentId) _voiceWavCache.set(attachmentId, wavPath);
2283
- } finally {
2284
- _voiceFfmpegInflight.delete(_ffmpegKey);
2285
- }
2286
- }
2287
- }
2288
- process.stderr.write(`mixdog: voice.transcription start runtime=${runtime.kind} cmd=${path.basename(whisperCmd)}\n`);
2289
- await ensureReady({ serverCmd: runtime.serverCmd, modelPath, threadCount, host: '127.0.0.1' });
2290
- const text = await transcribe(wavPath, { language: lang });
2291
- const result = text.trim() || null;
2292
- if (attachmentId && result) _voiceTranscriptCache.set(attachmentId, result);
2293
- return result;
2294
- } catch (err) {
2295
- if (err?.message?.startsWith('voice runtime not installed')) throw err; // propagate setup errors; caller posts user-visible failure
2296
- process.stderr.write(`mixdog: voice.transcription failed: ${err}\n`);
2297
- return null;
2298
- }
2299
- }
2300
- import { TOOL_DEFS } from './tool-defs.mjs';
2301
- function createHttpMcpServer() {
2302
- const s = new Server(
2303
- { name: "mixdog", version: PLUGIN_VERSION },
2304
- { capabilities: { tools: {} }, instructions: INSTRUCTIONS }
2305
- );
2306
- s.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_DEFS }));
2307
- s.setRequestHandler(CallToolRequestSchema, async (req) => {
2308
- const toolName = req.params.name;
2309
- const args = req.params.arguments ?? {};
2310
- return handleToolCallWithBridgeRetry(toolName, args);
2311
- });
2312
- return s;
2313
- }
2314
- // Tool dispatch in worker mode goes through the IPC `call` handler at the
2315
- // bottom of this file (parent's `callWorker` → `handleToolCall`). The HTTP
2316
- // MCP path uses its own short-lived `Server` instance built by
2317
- // `createHttpMcpServer()` above. There is no orphan worker-level Server.
2318
- const BACKEND_TOOLS = /* @__PURE__ */ new Set(["reply", "fetch", "react", "edit_message", "download_attachment", "trigger_schedule"]);
2319
- // ── Backend-tool dispatch helpers ───────────────────────────────────────────
2320
- // Each helper transparently routes through proxyRequest() when this instance
2321
- // is in proxyMode (non-owner), or through the local backend otherwise. The
2322
- // MCP-result formatting (text shape, cache invalidation, isError flag) is
2323
- // shared so both branches produce byte-identical output.
2324
- // schedule_status / schedule_control share their result-formatting between
2325
- // the local (owner) MCP case handlers and the owner-side HTTP routes that
2326
- // serve proxied standby sessions. Keeping the body here makes both paths
2327
- // byte-identical and reads the LIVE scheduler.
2328
- function scheduleStatusResult() {
2329
- const statuses = scheduler.getStatus();
2330
- if (statuses.length === 0) {
2331
- return { content: [{ type: "text", text: "no schedules configured" }] };
2332
- }
2333
- const lines = statuses.map((s) => {
2334
- const state = s.running ? " [RUNNING]" : "";
2335
- const last = s.lastFired ? ` (last: ${s.lastFired})` : "";
2336
- return ` ${s.name} ${s.time} ${s.days} (${s.type})${state}${last}`;
2337
- });
2338
- return { content: [{ type: "text", text: lines.join("\n") }] };
2339
- }
2340
- function scheduleControlResult(args) {
2341
- const scName = args.name;
2342
- const action = args.action;
2343
- // Validate that the named schedule actually exists.
2344
- const _scAll = [...(scheduler.nonInteractive || []), ...(scheduler.interactive || [])];
2345
- const _scKnown = _scAll.some(s => s.name === scName);
2346
- if (!_scKnown) {
2347
- return { content: [{ type: "text", text: `schedule_control: unknown schedule "${scName}" — use schedule_status to list valid names` }], isError: true };
2348
- }
2349
- if (action === "defer") {
2350
- const minutes = args.minutes ?? 30;
2351
- if (typeof minutes !== "number" || !Number.isFinite(minutes) || minutes <= 0) {
2352
- return { content: [{ type: "text", text: `schedule_control: minutes must be a positive number, got ${JSON.stringify(minutes)}` }], isError: true };
2353
- }
2354
- scheduler.defer(scName, minutes);
2355
- return { content: [{ type: "text", text: `deferred "${scName}" for ${minutes} minutes` }] };
2356
- } else if (action === "skip_today") {
2357
- scheduler.skipToday(scName);
2358
- return { content: [{ type: "text", text: `skipped "${scName}" for today` }] };
2359
- }
2360
- return { content: [{ type: "text", text: `unknown action: ${action}` }], isError: true };
2361
- }
2362
- async function dispatchReply(args) {
2363
- const sendOpts = {
2364
- replyTo: args.reply_to,
2365
- files: args.files ?? [],
2366
- embeds: args.embeds ?? [],
2367
- components: args.components ?? []
2368
- };
2369
- let ids;
2370
- if (proxyMode) {
2371
- const proxyResult = await proxyRequest("/send", "POST", {
2372
- chatId: args.chat_id,
2373
- text: args.text,
2374
- opts: sendOpts
2375
- });
2376
- if (!proxyResult.ok) {
2377
- return { content: [{ type: "text", text: `proxy reply failed: ${proxyResult.error}` }], isError: true };
2378
- }
2379
- ids = proxyResult.data?.sentIds ?? [];
2380
- } else {
2381
- // Pre-send activity bump keeps idle gating consistent during the await.
2382
- scheduler.noteActivity();
2383
- const sendResult = await backend.sendMessage(args.chat_id, args.text, sendOpts);
2384
- // Lead-originated reply via proxy-mode MCP — bump activity.
2385
- scheduler.noteActivity();
2386
- ids = sendResult.sentIds;
2387
- }
2388
- const text = ids.length === 1 ? `sent (id: ${ids[0]})` : `sent ${ids.length} parts (ids: ${ids.join(", ")})`;
2389
- return { content: [{ type: "text", text }] };
2390
- }
2391
- async function dispatchFetch(args) {
2392
- const channelId = resolveChannelLabel(config.channelsConfig, args.channel);
2393
- const limit = args.limit ?? 20;
2394
- let msgs;
2395
- if (proxyMode) {
2396
- const proxyResult = await proxyRequest(`/fetch?channel=${encodeURIComponent(channelId)}&limit=${limit}`, "GET");
2397
- if (!proxyResult.ok) {
2398
- return { content: [{ type: "text", text: `proxy fetch failed: ${proxyResult.error}` }], isError: true };
2399
- }
2400
- msgs = proxyResult.data?.messages ?? [];
2401
- // recordFetchedMessages already ran on the owner side (/fetch route).
2402
- } else {
2403
- msgs = await backend.fetchMessages(channelId, limit);
2404
- recordFetchedMessages(channelId, args.channel !== channelId ? args.channel : labelForChannelId(channelId), msgs);
2405
- }
2406
- const text = msgs.length === 0 ? "(no messages)" : msgs.map((m) => {
2407
- const atts = m.attachmentCount > 0 ? ` +${m.attachmentCount}att` : "";
2408
- return `[${m.ts}] ${m.user}: ${m.text} (id: ${m.id}${atts})`;
2409
- }).join("\n");
2410
- return { content: [{ type: "text", text }] };
2411
- }
2412
- async function dispatchReact(args) {
2413
- if (proxyMode) {
2414
- const proxyResult = await proxyRequest("/react", "POST", {
2415
- chatId: args.chat_id,
2416
- messageId: args.message_id,
2417
- emoji: args.emoji
2418
- });
2419
- if (!proxyResult.ok) {
2420
- return { content: [{ type: "text", text: `proxy react failed: ${proxyResult.error}` }], isError: true };
2421
- }
2422
- } else {
2423
- await backend.react(args.chat_id, args.message_id, args.emoji);
2424
- }
2425
- return { content: [{ type: "text", text: "reacted" }] };
2426
- }
2427
- async function dispatchEditMessage(args) {
2428
- const opts = { embeds: args.embeds ?? [], components: args.components ?? [] };
2429
- let id;
2430
- if (proxyMode) {
2431
- const proxyResult = await proxyRequest("/edit", "POST", {
2432
- chatId: args.chat_id,
2433
- messageId: args.message_id,
2434
- text: args.text,
2435
- opts
2436
- });
2437
- if (!proxyResult.ok) {
2438
- return { content: [{ type: "text", text: `proxy edit failed: ${proxyResult.error}` }], isError: true };
2439
- }
2440
- id = proxyResult.data?.id;
2441
- } else {
2442
- id = await backend.editMessage(args.chat_id, args.message_id, args.text, opts);
2443
- }
2444
- return { content: [{ type: "text", text: `edited (id: ${id})` }] };
2445
- }
2446
- async function dispatchDownloadAttachment(args) {
2447
- let files;
2448
- if (proxyMode) {
2449
- const proxyResult = await proxyRequest("/download", "POST", {
2450
- chatId: args.chat_id,
2451
- messageId: args.message_id
2452
- });
2453
- if (!proxyResult.ok) {
2454
- return { content: [{ type: "text", text: `proxy download failed: ${proxyResult.error}` }], isError: true };
2455
- }
2456
- files = proxyResult.data?.files ?? [];
2457
- } else {
2458
- files = await backend.downloadAttachment(args.chat_id, args.message_id);
2459
- }
2460
- if (files.length === 0) {
2461
- return { content: [{ type: "text", text: "message has no attachments" }] };
2462
- }
2463
- const lines = files.map(
2464
- (f) => ` ${f.path} (${f.name}, ${f.contentType}, ${(f.size / 1024).toFixed(0)}KB)`
2465
- );
2466
- // Each downloaded file lands on the local FS; if any of them
2467
- // had a stale prefetch entry from a prior session, drop it so
2468
- // the next prefetch sees the fresh contents.
2469
- for (const f of files) {
2470
- if (f && typeof f.path === "string" && f.path) {
2471
- invalidatePrefetchCache(f.path);
2472
- }
2473
- }
2474
- return { content: [{ type: "text", text: `downloaded ${files.length} attachment(s):
2475
- ${lines.join("\n")}` }] };
2476
- }
2477
- async function handleToolCall(name, args, signal) {
2478
- if (_channelsDegraded) {
2479
- return { content: [{ type: 'text', text: `[channels degraded] ${name} unavailable — restart MCP to recover` }], isError: true }
2480
- }
2481
- let result;
2482
- try {
2483
- switch (name) {
2484
- case "reply":
2485
- result = await dispatchReply(args);
2486
- break;
2487
- case "fetch":
2488
- result = await dispatchFetch(args);
2489
- break;
2490
- case "react":
2491
- result = await dispatchReact(args);
2492
- break;
2493
- case "edit_message":
2494
- result = await dispatchEditMessage(args);
2495
- break;
2496
- case "download_attachment":
2497
- result = await dispatchDownloadAttachment(args);
2498
- break;
2499
- case "schedule_status": {
2500
- if (proxyMode) {
2501
- const proxyResult = await proxyRequest("/schedule-status", "GET");
2502
- if (!proxyResult.ok) {
2503
- result = { content: [{ type: "text", text: `proxy schedule_status failed: ${proxyResult.error}` }], isError: true };
2504
- break;
2505
- }
2506
- result = proxyResult.data?.result ?? { content: [{ type: "text", text: "no schedules configured" }] };
2507
- } else {
2508
- result = scheduleStatusResult();
2509
- }
2510
- break;
2511
- }
2512
- case "trigger_schedule": {
2513
- if (proxyMode) {
2514
- const proxyResult = await proxyRequest("/trigger-schedule", "POST", { name: args.name });
2515
- if (!proxyResult.ok) {
2516
- result = { content: [{ type: "text", text: `proxy trigger_schedule failed: ${proxyResult.error}` }], isError: true };
2517
- break;
2518
- }
2519
- const triggerResult = proxyResult.data?.result;
2520
- result = { content: [{ type: "text", text: triggerResult == null ? "" : String(triggerResult) }] };
2521
- } else {
2522
- const triggerResult = await scheduler.triggerManual(args.name);
2523
- result = { content: [{ type: "text", text: triggerResult }] };
2524
- }
2525
- break;
2526
- }
2527
- case "schedule_control": {
2528
- if (proxyMode) {
2529
- const proxyResult = await proxyRequest("/schedule-control", "POST", {
2530
- name: args.name,
2531
- action: args.action,
2532
- minutes: args.minutes
2533
- });
2534
- if (!proxyResult.ok) {
2535
- result = { content: [{ type: "text", text: `proxy schedule_control failed: ${proxyResult.error}` }], isError: true };
2536
- break;
2537
- }
2538
- result = proxyResult.data?.result ?? { content: [{ type: "text", text: `unknown action: ${args.action}` }], isError: true };
2539
- } else {
2540
- result = scheduleControlResult(args);
2541
- }
2542
- break;
2543
- }
2544
- case "activate_channel_bridge": {
2545
- if (proxyMode) {
2546
- const proxyRes = await proxyRequest("/bridge/activate", "POST", { active: args.active === true });
2547
- if (!proxyRes.ok) {
2548
- result = { content: [{ type: "text", text: `proxy bridge activate failed: ${proxyRes.error}` }], isError: true };
2549
- } else {
2550
- channelBridgeActive = Boolean(args.active);
2551
- writeBridgeState(channelBridgeActive);
2552
- // Remote owner just deactivated and is tearing its owner-HTTP
2553
- // server down. Drop our proxy pointer so subsequent direct
2554
- // tool calls don't route through proxyRequest() to a port
2555
- // about to close (ECONNREFUSED) or stripped of auth (401).
2556
- if (!args.active) {
2557
- proxyMode = false;
2558
- ownerHttpPort = 0;
2559
- }
2560
- result = { content: [{ type: "text", text: `channel bridge ${args.active ? "activated" : "deactivated"}` }] };
2561
- }
2562
- } else {
2563
- const active = args.active === true;
2564
- const wasActive = channelBridgeActive;
2565
- channelBridgeActive = active;
2566
- writeBridgeState(active);
2567
- if (active && !wasActive) {
2568
- refreshBridgeOwnershipSafe({ restoreBinding: true });
2569
- }
2570
- if (!active && wasActive) {
2571
- stopServerTyping();
2572
- // Tear down the owner-side runtime so Discord/scheduler/webhook/
2573
- // event-pipeline/owner-HTTP/heartbeat don't keep running on a
2574
- // deactivated bridge (and to prevent this owner from later
2575
- // entering proxyMode against its own port).
2576
- try { await stopOwnedRuntime("bridge deactivated"); } catch (e) {
2577
- process.stderr.write(`mixdog: stopOwnedRuntime on deactivate failed: ${e?.message || e}\n`);
2578
- }
2579
- // Also clear proxyMode/ownerHttpPort. Without this, a session
2580
- // that was acting as proxy when deactivate landed keeps the
2581
- // stale flag + port set; later direct tool calls then route
2582
- // through proxyRequest() to a port whose owner has just been
2583
- // stopped or stripped of auth, returning ECONNREFUSED/401.
2584
- if (proxyMode) {
2585
- proxyMode = false;
2586
- ownerHttpPort = 0;
2587
- }
2588
- }
2589
- result = { content: [{ type: "text", text: `channel bridge ${active ? "activated" : "deactivated"}` }] };
2590
- }
2591
- break;
2592
- }
2593
- case "reload_config": {
2594
- await reloadRuntimeConfig();
2595
- // Extend reload to the agent module so providers/presets/maintenance
2596
- // hot-reload on the same call (dynamic import: agent/index.mjs does not
2597
- // import channels, so this stays acyclic and tolerant of load order).
2598
- let agentReloadMsg = "";
2599
- try {
2600
- const { reloadAgentConfig } = await import("../agent/index.mjs");
2601
- await reloadAgentConfig("reload_config tool");
2602
- agentReloadMsg = ", agent providers/presets/maintenance";
2603
- } catch (err) {
2604
- process.stderr.write(`[reload_config] agent reload failed: ${err?.message || String(err)}\n`);
2605
- }
2606
- result = { content: [{ type: "text", text: `config reloaded — schedules, webhooks, events${agentReloadMsg} re-registered` }] };
2607
- break;
2608
- }
2609
- case "inject_command": {
2610
- const cmd = String(args?.command || "").trim();
2611
- const ALLOW = new Set(["reload-plugins", "clear"]);
2612
- if (!ALLOW.has(cmd)) {
2613
- result = { content: [{ type: "text", text: `inject_command: '${cmd}' not in allow-list (${[...ALLOW].join(", ")})` }], isError: true };
2614
- break;
2615
- }
2616
- if (process.platform !== "win32") {
2617
- result = { content: [{ type: "text", text: "inject_command: Windows-only (uses AttachConsole + WriteConsoleInputW)" }], isError: true };
2618
- break;
2619
- }
2620
- try {
2621
- const ccPid = _resolveClaudeCodePid();
2622
- const scriptPath = _injectScriptPath();
2623
- const r = spawnSync("powershell", [
2624
- "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath,
2625
- "-TargetPid", String(ccPid), "-Text", `/${cmd}\r`,
2626
- ], { encoding: "utf8", windowsHide: true });
2627
- if (r.status !== 0) {
2628
- const detail = (r.stderr || r.stdout || "").trim().slice(0, 400);
2629
- result = { content: [{ type: "text", text: `inject_command failed (status=${r.status}): ${detail}` }], isError: true };
2630
- break;
2631
- }
2632
- result = { content: [{ type: "text", text: `injected /${cmd} into CC pid=${ccPid}` }] };
2633
- } catch (err) {
2634
- result = { content: [{ type: "text", text: `inject_command error: ${err?.message || err}` }], isError: true };
2635
- }
2636
- break;
2637
- }
2638
- // memory — handled by memory-service.mjs MCP
2639
- default:
2640
- result = {
2641
- content: [{ type: "text", text: `unknown tool: ${name}` }],
2642
- isError: true
2643
- };
2644
- }
2645
- } catch (err) {
2646
- const msg = err instanceof Error ? err.message : String(err);
2647
- result = {
2648
- content: [{ type: "text", text: `${name} failed: ${msg}` }],
2649
- isError: true
2650
- };
2651
- }
2652
- return result;
2653
- }
2654
- // Bridge auto-connect retry + forwarder-aware tool dispatch wrapper. Used by
2655
- // both the HTTP MCP path (createHttpMcpServer's CallTool handler can call this)
2656
- // and the worker IPC handler at the bottom of this file. The pre-v0.6.7 code
2657
- // registered this on the orphan worker-level `Server`, which never had a
2658
- // transport, so the wrapper never actually fired. Centralised here for reuse.
2659
- // Last timestamp a forwardNewText() call was dispatched (debounce for item 4).
2660
- let _lastForwardMs = 0;
2661
-
2662
- async function handleToolCallWithBridgeRetry(toolName, args, signal) {
2663
- // Debounce: only forward when ≥250 ms have elapsed since the last forward,
2664
- // to avoid one HTTP roundtrip per tool call on rapid-fire sequences.
2665
- const now = Date.now();
2666
- if (now - _lastForwardMs >= 250) {
2667
- _lastForwardMs = now;
2668
- await forwarder.forwardNewText();
2669
- }
2670
- if (BACKEND_TOOLS.has(toolName) && !bridgeRuntimeConnected && !proxyMode) {
2671
- // Do NOT pre-claim ownership here. claimBridgeOwnership() overwrites the
2672
- // active-instance advert immediately, which kicks a live owner offline if
2673
- // refreshBridgeOwnership() would have otherwise discovered them via
2674
- // pingOwner() and entered proxyMode. Let refreshBridgeOwnership() below
2675
- // ping/proxy the existing owner first and only fall through to a takeover
2676
- // when the live owner is unreachable.
2677
- for (let i = 0; i < 2 && !bridgeRuntimeConnected && !proxyMode; i++) {
2678
- try {
2679
- await refreshBridgeOwnership();
2680
- } catch {
2681
- }
2682
- if (!bridgeRuntimeConnected && !proxyMode) await new Promise((r) => setTimeout(r, 300));
2683
- }
2684
- if (!bridgeRuntimeConnected && !proxyMode) {
2685
- return {
2686
- content: [{ type: "text", text: `Discord auto-connect failed after retries. Check token and network.` }],
2687
- isError: true
2688
- };
2689
- }
2690
- }
2691
- const result = await handleToolCall(toolName, args, signal);
2692
- const toolLine = OutputForwarder.buildToolLine(toolName, args);
2693
- if (toolLine) {
2694
- // Distinct from the dispatch-log ok line (server-main.mjs): this forwards
2695
- // a human-readable tool summary to Discord for the user, not operator stdout.
2696
- void forwarder.forwardToolLog(toolLine, toolName, args);
2697
- }
2698
- return result;
2699
- }
2700
- const INBOUND_DEDUP_TTL = 5 * 6e4;
2701
- const inboundSeen = /* @__PURE__ */ new Map();
2702
- const INBOUND_DEDUP_DIR = path.join(os.tmpdir(), "mixdog-inbound");
2703
- ensureDir(INBOUND_DEDUP_DIR);
2704
- function writeChannelOwner(channelId) {
2705
- const ownerPath = getChannelOwnerPath(channelId);
2706
- try {
2707
- fs.writeFileSync(ownerPath, JSON.stringify({ instanceId: INSTANCE_ID, pid: process.pid, updatedAt: Date.now() }));
2708
- return true;
2709
- } catch {
2710
- return false;
2711
- }
2712
- }
2713
- function shouldDropDuplicateInbound(msg) {
2714
- const key = `${msg.chatId}:${msg.messageId}`;
2715
- const now = Date.now();
2716
- if (inboundSeen.has(key) && now - inboundSeen.get(key) < INBOUND_DEDUP_TTL) return true;
2717
- inboundSeen.set(key, now);
2718
- const marker = path.join(INBOUND_DEDUP_DIR, key.replace(/:/g, "_"));
2719
- try {
2720
- fs.writeFileSync(marker, String(now), { flag: "wx" });
2721
- } catch (e) {
2722
- if (e.code === "EEXIST") {
2723
- try {
2724
- const stat = fs.statSync(marker);
2725
- if (now - stat.mtimeMs < INBOUND_DEDUP_TTL) return true;
2726
- } catch {}
2727
- }
2728
- }
2729
- if (Math.random() < 0.1) {
2730
- try {
2731
- for (const f of fs.readdirSync(INBOUND_DEDUP_DIR)) {
2732
- const fp = path.join(INBOUND_DEDUP_DIR, f);
2733
- try {
2734
- if (now - fs.statSync(fp).mtimeMs > INBOUND_DEDUP_TTL) removeFileIfExists(fp);
2735
- } catch {
2736
- }
2737
- }
2738
- } catch {
2739
- }
2740
- }
2741
- for (const [k, t] of inboundSeen) {
2742
- if (now - t > INBOUND_DEDUP_TTL) inboundSeen.delete(k);
2743
- }
2744
- return false;
2745
- }
2746
- function resolveInboundRoute(chatId, parentChatId) {
2747
- const main = config.channelsConfig?.main;
2748
- const findEntry = (id) => {
2749
- if (!id || !config.channelsConfig) return null;
2750
- if (typeof main === "object" && main !== null && main.channelId === id) {
2751
- return { label: "main", entry: main };
2752
- }
2753
- for (const [label, entry] of Object.entries(config.channelsConfig)) {
2754
- if (typeof entry === "object" && entry !== null && entry.channelId === id) {
2755
- return { label, entry };
2756
- }
2757
- }
2758
- return null;
2759
- };
2760
- // Prefer a direct channelsConfig match on the thread/channel id; fall back
2761
- // to the parent channel id so thread messages inherit the parent's label
2762
- // and mode (e.g. monitor) instead of being routed as untagged interactive.
2763
- const direct = findEntry(chatId);
2764
- if (direct) {
2765
- const mode = direct.entry.mode === "monitor" ? "monitor" : (direct.entry.mode || "interactive");
2766
- return { targetChatId: chatId, sourceChatId: chatId, sourceLabel: direct.label, sourceMode: mode };
2767
- }
2768
- if (parentChatId) {
2769
- const viaParent = findEntry(parentChatId);
2770
- if (viaParent) {
2771
- const mode = viaParent.entry.mode === "monitor" ? "monitor" : (viaParent.entry.mode || "interactive");
2772
- return { targetChatId: chatId, sourceChatId: parentChatId, sourceLabel: viaParent.label, sourceMode: mode };
2773
- }
2774
- }
2775
- return { targetChatId: chatId, sourceChatId: chatId, sourceLabel: undefined, sourceMode: "interactive" };
2776
- }
2777
- const inboundQueue = (() => {
2778
- let tail = Promise.resolve();
2779
- let _iqDepth = 0;
2780
- const _IQ_MAX_DEPTH = 1000;
2781
- return (fn) => {
2782
- if (_iqDepth >= _IQ_MAX_DEPTH) {
2783
- try { process.stderr.write(`mixdog: inboundQueue overflow (depth=${_iqDepth}), dropping message\n`); } catch {}
2784
- return;
2785
- }
2786
- _iqDepth++;
2787
- tail = Promise.resolve(tail).then(fn).catch((err) => {
2788
- try { process.stderr.write(`mixdog: inboundQueue error: ${err && err.message || err}\n`); } catch {}
2789
- }).finally(() => { _iqDepth--; });
2790
- };
2791
- })();
2792
- // ── Reverse-lookup channelId → human label from channelsConfig ──────────────
2793
- function labelForChannelId(channelId) {
2794
- if (!channelId || !config.channelsConfig) return channelId;
2795
- for (const [label, entry] of Object.entries(config.channelsConfig)) {
2796
- if (entry?.channelId === channelId) return label;
2797
- }
2798
- return channelId;
2799
- }
2800
-
2801
- backend.onMessage = (msg) => {
2802
- const receivedAtMs = Number.isFinite(msg.receivedAtMs) ? msg.receivedAtMs : Date.now();
2803
- const onMessageAtMs = Date.now();
2804
- if (!bridgeRuntimeConnected || !getBridgeOwnershipSnapshot().owned) {
2805
- refreshBridgeOwnershipSafe();
2806
- return;
2807
- }
2808
- if (!channelBridgeActive) return;
2809
- if (shouldDropDuplicateInbound(msg)) return;
2810
- recordFetchedMessages(msg.chatId, labelForChannelId(msg.chatId), [{ id: msg.messageId }], { markRead: true });
2811
- if (!writeChannelOwner(msg.chatId)) return;
2812
- const route = resolveInboundRoute(msg.chatId, msg.parentChatId);
2813
- scheduler.noteActivity();
2814
- startServerTyping(route.targetChatId);
2815
- backend.resetSendCount();
2816
- // Pin the prior turn's bound channel before this fire-and-forget flush so the
2817
- // imminent rebind below (which mutates forwarder.channelId synchronously)
2818
- // cannot redirect the previous turn's final output to the new channel.
2819
- const priorForwardChannelId = forwarder.channelId || null;
2820
- void forwarder.forwardFinalText(0, priorForwardChannelId).catch((err) => {
2821
- try { process.stderr.write(`mixdog: forwardFinalText rejection: ${err?.stack || err}\n`); } catch {}
2822
- }).finally(() => forwarder.reset());
2823
- const previousPath = getPersistedTranscriptPath();
2824
- let boundTranscript = null;
2825
- let transcriptPath = forwarder.hasBinding() ? forwarder.transcriptPath : "";
2826
- if (transcriptPath) {
2827
- boundTranscript = {
2828
- sessionId: sessionIdFromTranscriptPath(transcriptPath),
2829
- sessionCwd: statusState.read().sessionCwd ?? null,
2830
- transcriptPath,
2831
- exists: true
2832
- };
2833
- } else {
2834
- boundTranscript = discoverSessionBoundTranscript();
2835
- transcriptPath = pickUsableTranscriptPath(boundTranscript, previousPath);
2836
- // Only fall back to latest-by-mtime when discovery did NOT produce a
2837
- // confident, existing current-session transcript. detectCurrentSessionTranscript()
2838
- // already weighs mtime (with a 30s decisive threshold) against active-pid /
2839
- // cwd affinity, so overriding a real detected binding with the raw newest
2840
- // file would clobber the current session with an unrelated, more-recently
2841
- // touched transcript (wrong-session forward).
2842
- if (!boundTranscript?.exists) {
2843
- const latestByMtime = findLatestTranscriptByMtime(boundTranscript?.sessionCwd);
2844
- if (latestByMtime && latestByMtime !== transcriptPath) {
2845
- transcriptPath = latestByMtime;
2846
- }
2847
- }
2848
- }
2849
- if (transcriptPath) {
2850
- applyTranscriptBinding(route.targetChatId, transcriptPath, { cwd: boundTranscript?.sessionCwd });
2851
- } else {
2852
- refreshActiveInstance(INSTANCE_ID, { channelId: route.targetChatId });
2853
- }
2854
- void (async () => {
2855
- try {
2856
- await backend.react(msg.chatId, msg.messageId, "\u{1F914}");
2857
- } catch {
2858
- }
2859
- statusState.update((state) => {
2860
- state.channelId = route.targetChatId;
2861
- state.userMessageId = msg.messageId;
2862
- state.emoji = "\u{1F914}";
2863
- state.sentCount = 0;
2864
- state.sessionIdle = false;
2865
- if (transcriptPath) state.transcriptPath = transcriptPath;
2866
- else delete state.transcriptPath;
2867
- state.sessionCwd = boundTranscript?.sessionCwd ?? null;
2868
- });
2869
- if (!boundTranscript?.exists) {
2870
- await rebindTranscriptContext(route.targetChatId, {
2871
- previousPath: transcriptPath,
2872
- catchUp: true,
2873
- persistStatus: true
2874
- });
2875
- }
2876
- })();
2877
- const queuedAtMs = Date.now();
2878
- const preQueueMs = queuedAtMs - onMessageAtMs;
2879
- const gatewayToQueueMs = queuedAtMs - receivedAtMs;
2880
- if (preQueueMs > 250 || gatewayToQueueMs > 500) {
2881
- process.stderr.write(`mixdog: inbound latency prequeue=${preQueueMs}ms gateway_to_queue=${gatewayToQueueMs}ms channel=${route.targetChatId}\n`);
2882
- }
2883
- inboundQueue(() => handleInbound(msg, route, {
2884
- sessionId: boundTranscript?.sessionId ?? sessionIdFromTranscriptPath(transcriptPath),
2885
- receivedAtMs,
2886
- queuedAtMs
2887
- }).catch((err) => {
2888
- process.stderr.write(`mixdog: handleInbound error: ${err}
2889
- `);
2890
- }).finally(() => {
2891
- stopServerTyping();
2892
- }));
2893
- };
2894
- async function handleInbound(msg, route, options = {}) {
2895
- const handleStartMs = Date.now();
2896
- let text = msg.text;
2897
- const voiceAtts = msg.attachments.filter((a) => isVoiceAttachment(a.contentType));
2898
- if (voiceAtts.length > 0) {
2899
- try {
2900
- const files = await backend.downloadAttachment(msg.chatId, msg.messageId);
2901
- // concurrency handled inside transcribeVoice queue; loop is sequential so last att wins
2902
- for (const f of voiceAtts.map(a => files.find(df => df.id === a.id) ?? null).filter(Boolean)) {
2903
- const _t0 = Date.now();
2904
- const transcript = await transcribeVoice(f.path, { attachmentId: f.id });
2905
- const _elapsed = Date.now() - _t0;
2906
- if (transcript) {
2907
- text = transcript;
2908
- process.stderr.write(`mixdog: voice.transcription ok (${f.name}, ${_elapsed}ms): ${transcript.slice(0, 50)}\n`);
2909
- } else {
2910
- process.stderr.write(`mixdog: voice.transcription empty (${f.name})\n`);
2911
- text = text || "[voice message \u2014 transcription failed]";
2912
- }
2913
- }
2914
- } catch (err) {
2915
- process.stderr.write(`mixdog: voice.transcription error: ${err}\n`);
2916
- text = text || `[voice message \u2014 transcription error: ${err?.message || err}]`;
2917
- }
2918
- }
2919
- const hasVoiceAtt = voiceAtts.length > 0;
2920
- const attMeta = msg.attachments.length > 0 && !hasVoiceAtt ? {
2921
- attachment_count: String(msg.attachments.length),
2922
- attachments: msg.attachments.map((a) => `${a.name} (${a.contentType}, ${(a.size / 1024).toFixed(0)}KB)`).join("; ")
2923
- } : {};
2924
- const messageBody = route.sourceMode === "monitor" && route.sourceLabel ? `[monitor:${route.sourceLabel}] ${text}` : text;
2925
- const now = (/* @__PURE__ */ new Date()).toLocaleString();
2926
- const notificationMeta = {
2927
- chat_id: route.targetChatId,
2928
- message_id: msg.messageId,
2929
- user: msg.user,
2930
- user_id: msg.userId,
2931
- ts: msg.ts,
2932
- ...route.sourceMode === "monitor" ? {
2933
- source_chat_id: route.sourceChatId,
2934
- source_mode: route.sourceMode,
2935
- ...route.sourceLabel ? { source_label: route.sourceLabel } : {}
2936
- } : {},
2937
- ...attMeta,
2938
- ...msg.imagePath ? { image_path: msg.imagePath } : {}
2939
- };
2940
- const notificationContent = `[${now}]
2941
- ${messageBody}`;
2942
- sendNotifyToParent("notifications/claude/channel", {
2943
- content: notificationContent,
2944
- meta: notificationMeta
2945
- });
2946
- const notifiedAtMs = Date.now();
2947
- const receivedAtMs = Number.isFinite(options.receivedAtMs) ? options.receivedAtMs : handleStartMs;
2948
- const queuedAtMs = Number.isFinite(options.queuedAtMs) ? options.queuedAtMs : handleStartMs;
2949
- const queueMs = handleStartMs - queuedAtMs;
2950
- const handleMs = notifiedAtMs - handleStartMs;
2951
- const totalMs = notifiedAtMs - receivedAtMs;
2952
- if (queueMs > 250 || handleMs > 250 || totalMs > 500) {
2953
- process.stderr.write(`mixdog: inbound latency delivered total=${totalMs}ms queue=${queueMs}ms handle=${handleMs}ms channel=${route.targetChatId} attachments=${msg.attachments.length}\n`);
2954
- }
2955
- void memoryAppendEntry({
2956
- ts: msg.ts,
2957
- role: "user",
2958
- content: messageBody,
2959
- sourceRef: `discord:${route.targetChatId}#${msg.messageId}`,
2960
- sessionId: `discord:${route.targetChatId}`,
2961
- cwd: statusState.read().sessionCwd,
2962
- });
2963
- }
2964
- async function init(_sharedMcp) {
2965
- // _sharedMcp is no longer used. Notifications now flow via IPC to the parent
2966
- // (sendNotifyToParent above). The parameter is retained for backward
2967
- // compatibility with any caller that still passes a Server reference.
2968
- scheduler.setInjectHandler((channelId, name, content, options) => {
2969
- injectAndRecord(channelId, name, content, options);
2970
- });
2971
- }
2972
- async function start() {
2973
- channelBridgeActive = true;
2974
- writeBridgeState(true);
2975
- await refreshBridgeOwnership({ restoreBinding: true });
2976
- // Pre-warm the whisper-server manager once at owner startup so the first
2977
- // voice transcription does not pay cold-start cost. Non-blocking: failures
2978
- // (e.g. runtime not installed) are swallowed; per-request ensureReady retries.
2979
- void (async () => {
2980
- try {
2981
- const runtime = resolveVoiceRuntime(DATA_DIR);
2982
- if (!runtime?.installed) return;
2983
- const _cpuCount = (() => { try { return os.cpus().length; } catch { return 2; } })();
2984
- const threadCount = config.voice?.transcription?.threadCount ?? Math.max(1, Math.ceil(_cpuCount / 4));
2985
- await ensureReady({ serverCmd: runtime.serverCmd, modelPath: runtime.modelPath, threadCount, host: '127.0.0.1' });
2986
- } catch (err) {
2987
- try { process.stderr.write(`mixdog: voice.transcription pre-warm skipped: ${err}\n`); } catch {}
2988
- }
2989
- })();
2990
- }
2991
- async function stop() {
2992
- try { await stopVoiceWhisperServer(); } catch {}
2993
- await stopOwnedRuntime("unified server stop");
2994
- cleanupInstanceRuntimeFiles(INSTANCE_ID);
2995
- if (bridgeOwnershipTimer) {
2996
- clearInterval(bridgeOwnershipTimer);
2997
- bridgeOwnershipTimer = null;
2998
- }
2999
- if (turnEndWatcher) {
3000
- try { turnEndWatcher.close(); } catch {}
3001
- turnEndWatcher = null;
3002
- }
3003
- }
3004
- {
3005
- let detectChannelFlag = function() {
3006
- const isWin = process.platform === "win32";
3007
- const flagRe = /--channels\b|--dangerously-load-development-channels\b/;
3008
- if (process.env.MIXDOG_CHANNEL_FLAG === "1") return true;
3009
- if (process.env.MIXDOG_CHANNEL_FLAG === "0") return false;
3010
- if (isWin) {
3011
- // Single CIM snapshot + in-process chain walk: one powershell.exe spawn
3012
- // instead of up to 12 synchronous wmic/powershell spawns. Snapshots all
3013
- // processes into a map, walks from process.ppid up to 6 ancestors
3014
- // (closest first), and emits each ancestor CommandLine on its own line
3015
- // for the same flagRe test below. Any failure returns false.
3016
- try {
3017
- const ps = [
3018
- '$procs = Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine;',
3019
- '$map = @{};',
3020
- 'foreach ($p in $procs) { $map[[int]$p.ProcessId] = $p }',
3021
- `$cur = ${Number(process.ppid)};`,
3022
- 'for ($i = 0; $i -lt 6; $i++) {',
3023
- ' if (-not $cur -or $cur -le 1) { break }',
3024
- ' $p = $map[[int]$cur]; if ($null -eq $p) { break }',
3025
- ' [Console]::WriteLine($p.CommandLine);',
3026
- ' $next = [int]$p.ParentProcessId;',
3027
- ' if ($next -eq [int]$cur -or $next -le 1) { break }',
3028
- ' $cur = $next',
3029
- '}',
3030
- ].join(" ");
3031
- const r = spawnSync("powershell.exe", ["-NoProfile", "-Command", ps], {
3032
- encoding: "utf8",
3033
- timeout: 5e3,
3034
- windowsHide: true,
3035
- });
3036
- const out = String(r.stdout || "");
3037
- for (const line of out.split(/\r?\n/)) {
3038
- if (flagRe.test(line)) return true;
3039
- }
3040
- } catch {}
3041
- return false;
3042
- }
3043
- let pid = process.ppid;
3044
- for (let depth = 0; pid && pid > 1 && depth < 6; depth++) {
3045
- try {
3046
- const cmdLine = execSync(`ps -p ${pid} -o args=`, { encoding: "utf8", timeout: 3e3, windowsHide: true });
3047
- if (flagRe.test(cmdLine)) return true;
3048
- pid = parseInt(execSync(`ps -p ${pid} -o ppid=`, { encoding: "utf8", timeout: 3e3, windowsHide: true }).trim(), 10);
3049
- } catch {
3050
- break;
3051
- }
3052
- }
3053
- return false;
3054
- };
3055
- _channelFlagDetected = detectChannelFlag();
3056
- if (isMixdogDebug()) {
3057
- fs.appendFileSync(_bootLog, `[${localTimestamp()}] channelFlag: ${_channelFlagDetected}\n`);
3058
- if (_channelFlagDetected) {
3059
- fs.appendFileSync(_bootLog, `[${localTimestamp()}] channel mode detected — bridge auto-activated\n`);
3060
- }
3061
- }
3062
- if (_channelFlagDetected) {
3063
- channelBridgeActive = true;
3064
- }
3065
- writeBridgeState(channelBridgeActive);
3066
- const previousOwner = readActiveInstance();
3067
- noteStartupHandoff(previousOwner);
3068
- // Do not claim ownership just because this terminal is channel-capable.
3069
- // refreshBridgeOwnership() below pings/proxies a live owner first and only
3070
- // claims when there is no reachable active owner or the record is stale.
3071
- const _bindingReadyStart = Date.now();
3072
- void refreshBridgeOwnership({ restoreBinding: true }).then(
3073
- (v) => {
3074
- bindingReadyStatus = "resolved";
3075
- dropTrace("bindingReady.resolve", { elapsedMs: Date.now() - _bindingReadyStart, status: bindingReadyStatus });
3076
- _bindingReadyResolve(v);
3077
- },
3078
- (e) => {
3079
- bindingReadyStatus = "rejected";
3080
- dropTrace("bindingReady.reject", { elapsedMs: Date.now() - _bindingReadyStart, status: bindingReadyStatus, err: String(e) });
3081
- _bindingReadyResolve(e);
3082
- }
3083
- );
3084
- bridgeOwnershipTimer = setInterval(() => {
3085
- refreshBridgeOwnershipSafe();
3086
- }, 3e3);
3087
- // Hook/statusline IPC is owned by the MCP parent process so it is available
3088
- // before channels finishes bridge ownership and backend startup.
3089
- const configPath = path.join(DATA_DIR, "mixdog-config.json");
3090
- let reloadDebounce = null;
3091
- let configWatcher = null;
3092
- try {
3093
- configWatcher = fs.watch(configPath, () => {
3094
- if (reloadDebounce) clearTimeout(reloadDebounce);
3095
- reloadDebounce = setTimeout(() => {
3096
- reloadRuntimeConfig().catch(() => {});
3097
- }, 500);
3098
- });
3099
- } catch {
3100
- }
3101
- process.on("exit", () => {
3102
- if (configWatcher) { try { configWatcher.close(); } catch {} }
3103
- if (bridgeOwnershipTimer) { clearInterval(bridgeOwnershipTimer); }
3104
- });
3105
- }
3106
- // ── IPC worker mode ──────────────────────────────────────────────
3107
- if (_isWorkerMode && process.send) {
3108
- // SIGTERM/SIGINT/IPC shutdown handler — mirrors src/memory/index.mjs pattern.
3109
- // Cleans up in-progress webhook/scheduler state, removes runtime files, then exits.
3110
- let _channelsStopInFlight = false
3111
- const _channelsShutdownHandler = async (sig) => {
3112
- if (_channelsStopInFlight) {
3113
- process.stderr.write(`[channels-worker] ${sig} — shutdown already in flight, ignoring\n`)
3114
- return
3115
- }
3116
- _channelsStopInFlight = true
3117
- process.stderr.write(`[channels-worker] received ${sig} — shutting down cleanly\n`)
3118
- try { await stopVoiceWhisperServer() } catch (e) {
3119
- process.stderr.write(`[channels-worker] stopVoiceWhisperServer() error on ${sig}: ${e && (e.message || e)}\n`)
3120
- }
3121
- try { await stop() } catch (e) {
3122
- process.stderr.write(`[channels-worker] stop() error on ${sig}: ${e && (e.message || e)}\n`)
3123
- }
3124
- try { cleanupInstanceRuntimeFiles(INSTANCE_ID) } catch {}
3125
- try { clearServerPid() } catch {}
3126
- process.exit(0)
3127
- }
3128
- process.on('SIGTERM', () => _channelsShutdownHandler('SIGTERM'))
3129
- process.on('SIGINT', () => _channelsShutdownHandler('SIGINT'))
3130
-
3131
- // Map of callId → AbortController for in-flight IPC calls.
3132
- const _inFlightChannelCalls = new Map()
3133
-
3134
- process.on('message', async (msg) => {
3135
- // Parent-initiated graceful shutdown — mirrors memory worker IPC pattern.
3136
- if (msg && msg.type === 'shutdown') {
3137
- process.stderr.write('[channels-worker] received IPC shutdown — calling stop()\n')
3138
- _channelsShutdownHandler('IPC:shutdown')
3139
- return
3140
- }
3141
- // Silent-to-agent lifecycle forward — parent (server.mjs) asks the
3142
- // channels worker to post status pings to the active bridge Discord
3143
- // channel without the Lead-notify hop. Best-effort: unknown channel or
3144
- // backend failure is swallowed; lifecycle pings are non-critical.
3145
- if (msg && msg.type === 'forward_to_discord') {
3146
- try {
3147
- const target = msg.channelId
3148
- || (statusState?.read?.().channelId)
3149
- || null;
3150
- if (target && backend?.sendMessage && typeof msg.content === 'string' && msg.content) {
3151
- await backend.sendMessage(target, msg.content).catch(() => {});
3152
- }
3153
- } catch { /* best-effort */ }
3154
- return;
3155
- }
3156
- // Claude Code permission request → Discord Allow/Deny prompt.
3157
- // Parent (server.mjs) receives notifications/claude/channel/permission_request
3158
- // from Claude Code and forwards the params here. We post a buttoned message;
3159
- // button clicks are handled in backend.onInteraction and sent back to CC as
3160
- // notifications/claude/channel/permission via sendNotifyToParent.
3161
- if (msg && msg.type === 'permission_request_inbound') {
3162
- try {
3163
- const { request_id, tool_name, description, input_preview } = msg.params || {};
3164
- // tool_input arrives via the passthrough() schema in server.mjs when
3165
- // Claude Code includes it in the permission_request notification.
3166
- // Used to bind the pendingPermRequest to a specific file so two
3167
- // concurrent Edit/Write requests cannot cross-approve via the
3168
- // terminal signal.
3169
- const toolInputParam = (msg.params && (msg.params.tool_input || msg.params.toolInput)) || {};
3170
- const filePathParam = toolInputParam.file_path || '';
3171
- if (!request_id || !tool_name) return;
3172
- if (pendingPermRequests.size > 100) {
3173
- const cutoff = Date.now() - 30 * 60 * 1000;
3174
- for (const [k, v] of pendingPermRequests) {
3175
- if (v.createdAt < cutoff) pendingPermRequests.delete(k);
3176
- }
3177
- refreshToolExecConsumerMarker();
3178
- }
3179
- const mainLabel = config?.mainChannel || 'main';
3180
- const target = (statusState?.read?.().channelId)
3181
- || resolveChannelLabel(config?.channelsConfig, mainLabel)
3182
- || null;
3183
- if (!target || !backend?.sendMessage) {
3184
- process.stderr.write(`mixdog channels: permission_request dropped, no target channel (request_id=${request_id})\n`);
3185
- return;
3186
- }
3187
- const lines = [`🔐 **Permission Request**`, `Tool: \`${tool_name}\``];
3188
- if (description) lines.push(description);
3189
- if (input_preview) lines.push('```\n' + String(input_preview).slice(0, 800) + '\n```');
3190
- const content = lines.join('\n');
3191
- const components = [{
3192
- type: 1,
3193
- components: [
3194
- { type: 2, style: 3, label: 'Allow', custom_id: `perm-ch-${request_id}-allow` },
3195
- { type: 2, style: 1, label: 'Session Allow', custom_id: `perm-ch-${request_id}-session` },
3196
- { type: 2, style: 4, label: 'Deny', custom_id: `perm-ch-${request_id}-deny` },
3197
- ],
3198
- }];
3199
- let sentIds = null;
3200
- try {
3201
- const sendResult = await backend.sendMessage(target, content, { components });
3202
- sentIds = sendResult?.sentIds;
3203
- } catch (err) {
3204
- process.stderr.write(`mixdog channels: permission_request Discord send failed: ${err && err.message || err}\n`);
3205
- return;
3206
- }
3207
- const messageId = Array.isArray(sentIds) && sentIds.length > 0 ? sentIds[0] : null;
3208
- pendingPermRequests.set(request_id, {
3209
- toolName: tool_name,
3210
- filePath: filePathParam,
3211
- createdAt: Date.now(),
3212
- channelId: target,
3213
- messageId,
3214
- });
3215
- refreshToolExecConsumerMarker();
3216
- } catch (err) {
3217
- try { process.stderr.write(`mixdog channels: permission_request handler error: ${err && err.message || err}\n`); } catch {}
3218
- }
3219
- return;
3220
- }
3221
- if (msg && msg.type === 'memory_call_response' && msg.callId) {
3222
- // Response side of the worker → parent → memory bridge. Routed into
3223
- // this existing listener (instead of a second process.on('message'))
3224
- // to keep IPC dispatch in one place.
3225
- const pending = _memoryCallPending.get(msg.callId);
3226
- if (!pending) return;
3227
- _memoryCallPending.delete(msg.callId);
3228
- if (msg.ok) pending.resolve(msg.result);
3229
- else pending.reject(new Error(msg.error || 'memory_call failed'));
3230
- return;
3231
- }
3232
- if (msg.type === 'cancel' && msg.callId) {
3233
- const entry = _inFlightChannelCalls.get(msg.callId)
3234
- if (entry) {
3235
- entry.abort()
3236
- _inFlightChannelCalls.delete(msg.callId)
3237
- }
3238
- process.send({ type: 'result', callId: msg.callId, error: 'cancelled' })
3239
- return
3240
- }
3241
- if (msg.type !== 'call' || !msg.callId) return
3242
- try {
3243
- const ac = new AbortController()
3244
- _inFlightChannelCalls.set(msg.callId, ac)
3245
- let result
3246
- try {
3247
- result = await handleToolCallWithBridgeRetry(msg.name, msg.args || {}, ac.signal)
3248
- } finally {
3249
- _inFlightChannelCalls.delete(msg.callId)
3250
- }
3251
- process.send({ type: 'result', callId: msg.callId, result })
3252
- } catch (e) {
3253
- process.send({ type: 'result', callId: msg.callId, error: e.message })
3254
- }
3255
- })
3256
- process.send({ type: 'ready', channelFlag: _channelFlagDetected })
3257
- }
3258
-
3259
- export {
3260
- TOOL_DEFS,
3261
- handleToolCall,
3262
- init,
3263
- INSTRUCTIONS as instructions,
3264
- isChannelBridgeActive,
3265
- isChannelsDegraded,
3266
- start,
3267
- stop
3268
- };