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,3332 +0,0 @@
1
- #!/usr/bin/env bun
2
- process.removeAllListeners('warning')
3
- process.on('warning', () => {})
4
-
5
- import http from 'node:http'
6
- import crypto from 'node:crypto'
7
- import os from 'node:os'
8
- import fs from 'node:fs'
9
- import path from 'node:path'
10
- import { fileURLToPath, pathToFileURL } from 'node:url'
11
-
12
- const PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
13
-
14
- function readPluginVersion() {
15
- try {
16
- const manifestPath = path.join(PLUGIN_ROOT, '.claude-plugin', 'plugin.json')
17
- return JSON.parse(fs.readFileSync(manifestPath, 'utf8')).version || '0.0.1'
18
- } catch { return '0.0.1' }
19
- }
20
- const PLUGIN_VERSION = readPluginVersion()
21
- const PROMOTION_FINGERPRINT_ROOTS = ['src/memory']
22
- function collectPromotionFingerprintFiles() {
23
- const out = []
24
- const walk = (relDir) => {
25
- let entries = []
26
- try { entries = fs.readdirSync(path.join(PLUGIN_ROOT, relDir), { withFileTypes: true }) }
27
- catch { return }
28
- for (const ent of entries) {
29
- const rel = `${relDir}/${ent.name}`.replace(/\\/g, '/')
30
- if (ent.isDirectory()) {
31
- walk(rel)
32
- } else if (ent.isFile() && rel.endsWith('.mjs')) {
33
- out.push(rel)
34
- }
35
- }
36
- }
37
- for (const root of PROMOTION_FINGERPRINT_ROOTS) walk(root)
38
- return out.sort()
39
- }
40
- function readPromotionCodeFingerprint() {
41
- const hash = crypto.createHash('sha256')
42
- const files = collectPromotionFingerprintFiles()
43
- for (const rel of files) {
44
- hash.update(rel)
45
- hash.update('\0')
46
- try {
47
- hash.update(fs.readFileSync(path.join(PLUGIN_ROOT, rel)))
48
- } catch {
49
- hash.update('missing')
50
- }
51
- hash.update('\0')
52
- }
53
- return `src/memory:${files.length}:${hash.digest('hex').slice(0, 16)}`
54
- }
55
- const BOOT_PROMOTION_CODE_FINGERPRINT = readPromotionCodeFingerprint()
56
- function promotionCodeChangedOnDisk() {
57
- return readPromotionCodeFingerprint() !== BOOT_PROMOTION_CODE_FINGERPRINT
58
- }
59
-
60
- try { os.setPriority(os.constants.priority.PRIORITY_BELOW_NORMAL) } catch {}
61
- try {
62
- const { env } = await import('@huggingface/transformers')
63
- env.backends.onnx.wasm.numThreads = 1
64
- } catch {}
65
-
66
- import { Server } from '@modelcontextprotocol/sdk/server/index.js'
67
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
68
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
69
- import {
70
- ListToolsRequestSchema,
71
- CallToolRequestSchema,
72
- } from '@modelcontextprotocol/sdk/types.js'
73
-
74
- import { TOOL_DEFS } from './tool-defs.mjs'
75
-
76
- import {
77
- openDatabase,
78
- closeDatabase,
79
- isBootstrapComplete,
80
- getMetaValue,
81
- setMetaValue,
82
- cleanMemoryText,
83
- } from './lib/memory.mjs'
84
- import { configureEmbedding, embedText, embedTexts, getEmbeddingDims, getEmbeddingModelId, getKnownDimsForCurrentModel, primeEmbeddingDims, warmupEmbeddingProvider } from './lib/embedding-provider.mjs'
85
- import { startLlmWorker, stopLlmWorker } from './lib/llm-worker-host.mjs'
86
- import { runCycle1, runCycle2, runCycle3, runUnifiedGate, parseInterval, syncRootEmbedding, applySimpleStatus, applyUpdate, applyMerge, CYCLE2_ACTIVE_TARGET_CAP } from './lib/memory-cycle.mjs'
87
- import { getInFlightCycle1 } from './lib/memory-cycle1.mjs'
88
- import { searchRelevantHybrid } from './lib/memory-recall-store.mjs'
89
- import { fetchEntriesByIdsScoped } from './lib/memory-recall-id-patch.mjs'
90
- import { retrieveEntries } from './lib/memory-retrievers.mjs'
91
- import { pruneOldEntries } from './lib/memory-maintenance-store.mjs'
92
- import { computeEntryScore } from './lib/memory-score.mjs'
93
- import { runFullBackfill } from './lib/memory-ops-policy.mjs'
94
- import { listCore, addCore, editCore, deleteCore, compactCoreIds, CORE_SUMMARY_MAX } from './lib/core-memory-store.mjs'
95
- import { resolveProjectId, resolveProjectScope } from './lib/project-id-resolver.mjs'
96
- import { openTraceDatabase, insertTraceEvents, enqueueTraceEvents, insertBridgeCalls, registerTraceExitDrain } from './lib/trace-store.mjs'
97
- import { updateJsonAtomicSync, writeJsonAtomicSync } from '../shared/atomic-file.mjs'
98
- const DATA_DIR = process.env.CLAUDE_PLUGIN_DATA || process.argv[2] || null
99
- if (!DATA_DIR) {
100
- process.stderr.write('[memory-service] CLAUDE_PLUGIN_DATA not set and no explicit data dir provided\n')
101
- process.exit(1)
102
- }
103
- process.stderr.write(`[memory-service] DATA_DIR=${DATA_DIR}\n`)
104
-
105
- import { execFileSync } from 'child_process'
106
-
107
- const RUNTIME_ROOT = process.env.MIXDOG_RUNTIME_ROOT
108
- ? path.resolve(process.env.MIXDOG_RUNTIME_ROOT)
109
- : path.join(os.tmpdir(), 'mixdog')
110
-
111
- let _periodicAdvertiseInstalled = false
112
- // Track the most recently advertised port so the periodic tick re-reads it
113
- // every interval. Without this the setInterval closure binds the FIRST port
114
- // (the upstream we proxied to) and keeps re-advertising the dead upstream
115
- // port after fork-proxy promotion swaps in our own locally-bound port.
116
- let _currentAdvertisedPort = null
117
-
118
- function parsePositivePid(value) {
119
- const pid = Number(value)
120
- return Number.isFinite(pid) && pid > 0 ? pid : null
121
- }
122
-
123
- const MEMORY_SERVER_PID = parsePositivePid(process.env.MIXDOG_SERVER_PID)
124
-
125
- function advertiseMemoryPort(boundPort, attempt = 0) {
126
- if (!Number.isFinite(boundPort) || boundPort <= 0) return
127
- _currentAdvertisedPort = boundPort
128
- const dir = RUNTIME_ROOT
129
- const file = path.join(dir, 'active-instance.json')
130
- try {
131
- fs.mkdirSync(dir, { recursive: true })
132
- updateJsonAtomicSync(file, (curRaw) => {
133
- const cur = curRaw ?? {}
134
- const curMemPort = Number(cur?.memory_port)
135
- const curMemPid = parsePositivePid(cur?.memory_server_pid)
136
- const portConflict = Number.isFinite(curMemPort) && curMemPort > 0 && curMemPort !== boundPort
137
- const otherOwnerAlive =
138
- curMemPid != null &&
139
- curMemPid !== MEMORY_SERVER_PID &&
140
- _isPidAliveLocal(curMemPid)
141
- if (portConflict && otherOwnerAlive) {
142
- process.stderr.write(`[memory-service] skip memory_port advertise port=${boundPort} curMemPort=${curMemPort} curMemPid=${curMemPid} memoryServerPid=${MEMORY_SERVER_PID}\n`)
143
- return undefined
144
- }
145
- const next = {
146
- ...cur,
147
- memory_port: boundPort,
148
- ...(MEMORY_SERVER_PID ? { memory_server_pid: MEMORY_SERVER_PID } : {}),
149
- updatedAt: Date.now(),
150
- }
151
- return next
152
- }, { compact: true, fsyncDir: true })
153
- if (!_periodicAdvertiseInstalled) {
154
- _periodicAdvertiseInstalled = true
155
- setInterval(() => {
156
- try {
157
- if (_currentAdvertisedPort != null) {
158
- advertiseMemoryPort(_currentAdvertisedPort)
159
- }
160
- } catch {}
161
- }, 30_000).unref()
162
- }
163
- } catch (e) {
164
- const transient = e?.code === 'EPERM' || e?.code === 'EBUSY' || e?.code === 'EACCES'
165
- if (transient && attempt < 3) {
166
- setTimeout(() => advertiseMemoryPort(boundPort, attempt + 1), 50 * (attempt + 1))
167
- return
168
- }
169
- process.stderr.write(`[memory-service] active-instance memory_port advertise failed: ${e?.message || e}\n`)
170
- }
171
- }
172
-
173
- const LOCK_FILE = path.join(DATA_DIR, '.memory-service.lock')
174
- // Owner-election lock. Separate from LOCK_FILE so single-instance mode keeps
175
- // its kill-the-previous protocol while multi-instance fork-proxy workers use
176
- // atomic CAS for takeover. Created via fs.openSync(path,'wx') — node guarantees
177
- // EEXIST when another process won the race.
178
- const OWNER_LOCK_FILE = path.join(DATA_DIR, '.memory-owner.lock')
179
-
180
- function _isPidAliveLocal(pid) {
181
- if (!Number.isFinite(pid) || pid <= 0) return false
182
- try { process.kill(pid, 0); return true }
183
- catch (e) { return e.code !== 'ESRCH' }
184
- }
185
-
186
- function tryAcquireMemoryOwnerLock() {
187
- // Returns true on success (this process now owns memory worker for the data
188
- // dir), false when a live peer holds the lock. Stale locks (dead PID) are
189
- // unlinked and retried atomically. Throws on unexpected fs errors so callers
190
- // surface lock-system corruption rather than silently downgrading.
191
- //
192
- // EPERM/EBUSY/EACCES at openSync are transient — AV scanners (SignKorea /
193
- // SKCert / ezPDFWS etc) briefly lock newly-created files during inspection.
194
- // The 0.1.x baseline threw immediately and the worker promoted to
195
- // permanentlyDegraded, killing memory tools for the rest of the session.
196
- // Treat the AV error codes as retryable with bounded backoff (~750ms total)
197
- // before giving up and rethrowing.
198
- for (let attempt = 0; attempt < 5; attempt++) {
199
- try {
200
- const fd = fs.openSync(OWNER_LOCK_FILE, 'wx')
201
- fs.writeSync(fd, String(process.pid))
202
- fs.closeSync(fd)
203
- return true
204
- } catch (e) {
205
- if (e.code === 'EEXIST') {
206
- let ownerPid = NaN
207
- try { ownerPid = Number(fs.readFileSync(OWNER_LOCK_FILE, 'utf8').trim()) } catch {}
208
- if (_isPidAliveLocal(ownerPid)) return false
209
- // Stale lock: dead owner — unlink and retry exclusive create.
210
- try { fs.unlinkSync(OWNER_LOCK_FILE) } catch {}
211
- continue
212
- }
213
- const transient = e.code === 'EPERM' || e.code === 'EBUSY' || e.code === 'EACCES'
214
- if (transient && attempt < 4) {
215
- // Sync busy-wait acceptable here: this runs on memory worker boot
216
- // path, once per process; the parent handler is not blocked.
217
- const end = Date.now() + 50 * (attempt + 1)
218
- while (Date.now() < end) {}
219
- continue
220
- }
221
- throw e
222
- }
223
- }
224
- return false
225
- }
226
-
227
- function releaseMemoryOwnerLock() {
228
- try {
229
- const ownerPid = Number(fs.readFileSync(OWNER_LOCK_FILE, 'utf8').trim())
230
- if (ownerPid === process.pid) fs.unlinkSync(OWNER_LOCK_FILE)
231
- } catch {}
232
- }
233
-
234
- const ACTIVE_INSTANCE_FILE = path.join(RUNTIME_ROOT, 'active-instance.json')
235
- const BASE_PORT = 3350
236
- const MAX_PORT = 3357
237
-
238
- let _traceDb = null
239
-
240
- const MEMORY_INSTRUCTIONS_TEXT = ''
241
-
242
- function killPreviousServer(pid) {
243
- if (pid <= 0 || pid === process.pid) return false
244
- if (process.platform === 'win32') {
245
- try {
246
- execFileSync('taskkill', ['/F', '/T', '/PID', String(pid)], { encoding: 'utf8', timeout: 5000, windowsHide: true })
247
- process.stderr.write(`[memory-service] Killed previous server PID ${pid}\n`)
248
- return true
249
- } catch (e) {
250
- // Exit code 128 = process not found; treat stale lock as already-dead = success.
251
- // Status 128 reliably means "process not found" regardless of locale; no text match needed.
252
- // Status 1 with English text match handles edge cases on some Windows versions.
253
- const notFoundText = /not found|no running instance/i.test(e.stdout || '')
254
- || /not found|no running instance/i.test(e.stderr || '')
255
- || /not found|no running instance/i.test(e.message || '')
256
- const alreadyDead = e.status === 128 || (e.status === 1 && notFoundText)
257
- if (alreadyDead) {
258
- process.stderr.write(`[memory-service] PID ${pid} already dead (stale lock), proceeding\n`)
259
- return true
260
- }
261
- process.stderr.write(`[memory-service] taskkill failed for PID ${pid}: ${e.message}\n`)
262
- return false
263
- }
264
- } else {
265
- // Pre-flight: if the process is already gone, treat stale lock as success.
266
- try {
267
- process.kill(pid, 0)
268
- } catch (e) {
269
- if (e.code === 'ESRCH') {
270
- process.stderr.write(`[memory-service] PID ${pid} already dead (stale lock), proceeding\n`)
271
- return true
272
- }
273
- }
274
- try { process.kill(pid, 'SIGTERM') } catch {}
275
- try { process.kill(pid, 'SIGKILL') } catch {}
276
- // Poll for death up to 2s
277
- const deadline = Date.now() + 2000
278
- while (Date.now() < deadline) {
279
- try {
280
- process.kill(pid, 0)
281
- } catch (e) {
282
- if (e.code === 'ESRCH') {
283
- process.stderr.write(`[memory-service] Killed previous server PID ${pid}\n`)
284
- return true
285
- }
286
- }
287
- // Synchronous 50ms sleep via shared buffer spin
288
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50)
289
- }
290
- process.stderr.write(`[memory-service] PID ${pid} still alive after SIGKILL\n`)
291
- return false
292
- }
293
- }
294
-
295
- function acquireLock() {
296
- // Multi-instance guard. In multi-terminal mode the lock owner is a *peer*
297
- // memory worker serving recall for another CC session. killPreviousServer
298
- // would taskkill /F that healthy peer mid-flight, then this fork-proxy
299
- // mode wouldn't even need a lock anyway. Skip the entire kill-the-previous
300
- // protocol; fork-proxy detection in init() takes priority. If neither
301
- // proxy nor lock-owner path applies (race window during simultaneous
302
- // boot), the worker simply continues without the lock — server-main /
303
- // PG / port-listen handle the actual conflict cases.
304
- if (process.env.MIXDOG_MULTI_INSTANCE === '1') return
305
- try {
306
- if (fs.existsSync(LOCK_FILE)) {
307
- const lockedPid = Number(fs.readFileSync(LOCK_FILE, 'utf8').trim())
308
- if (lockedPid > 0 && lockedPid !== process.pid) {
309
- const killed = killPreviousServer(lockedPid)
310
- if (!killed) {
311
- process.stderr.write(`[memory-service] Could not kill previous server PID ${lockedPid}, aborting\n`)
312
- process.exit(1)
313
- }
314
- try { fs.unlinkSync(LOCK_FILE) } catch {}
315
- }
316
- }
317
- const fd = fs.openSync(LOCK_FILE, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600)
318
- try {
319
- fs.writeSync(fd, String(process.pid))
320
- } finally {
321
- fs.closeSync(fd)
322
- }
323
- } catch (e) {
324
- if (e.code === 'EEXIST') {
325
- process.stderr.write(`[memory-service] Lock file exists (EEXIST) — another instance is already running, exiting\n`)
326
- process.exit(0)
327
- }
328
- process.stderr.write(`[memory-service] Lock acquisition failed: ${e.message}\n`)
329
- process.exit(1)
330
- }
331
- }
332
-
333
- function releaseLock() {
334
- try {
335
- const content = fs.readFileSync(LOCK_FILE, 'utf8').trim()
336
- if (Number(content) === process.pid) fs.unlinkSync(LOCK_FILE)
337
- } catch {}
338
- }
339
-
340
- import { readSection } from '../shared/config.mjs'
341
-
342
- function readMainConfig() {
343
- return readSection('memory')
344
- }
345
-
346
- let db = null
347
- let mainConfig = null
348
- let _cycleInterval = null
349
- let _startupTimeout = null
350
- // Outer-layer cycle1 in-flight tracker (MCP-server scope).
351
- //
352
- // The AUTHORITATIVE guard lives in memory-cycle.mjs:runCycle1 itself — that
353
- // one catches every caller, including direct imports (setup-server backfill,
354
- // policy-layer backfill). This outer tracker is kept as a defense-in-depth
355
- // layer local to the MCP server process: it coalesces simultaneous
356
- // _awaitCycle1Run callers (MCP action, scheduler, flush) onto a shared
357
- // promise so they all observe the SAME result object rather than some
358
- // getting the real stats and others getting `skippedInFlight: true` from
359
- // the inner guard.
360
- let _cycle1InFlight = null // shared cycle1 promise (outer coalesce layer)
361
- let _initialized = false
362
- let _initPromise = null
363
- let _bootTimestamp = null
364
- let _transcriptOffsets = new Map()
365
- // Boot-edge background warmup. ONNX session creation on the embedding worker
366
- // thread is CPU-heavy, so it must not overlap the worker's own init (DB open,
367
- // schema, cycle wiring). Previously this was gated behind a fixed setTimeout —
368
- // a wall-clock guess at "boot settled". Now the warmup is queued during
369
- // _initStore and fired at the _initRuntime completion edge (see _initRuntime),
370
- // so it starts the instant boot's CPU-heavy work is done — no magic-number
371
- // delay. MIXDOG_EMBED_WARMUP=0 disables it (model loads lazily on first use).
372
- let _pendingEmbeddingWarmup = null
373
-
374
- const TRANSCRIPT_OFFSETS_KEY = 'state.transcript_offsets'
375
- const CYCLE_LAST_RUN_KEY = 'state.cycle_last_run'
376
-
377
- function embeddingWarmupEnabled() {
378
- const raw = String(process.env.MIXDOG_EMBED_WARMUP ?? '1').trim().toLowerCase()
379
- return !(raw === '0' || raw === 'false' || raw === 'off' || raw === 'no')
380
- }
381
-
382
- function scheduleBackgroundEmbeddingWarmup(metaPath, metaKey) {
383
- if (!embeddingWarmupEnabled()) return
384
- // Queue the warmup; _initRuntime fires it once boot completes.
385
- _pendingEmbeddingWarmup = () => {
386
- warmupEmbeddingProvider()
387
- .then(() => {
388
- const measured = Number(getEmbeddingDims())
389
- try {
390
- writeJsonAtomicSync(metaPath, { ...metaKey, dims: measured }, { lock: true })
391
- } catch (e) {
392
- process.stderr.write(`[memory-service] could not persist embedding-meta: ${e?.message || e}\n`)
393
- }
394
- })
395
- .catch(err => {
396
- process.stderr.write(`[memory-service] background warmup failed: ${err?.message || err}\n`)
397
- process.exit(1)
398
- })
399
- }
400
- }
401
-
402
- function fireDeferredEmbeddingWarmup() {
403
- const fire = _pendingEmbeddingWarmup
404
- if (!fire) return
405
- _pendingEmbeddingWarmup = null
406
- fire()
407
- }
408
-
409
- async function _initStore() {
410
- mainConfig = readMainConfig()
411
- const embeddingConfig = mainConfig?.embedding
412
- if (embeddingConfig?.provider || embeddingConfig?.ollamaModel || embeddingConfig?.dtype) {
413
- configureEmbedding({
414
- provider: embeddingConfig.provider,
415
- ollamaModel: embeddingConfig.ollamaModel,
416
- dtype: embeddingConfig.dtype,
417
- })
418
- }
419
-
420
- // Persist embedding dims so warmup is off the boot critical path.
421
- // On a cache hit (provider+model+dtype match) open the DB immediately,
422
- // prime the known dimensions, then run the model warmup later in the
423
- // background. If cycle1/recall needs embeddings first, that on-demand
424
- // call owns the same worker queue and the delayed warmup becomes a no-op.
425
- const EMBEDDING_META_PATH = path.join(DATA_DIR, 'embedding-meta.json')
426
- const metaKey = {
427
- provider: embeddingConfig?.provider ?? null,
428
- model: getEmbeddingModelId(),
429
- dtype: embeddingConfig?.dtype ?? null,
430
- }
431
- let dimsResolved = null
432
- try {
433
- const saved = JSON.parse(fs.readFileSync(EMBEDDING_META_PATH, 'utf8'))
434
- if (saved.provider === metaKey.provider && saved.model === metaKey.model && saved.dtype === metaKey.dtype) {
435
- dimsResolved = Number(saved.dims)
436
- }
437
- } catch { /* miss or missing — fall through */ }
438
-
439
- // Registry fallback: model with statically known dims bypasses measurement.
440
- // Delayed background warmup invariant-checks measured vs registry value;
441
- // mismatch throws and crashes the worker for fail-fast parity with the cold
442
- // path's boot-time degraded signal.
443
- if (dimsResolved == null) {
444
- const known = getKnownDimsForCurrentModel()
445
- if (known != null) dimsResolved = known
446
- }
447
-
448
- if (dimsResolved) {
449
- primeEmbeddingDims(dimsResolved)
450
- db = await openDatabase(DATA_DIR, dimsResolved)
451
- scheduleBackgroundEmbeddingWarmup(EMBEDDING_META_PATH, metaKey)
452
- } else {
453
- // Cold path: meta missed AND model not registered. Sequential.
454
- await warmupEmbeddingProvider()
455
- dimsResolved = Number(getEmbeddingDims())
456
- db = await openDatabase(DATA_DIR, dimsResolved)
457
- try {
458
- writeJsonAtomicSync(EMBEDDING_META_PATH, { ...metaKey, dims: dimsResolved }, { lock: true })
459
- } catch (e) {
460
- process.stderr.write(`[memory-service] could not persist embedding-meta: ${e?.message || e}\n`)
461
- }
462
- }
463
-
464
- if (!await isBootstrapComplete(db)) {
465
- throw new Error('memory-service: bootstrap not complete after openDatabase')
466
- }
467
- startLlmWorker()
468
- _bootTimestamp = Date.now()
469
- await loadTranscriptOffsets()
470
- }
471
-
472
- async function loadTranscriptOffsets() {
473
- try {
474
- const raw = await getMetaValue(db, TRANSCRIPT_OFFSETS_KEY, '{}')
475
- const obj = JSON.parse(raw)
476
- _transcriptOffsets = new Map(Object.entries(obj))
477
- } catch {
478
- _transcriptOffsets = new Map()
479
- }
480
- }
481
-
482
- async function persistTranscriptOffsets() {
483
- try {
484
- const obj = Object.fromEntries(_transcriptOffsets)
485
- await setMetaValue(db, TRANSCRIPT_OFFSETS_KEY, JSON.stringify(obj))
486
- } catch (e) {
487
- process.stderr.write(`[memory] persist transcript offsets failed: ${e.message}\n`)
488
- }
489
- }
490
-
491
- async function getCycleLastRun() {
492
- try {
493
- const raw = await getMetaValue(db, CYCLE_LAST_RUN_KEY, '{}')
494
- const obj = JSON.parse(raw)
495
- return {
496
- cycle1: Number(obj.cycle1) || 0,
497
- cycle2: Number(obj.cycle2) || 0,
498
- cycle3: Number(obj.cycle3) || 0,
499
- // Phase B §2.4 auto-restart book-keeping — last time an overdue cycle1
500
- // triggered an unscheduled run, rate-limited separately from the
501
- // normal cycle timestamp so a long chain of failures cannot tight-loop.
502
- cycle1_autoRestart: Number(obj.cycle1_autoRestart) || 0,
503
- // #13/#14: heartbeat (every attempt, success or skip) and the auto-
504
- // restart attempt timestamp (committed BEFORE the call) are tracked
505
- // separately from the success timestamps above so a long string of
506
- // failed/skipped runs cannot disguise itself as a healthy keeper.
507
- cycle1_heartbeat: Number(obj.cycle1_heartbeat) || 0,
508
- cycle1_autoRestart_attempt: Number(obj.cycle1_autoRestart_attempt) || 0,
509
- // Last cycle2 failure message; cleared to '' on success.
510
- cycle2_last_error: typeof obj.cycle2_last_error === 'string' ? obj.cycle2_last_error : '',
511
- }
512
- } catch {
513
- return {
514
- cycle1: 0, cycle2: 0, cycle3: 0, cycle1_autoRestart: 0,
515
- cycle1_heartbeat: 0, cycle1_autoRestart_attempt: 0,
516
- cycle2_last_error: '',
517
- }
518
- }
519
- }
520
-
521
- async function setCycleLastRun(kind, ts) {
522
- const cur = await getCycleLastRun()
523
- cur[kind] = ts
524
- await setMetaValue(db, CYCLE_LAST_RUN_KEY, JSON.stringify(cur))
525
- }
526
-
527
- // Raw-row priority lookup for narrow-window queries. Raw rows (is_root=0,
528
- // chunk_root IS NULL) are inserted immediately by ingestTranscriptFile before
529
- // cycle1 runs, so they always carry the freshest turns in the DB.
530
- async function readRawRowsInWindow(db, tsFromMs, tsToMs, hardLimit = 10, { projectScope } = {}) {
531
- try {
532
- let sql, params
533
- if (projectScope === 'common') {
534
- sql = `SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
535
- element, category, summary, status, score, last_seen_at, project_id
536
- FROM entries
537
- WHERE chunk_root IS NULL AND is_root = 0
538
- AND ts >= $1 AND ts <= $2
539
- AND project_id IS NULL
540
- ORDER BY ts DESC
541
- LIMIT $3`
542
- params = [tsFromMs ?? 0, tsToMs ?? Date.now(), hardLimit]
543
- } else if (projectScope && projectScope !== 'all') {
544
- sql = `SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
545
- element, category, summary, status, score, last_seen_at, project_id
546
- FROM entries
547
- WHERE chunk_root IS NULL AND is_root = 0
548
- AND ts >= $1 AND ts <= $2
549
- AND (project_id IS NULL OR project_id = $3)
550
- ORDER BY ts DESC
551
- LIMIT $4`
552
- params = [tsFromMs ?? 0, tsToMs ?? Date.now(), projectScope, hardLimit]
553
- } else {
554
- sql = `SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
555
- element, category, summary, status, score, last_seen_at, project_id
556
- FROM entries
557
- WHERE chunk_root IS NULL AND is_root = 0
558
- AND ts >= $1 AND ts <= $2
559
- ORDER BY ts DESC
560
- LIMIT $3`
561
- params = [tsFromMs ?? 0, tsToMs ?? Date.now(), hardLimit]
562
- }
563
- const rows = (await db.query(sql, params)).rows
564
- return rows.map(r => ({ ...r, retrievalScore: 0, rrf: 0 }))
565
- } catch { return [] }
566
- }
567
-
568
- async function ingestTranscriptFile(transcriptPath, { cwd } = {}) {
569
- let stat
570
- try { stat = await fs.promises.stat(transcriptPath) } catch { return 0 }
571
- const sessionUuid = path.basename(transcriptPath, '.jsonl')
572
- const prev = _transcriptOffsets.get(transcriptPath) ?? { bytes: 0, lineIndex: 0 }
573
- if (stat.size < prev.bytes) {
574
- prev.bytes = 0
575
- prev.lineIndex = 0
576
- }
577
- if (stat.size <= prev.bytes) return 0
578
-
579
- const fh = await fs.promises.open(transcriptPath, 'r')
580
- const buf = Buffer.alloc(stat.size - prev.bytes)
581
- try {
582
- await fh.read(buf, 0, buf.length, prev.bytes)
583
- } finally {
584
- await fh.close()
585
- }
586
- const text = buf.toString('utf8')
587
-
588
- const resolvedCwd = typeof cwd === 'string' && cwd ? cwd : cwdFromTranscriptPath(transcriptPath)
589
- // No cwd resolved -> classify as COMMON (project_id NULL). Falling back to
590
- // process.cwd() would misclassify rows under the service/plugin cwd.
591
- const projectId = resolvedCwd ? resolveProjectId(resolvedCwd) : null
592
-
593
- let count = 0
594
- let index = prev.lineIndex
595
- // Track the byte boundary of the LAST line we fully consumed (parsed +
596
- // either inserted or intentionally skipped). On parse failure or
597
- // transient insert error we stop and leave the boundary untouched so the
598
- // next sweep retries from the same position. This prevents malformed
599
- // trailing JSONL (mid-write partial lines) and DB hiccups from being
600
- // silently consumed forever.
601
- let lastGoodBytes = prev.bytes
602
- let lastGoodLineIndex = prev.lineIndex
603
- let cursor = 0
604
- while (cursor < text.length) {
605
- const nl = text.indexOf('\n', cursor)
606
- // No trailing newline -> partial line still being written; stop here
607
- // without advancing so the rest is re-read once the writer flushes.
608
- if (nl === -1) break
609
- const rawLine = text.slice(cursor, nl)
610
- const consumedBytes = Buffer.byteLength(rawLine, 'utf8') + 1
611
- cursor = nl + 1
612
- const line = rawLine.replace(/\r$/, '')
613
- if (!line) {
614
- lastGoodBytes += consumedBytes
615
- continue
616
- }
617
- index += 1
618
- let parsed
619
- try { parsed = JSON.parse(line) } catch {
620
- // Malformed line: do not advance past it; retry on next sweep.
621
- index -= 1
622
- break
623
- }
624
- const role = parsed.message?.role
625
- if (role !== 'user' && role !== 'assistant') {
626
- lastGoodBytes += consumedBytes
627
- lastGoodLineIndex = index
628
- continue
629
- }
630
- const content = firstTextContent(parsed.message?.content)
631
- if (!content || !content.trim()) {
632
- lastGoodBytes += consumedBytes
633
- lastGoodLineIndex = index
634
- continue
635
- }
636
- const cleaned = cleanMemoryText(content)
637
- if (!cleaned) {
638
- lastGoodBytes += consumedBytes
639
- lastGoodLineIndex = index
640
- continue
641
- }
642
- const tsMs = parseTsToMs(parsed.timestamp ?? parsed.ts ?? Date.now())
643
- const sourceRef = `transcript:${sessionUuid}#${index}`
644
- try {
645
- const result = await db.query(
646
- `INSERT INTO entries(ts, role, content, source_ref, session_id, source_turn, project_id)
647
- VALUES ($1, $2, $3, $4, $5, $6, $7)
648
- ON CONFLICT DO NOTHING`,
649
- [tsMs, role, cleaned, sourceRef, sessionUuid, index, projectId]
650
- )
651
- if (Number(result.rowCount ?? result.affectedRows ?? 0) > 0) count += 1
652
- lastGoodBytes += consumedBytes
653
- lastGoodLineIndex = index
654
- } catch (e) {
655
- process.stderr.write(`[transcript-watch] insert error (${sourceRef}): ${e.message}\n`)
656
- // Transient insert failure: leave the boundary before this line so
657
- // the next sweep retries it. Roll back the line counter too.
658
- index -= 1
659
- break
660
- }
661
- }
662
- prev.bytes = lastGoodBytes
663
- prev.lineIndex = lastGoodLineIndex
664
- _transcriptOffsets.set(transcriptPath, prev)
665
- await persistTranscriptOffsets()
666
- return count
667
- }
668
-
669
- function firstTextContent(content) {
670
- if (typeof content === 'string') return content
671
- if (!Array.isArray(content)) return ''
672
- for (const item of content) {
673
- if (typeof item === 'string') return item
674
- if (item?.type === 'text' && typeof item.text === 'string') return item.text
675
- }
676
- return ''
677
- }
678
-
679
- function parseTsToMs(value) {
680
- if (typeof value === 'number' && Number.isFinite(value)) return value < 1e12 ? value * 1000 : value
681
- const parsed = Date.parse(String(value))
682
- return Number.isFinite(parsed) ? parsed : Date.now()
683
- }
684
-
685
- // Extract cwd from the transcript file's JSONL rows. Claude Code embeds
686
- // the session cwd as a top-level `cwd` field on every message row, so
687
- // scanning the first few lines is reliable on all platforms (Windows/Linux)
688
- // without slug-decoding ambiguity. Returns undefined when no cwd is found
689
- // or the extracted path does not exist on disk (falls back to COMMON).
690
- function cwdFromTranscriptPath(fp) {
691
- let fd
692
- try {
693
- fd = fs.openSync(fp, 'r')
694
- const buf = Buffer.alloc(Math.min(fs.fstatSync(fd).size, 100 * 1024))
695
- fs.readSync(fd, buf, 0, buf.length, 0)
696
- fs.closeSync(fd)
697
- fd = undefined
698
- const lines = buf.toString('utf8').split('\n')
699
- for (let i = 0; i < Math.min(lines.length, 5); i++) {
700
- const line = lines[i].trim()
701
- if (!line) continue
702
- try {
703
- const obj = JSON.parse(line)
704
- if (typeof obj.cwd === 'string' && obj.cwd) {
705
- const candidate = obj.cwd
706
- try { if (fs.statSync(candidate).isDirectory()) return candidate } catch {}
707
- }
708
- } catch {}
709
- }
710
- } catch {} finally {
711
- if (fd != null) { try { fs.closeSync(fd) } catch {} }
712
- }
713
- return undefined
714
- }
715
-
716
- function _initTranscriptWatcher() {
717
- const projectsRoot = path.join(os.homedir(), '.claude', 'projects')
718
- const SAFETY_POLL_MS = 5 * 60_000
719
- const DEBOUNCE_MS = 500
720
- const watchedFiles = new Map()
721
- const pendingByFile = new Map()
722
- const watchers = []
723
- const intervals = []
724
- const polledFiles = new Set()
725
- let safetySweepTimeout = null
726
-
727
- function isWatchable(relOrBase) {
728
- const base = path.basename(relOrBase)
729
- if (!base.endsWith('.jsonl') || base.startsWith('agent-')) return false
730
- if (relOrBase.includes('tmp') || relOrBase.includes('cache') || relOrBase.includes('plugins')) return false
731
- return true
732
- }
733
-
734
- async function ingestOne(fp) {
735
- try {
736
- if (!fs.existsSync(fp)) return
737
- const stat = fs.statSync(fp)
738
- const mtime = stat.mtimeMs
739
- const prev = watchedFiles.get(fp)
740
- if (prev && prev >= mtime) return
741
- const n = await ingestTranscriptFile(fp, { cwd: cwdFromTranscriptPath(fp) })
742
- // Only mark this mtime as 'consumed' once the persisted offset has
743
- // fully advanced past the observed file size. On a transient insert
744
- // error (or a malformed trailing line) ingestTranscriptFile leaves
745
- // the persisted offset before the failed line for retry; caching
746
- // the new mtime unconditionally would suppress the next sweep until
747
- // the file mutated again, losing the retry. Leave the cache
748
- // untouched on partial advance so the next sweep re-ingests.
749
- const off = _transcriptOffsets.get(fp)
750
- if (off && off.bytes >= stat.size) {
751
- watchedFiles.set(fp, mtime)
752
- }
753
- if (n > 0) {
754
- process.stderr.write(`[transcript-watch] ingested ${n} entries from ${path.basename(fp)}\n`)
755
- }
756
- } catch (e) {
757
- process.stderr.write(`[transcript-watch] ingest error: ${e.message}\n`)
758
- }
759
- }
760
-
761
- function scheduleIngest(fp) {
762
- const existing = pendingByFile.get(fp)
763
- if (existing) clearTimeout(existing)
764
- const timer = setTimeout(() => {
765
- pendingByFile.delete(fp)
766
- ingestOne(fp)
767
- }, DEBOUNCE_MS)
768
- pendingByFile.set(fp, timer)
769
- }
770
-
771
- async function discoverActiveTranscripts() {
772
- let topLevel
773
- try { topLevel = await fs.promises.readdir(projectsRoot) }
774
- catch { return [] }
775
- const files = []
776
- for (const d of topLevel) {
777
- if (d.includes('tmp') || d.includes('cache') || d.includes('plugins')) continue
778
- const full = path.join(projectsRoot, d)
779
- let inner
780
- try { inner = await fs.promises.readdir(full) } catch { continue }
781
- for (const f of inner) {
782
- if (!f.endsWith('.jsonl') || f.startsWith('agent-')) continue
783
- const fp = path.join(full, f)
784
- try {
785
- const stat = await fs.promises.stat(fp)
786
- files.push({ path: fp, mtime: stat.mtimeMs })
787
- } catch {}
788
- }
789
- }
790
- const cutoff = Date.now() - 30 * 60_000
791
- return files.filter(f => f.mtime > cutoff)
792
- }
793
-
794
- async function safetySweep() {
795
- try {
796
- const active = await discoverActiveTranscripts()
797
- for (const { path: fp } of active) ingestOne(fp)
798
- } catch (e) {
799
- process.stderr.write(`[transcript-watch] safety sweep error: ${e.message}\n`)
800
- }
801
- }
802
-
803
- safetySweepTimeout = setTimeout(safetySweep, 3_000)
804
-
805
- // fs.watch({recursive}) is only reliable on win32.
806
- // darwin: recursive option unreliable — use flat watch per-entry (glob dirs at start).
807
- // linux/WSL: recursive not supported — use fs.watchFile polling per file found via
808
- // the safety sweep, or fall back entirely to safety sweep.
809
- if (process.platform === 'win32') {
810
- try {
811
- const watcher = fs.watch(projectsRoot, { recursive: true, persistent: true }, (_event, filename) => {
812
- if (!filename) return
813
- if (!isWatchable(filename)) return
814
- const fp = path.join(projectsRoot, filename)
815
- scheduleIngest(fp)
816
- })
817
- watcher.on('error', (err) => {
818
- process.stderr.write(`[transcript-watch] fs.watch error: ${err.message}\n`)
819
- })
820
- watchers.push(watcher)
821
- process.stderr.write(`[transcript-watch] fs.watch(recursive) active on ${projectsRoot}\n`)
822
- } catch (e) {
823
- process.stderr.write(`[transcript-watch] fs.watch setup failed: ${e.message} — relying on safety sweep only\n`)
824
- }
825
- intervals.push(setInterval(safetySweep, SAFETY_POLL_MS))
826
- } else if (process.platform === 'darwin') {
827
- // Flat watch: register a non-recursive watcher on each immediate subdirectory.
828
- // New subdirs are picked up on the next safety sweep cycle.
829
- try {
830
- const registerFlat = (dir) => {
831
- try {
832
- const w = fs.watch(dir, { persistent: true }, (_event, filename) => {
833
- if (!filename) return
834
- const fp = path.join(dir, filename)
835
- if (!isWatchable(fp)) return
836
- scheduleIngest(fp)
837
- })
838
- w.on('error', () => { /* ignore individual dir errors */ })
839
- watchers.push(w)
840
- } catch { /* dir may not exist yet */ }
841
- }
842
- registerFlat(projectsRoot)
843
- try {
844
- for (const entry of fs.readdirSync(projectsRoot, { withFileTypes: true })) {
845
- if (entry.isDirectory()) registerFlat(path.join(projectsRoot, entry.name))
846
- }
847
- } catch { /* best effort */ }
848
- process.stderr.write(`[transcript-watch] flat fs.watch active on ${projectsRoot} (darwin)\n`)
849
- } catch (e) {
850
- process.stderr.write(`[transcript-watch] flat watch setup failed: ${e.message} — relying on safety sweep only\n`)
851
- }
852
- intervals.push(setInterval(safetySweep, SAFETY_POLL_MS))
853
- } else {
854
- // linux/WSL: fs.watch recursive is unsupported. Use fs.watchFile polling for
855
- // individual files surfaced by the safety sweep, in addition to the sweep itself.
856
- process.stderr.write(`[transcript-watch] linux/WSL — using safety sweep + fs.watchFile polling (no recursive watch)\n`)
857
- // Wrap by reassigning the closure-captured reference is not possible here;
858
- // instead, register watchFile inside the safety sweep callback by intercepting
859
- // active file list after each sweep. The interval already calls safetySweep
860
- // which calls ingestOne; watchFile additions happen as a side-effect of the sweep.
861
- const _patchedSweep = async () => {
862
- try {
863
- const active = await discoverActiveTranscripts()
864
- for (const { path: fp } of active) {
865
- if (!polledFiles.has(fp)) {
866
- polledFiles.add(fp)
867
- fs.watchFile(fp, { persistent: false, interval: 2000 }, () => {
868
- if (isWatchable(fp)) scheduleIngest(fp)
869
- })
870
- }
871
- ingestOne(fp)
872
- }
873
- } catch (e) {
874
- process.stderr.write(`[transcript-watch] linux sweep error: ${e.message}\n`)
875
- }
876
- }
877
- // Replace the safety sweep interval with the patched version.
878
- intervals.push(setInterval(_patchedSweep, SAFETY_POLL_MS))
879
- }
880
-
881
- return {
882
- stop() {
883
- if (safetySweepTimeout) { clearTimeout(safetySweepTimeout); safetySweepTimeout = null }
884
- for (const t of pendingByFile.values()) { try { clearTimeout(t) } catch {} }
885
- pendingByFile.clear()
886
- for (const i of intervals) { try { clearInterval(i) } catch {} }
887
- intervals.length = 0
888
- for (const w of watchers) { try { w.close() } catch {} }
889
- watchers.length = 0
890
- for (const fp of polledFiles) { try { fs.unwatchFile(fp) } catch {} }
891
- polledFiles.clear()
892
- },
893
- }
894
- }
895
-
896
- // Phase B §2.4 — cache-keeper health thresholds.
897
- // warning fires when cycle1 is overdue past HEALTH_OVERDUE_MS; an auto-
898
- // restart attempt fires when the warning has been emitted AND the most
899
- // recent unscheduled restart was more than AUTO_RESTART_COOLDOWN_MS ago.
900
- // Both default to 5 min per spec; caller overrides are not exposed yet.
901
- const CYCLE1_HEALTH_OVERDUE_MS = 5 * 60_000
902
- const CYCLE1_AUTO_RESTART_COOLDOWN_MS = 5 * 60_000
903
-
904
- function _startCycle1Run(config = {}, options = {}) {
905
- _cycle1InFlight = (async () => {
906
- try {
907
- const result = await runCycle1(db, config, options, DATA_DIR)
908
- // #13: heartbeat (attempt) is always recorded so the overdue check
909
- // can tell the keeper is alive; success timestamp only advances when
910
- // the run actually did work. Skipped/in-flight runs do NOT count as
911
- // success because the next overdue check would otherwise see a fake
912
- // green and stop forcing auto-restarts.
913
- const now = Date.now()
914
- await setCycleLastRun('cycle1_heartbeat', now)
915
- const skipped = result?.skippedInFlight === true
916
- const allFailed = !skipped
917
- && Number(result?.chunks ?? 0) === 0
918
- && Number(result?.processed ?? 0) === 0
919
- && Number(result?.skipped ?? 0) > 0
920
- if (!skipped && !allFailed) {
921
- await setCycleLastRun('cycle1', now)
922
- }
923
- return result
924
- } finally {
925
- if (_cycle1InFlight === promise) _cycle1InFlight = null
926
- }
927
- })()
928
- const promise = _cycle1InFlight
929
- return _cycle1InFlight
930
- }
931
-
932
- async function _awaitCycle1Run(config = {}, options = {}) {
933
- const target = _cycle1InFlight || _startCycle1Run(config, options)
934
- const callerDeadlineMs = Number(options.callerDeadlineMs) || 0
935
- if (callerDeadlineMs <= 0) return await target
936
- // Caller-deadline race. When the channels-side timeout fires, we
937
- // (a) graceful-return a skippedInFlight envelope so the calling
938
- // SessionStart slot stops blocking with a 200 OK + flags instead of a
939
- // 503-class throw, and (b) release the outer in-flight handle. The
940
- // underlying LLM run keeps progressing in the background — it still
941
- // owns the inner dedup guard (memory-cycle.mjs _runCycle1InFlight).
942
- // Releasing the outer handle is what breaks the cascade: any later
943
- // _awaitCycle1Run call now re-enters _startCycle1Run, whose inner
944
- // runCycle1 short-circuits with skippedInFlight:true the moment it
945
- // sees the same db still busy. Returning a graceful object (vs the
946
- // pre-0.1.198 throw) keeps the channel route response shape stable
947
- // and lets pollers read inFlight=true rather than parse an error.
948
- let timer
949
- const deadlinePromise = new Promise((resolve) => {
950
- timer = setTimeout(() => {
951
- if (_cycle1InFlight === target) _cycle1InFlight = null
952
- resolve({
953
- processed: 0,
954
- chunks: 0,
955
- skipped: 0,
956
- sessions: 0,
957
- skippedInFlight: true,
958
- timedOutWaiting: true,
959
- callerDeadlineMs,
960
- })
961
- }, callerDeadlineMs)
962
- })
963
- try {
964
- return await Promise.race([target, deadlinePromise])
965
- } finally {
966
- clearTimeout(timer)
967
- }
968
- }
969
-
970
- // Periodic cycle1 sizing: only enter when ≥ 20 pending rows have built up,
971
- // then split into 2 windows of 50 rows each (≤100 rows per tick) and process
972
- // both windows in parallel. The on-demand path used by SessionStart hooks runs
973
- // with a 1-row threshold and 5×20 windows instead — see hooks/session-start.cjs
974
- // ON_DEMAND_CYCLE1_ARGS.
975
- // mainConfig.cycle1 values still win, so users can override any of these in
976
- // config.json.
977
- function periodicCycle1Config() {
978
- return {
979
- min_batch: 20,
980
- session_cap: 2,
981
- batch_size: 50,
982
- concurrency: 2,
983
- ...(mainConfig?.cycle1 || {}),
984
- }
985
- }
986
-
987
- async function _finalizeCycle2Run(result) {
988
- if (result.ok) {
989
- await setCycleLastRun('cycle2', Date.now())
990
- await setCycleLastRun('cycle2_last_error', '')
991
- process.stderr.write('[cycle2] completed\n')
992
- } else {
993
- await setCycleLastRun('cycle2_last_error', result.error || 'unknown error')
994
- process.stderr.write(`[cycle2] failed: ${result.error}\n`)
995
- }
996
- }
997
-
998
- async function checkCycles() {
999
- // Poll-on-use: re-read memory config each tick so changed enabled/interval
1000
- // values apply without a restart (mirrors search/cwd poll-on-use). The fixed
1001
- // 60s poll bounds latency; manual `memory` tool calls already re-read per-call.
1002
- mainConfig = readMainConfig();
1003
- if (mainConfig?.enabled === false) return
1004
-
1005
- const cycle1Ms = parseInterval(mainConfig?.cycle1?.interval || '10m')
1006
- const cycle2Ms = parseInterval(mainConfig?.cycle2?.interval || '1h')
1007
- const cycle3Ms = parseInterval(mainConfig?.cycle3?.interval || '24h')
1008
-
1009
- const now = Date.now()
1010
- const last = await getCycleLastRun()
1011
-
1012
- // Phase B §2.4 — cache-keeper health check + auto-restart.
1013
- //
1014
- // `last.cycle1 + cycle1Ms` is the next scheduled run time; anything beyond
1015
- // that by > HEALTH_OVERDUE_MS means the keeper missed its window and the
1016
- // Anthropic shard is drifting cold. Emit a warning, and — if we haven't
1017
- // retried in the last cooldown window — force an unscheduled run so the
1018
- // shard gets re-touched before the next Worker / Sub call pays the 2×
1019
- // write premium. Cooldown prevents a tight retry loop when the underlying
1020
- // cause (network, provider outage) is still broken.
1021
- //
1022
- // Cold-start guard: a fresh DB has last.cycle1 = 0, which would make
1023
- // (now - 0 - cycle1Ms) blow past HEALTH_OVERDUE_MS on every first boot
1024
- // and force-trigger the auto-restart branch even though the shard never
1025
- // existed in the first place. The "drifting cold" concept doesn't apply
1026
- // until at least one successful run has anchored a baseline.
1027
- const cycle1OverdueMs = last.cycle1 > 0
1028
- ? Math.max(0, now - last.cycle1 - cycle1Ms)
1029
- : 0
1030
- if (cycle1OverdueMs > CYCLE1_HEALTH_OVERDUE_MS) {
1031
- const lastSeen = last.cycle1 ? new Date(last.cycle1).toISOString() : 'never'
1032
- process.stderr.write(
1033
- `[cycle1] overdue by ${Math.floor(cycle1OverdueMs / 60_000)}min `
1034
- + `(last=${lastSeen}). Pool B Anthropic shard may be cold.\n`
1035
- )
1036
- const lastAutoRestart = last.cycle1_autoRestart || 0
1037
- if (now - lastAutoRestart >= CYCLE1_AUTO_RESTART_COOLDOWN_MS) {
1038
- // #14: record the attempt timestamp BEFORE the call (so a hung run
1039
- // cannot tight-loop) and the result timestamp only on success. On
1040
- // failure we return immediately instead of falling through into the
1041
- // due branch — falling through would silently re-enter the same
1042
- // failing path within the same tick.
1043
- await setCycleLastRun('cycle1_autoRestart_attempt', now)
1044
- try {
1045
- const result = await _awaitCycle1Run(periodicCycle1Config())
1046
- await setCycleLastRun('cycle1_autoRestart', Date.now())
1047
- process.stderr.write(
1048
- `[cycle1] auto-restart completed chunks=${result?.chunks ?? 0} processed=${result?.processed ?? 0}\n`
1049
- )
1050
- return
1051
- } catch (e) {
1052
- process.stderr.write(`[cycle1] auto-restart error: ${e.message}\n`)
1053
- // Cooldown attempt timestamp is committed; do NOT fall through
1054
- // to the due branch — next tick will retry after cooldown.
1055
- return
1056
- }
1057
- }
1058
- }
1059
-
1060
- if (now - last.cycle1 >= cycle1Ms) {
1061
- const result = await _awaitCycle1Run(periodicCycle1Config())
1062
- process.stderr.write(`[cycle1] completed chunks=${result?.chunks ?? 0} processed=${result?.processed ?? 0}\n`)
1063
- }
1064
-
1065
- if (now - last.cycle2 >= cycle2Ms) {
1066
- if (!_cycle2InFlight) {
1067
- _cycle2InFlight = true
1068
- // Detached: cycle2 can take minutes; awaiting here would delay the
1069
- // next periodic checkCycles() tick and block sibling IPC (search,
1070
- // append) on the memory worker. The in-flight guard prevents
1071
- // concurrent runs; rejection is logged but does not propagate.
1072
- runCycle2(db, mainConfig?.cycle2 || {}, {}, DATA_DIR)
1073
- .then(_finalizeCycle2Run)
1074
- .catch(err => process.stderr.write(`[cycle2] detached run failed: ${err?.message || err}\n`))
1075
- .finally(() => { _cycle2InFlight = false })
1076
- }
1077
- }
1078
-
1079
- if (now - last.cycle3 >= cycle3Ms) {
1080
- if (!_cycle3InFlight) {
1081
- _cycle3InFlight = true
1082
- // Detached like cycle2 — core review walks every core_entries row with a
1083
- // recall + LLM call per row, so it can take a while. 24h cadence default.
1084
- runCycle3(db, mainConfig || {}, DATA_DIR)
1085
- .then(() => setCycleLastRun('cycle3', Date.now()))
1086
- .catch(err => process.stderr.write(`[cycle3] detached run failed: ${err?.message || err}\n`))
1087
- .finally(() => { _cycle3InFlight = false })
1088
- }
1089
- }
1090
- }
1091
- let _cycle2InFlight = false
1092
- let _cycle3InFlight = false
1093
-
1094
- // #12: self-rescheduling timer. setInterval would fire ticks regardless of
1095
- // whether the previous checkCycles() call had finished; with cycle1/cycle2
1096
- // each potentially taking minutes, that races. Use setTimeout that re-arms
1097
- // itself only after the prior tick resolves, plus an in-flight guard so a
1098
- // stray manual call cannot stack ticks.
1099
- let _checkCyclesInFlight = false
1100
- async function _runCheckCyclesGuarded() {
1101
- if (_checkCyclesInFlight) return
1102
- _checkCyclesInFlight = true
1103
- try { await checkCycles() }
1104
- catch (e) { process.stderr.write(`[cycle-tick] error: ${e.message}\n`) }
1105
- finally { _checkCyclesInFlight = false }
1106
- }
1107
- function _scheduleNextCheck() {
1108
- _cycleInterval = setTimeout(async () => {
1109
- _cycleInterval = null
1110
- try {
1111
- await _runCheckCyclesGuarded()
1112
- } catch (e) {
1113
- process.stderr.write(`[cycle-tick] re-arm guard caught: ${e?.message || e}\n`)
1114
- } finally {
1115
- // Re-arm regardless of inner outcome — _runCheckCyclesGuarded already
1116
- // swallows its own errors, but defensive try/finally guarantees the
1117
- // periodic tick continues even if a synchronous throw escapes.
1118
- if (_cyclesActive) _scheduleNextCheck()
1119
- }
1120
- }, 60_000)
1121
- }
1122
- let _cyclesActive = false
1123
- let _transcriptWatcher = null
1124
- function _startCycles() {
1125
- if (_cyclesActive) return
1126
- _cyclesActive = true
1127
- _scheduleNextCheck()
1128
- _startupTimeout = setTimeout(() => { void _runCheckCyclesGuarded() }, 30_000)
1129
- }
1130
-
1131
- function _stopCycles() {
1132
- _cyclesActive = false
1133
- if (_cycleInterval) { clearTimeout(_cycleInterval); _cycleInterval = null }
1134
- if (_startupTimeout) { clearTimeout(_startupTimeout); _startupTimeout = null }
1135
- if (_transcriptWatcher) { try { _transcriptWatcher.stop() } catch {} _transcriptWatcher = null }
1136
- }
1137
-
1138
- async function _initRuntime() {
1139
- if (_initialized) return
1140
- await _initStore()
1141
- // Restore the core_entries.id == 1..N invariant once per boot: SERIAL only
1142
- // increments, so deleted rows leave permanent gaps. Fast no-op when already
1143
- // contiguous (or empty). Runs only here — never in cycle2/addCore/deleteCore.
1144
- await compactCoreIds(DATA_DIR)
1145
- _transcriptWatcher = _initTranscriptWatcher()
1146
- _startCycles()
1147
- _initialized = true
1148
- // Boot complete — continue straight into the deferred embedding warmup.
1149
- // Fire-and-forget on the embedding worker thread; never awaited so it does
1150
- // not delay init() returning or the memory-ready signal.
1151
- fireDeferredEmbeddingWarmup()
1152
- }
1153
-
1154
- function _beginRuntimeInit() {
1155
- if (_initialized) return Promise.resolve()
1156
- if (!_initPromise) {
1157
- _initPromise = _initRuntime().catch((e) => {
1158
- process.stderr.write(`[memory-service] runtime init failed: ${e?.stack || e?.message || e}\n`)
1159
- throw e
1160
- })
1161
- }
1162
- return _initPromise
1163
- }
1164
-
1165
- function fmtDateOnly(d) {
1166
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
1167
- }
1168
-
1169
- function parsePeriod(period, hasQuery) {
1170
- if (!period && hasQuery) period = '30d'
1171
- if (!period) return null
1172
- if (period === 'all') return null
1173
- if (period === 'last') return { mode: 'last' }
1174
- // Calendar-day windows: 'today' anchors at local midnight rather than
1175
- // rolling 24h. Without this, a query asking 'today' at 01:30 would silently
1176
- // include yesterday's last 22.5h of activity, mislabelling them as
1177
- // 'today's work'. 'yesterday' is the previous calendar day.
1178
- if (period === 'today') {
1179
- const start = new Date()
1180
- start.setHours(0, 0, 0, 0)
1181
- return { startMs: start.getTime(), endMs: Date.now() }
1182
- }
1183
- if (period === 'yesterday') {
1184
- const start = new Date()
1185
- start.setDate(start.getDate() - 1)
1186
- start.setHours(0, 0, 0, 0)
1187
- const end = new Date(start)
1188
- end.setHours(23, 59, 59, 999)
1189
- return { startMs: start.getTime(), endMs: end.getTime() }
1190
- }
1191
- if (period === 'this_week' || period === 'last_week') {
1192
- // R6 P9: calendar Mon-Sun previous/current week. Mon-start ISO
1193
- // convention. Replaces R5 rolling 7-14d range which was empty for
1194
- // sessions where "last week" decisions actually fell on Mon (4/27) of
1195
- // this week. Precise calendar bounds match natural-language intuition.
1196
- const d = new Date()
1197
- d.setHours(0, 0, 0, 0)
1198
- const dayOfWeek = d.getDay()
1199
- const daysSinceMon = (dayOfWeek + 6) % 7
1200
- const thisWeekMon = new Date(d)
1201
- thisWeekMon.setDate(d.getDate() - daysSinceMon)
1202
- if (period === 'this_week') {
1203
- return { startMs: thisWeekMon.getTime(), endMs: Date.now() }
1204
- }
1205
- const lastWeekMon = new Date(thisWeekMon)
1206
- lastWeekMon.setDate(thisWeekMon.getDate() - 7)
1207
- const lastWeekSunEnd = new Date(thisWeekMon.getTime() - 1)
1208
- return { startMs: lastWeekMon.getTime(), endMs: lastWeekSunEnd.getTime() }
1209
- }
1210
- const relMatch = period.match(/^(\d+)(m|h|d)$/)
1211
- if (relMatch) {
1212
- const n = parseInt(relMatch[1])
1213
- const unit = relMatch[2]
1214
- const now = new Date()
1215
- if (unit === 'm') {
1216
- // Minute granularity is for "resume from the previous turn / pick
1217
- // up where we left off" style recall — sub-hour windows where 1h
1218
- // is too coarse. n=0 is invalid (the regex requires \d+ which
1219
- // matches "0" but a zero-width window returns no rows; leave that
1220
- // as caller-supplied no-op).
1221
- const start = new Date(now.getTime() - n * 60_000)
1222
- return { startMs: start.getTime(), endMs: now.getTime() }
1223
- }
1224
- if (unit === 'h') {
1225
- const start = new Date(now.getTime() - n * 3600_000)
1226
- return { startMs: start.getTime(), endMs: now.getTime() }
1227
- }
1228
- const start = new Date(now)
1229
- start.setDate(start.getDate() - n)
1230
- return { startMs: start.getTime(), endMs: now.getTime() }
1231
- }
1232
- const rangeMatch = period.match(/^(\d{4}-\d{2}-\d{2})~(\d{4}-\d{2}-\d{2})$/)
1233
- if (rangeMatch) {
1234
- return {
1235
- startMs: Date.parse(rangeMatch[1] + 'T00:00:00'),
1236
- endMs: Date.parse(rangeMatch[2] + 'T23:59:59.999'),
1237
- }
1238
- }
1239
- const dateMatch = period.match(/^(\d{4}-\d{2}-\d{2})$/)
1240
- if (dateMatch) {
1241
- return {
1242
- startMs: Date.parse(dateMatch[1] + 'T00:00:00'),
1243
- endMs: Date.parse(dateMatch[1] + 'T23:59:59.999'),
1244
- exact: true,
1245
- }
1246
- }
1247
- return null
1248
- }
1249
-
1250
- function formatTs(tsMs) {
1251
- const n = Number(tsMs)
1252
- if (Number.isFinite(n) && n > 1e12) {
1253
- return new Date(n).toLocaleString('sv-SE').slice(0, 16)
1254
- }
1255
- return String(tsMs ?? '').slice(0, 16)
1256
- }
1257
-
1258
- async function handleSearch(args, signal) {
1259
- // Cooperative abort check: throw early if the caller already aborted
1260
- // (IPC cancel handler signals the AbortController before re-entry).
1261
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1262
- // id mode (follow-up lookup): caller passed `#N` markers from a prior
1263
- // recall result. Fetch those rows directly + their chunk members,
1264
- // bypassing hybrid search entirely. Output reuses renderEntryLines so
1265
- // the shape stays identical to the search path (chunk members first,
1266
- // root summary fallback).
1267
- if (Array.isArray(args.ids) && args.ids.length > 0) {
1268
- const ids = args.ids
1269
- .map(v => Number(v))
1270
- .filter(v => Number.isFinite(v) && v > 0)
1271
- if (ids.length === 0) return { text: '(no valid ids)' }
1272
- const includeArchived = args.includeArchived !== false
1273
- const category = args.category
1274
- const period = String(args.period ?? '').trim() || undefined
1275
- const temporal = parsePeriod(period, false)
1276
- let projectScope
1277
- if (typeof args.projectScope === 'string' && args.projectScope) {
1278
- projectScope = args.projectScope
1279
- } else {
1280
- const projectId = resolveProjectScope(typeof args.cwd === 'string' && args.cwd ? args.cwd : null)
1281
- projectScope = projectId !== null ? projectId : 'common'
1282
- }
1283
- const excludeStatuses = includeArchived ? [] : ['archived']
1284
- const rows = await fetchEntriesByIdsScoped(db, ids, {
1285
- ts_from: temporal?.startMs,
1286
- ts_to: temporal?.endMs,
1287
- excludeStatuses,
1288
- category,
1289
- projectScope,
1290
- })
1291
- if (rows.length === 0) return { text: '(no results)' }
1292
- // Members for any root rows in the result set.
1293
- const rootIds = rows.filter(r => r.is_root === 1).map(r => Number(r.id))
1294
- const memberLeafIds = new Set()
1295
- if (rootIds.length > 0) {
1296
- const { rows: memberRows } = await db.query(
1297
- `SELECT id, ts, role, content, chunk_root
1298
- FROM entries WHERE chunk_root = ANY($1::bigint[]) AND is_root = 0
1299
- ORDER BY ts ASC, id ASC`,
1300
- [rootIds],
1301
- )
1302
- const membersByRoot = new Map()
1303
- for (const m of memberRows) {
1304
- const k = Number(m.chunk_root)
1305
- if (!membersByRoot.has(k)) membersByRoot.set(k, [])
1306
- membersByRoot.get(k).push(m)
1307
- memberLeafIds.add(Number(m.id))
1308
- }
1309
- for (const r of rows) {
1310
- if (r.is_root === 1) r.members = membersByRoot.get(Number(r.id)) ?? []
1311
- }
1312
- }
1313
- // Preserve caller-supplied id order; drop leaves already inlined as a
1314
- // root's chunk member to prevent double emission when the caller names
1315
- // a root and one of its leaves in the same batch.
1316
- const byId = new Map(rows.map(r => [Number(r.id), r]))
1317
- const ordered = ids
1318
- .map(id => byId.get(id))
1319
- .filter(Boolean)
1320
- .filter(r => !(r.is_root === 0 && memberLeafIds.has(Number(r.id))))
1321
- return { text: renderEntryLines(ordered) }
1322
- }
1323
- // Array query — fan out in parallel, each query runs its own hybrid search
1324
- // path, and results are grouped in the response so the caller sees one
1325
- // ranked list per angle. Collapses what would otherwise be N sequential
1326
- // tool calls into a single invocation.
1327
- if (Array.isArray(args.query)) {
1328
- // Dedup + fan-out cap. The cap protects the result envelope from
1329
- // over-eager callers (20+ near-duplicate queries N× the IO) without
1330
- // silently swallowing the caller's intent: when the input exceeds
1331
- // QUERIES_CAP, prepend a one-line note so the caller can see the
1332
- // truncation and re-shape their query list.
1333
- const QUERIES_CAP = 5
1334
- const dedup = [...new Set(args.query.map(q => String(q || '').trim()).filter(Boolean))]
1335
- if (dedup.length === 0) return { text: '' }
1336
- const queries = dedup.slice(0, QUERIES_CAP)
1337
- const dropped = dedup.length - queries.length
1338
- const rest = { ...args }
1339
- delete rest.query
1340
- const deadlineSec = Math.max(1, Number(process.env.MEMORY_FANOUT_DEADLINE_S) || 180)
1341
- const deadlineMs = deadlineSec * 1000
1342
- const fanOutAbort = new AbortController()
1343
- let deadlineTimer
1344
- const deadlineRace = new Promise((_res, rej) => {
1345
- deadlineTimer = setTimeout(() => {
1346
- fanOutAbort.abort(new Error(`memory fan-out deadline exceeded (${deadlineSec}s)`))
1347
- rej(Object.assign(new Error(`memory fan-out deadline exceeded (${deadlineSec}s)`), { _deadline: true }))
1348
- }, deadlineMs)
1349
- })
1350
- let settled
1351
- try {
1352
- // Pre-warm the per-query embedding cache with one batched ONNX run so
1353
- // each sub-search lands an embedText cache hit (~0ms) instead of
1354
- // serially queueing through the worker's single-flight inference lock.
1355
- // Replaces N sequential ~130ms inferences with one ~150-200ms batch.
1356
- //
1357
- // Race against the same deadline as the fan-out itself: a stuck
1358
- // embedding worker would previously park here indefinitely because
1359
- // the timer hadn't been started yet from the fan-out's perspective.
1360
- await Promise.race([embedTexts(queries), deadlineRace])
1361
- settled = await Promise.race([
1362
- Promise.all(queries.map(async (q) => {
1363
- if (fanOutAbort.signal.aborted) throw fanOutAbort.signal.reason
1364
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1365
- const sub = await handleSearch({ ...rest, query: q }, signal)
1366
- return `[${q}]\n${sub.text || '(no results)'}`
1367
- })),
1368
- deadlineRace,
1369
- ])
1370
- } catch (err) {
1371
- throw err
1372
- } finally {
1373
- clearTimeout(deadlineTimer)
1374
- }
1375
- const parts = settled
1376
- const header = dropped > 0
1377
- ? `note: ${dedup.length} queries received, ${queries.length} processed, ${dropped} dropped (cap ${QUERIES_CAP})\n\n`
1378
- : ''
1379
- return { text: header + parts.join('\n\n') }
1380
- }
1381
- const query = String(args.query ?? '').trim()
1382
- let period = String(args.period ?? '').trim() || undefined
1383
- // Period and sort are caller-supplied only. Lead is responsible for
1384
- // mapping vague time phrases / chronological intent into the period
1385
- // argument before calling; the engine does not infer them from query
1386
- // text.
1387
- const RECALL_LIMIT_CAP = 100
1388
- const RECALL_OFFSET_CAP = 500
1389
- const requestedLimit = Number(args.limit)
1390
- const requestedOffset = Number(args.offset)
1391
- let limit = Math.max(1, Number.isFinite(requestedLimit) ? requestedLimit : 10)
1392
- let offset = Math.max(0, Number.isFinite(requestedOffset) ? requestedOffset : 0)
1393
- const recallCapNotes = []
1394
- if (Number.isFinite(requestedLimit) && requestedLimit > RECALL_LIMIT_CAP) {
1395
- limit = RECALL_LIMIT_CAP
1396
- recallCapNotes.push(`limit capped to ${RECALL_LIMIT_CAP} (requested ${requestedLimit})`)
1397
- } else {
1398
- limit = Math.min(RECALL_LIMIT_CAP, limit)
1399
- }
1400
- if (Number.isFinite(requestedOffset) && requestedOffset > RECALL_OFFSET_CAP) {
1401
- offset = RECALL_OFFSET_CAP
1402
- recallCapNotes.push(`offset capped to ${RECALL_OFFSET_CAP} (requested ${requestedOffset})`)
1403
- } else {
1404
- offset = Math.min(RECALL_OFFSET_CAP, offset)
1405
- }
1406
- const recallCapPrefix = recallCapNotes.length ? `${recallCapNotes.join('; ')}\n` : ''
1407
- const sort = args.sort != null ? String(args.sort) : 'importance'
1408
- // Chunk content is the primary recall output. Members default to true so
1409
- // callers receive the raw chunk leaves (the cycle1-produced semantic
1410
- // chunks) rather than just the root's cycle2-compressed summary line.
1411
- // Explicit `includeMembers:false` keeps the legacy summary-only mode.
1412
- const includeMembers = args.includeMembers !== false
1413
- const includeRaw = Boolean(args.includeRaw)
1414
- const includeArchived = args.includeArchived !== false
1415
- const category = args.category
1416
- const temporal = parsePeriod(period, Boolean(query))
1417
-
1418
- // Derive projectScope from caller cwd (falls back to process.cwd()).
1419
- // Explicit args.projectScope (string) takes priority so callers can
1420
- // override to 'all', 'common', or a specific slug.
1421
- let projectScope
1422
- if (typeof args.projectScope === 'string' && args.projectScope) {
1423
- projectScope = args.projectScope
1424
- } else {
1425
- const projectId = resolveProjectScope(typeof args.cwd === 'string' && args.cwd ? args.cwd : null)
1426
- projectScope = projectId !== null ? projectId : 'common'
1427
- }
1428
-
1429
- // R11 reviewer M4: calendar-bounded periods disable freshness decay
1430
- // so within-period ranking doesn't downgrade Mon entries vs Sun.
1431
- const CALENDAR_PERIODS = new Set(['yesterday', 'today', 'this_week', 'last_week'])
1432
- const isCalendarPeriod = period != null
1433
- && (CALENDAR_PERIODS.has(period) || /^\d{4}-\d{2}-\d{2}/.test(period))
1434
- const applyFreshness = !isCalendarPeriod
1435
-
1436
- if (query) {
1437
- const _t0 = Date.now()
1438
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1439
- const queryVector = await embedText(query)
1440
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1441
- const _t1 = Date.now()
1442
- if (process.env.MIXDOG_DEBUG_MEMORY) {
1443
- process.stderr.write(`[search-time] embed=${_t1 - _t0}ms query="${query.slice(0, 60)}"\n`)
1444
- }
1445
- // Push ts and status filters into the hybrid candidate query so FTS / vec
1446
- // rank inside the requested window, not the whole tree. The previous post-
1447
- // filter approach silently emptied results when relevant matches sat
1448
- // outside `period` (default 30d) and could not bubble through.
1449
- // Recall is history-first: archived roots hold most prior work. Callers
1450
- // that need only live invariants can pass includeArchived:false.
1451
- const excludeStatuses = includeArchived ? [] : ['archived']
1452
- const results = await searchRelevantHybrid(db, query, {
1453
- limit: limit + offset,
1454
- queryVector: Array.isArray(queryVector) ? queryVector : null,
1455
- includeMembers,
1456
- ts_from: temporal?.startMs,
1457
- ts_to: temporal?.endMs,
1458
- applyFreshness,
1459
- projectScope,
1460
- category,
1461
- excludeStatuses,
1462
- // useHotActive was set to true here so default (no-period) calls
1463
- // routed through the mv_hot_active materialized view — a narrow
1464
- // active-roots-only pool. Live usage is dominated by vague-time
1465
- // queries ("recent / lately") where Lead callers omit the period
1466
- // filter, leaving the MV as the sole source. That hid every
1467
- // orphan leaf and every pending root — fresh work from the last 1-60
1468
- // minutes never surfaced. Now that the entries-table CTE legs run
1469
- // against broaden HNSW + GIN trgm partial indexes (the
1470
- // is_root=1 predicate was dropped in the same revision), the
1471
- // entries path is fast enough (1-2 ms ANN on ~10K rows, O(log N)
1472
- // through 1M+) to be the single source of truth. The MV is left in
1473
- // place for now but no longer routed to from search; cycle2 may stop
1474
- // refreshing it in a follow-up commit once nothing else reads it.
1475
- useHotActive: false,
1476
- })
1477
- let filtered = results
1478
- if (sort === 'date') {
1479
- // R11 reviewer L5: NaN guard — entries with null/undefined ts default
1480
- // to 0 so the comparator stays numeric and stable.
1481
- filtered.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0))
1482
- } else {
1483
- filtered.sort((a, b) => {
1484
- const sa = (v) => { const n = Number(v); return Number.isFinite(n) ? n : 0 }
1485
- return (sa(b.retrievalScore ?? b.rrf ?? 0) - sa(a.retrievalScore ?? a.rrf ?? 0))
1486
- || (sa(b.score ?? 0) - sa(a.score ?? 0))
1487
- || (sa(b.ts ?? 0) - sa(a.ts ?? 0))
1488
- || (Number(a.id ?? 0) - Number(b.id ?? 0))
1489
- })
1490
- }
1491
- if (includeRaw) {
1492
- // Reserve slots for raw rows under sort=importance: hybrid rows are
1493
- // already score-sorted descending, so a full hybrid page (limit rows)
1494
- // would shut out raw rows entirely after slice(offset, offset+limit).
1495
- // Reserve up to RAW_RESERVE slots near the top of the post-slice
1496
- // window by trimming the hybrid prefix before merging, then re-sort
1497
- // for sort=date or otherwise append (already ranked) for importance.
1498
- const RAW_FETCH = 20
1499
- const rawRows = await readRawRowsInWindow(
1500
- db,
1501
- temporal?.startMs ?? null,
1502
- temporal?.endMs ?? Date.now(),
1503
- RAW_FETCH,
1504
- { projectScope },
1505
- )
1506
- const seenIds = new Set(filtered.map(r => r.id))
1507
- const newRaw = rawRows.filter(r => !seenIds.has(r.id))
1508
- if (sort === 'date') {
1509
- for (const r of newRaw) filtered.push(r)
1510
- filtered.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0))
1511
- } else {
1512
- // sort=importance: append raw rows after the hybrid page (mostly
1513
- // ineffective — slice(offset, offset+limit) typically shuts them
1514
- // out). Proper includeRaw paging fix deferred (needs fetching extra rows / paging redesign).
1515
- for (const r of newRaw) filtered.push(r)
1516
- }
1517
- }
1518
- const sliced = filtered.slice(offset, offset + limit)
1519
- const _t2 = Date.now()
1520
- if (process.env.MIXDOG_DEBUG_MEMORY) {
1521
- process.stderr.write(`[search-time] hybrid+sort+raw=${_t2 - _t1}ms rows=${filtered.length} sliced=${sliced.length}\n`)
1522
- }
1523
- // Emit a recall trace event so getTraceWithEntries() can correlate
1524
- // this search with the top-ranked memory entry. One event per
1525
- // handleSearch call (not per returned row) — cheapest meaningful link.
1526
- // parent_span_id left null: the bridge-side span id is only known after
1527
- // the DB insert of the loop/tool events, which happens async on the
1528
- // client side and is not available here.
1529
- if (_traceDb && filtered.length > 0) {
1530
- const topHit = filtered[0]
1531
- const topId = topHit?.id != null ? Number(topHit.id) : null
1532
- if (topId !== null && Number.isFinite(topId)) {
1533
- insertTraceEvents(_traceDb, [{
1534
- ts: Date.now(),
1535
- kind: 'recall',
1536
- entry_id: topId,
1537
- payload: { query: query.slice(0, 200), hit_count: filtered.length },
1538
- }]).catch(e => process.stderr.write(`[trace] insertTraceEvents error: ${e?.message}\n`))
1539
- }
1540
- }
1541
- const out = { text: recallCapPrefix + renderEntryLines(sliced) }
1542
- if (process.env.MIXDOG_DEBUG_MEMORY) {
1543
- process.stderr.write(`[search-time] render+trace=${Date.now() - _t2}ms total=${Date.now() - _t0}ms textLen=${out.text.length}\n`)
1544
- }
1545
- return out
1546
- }
1547
-
1548
- const filters = { limit: limit + offset }
1549
- if (temporal?.startMs != null) { filters.ts_from = temporal.startMs; filters.ts_to = temporal.endMs }
1550
- if (temporal?.mode === 'last' && _bootTimestamp) {
1551
- filters.ts_to = _bootTimestamp - 1
1552
- }
1553
- filters.projectScope = projectScope
1554
- if (category != null) filters.category = category
1555
- filters.sort = sort
1556
- if (!includeArchived) filters.excludeStatuses = ['archived']
1557
- if (includeMembers) filters.includeMembers = true
1558
- const rows = await retrieveEntries(db, filters)
1559
- const sliced = rows.slice(offset, offset + limit)
1560
- return { text: recallCapPrefix + renderEntryLines(sliced) }
1561
- }
1562
-
1563
- function renderEntryLines(rows) {
1564
- if (!rows || rows.length === 0) return '(no results)'
1565
- const lines = []
1566
- // Bound total emitted lines (roots x members) so a many-member recall can't
1567
- // inject unbounded output. Per-line content is already capped at 1000 chars;
1568
- // this caps the line COUNT. Narrow the query (limit/period/projectScope) for more.
1569
- const RECALL_LINE_CAP = 200
1570
- let _capped = false
1571
- outer:
1572
- for (const r of rows) {
1573
- const hasMembers = Array.isArray(r.members) && r.members.length > 0
1574
- if (hasMembers) {
1575
- // Chunks present: emit each member as its own line. Root row is a
1576
- // grouping artifact for retrieval — the caller wants the chunk
1577
- // content (cycle1 raw), not the cycle2-compressed summary.
1578
- for (const m of r.members) {
1579
- if (lines.length >= RECALL_LINE_CAP) { _capped = true; break outer }
1580
- const mTs = formatTs(m.ts)
1581
- const role = m.role === 'user' ? 'u' : m.role === 'assistant' ? 'a' : (m.role || '?')
1582
- const content = cleanMemoryText(String(m.content ?? '')).slice(0, 1000)
1583
- lines.push(`[${mTs}] ${role}: ${content} #${m.id}`)
1584
- }
1585
- } else {
1586
- if (lines.length >= RECALL_LINE_CAP) { _capped = true; break }
1587
- // No chunks (root not yet chunked by cycle1, or orphan leaf): emit
1588
- // the row itself in the same shape. element/summary fall back to
1589
- // raw content when both are absent.
1590
- const ts = formatTs(r.ts)
1591
- const element = r.element ?? ''
1592
- const summary = r.summary ?? ''
1593
- // Standalone leaf rows (is_root=0, no parent chunks_root resolved
1594
- // into a `members` list) carry their u/a role just like inline
1595
- // chunk members — surface it so the format stays consistent across
1596
- // the two emission paths.
1597
- const rolePrefix = r.is_root === 0 && r.role
1598
- ? (r.role === 'user' ? 'u: ' : r.role === 'assistant' ? 'a: ' : `${r.role}: `)
1599
- : ''
1600
- const body = element || summary
1601
- ? `${element}${summary ? ' — ' + summary : ''}`
1602
- : cleanMemoryText(String(r.content ?? '')).slice(0, 1000)
1603
- lines.push(`[${ts}] ${rolePrefix}${body.slice(0, 1000)} #${r.id}`)
1604
- }
1605
- }
1606
- if (_capped) lines.push(`[recall truncated — showing first ${RECALL_LINE_CAP} lines; narrow the query (limit/period/projectScope) for the rest]`)
1607
- return lines.join('\n')
1608
- }
1609
-
1610
- async function entryStats() {
1611
- return await db.transaction(async (tx) => {
1612
- const total = (await tx.query(`SELECT COUNT(*) c FROM entries`)).rows[0].c
1613
- const roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1`)).rows[0].c
1614
- const active_roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'active'`)).rows[0].c
1615
- const archived_roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'archived'`)).rows[0].c
1616
- const unchunked_leaves = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE chunk_root IS NULL`)).rows[0].c
1617
- const cycle2_pending_roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'pending'`)).rows[0].c
1618
- const core_entries = (await tx.query(`SELECT COUNT(*) c FROM core_entries`)).rows[0].c
1619
- const core_embed_null = (await tx.query(`SELECT COUNT(*) c FROM core_entries WHERE embedding IS NULL`)).rows[0].c
1620
- const active_core_summaries = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'active' AND core_summary IS NOT NULL`)).rows[0].c
1621
- const active_core_summary_missing = (await tx.query(`
1622
- SELECT COUNT(*) c
1623
- FROM entries
1624
- WHERE is_root = 1
1625
- AND status = 'active'
1626
- AND (core_summary IS NULL OR btrim(core_summary) = '')
1627
- `)).rows[0].c
1628
- const byStatus = (await tx.query(`SELECT status, COUNT(*) c FROM entries WHERE is_root = 1 GROUP BY status`)).rows
1629
- const byCategory = (await tx.query(`SELECT category, COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'active' GROUP BY category ORDER BY c DESC`)).rows
1630
- const mvRows = (await tx.query(`SELECT relispopulated FROM pg_class WHERE relname = 'mv_hot_active' LIMIT 1`)).rows
1631
- const mv_hot_active_populated = mvRows.length ? Boolean(mvRows[0].relispopulated) : null
1632
- return {
1633
- total, roots, active_roots, archived_roots, unchunked_leaves, cycle2_pending_roots,
1634
- core_entries, core_embed_null, active_core_summaries, active_core_summary_missing,
1635
- mv_hot_active_populated,
1636
- byStatus, byCategory,
1637
- }
1638
- })
1639
- }
1640
-
1641
- async function _handleMemCycle1(args, config, signal) {
1642
- const minBatchOverride = Number(args?.min_batch)
1643
- const sessionCapOverride = Number(args?.session_cap)
1644
- const batchSizeOverride = Number(args?.batch_size)
1645
- const concurrencyOverride = Number(args?.concurrency)
1646
- const baseCycle1 = config?.cycle1 || {}
1647
- let cycle1Config = baseCycle1
1648
- // _runCycle1Impl reads `config?.min_batch ?? config?.cycle1?.min_batch ??
1649
- // default` — top-level wins, so pin the override at top-level only.
1650
- if (Number.isFinite(minBatchOverride) && minBatchOverride > 0) {
1651
- cycle1Config = { ...cycle1Config, min_batch: minBatchOverride }
1652
- }
1653
- if (Number.isFinite(sessionCapOverride) && sessionCapOverride > 0) {
1654
- cycle1Config = { ...cycle1Config, session_cap: sessionCapOverride }
1655
- }
1656
- if (Number.isFinite(batchSizeOverride) && batchSizeOverride > 0) {
1657
- cycle1Config = { ...cycle1Config, batch_size: batchSizeOverride }
1658
- }
1659
- if (Number.isFinite(concurrencyOverride) && concurrencyOverride > 0) {
1660
- cycle1Config = { ...cycle1Config, concurrency: Math.min(8, Math.floor(concurrencyOverride)) }
1661
- }
1662
- const callerDeadlineMs = Number(args?._callerDeadlineMs) || 0
1663
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1664
- const cycle1Options = callerDeadlineMs > 0 ? { callerDeadlineMs, signal } : { signal }
1665
- const result = await _awaitCycle1Run(
1666
- cycle1Config,
1667
- cycle1Options,
1668
- )
1669
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1670
- const pendingStr = result?.pendingRows != null ? result.pendingRows : 0
1671
- const inFlightStr = result?.skippedInFlight === true ? 'true' : 'false'
1672
- const timedOutPart = result?.timedOutWaiting === true ? ' timedOut=true' : ''
1673
- const omitted = Array.isArray(result?.omitted_row_ids) ? result.omitted_row_ids.length : Number(result?.quality?.omitted_rows || 0)
1674
- const prefiltered = Array.isArray(result?.prefiltered_row_ids) ? result.prefiltered_row_ids.length : Number(result?.quality?.prefiltered_rows || 0)
1675
- const failedRows = Array.isArray(result?.failed_row_ids) ? result.failed_row_ids.length : Number(result?.quality?.failed_rows || 0)
1676
- const invalidChunks = Array.isArray(result?.invalid_chunks) ? result.invalid_chunks.length : Number(result?.quality?.invalid_chunks || 0)
1677
- return {
1678
- text: `cycle1: chunks=${result.chunks} processed=${result.processed} skipped_chunks=${result.skipped}` +
1679
- ` omitted=${omitted} prefiltered=${prefiltered} failed_rows=${failedRows} invalid_chunks=${invalidChunks}` +
1680
- ` pending=${pendingStr} inFlight=${inFlightStr}${timedOutPart}`,
1681
- }
1682
- }
1683
-
1684
- async function _handleMemCycle2(args, config, signal) {
1685
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1686
- const result = await runCycle2(db, config?.cycle2 || {}, { signal }, DATA_DIR)
1687
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1688
- await _finalizeCycle2Run(result)
1689
- const counts = {
1690
- promoted: result?.promoted || 0,
1691
- archived: result?.archived || 0,
1692
- merged: result?.merged || 0,
1693
- updated: result?.updated || 0,
1694
- kept: result?.kept || 0,
1695
- rejected_verb: result?.rejected_verb || 0,
1696
- merge_rejected: result?.merge_rejected || 0,
1697
- missing_core: result?.missing_core_summary || 0,
1698
- core_backfill: result?.core_embedding_backfill || 0,
1699
- cascade_drop: result?.cascade?.dropped || 0,
1700
- phase_merge: result?.phase_merge?.merged || 0,
1701
- core_overlap: result?.phase_merge?.core_overlap || 0,
1702
- }
1703
- const parts = Object.entries(counts).filter(([, v]) => v > 0).map(([k, v]) => `${k}=${v}`)
1704
- if (parts.length) return { text: `cycle2 ${parts.join(' ')}` }
1705
- // No applied counts — disambiguate the "noop" so a broken gate is visible
1706
- // instead of looking like a clean, nothing-to-do run.
1707
- let cause = ''
1708
- if (result?.skippedInFlight) cause = ' (skipped: in-flight)'
1709
- else if (result?.ok === false) cause = ` (error: ${result.error || 'unknown'})`
1710
- else if (result?.gate_failed) cause = ' (gate_failed)'
1711
- return { text: `cycle2 noop${cause}` }
1712
- }
1713
-
1714
- async function _handleMemCycle3(args, config, signal) {
1715
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1716
- const confirmed = args?.confirm === 'APPLY CYCLE3'
1717
- const requestedMode = typeof args?.cycle3Mode === 'string' ? args.cycle3Mode : null
1718
- const applyMode = confirmed
1719
- ? 'confirmed'
1720
- : (requestedMode === 'proposal' || requestedMode === 'dry-run' || requestedMode === 'dryrun')
1721
- ? 'proposal'
1722
- : 'conservative'
1723
- const result = await runCycle3(db, config || {}, DATA_DIR, { signal, apply: confirmed ? true : undefined, applyMode })
1724
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1725
- const parts = ['reviewed', 'kept', 'updated', 'merged', 'deleted']
1726
- .map(k => `${k}=${result?.[k] || 0}`)
1727
- if (result?.proposed) {
1728
- parts.push(`proposal_update=${result.proposed.updated || 0}`)
1729
- parts.push(`proposal_merge=${result.proposed.merged || 0}`)
1730
- parts.push(`proposal_delete=${result.proposed.deleted || 0}`)
1731
- }
1732
- if (result?.held) {
1733
- parts.push(`held_update=${result.held.updated || 0}`)
1734
- parts.push(`held_merge=${result.held.merged || 0}`)
1735
- parts.push(`held_delete=${result.held.deleted || 0}`)
1736
- }
1737
- parts.push(`mode=${result?.applyMode || applyMode}`)
1738
- parts.push(`applied=${result?.applied === true ? 'true' : 'false'}`)
1739
- if (result?.skippedInFlight) parts.push('inFlight=true')
1740
- const errPart = result?.error ? ` error=${result.error}` : ''
1741
- return { text: `cycle3 ${parts.join(' ')}${errPart}` }
1742
- }
1743
-
1744
- async function _handleMemFlush(args, config, signal) {
1745
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1746
- const r1 = await _awaitCycle1Run(config?.cycle1 || {}, { signal })
1747
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1748
- const r2 = await runCycle2(db, config?.cycle2 || {}, { signal }, DATA_DIR)
1749
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1750
- await _finalizeCycle2Run(r2)
1751
- return { text: `flush: cycle1 chunks=${r1.chunks} processed=${r1.processed}, cycle2 ${JSON.stringify(r2)}` }
1752
- }
1753
-
1754
- async function _handleMemStatus(args, config) {
1755
- const stats = await entryStats()
1756
- const last = await getCycleLastRun()
1757
- let dims = 0
1758
- let dimsErr = null
1759
- try {
1760
- const raw = await getMetaValue(db, 'embedding.current_dims', null)
1761
- if (raw != null) dims = Number(JSON.parse(raw))
1762
- if (!Number.isFinite(dims)) dims = 0
1763
- } catch (e) {
1764
- // Surface the error in the status line instead of masquerading a meta
1765
- // read failure as dims=0 (which is indistinguishable from a fresh,
1766
- // pre-bootstrap DB). Keep status callable so other lines still render.
1767
- dims = 0
1768
- dimsErr = e?.message || String(e)
1769
- }
1770
- const bootstrapComplete = await isBootstrapComplete(db)
1771
- const lastCycle1Ago = last.cycle1 ? `${Math.round((Date.now() - last.cycle1) / 60000)}m ago` : 'never'
1772
- const lastCycle2Ago = last.cycle2 ? `${Math.round((Date.now() - last.cycle2) / 60000)}m ago` : 'never'
1773
- const activeTargetCap = Number.isFinite(Number(config?.cycle2?.active_target_cap))
1774
- ? Number(config?.cycle2?.active_target_cap)
1775
- : CYCLE2_ACTIVE_TARGET_CAP
1776
- const mvState = stats.mv_hot_active_populated === null
1777
- ? 'missing'
1778
- : stats.mv_hot_active_populated ? 'populated' : 'unpopulated'
1779
- const lines = [
1780
- `entries: total=${stats.total} roots=${stats.roots} cycle1_raw=${stats.unchunked_leaves} (unchunked leaves) cycle2_pending=${stats.cycle2_pending_roots} (awaiting cycle2 review)`,
1781
- `status: ${stats.byStatus.map(r => `${r.status ?? '?'}:${r.c}`).join(', ') || 'empty'}`,
1782
- `categories(active): ${stats.byCategory.map(r => `${r.category ?? 'NULL'}:${r.c}`).join(', ') || 'empty'} active_target_cap=${activeTargetCap}`,
1783
- `core_memory: user=${stats.core_entries} embed_null=${stats.core_embed_null} active_core=${stats.active_core_summaries} active_missing_core=${stats.active_core_summary_missing}`,
1784
- `embedding_index: ready dims=${dims}${dimsErr ? ` (meta_read_error: ${dimsErr})` : ''}`,
1785
- `recall_index: mv_hot_active=${mvState}`,
1786
- `bootstrap: ${bootstrapComplete ? 'complete' : 'incomplete'}`,
1787
- `last_cycle1: ${lastCycle1Ago}`,
1788
- `last_cycle2: ${lastCycle2Ago}`,
1789
- ...(last.cycle2_last_error ? [`last_cycle2_error: ${last.cycle2_last_error}`] : []),
1790
- ]
1791
- return { text: lines.join('\n') }
1792
- }
1793
-
1794
- async function _handleMemRebuild(args, config, signal) {
1795
- if (args.confirm !== 'REBUILD MEMORY') {
1796
- return { text: 'rebuild requires confirm: "REBUILD MEMORY" (truncates classification columns and re-runs cycles)', isError: true }
1797
- }
1798
- // Drain any pre-reset cycle1 BEFORE the destructive truncation so the
1799
- // post-reset run is not started concurrently against the same DB.
1800
- // _awaitCycle1Run() may release the outer handle on a caller deadline while
1801
- // the inner runCycle1 promise still owns the DB writes. Drain both layers,
1802
- // then loop once more if one layer exposed another promise while awaiting.
1803
- const drainedCycle1Promises = new Set()
1804
- for (;;) {
1805
- const pendingCycle1Promises = [_cycle1InFlight, getInFlightCycle1(db)]
1806
- .filter(p => p && !drainedCycle1Promises.has(p))
1807
- if (pendingCycle1Promises.length === 0) break
1808
- for (const pendingCycle1 of pendingCycle1Promises) {
1809
- drainedCycle1Promises.add(pendingCycle1)
1810
- try { await pendingCycle1 } catch {}
1811
- }
1812
- }
1813
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1814
- // Cleanup must run BEFORE demotion: the original order demoted normal
1815
- // roots (chunk_root = id) to is_root = 0 first, then ran the cleanup
1816
- // WHERE is_root = 1 — which missed exactly those demoted rows, leaving
1817
- // stale element/category/summary/score/embedding/summary_hash on rows that
1818
- // had just become raw leaves. Reorder so all roots get their classification
1819
- // columns cleared while is_root = 1 still selects them, then demote.
1820
- // Wrap the whole destructive sequence in one transaction so a mid-step
1821
- // failure rolls back rather than leaving a mixed state.
1822
- await db.transaction(async (tx) => {
1823
- await tx.query(`
1824
- UPDATE entries
1825
- SET element = NULL, category = NULL, summary = NULL,
1826
- status = 'pending', score = NULL, last_seen_at = NULL,
1827
- embedding = NULL, summary_hash = NULL,
1828
- core_summary = NULL, reviewed_at = NULL, promoted_at = NULL,
1829
- error_count = 0
1830
- WHERE is_root = 1
1831
- `)
1832
- await tx.query(`UPDATE entries SET chunk_root = NULL, is_root = 0 WHERE chunk_root = id`)
1833
- await tx.query(`UPDATE entries SET chunk_root = NULL WHERE is_root = 0`)
1834
- await tx.query(`
1835
- UPDATE entries
1836
- SET status = NULL,
1837
- element = NULL, category = NULL, summary = NULL,
1838
- score = NULL, last_seen_at = NULL,
1839
- embedding = NULL, summary_hash = NULL,
1840
- core_summary = NULL, reviewed_at = NULL, promoted_at = NULL,
1841
- error_count = 0
1842
- WHERE is_root = 0
1843
- `)
1844
- })
1845
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1846
- // Force a fresh post-reset cycle1: _cycle1InFlight is guaranteed null
1847
- // here (we drained above and have not awaited any cycle1-starting call
1848
- // since), so calling _startCycle1Run directly skips the coalesce branch
1849
- // inside _awaitCycle1Run and guarantees the newly demoted rows are read.
1850
- const r1 = await _startCycle1Run(config?.cycle1 || {}, { signal })
1851
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1852
- const r2 = await runCycle2(db, config?.cycle2 || {}, { signal }, DATA_DIR)
1853
- await _finalizeCycle2Run(r2)
1854
- return { text: `rebuild: cycle1 chunks=${r1.chunks} processed=${r1.processed}, cycle2 ${JSON.stringify(r2)}` }
1855
- }
1856
-
1857
- async function _handleMemPrune(args, config) {
1858
- if (args.confirm !== 'PRUNE OLD ENTRIES') {
1859
- return { text: 'prune requires confirm: "PRUNE OLD ENTRIES" (permanently deletes unclassified entries older than maxDays)', isError: true }
1860
- }
1861
- const days = Math.max(1, Number(args.maxDays ?? 30))
1862
- const result = await pruneOldEntries(db, days)
1863
- return { text: `prune: deleted ${result.deleted} unclassified entries older than ${days} days` }
1864
- }
1865
-
1866
- async function _handleMemBackfill(args, config, signal) {
1867
- // Whole-action mutex (transport-agnostic). _cycle1InFlight only protects
1868
- // cycle1; ingest workers + cycle2 can still overlap if a second backfill
1869
- // kicks in (timeout-retry, parallel callers, /api/tool vs /mcp vs
1870
- // /admin/backfill). Sentinel is set synchronously before any await so a
1871
- // burst of concurrent calls cannot all pass the check.
1872
- if (_backfillInFlight) {
1873
- return { text: 'backfill already in progress', isError: true }
1874
- }
1875
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1876
- const window = args.window != null ? String(args.window) : '7d'
1877
- const scope = args.scope != null ? String(args.scope) : 'all'
1878
- const limit = args.limit != null ? Math.max(1, Number(args.limit)) : null
1879
- // Capture the cycle2 envelope so we can route through _finalizeCycle2Run
1880
- // (which records cycle2_last_error and clears scheduler delay only on
1881
- // ok:true) rather than stamping cycle2 unconditionally afterward.
1882
- let _capturedCycle2
1883
- const promise = runFullBackfill(db, {
1884
- window,
1885
- scope,
1886
- limit,
1887
- config,
1888
- dataDir: DATA_DIR,
1889
- ingestTranscriptFile,
1890
- cwdFromTranscriptPath,
1891
- // Re-check the IPC cancel signal at every cycle1/cycle2 iteration the
1892
- // backfill driver dispatches. handleMemoryAction only checks once
1893
- // before dispatch; without per-iteration checkpoints a long-running
1894
- // backfill keeps spinning through ingest + cycle1 + cycle2 batches
1895
- // after the proxy has already responded "cancelled" to the caller.
1896
- runCycle1: (dbArg, cycle1Config = {}, options = {}, dir) => {
1897
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1898
- return _awaitCycle1Run(cycle1Config, { ...options, signal })
1899
- },
1900
- runCycle2: async (dbArg, c2Config, c2Options, c2DataDir) => {
1901
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1902
- const r2 = await runCycle2(dbArg, c2Config, { ...c2Options, signal }, c2DataDir)
1903
- _capturedCycle2 = r2
1904
- return r2
1905
- },
1906
- })
1907
- _backfillInFlight = promise
1908
- let result
1909
- try {
1910
- result = await promise
1911
- } finally {
1912
- if (_backfillInFlight === promise) _backfillInFlight = null
1913
- }
1914
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1915
- if (_capturedCycle2) {
1916
- await _finalizeCycle2Run(_capturedCycle2)
1917
- }
1918
- return {
1919
- text: `backfill: window=${result.window} scope=${result.scope} files=${result.files} ingested=${result.ingested} cycle1_iters=${result.cycle1_iters} promoted=${result.promoted} unclassified=${result.unclassified}`,
1920
- }
1921
- }
1922
-
1923
- async function handleMemoryAction(args, signal) {
1924
- // Cooperative abort check: surfaces caller-cancel (IPC cancel handler)
1925
- // before any long DB work begins on the worker side.
1926
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1927
- const action = String(args.action ?? '')
1928
- const config = readMainConfig()
1929
-
1930
- if (action === 'status') {
1931
- return _handleMemStatus(args, config)
1932
- }
1933
-
1934
- if (action === 'cycle1') {
1935
- return _handleMemCycle1(args, config, signal)
1936
- }
1937
-
1938
- if (action === 'cycle2' || action === 'sleep') {
1939
- return _handleMemCycle2(args, config, signal)
1940
- }
1941
-
1942
- if (action === 'cycle3') {
1943
- return _handleMemCycle3(args, config, signal)
1944
- }
1945
-
1946
- // Direct semantic-search surface for callers that want raw ranked rows
1947
- // without going through the Lead-side recall synthesizer. The
1948
- // handleSearch executor is exposed through the public `memory` tool action
1949
- // `search` so callers can hit the hybrid CTE directly.
1950
- if (action === 'search') {
1951
- return handleSearch(args, signal)
1952
- }
1953
-
1954
- if (action === 'flush') {
1955
- return _handleMemFlush(args, config, signal)
1956
- }
1957
-
1958
- if (action === 'rebuild') {
1959
- return _handleMemRebuild(args, config, signal)
1960
- }
1961
-
1962
- if (action === 'prune') {
1963
- return _handleMemPrune(args, config)
1964
- }
1965
-
1966
- if (action === 'backfill') {
1967
- return _handleMemBackfill(args, config, signal)
1968
- }
1969
-
1970
- if (action === 'manage') {
1971
- const op = String(args.op ?? '').trim().toLowerCase()
1972
- if (!['add', 'edit', 'delete'].includes(op)) {
1973
- return { text: 'manage requires op: "add" | "edit" | "delete"', isError: true }
1974
- }
1975
- const VALID_CAT = new Set(['rule', 'constraint', 'decision', 'fact', 'goal', 'preference', 'task', 'issue'])
1976
- const VALID_STATUS = new Set(['pending', 'active', 'archived'])
1977
-
1978
- if (op === 'add') {
1979
- const element = String(args.element ?? '').trim()
1980
- const summary = String(args.summary ?? args.element ?? '').trim()
1981
- const category = String(args.category ?? 'fact').trim().toLowerCase()
1982
- if (!element || !summary) {
1983
- return { text: 'manage add requires element and summary', isError: true }
1984
- }
1985
- if (!VALID_CAT.has(category)) {
1986
- return { text: `manage add: invalid category "${category}". Valid: ${[...VALID_CAT].join(', ')}`, isError: true }
1987
- }
1988
- const nowMs = Date.now()
1989
- const sourceRef = `manual:${nowMs}-${process.pid}`
1990
- const manageProjectId = resolveProjectScope(typeof args.cwd === 'string' && args.cwd ? args.cwd : null)
1991
- try {
1992
- let newId
1993
- await db.transaction(async (tx) => {
1994
- const result = await tx.query(`
1995
- INSERT INTO entries(ts, role, content, source_ref, session_id, project_id)
1996
- VALUES ($1, 'system', $2, $3, NULL, $4)
1997
- RETURNING id
1998
- `, [nowMs, element + ' — ' + summary, sourceRef, manageProjectId])
1999
- newId = Number(result.rows[0].id)
2000
- const score = computeEntryScore(category, nowMs, nowMs)
2001
- await tx.query(`
2002
- UPDATE entries
2003
- SET chunk_root = $1, is_root = 1, element = $2, category = $3, summary = $4,
2004
- status = 'active', score = $5, last_seen_at = $6
2005
- WHERE id = $7
2006
- `, [newId, element, category, summary, score, nowMs, newId])
2007
- })
2008
- await syncRootEmbedding(db, newId)
2009
- return { text: `added (id=${newId}): [${category}] ${element} — ${summary.slice(0, 200)}` }
2010
- } catch (e) {
2011
- return { text: `manage add failed: ${e.message}`, isError: true }
2012
- }
2013
- }
2014
-
2015
- if (op === 'edit') {
2016
- const id = Number(args.id)
2017
- if (!Number.isFinite(id) || id <= 0) {
2018
- return { text: 'manage edit requires numeric id', isError: true }
2019
- }
2020
- const existing = (await db.query(
2021
- `SELECT id, element, summary, category, status, ts, is_root FROM entries WHERE id = $1`,
2022
- [id]
2023
- )).rows[0]
2024
- if (!existing) return { text: `manage edit: no entry with id=${id}`, isError: true }
2025
- if (existing.is_root !== 1) return { text: `manage edit: id=${id} is not a root`, isError: true }
2026
-
2027
- const trimOrNull = v => {
2028
- if (v == null) return null
2029
- const s = String(v).trim()
2030
- return s === '' ? null : s
2031
- }
2032
- const newElement = trimOrNull(args.element)
2033
- const newSummary = trimOrNull(args.summary)
2034
- const newCategory = trimOrNull(args.category)?.toLowerCase() ?? null
2035
- const newStatus = trimOrNull(args.status)?.toLowerCase() ?? null
2036
-
2037
- if (!newElement && !newSummary && !newCategory && !newStatus) {
2038
- return { text: 'manage edit requires at least one field: element, summary, category, status', isError: true }
2039
- }
2040
- if (newCategory && !VALID_CAT.has(newCategory)) {
2041
- return { text: `manage edit: invalid category "${newCategory}". Valid: ${[...VALID_CAT].join(', ')}`, isError: true }
2042
- }
2043
- if (newStatus && !VALID_STATUS.has(newStatus)) {
2044
- return { text: `manage edit: invalid status "${newStatus}". Valid: ${[...VALID_STATUS].join(', ')}`, isError: true }
2045
- }
2046
-
2047
- const finalElement = newElement ?? existing.element
2048
- const finalSummary = newSummary ?? existing.summary
2049
- const finalCategory = newCategory ?? existing.category
2050
- const finalStatus = newStatus ?? existing.status
2051
- const nowMs = Date.now()
2052
- const score = computeEntryScore(finalCategory, nowMs, nowMs)
2053
- const textChanged = newElement != null || newSummary != null
2054
- // Guard null element/summary: a category/status-only edit on a root
2055
- // whose element or summary is NULL would otherwise persist literal
2056
- // 'null — null' content and explode on finalSummary.slice() below.
2057
- // Use empty-string sentinels for the content composition + render so
2058
- // the row stays consistent with what's actually stored.
2059
- const elementStr = finalElement == null ? '' : String(finalElement)
2060
- const summaryStr = finalSummary == null ? '' : String(finalSummary)
2061
- const composedContent = elementStr || summaryStr
2062
- ? `${elementStr}${summaryStr ? ' — ' + summaryStr : ''}`
2063
- : ''
2064
-
2065
- try {
2066
- await db.query(`
2067
- UPDATE entries
2068
- SET element = $1, summary = $2, category = $3, status = $4, score = $5,
2069
- last_seen_at = $6, content = $7
2070
- WHERE id = $8
2071
- `, [finalElement, finalSummary, finalCategory, finalStatus, score,
2072
- nowMs, composedContent, id])
2073
- } catch (e) {
2074
- return { text: `manage edit failed: ${e.message}`, isError: true }
2075
- }
2076
- if (textChanged) {
2077
- try { await syncRootEmbedding(db, id) } catch (e) {
2078
- process.stderr.write(`[memory.manage] embedding resync failed (id=${id}): ${e.message}\n`)
2079
- }
2080
- }
2081
- return { text: `edited (id=${id}): [${finalCategory}/${finalStatus}] ${elementStr}${summaryStr ? ' — ' + summaryStr.slice(0, 200) : ''}` }
2082
- }
2083
-
2084
- if (op === 'delete') {
2085
- const id = Number(args.id)
2086
- if (!Number.isFinite(id) || id <= 0) {
2087
- return { text: 'manage delete requires numeric id', isError: true }
2088
- }
2089
- const info = (await db.query(
2090
- `SELECT id, category, element, is_root FROM entries WHERE id = $1`,
2091
- [id]
2092
- )).rows[0]
2093
- if (!info) return { text: `manage delete: no entry with id=${id}`, isError: true }
2094
- try {
2095
- const result = info.is_root === 1
2096
- ? await db.query(`DELETE FROM entries WHERE id = $1 OR chunk_root = $2`, [id, id])
2097
- : await db.query(`DELETE FROM entries WHERE id = $1`, [id])
2098
- return { text: `deleted (id=${id}, rows=${Number(result.rowCount ?? result.affectedRows ?? 0)}): [${info.category ?? '-'}] ${info.element ?? ''}` }
2099
- } catch (e) {
2100
- return { text: `manage delete failed: ${e.message}`, isError: true }
2101
- }
2102
- }
2103
-
2104
- return { text: `manage: unhandled op "${op}"`, isError: true }
2105
- }
2106
-
2107
- if (action === 'core') {
2108
- const op = String(args.op ?? '').trim().toLowerCase()
2109
- if (!['add', 'edit', 'delete', 'list'].includes(op)) {
2110
- return { text: 'core requires op: "add" | "edit" | "delete" | "list"', isError: true }
2111
- }
2112
- const dataDir = process.env.CLAUDE_PLUGIN_DATA || (typeof DATA_DIR === 'string' ? DATA_DIR : null)
2113
- if (!dataDir) return { text: 'core: CLAUDE_PLUGIN_DATA unset', isError: true }
2114
- // Local trim helper — the manage-block trimOrNull at :1807 is scoped to
2115
- // that branch and unreachable from here.
2116
- // Normalize project_id: 'common' (case-insensitive) or null → null (COMMON pool); non-empty string → slug.
2117
- const hasProjectIdKey = Object.prototype.hasOwnProperty.call(args, 'project_id')
2118
- const projectId = (() => {
2119
- if (!hasProjectIdKey || args.project_id == null) return null
2120
- const s = String(args.project_id).trim()
2121
- if (s === '' || s.toLowerCase() === 'common') return null
2122
- if (s === '*') return '*'
2123
- return s
2124
- })()
2125
- try {
2126
- if (projectId === '*' && op !== 'list') {
2127
- return { text: `core ${op}: project_id "*" only valid for op="list"`, isError: true }
2128
- }
2129
- if (op === 'list') {
2130
- if (projectId !== '*') {
2131
- const entries = await listCore(dataDir, projectId)
2132
- if (entries.length === 0) return { text: 'core: empty' }
2133
- return { text: entries.map(e => `id=${e.id} [${e.category}] ${e.element} — ${String(e.summary || '').slice(0, 200)}`).join('\n') }
2134
- }
2135
- // Cross-pool listing — group by project_id, COMMON first
2136
- const entries = await listCore(dataDir, '*')
2137
- if (entries.length === 0) return { text: 'core: empty' }
2138
- const groups = new Map()
2139
- for (const e of entries) {
2140
- const key = e.project_id ?? null
2141
- if (!groups.has(key)) groups.set(key, [])
2142
- groups.get(key).push(e)
2143
- }
2144
- const lines = []
2145
- for (const [key, rows] of groups) {
2146
- lines.push(`${key === null ? 'COMMON' : key}:`)
2147
- for (const e of rows) {
2148
- lines.push(` id=${e.id} [${e.category}] ${e.element} — ${String(e.summary || '').slice(0, 200)}`)
2149
- }
2150
- }
2151
- return { text: lines.join('\n') }
2152
- }
2153
- if (op === 'add') {
2154
- if (!hasProjectIdKey) {
2155
- return { text: 'core add: project_id required — pass "common" for COMMON pool, or project slug like "owner/repo" for scoped pool', isError: true }
2156
- }
2157
- const entry = await addCore(dataDir, args, projectId)
2158
- return { text: `core added (id=${entry.id}): [${entry.category}] ${entry.element} — ${entry.summary.slice(0, 200)}` }
2159
- }
2160
- if (op === 'edit') {
2161
- const entry = await editCore(dataDir, args.id, args)
2162
- return { text: `core edited (id=${entry.id}): [${entry.category}] ${entry.element} — ${entry.summary.slice(0, 200)}` }
2163
- }
2164
- if (op === 'delete') {
2165
- const removed = await deleteCore(dataDir, args.id)
2166
- return { text: `core deleted (id=${removed.id}): [${removed.category}] ${removed.element}` }
2167
- }
2168
- } catch (e) {
2169
- return { text: `core ${op} failed: ${e.message}`, isError: true }
2170
- }
2171
- return { text: `core: unhandled op "${op}"`, isError: true }
2172
- }
2173
-
2174
- if (action === 'purge') {
2175
- if (args.confirm !== 'DELETE ALL MEMORY') {
2176
- return { text: 'purge requires confirm: "DELETE ALL MEMORY"', isError: true }
2177
- }
2178
- const preCount = (await db.query(`SELECT COUNT(*) c FROM entries`)).rows[0].c
2179
- const coreCount = (await db.query(`SELECT COUNT(*) c FROM core_entries`)).rows[0].c
2180
- try {
2181
- await db.query(`DELETE FROM entries`)
2182
- } catch (e) {
2183
- return { text: `purge failed: ${e.message}`, isError: true }
2184
- }
2185
- return { text: `purged generated memory entries (count=${preCount}); user core preserved (core_entries=${coreCount})` }
2186
- }
2187
-
2188
- if (action === 'retro_eval_active') {
2189
- if (args.confirm !== 'REEVAL ACTIVE') {
2190
- return { text: 'retro_eval_active requires confirm: "REEVAL ACTIVE" (heavy LLM batch op — reviews all active roots through the unified gate)', isError: true }
2191
- }
2192
- const RETRO_BATCH = 50
2193
- const cycle2Config = config?.cycle2 || {}
2194
- const allActive = (await db.query(
2195
- `SELECT id, element, category, summary, score, last_seen_at, project_id, status
2196
- FROM entries WHERE is_root = 1 AND status = 'active'
2197
- ORDER BY reviewed_at ASC, id ASC`
2198
- )).rows
2199
- const total = allActive.length
2200
- let archived = 0, kept = 0, updated = 0, merged = 0, errors = 0
2201
- const nowMs = Date.now()
2202
- for (let offset = 0; offset < total; offset += RETRO_BATCH) {
2203
- const batch = allActive.slice(offset, offset + RETRO_BATCH)
2204
- const batchIds = batch.map(r => Number(r.id))
2205
- const activeContext = (await db.query(
2206
- `SELECT id, element, category, summary, score, last_seen_at, project_id, status
2207
- FROM entries WHERE is_root = 1 AND status = 'active'
2208
- ORDER BY score DESC, last_seen_at DESC, id ASC LIMIT 200`
2209
- )).rows
2210
- let gateResult
2211
- try {
2212
- gateResult = await runUnifiedGate(db, batch, activeContext, cycle2Config, { activeCap: 200 })
2213
- } catch (err) {
2214
- process.stderr.write(`[retro_eval_active] runUnifiedGate failed (offset=${offset}): ${err.message}\n`)
2215
- errors += batch.length
2216
- continue
2217
- }
2218
- if (gateResult?.parseOk === false || gateResult?.actions === null) {
2219
- errors += batch.length
2220
- continue
2221
- }
2222
- const actions = gateResult?.actions ?? []
2223
- // Separate explicit `core` summary lines from primary verbs so an
2224
- // update/merge/active also refreshes the injected core_summary — mirrors
2225
- // the cycle2 apply path (memory-cycle2.mjs). Without this, retro could
2226
- // rewrite a root's summary while leaving its core_summary stale.
2227
- const coreSummaryById = new Map()
2228
- const primaryActions = []
2229
- for (const a of actions) {
2230
- if (a?.action === 'core') {
2231
- const cid = Number(a.entry_id)
2232
- const core = String(a.core_summary ?? '').replace(/\s+/g, ' ').trim().slice(0, CORE_SUMMARY_MAX)
2233
- if (Number.isFinite(cid) && core) coreSummaryById.set(cid, core)
2234
- } else {
2235
- primaryActions.push(a)
2236
- }
2237
- }
2238
- const allowed = new Set(batchIds)
2239
- const rejected = gateResult?.rejected ?? new Set()
2240
- // Partial-apply contract: rows the gate never returned a verdict for
2241
- // (missingIds) must NOT be marked reviewed — they are left for a later
2242
- // run. Exclude both rejected and missing ids from the reviewed set.
2243
- const missing = new Set((gateResult?.missingIds ?? []).map(Number))
2244
- const successIds = new Set(batchIds.filter(id => !rejected.has(id) && !missing.has(id)))
2245
- for (const id of successIds) {
2246
- try { await db.query(`UPDATE entries SET reviewed_at = $1 WHERE id = $2`, [nowMs, id]) } catch {}
2247
- }
2248
- const setCoreSummary = async (entryId, core) => {
2249
- if (!core) return
2250
- try { await db.query(`UPDATE entries SET core_summary = $1 WHERE id = $2 AND is_root = 1`, [core, Number(entryId)]) }
2251
- catch (err) { process.stderr.write(`[retro_eval_active] core_summary update failed (id=${entryId}): ${err.message}\n`) }
2252
- }
2253
- if (!primaryActions.length) { kept += batch.filter(r => successIds.has(Number(r.id))).length; continue }
2254
- const acted = new Set()
2255
- for (const act of primaryActions) {
2256
- try {
2257
- const eid = Number(act?.entry_id)
2258
- if (!Number.isFinite(eid) || !allowed.has(eid)) continue
2259
- acted.add(eid)
2260
- if (act.action === 'archived') {
2261
- if (await applySimpleStatus(db, eid, 'archived')) archived += 1
2262
- } else if (act.action === 'active') {
2263
- // active → active is a keep verdict from the gate.
2264
- kept += 1
2265
- await setCoreSummary(eid, coreSummaryById.get(eid))
2266
- } else if (act.action === 'update') {
2267
- if (await applyUpdate(db, eid, act.element, act.summary)) updated += 1
2268
- await setCoreSummary(eid, coreSummaryById.get(eid))
2269
- } else if (act.action === 'merge') {
2270
- const targetId = Number(act?.target_id)
2271
- const sourceIds = Array.isArray(act?.source_ids) ? act.source_ids : []
2272
- if (!Number.isFinite(targetId) || !allowed.has(targetId)) {
2273
- process.stderr.write(`[retro_eval_active] merge target outside batch (id=${targetId})\n`)
2274
- acted.delete(eid)
2275
- continue
2276
- }
2277
- const filteredSources = sourceIds.filter(s => allowed.has(Number(s)))
2278
- if (filteredSources.length !== sourceIds.length) {
2279
- process.stderr.write(
2280
- `[retro_eval_active] merge sources filtered: ${JSON.stringify(sourceIds)} -> ${JSON.stringify(filteredSources)}\n`,
2281
- )
2282
- }
2283
- acted.add(targetId)
2284
- filteredSources.forEach(s => acted.add(Number(s)))
2285
- const moved = await applyMerge(db, targetId, filteredSources)
2286
- if (moved > 0) {
2287
- merged += moved
2288
- if (typeof act.element === 'string' || typeof act.summary === 'string') {
2289
- try {
2290
- if (await applyUpdate(db, targetId, act.element, act.summary)) updated += 1
2291
- } catch (err) {
2292
- process.stderr.write(`[retro_eval_active] merge target update failed (target=${targetId}): ${err.message}\n`)
2293
- }
2294
- }
2295
- await setCoreSummary(targetId, coreSummaryById.get(targetId) || coreSummaryById.get(eid))
2296
- }
2297
- }
2298
- } catch (err) {
2299
- process.stderr.write(`[retro_eval_active] action error (id=${act?.entry_id}): ${err.message}\n`)
2300
- errors += 1
2301
- }
2302
- }
2303
- // Entries in successIds but not acted-upon (omit / no-op) are kept.
2304
- kept += batch.filter(r => successIds.has(Number(r.id)) && !acted.has(Number(r.id))).length
2305
- }
2306
- return { text: `retro_eval_active: total=${total} archived=${archived} kept=${kept} updated=${updated} merged=${merged} errors=${errors}` }
2307
- }
2308
-
2309
- return { text: `unknown memory action: ${action}`, isError: true }
2310
- }
2311
-
2312
- async function handleToolCall(name, args, signal) {
2313
- try {
2314
- if (name === 'search_memories') {
2315
- const result = await handleSearch(args || {}, signal)
2316
- return { content: [{ type: 'text', text: result.text }], isError: result.isError || false }
2317
- }
2318
- if (name === 'recall') {
2319
- // recall is aiWrapped in the unified build; in standalone mode map it to
2320
- // search_memories so the advertised tool name actually works. Forward
2321
- // every advertised arg so id/limit/offset/sort/includeArchived/
2322
- // includeMembers/includeRaw reach handleSearch instead of being dropped.
2323
- const a = args || {}
2324
- const searchArgs = {
2325
- ...(a.query !== undefined ? { query: a.query } : {}),
2326
- ...(a.id !== undefined ? { ids: Array.isArray(a.id) ? a.id : [a.id] } : {}),
2327
- ...(a.period ? { period: a.period } : {}),
2328
- ...(a.limit !== undefined ? { limit: a.limit } : {}),
2329
- ...(a.offset !== undefined ? { offset: a.offset } : {}),
2330
- ...(a.sort !== undefined ? { sort: a.sort } : {}),
2331
- ...(a.category !== undefined ? { category: a.category } : {}),
2332
- ...(a.includeArchived !== undefined ? { includeArchived: a.includeArchived } : {}),
2333
- ...(a.includeMembers !== undefined ? { includeMembers: a.includeMembers } : {}),
2334
- ...(a.includeRaw !== undefined ? { includeRaw: a.includeRaw } : {}),
2335
- ...(a.cwd ? { cwd: a.cwd } : {}),
2336
- ...(a.projectScope ? { projectScope: a.projectScope } : {}),
2337
- }
2338
- const result = await handleSearch(searchArgs, signal)
2339
- return { content: [{ type: 'text', text: result.text }], isError: result.isError || false }
2340
- }
2341
- if (name === 'memory') {
2342
- const result = await handleMemoryAction(args || {}, signal)
2343
- return { content: [{ type: 'text', text: result.text }], isError: result.isError || false }
2344
- }
2345
- return { content: [{ type: 'text', text: `unknown tool: ${name}` }], isError: true }
2346
- } catch (err) {
2347
- const msg = err instanceof Error ? err.message : String(err)
2348
- return { content: [{ type: 'text', text: `${name} failed: ${msg}` }], isError: true }
2349
- }
2350
- }
2351
-
2352
- const mcp = new Server(
2353
- { name: 'mixdog-memory', version: PLUGIN_VERSION },
2354
- { capabilities: { tools: {} }, instructions: MEMORY_INSTRUCTIONS_TEXT },
2355
- )
2356
- mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_DEFS }))
2357
- mcp.setRequestHandler(CallToolRequestSchema, (req) => handleToolCall(req.params.name, req.params.arguments ?? {}))
2358
-
2359
- function createHttpMcpServer() {
2360
- const s = new Server(
2361
- { name: 'mixdog-memory', version: PLUGIN_VERSION },
2362
- { capabilities: { tools: {} }, instructions: MEMORY_INSTRUCTIONS_TEXT },
2363
- )
2364
- s.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_DEFS }))
2365
- s.setRequestHandler(CallToolRequestSchema, (req) => handleToolCall(req.params.name, req.params.arguments ?? {}))
2366
- return s
2367
- }
2368
-
2369
- function readBody(req) {
2370
- return new Promise((resolve, reject) => {
2371
- const chunks = []
2372
- req.on('data', c => chunks.push(c))
2373
- req.on('end', () => {
2374
- const raw = Buffer.concat(chunks).toString('utf8').trim()
2375
- if (!raw) { resolve({}); return }
2376
- try { resolve(JSON.parse(raw)) }
2377
- catch (error) {
2378
- const e = new Error(`invalid JSON body: ${error.message}`)
2379
- e.statusCode = 400
2380
- reject(e)
2381
- }
2382
- })
2383
- req.on('error', reject)
2384
- })
2385
- }
2386
-
2387
- function sendJson(res, data, status = 200) {
2388
- const body = JSON.stringify(data)
2389
- res.writeHead(status, {
2390
- 'Content-Type': 'application/json; charset=utf-8',
2391
- 'Content-Length': Buffer.byteLength(body),
2392
- })
2393
- res.end(body)
2394
- }
2395
-
2396
- function sendError(res, msg, status = 500) {
2397
- sendJson(res, { error: msg }, status)
2398
- }
2399
-
2400
- async function awaitRuntimeReadyForHttp(res) {
2401
- if (_initialized) return true
2402
- if (!_initPromise) {
2403
- sendJson(res, { error: 'memory runtime is starting' }, 503)
2404
- return false
2405
- }
2406
- try {
2407
- await _initPromise
2408
- return true
2409
- } catch (e) {
2410
- sendJson(res, { error: `memory runtime failed: ${e?.message || e}` }, 503)
2411
- return false
2412
- }
2413
- }
2414
-
2415
- // Origin/Referer guard for /admin/* mutation routes. Memory-service binds
2416
- // 127.0.0.1, but browser DNS-rebinding or a stray cross-origin fetch could
2417
- // still reach destructive endpoints (purge, backfill, entry mutations).
2418
- // Server-to-server callers (setup-server, hooks) issue raw http.request
2419
- // without a browser Origin/Referer, so absent headers pass; any non-loopback
2420
- // Origin/Referer is rejected. Mirrors setup-server.mjs isAllowedOrigin.
2421
- function isLocalOrigin(req) {
2422
- const LOOP = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?(\/|$)/i
2423
- const origin = req.headers.origin || ''
2424
- const referer = req.headers.referer || ''
2425
- if (origin && !LOOP.test(origin)) return false
2426
- if (referer && !LOOP.test(referer)) return false
2427
- return true
2428
- }
2429
-
2430
- function normalizeCoreProjectId(value, { allowStar = false } = {}) {
2431
- if (value == null) return null
2432
- const s = String(value).trim()
2433
- if (!s || s.toLowerCase() === 'common') return null
2434
- if (allowStar && s === '*') return '*'
2435
- return s
2436
- }
2437
-
2438
- async function buildSessionCoreMemoryPayload(cwd) {
2439
- const projectId = resolveProjectScope(typeof cwd === 'string' && cwd ? cwd : null)
2440
- const generatedScopeClause = projectId !== null
2441
- ? `project_id IS NULL OR project_id = $1`
2442
- : `project_id IS NULL`
2443
- const dbRows = (await db.query(`
2444
- SELECT core_summary
2445
- FROM entries
2446
- WHERE is_root = 1
2447
- AND status = 'active'
2448
- AND core_summary IS NOT NULL
2449
- AND (${generatedScopeClause})
2450
- ORDER BY score DESC, last_seen_at DESC
2451
- `, projectId !== null ? [projectId] : [])).rows
2452
- const commonRows = (await db.query(
2453
- `SELECT summary FROM core_entries WHERE project_id IS NULL ORDER BY id ASC`
2454
- )).rows
2455
- const scopedRows = projectId !== null
2456
- ? (await db.query(
2457
- `SELECT summary FROM core_entries WHERE project_id = $1 ORDER BY id ASC`,
2458
- [projectId]
2459
- )).rows
2460
- : []
2461
- return {
2462
- projectId,
2463
- dbLines: dbRows.map(r => String(r.core_summary || '').trim()).filter(Boolean),
2464
- userLines: [
2465
- ...commonRows.map(r => String(r.summary || '').trim()).filter(Boolean),
2466
- ...scopedRows.map(r => String(r.summary || '').trim()).filter(Boolean),
2467
- ],
2468
- }
2469
- }
2470
-
2471
- // Whole-action backfill mutex. memory-cycle1's _cycle1InFlight only protects
2472
- // cycle1; ingest workers (memory-ops-policy.mjs) and cycle2 can still overlap
2473
- // if a second backfill kicks in (e.g. setup-server timeout + retry). Track the
2474
- // in-flight promise here and reject overlaps with 409.
2475
- let _backfillInFlight = null
2476
-
2477
- // Owner-side /api/tool in-flight controllers keyed by caller-supplied
2478
- // X-Mixdog-Call-Id. /api/cancel aborts the matching AbortSignal so the
2479
- // upstream handleToolCall actually stops when the fork-proxy parent cancels.
2480
- const _ownerInFlightHttpCalls = new Map()
2481
-
2482
- const httpServer = http.createServer(async (req, res) => {
2483
- if (req.method === 'POST' && req.url === '/session-reset') {
2484
- _bootTimestamp = Date.now()
2485
- sendJson(res, { ok: true, bootTimestamp: _bootTimestamp })
2486
- return
2487
- }
2488
- if (req.method === 'POST' && req.url === '/rebind') {
2489
- _bootTimestamp = Date.now()
2490
- sendJson(res, { ok: true })
2491
- return
2492
- }
2493
-
2494
- if (req.method === 'GET' && req.url === '/health') {
2495
- if (!_initialized) {
2496
- sendJson(res, { status: 'starting' }, 503)
2497
- return
2498
- }
2499
- try {
2500
- const stats = await entryStats()
2501
- sendJson(res, {
2502
- status: 'ok',
2503
- worker_pid: process.pid,
2504
- server_pid: Number(process.env.MIXDOG_SERVER_PID) || null,
2505
- owner_lead_pid: Number(process.env.MIXDOG_OWNER_LEAD_PID) || null,
2506
- code_fingerprint: BOOT_PROMOTION_CODE_FINGERPRINT,
2507
- bootstrap: await isBootstrapComplete(db),
2508
- entries: stats.total,
2509
- roots: stats.roots,
2510
- active_roots: stats.active_roots,
2511
- archived_roots: stats.archived_roots,
2512
- unchunked_leaves: stats.unchunked_leaves,
2513
- cycle2_pending_roots: stats.cycle2_pending_roots,
2514
- core_entries: stats.core_entries,
2515
- core_embed_null: stats.core_embed_null,
2516
- active_core_summaries: stats.active_core_summaries,
2517
- active_core_summary_missing: stats.active_core_summary_missing,
2518
- mv_hot_active_populated: stats.mv_hot_active_populated,
2519
- })
2520
- } catch (e) { sendError(res, e.message) }
2521
- return
2522
- }
2523
-
2524
- if (!await awaitRuntimeReadyForHttp(res)) return
2525
-
2526
- if (req.method === 'GET' && req.url === '/admin/entries/active') {
2527
- try {
2528
- const { rows } = await db.query(`
2529
- SELECT id, element, category, summary, score, last_seen_at
2530
- FROM entries
2531
- WHERE is_root = 1 AND status = 'active'
2532
- ORDER BY score DESC
2533
- `)
2534
- sendJson(res, { ok: true, items: rows })
2535
- } catch (e) { sendJson(res, { ok: false, error: e.message }, 500) }
2536
- return
2537
- }
2538
-
2539
- if (req.method === 'GET' && req.url === '/admin/core/entries') {
2540
- try {
2541
- const rows = await listCore(DATA_DIR, '*')
2542
- sendJson(res, { ok: true, items: rows })
2543
- } catch (e) { sendJson(res, { ok: false, error: e.message }, 500) }
2544
- return
2545
- }
2546
-
2547
- if (req.method === 'POST' && req.url === '/admin/core/entries') {
2548
- if (!isLocalOrigin(req)) {
2549
- sendJson(res, { ok: false, error: 'forbidden: cross-origin' }, 403)
2550
- return
2551
- }
2552
- try {
2553
- const body = await readBody(req)
2554
- const projectId = normalizeCoreProjectId(body.project_id)
2555
- const entry = await addCore(DATA_DIR, body, projectId)
2556
- sendJson(res, { ok: true, item: entry })
2557
- } catch (e) { sendJson(res, { ok: false, error: e.message }, 500) }
2558
- return
2559
- }
2560
-
2561
- if (req.method === 'POST' && req.url === '/admin/core/entries/delete') {
2562
- if (!isLocalOrigin(req)) {
2563
- sendJson(res, { ok: false, error: 'forbidden: cross-origin' }, 403)
2564
- return
2565
- }
2566
- try {
2567
- const body = await readBody(req)
2568
- const removed = await deleteCore(DATA_DIR, body.id)
2569
- sendJson(res, { ok: true, item: removed })
2570
- } catch (e) { sendJson(res, { ok: false, error: e.message }, 500) }
2571
- return
2572
- }
2573
-
2574
- if (req.method === 'POST' && req.url === '/admin/entries/status') {
2575
- if (!isLocalOrigin(req)) {
2576
- sendJson(res, { ok: false, error: 'forbidden: cross-origin' }, 403)
2577
- return
2578
- }
2579
- try {
2580
- const body = await readBody(req)
2581
- const id = Number(body.id)
2582
- const status = String(body.status ?? '').trim().toLowerCase()
2583
- const VALID = ['pending', 'active', 'archived']
2584
- if (!Number.isInteger(id) || id <= 0 || !VALID.includes(status)) {
2585
- sendJson(res, { ok: false, error: 'valid id and status required' }, 400)
2586
- return
2587
- }
2588
- const result = await db.query(
2589
- `UPDATE entries SET status = $1 WHERE id = $2 AND is_root = 1`,
2590
- [status, id]
2591
- )
2592
- sendJson(res, { ok: true, changes: Number(result.rowCount ?? result.affectedRows ?? 0) })
2593
- } catch (e) { sendJson(res, { ok: false, error: e.message }, 500) }
2594
- return
2595
- }
2596
-
2597
- if (req.method === 'POST' && req.url === '/admin/entries/add') {
2598
- if (!isLocalOrigin(req)) {
2599
- sendJson(res, { ok: false, error: 'forbidden: cross-origin' }, 403)
2600
- return
2601
- }
2602
- try {
2603
- const body = await readBody(req)
2604
- const result = await handleMemoryAction({
2605
- action: 'manage',
2606
- op: 'add',
2607
- element: body.element,
2608
- summary: body.summary,
2609
- category: body.category,
2610
- cwd: body.cwd,
2611
- })
2612
- if (result.isError) {
2613
- sendJson(res, { ok: false, error: result.text }, 400)
2614
- return
2615
- }
2616
- const idMatch = String(result.text || '').match(/id=(\d+)/)
2617
- const newId = idMatch ? Number(idMatch[1]) : null
2618
- sendJson(res, { ok: true, id: newId, text: result.text })
2619
- } catch (e) { sendJson(res, { ok: false, error: e.message }, 500) }
2620
- return
2621
- }
2622
-
2623
- if (req.method === 'POST' && req.url === '/admin/backfill') {
2624
- if (!isLocalOrigin(req)) {
2625
- sendJson(res, { ok: false, error: 'forbidden: cross-origin' }, 403)
2626
- return
2627
- }
2628
- let body
2629
- try { body = await readBody(req) }
2630
- catch (e) { sendJson(res, { ok: false, error: e.message }, Number(e?.statusCode) || 500); return }
2631
- try {
2632
- const result = await handleMemoryAction({
2633
- action: 'backfill',
2634
- window: body.window,
2635
- scope: body.scope,
2636
- limit: body.limit,
2637
- })
2638
- if (result.isError) {
2639
- // 'backfill already in progress' → 409, other failures → 500
2640
- const status = result.text === 'backfill already in progress' ? 409 : 500
2641
- sendJson(res, { ok: false, error: result.text }, status)
2642
- return
2643
- }
2644
- sendJson(res, { ok: true, text: result.text })
2645
- } catch (e) {
2646
- sendJson(res, { ok: false, error: e.message }, 500)
2647
- }
2648
- return
2649
- }
2650
-
2651
- if (req.method === 'POST' && req.url === '/admin/purge') {
2652
- if (!isLocalOrigin(req)) {
2653
- sendJson(res, { ok: false, error: 'forbidden: cross-origin' }, 403)
2654
- return
2655
- }
2656
- try {
2657
- const body = await readBody(req)
2658
- if (body?.confirm !== 'DELETE ALL MEMORY') {
2659
- sendJson(res, { ok: false, error: 'confirm must be exactly "DELETE ALL MEMORY"' }, 400)
2660
- return
2661
- }
2662
- const { rows: countRows } = await db.query(`SELECT COUNT(*) AS c FROM entries`)
2663
- const preCount = Number(countRows[0].c)
2664
- const { rows: coreCountRows } = await db.query(`SELECT COUNT(*) AS c FROM core_entries`)
2665
- const coreCount = Number(coreCountRows[0].c)
2666
- await db.transaction(async (tx) => {
2667
- await tx.query(`DELETE FROM entries`)
2668
- })
2669
- sendJson(res, { ok: true, deleted: preCount, core_preserved: coreCount })
2670
- } catch (e) { sendJson(res, { ok: false, error: e.message }, 500) }
2671
- return
2672
- }
2673
-
2674
- if (req.method === 'POST' && req.url === '/admin/trace-record') {
2675
- if (!isLocalOrigin(req)) {
2676
- sendJson(res, { ok: false, error: 'forbidden: cross-origin' }, 403)
2677
- return
2678
- }
2679
- let body
2680
- try { body = await readBody(req) }
2681
- catch (e) { sendJson(res, { ok: false, error: e.message }, 400); return }
2682
- if (!Array.isArray(body?.events)) {
2683
- sendJson(res, { ok: false, error: 'body.events must be an array' }, 400)
2684
- return
2685
- }
2686
- if (body.events.length > 500) {
2687
- sendJson(res, { ok: false, error: 'too many events (max 500)' }, 413)
2688
- return
2689
- }
2690
- if (!_traceDb) {
2691
- try {
2692
- _traceDb = await openTraceDatabase(DATA_DIR)
2693
- registerTraceExitDrain(_traceDb)
2694
- } catch (e) {
2695
- sendJson(res, { ok: false, error: `trace DB unavailable: ${e.message}` }, 503)
2696
- return
2697
- }
2698
- }
2699
- try {
2700
- // Enqueue for async batched flush (100ms / 500-row window).
2701
- enqueueTraceEvents(_traceDb, body.events)
2702
- // Use `queued` — events are async; `inserted` would imply durability.
2703
- sendJson(res, { ok: true, queued: body.events.length })
2704
- // Fire-and-forget into focused bridge analytic tables.
2705
- insertBridgeCalls(_traceDb, body.events).catch(e =>
2706
- process.stderr.write(`[trace] insertBridgeCalls error: ${e?.message}\n`)
2707
- )
2708
- } catch (e) {
2709
- sendJson(res, { ok: false, error: e.message }, 500)
2710
- }
2711
- return
2712
- }
2713
-
2714
- if (req.method === 'POST' && req.url === '/session-start/core-memory') {
2715
- try {
2716
- const body = await readBody(req)
2717
- const { projectId, dbLines, userLines } = await buildSessionCoreMemoryPayload(body.cwd)
2718
- sendJson(res, { ok: true, projectId, dbLines, userLines })
2719
- } catch (e) { sendError(res, e.message) }
2720
- return
2721
- }
2722
-
2723
- if (req.method === 'POST' && req.url === '/admin/shutdown') {
2724
- if (!isLocalOrigin(req)) {
2725
- sendJson(res, { ok: false, error: 'forbidden: cross-origin' }, 403)
2726
- return
2727
- }
2728
- sendJson(res, { shutting_down: true }, 202)
2729
- setImmediate(() => {
2730
- const watchdog = setTimeout(() => {
2731
- process.stderr.write('[shutdown] watchdog fired — forcing exit after 8s\n')
2732
- process.exit(1)
2733
- }, 8000)
2734
- watchdog.unref?.()
2735
- stop()
2736
- .then(() => { clearTimeout(watchdog); process.exit(0) })
2737
- .catch(e => {
2738
- process.stderr.write(`[shutdown] error ${e.message}\n`)
2739
- clearTimeout(watchdog)
2740
- process.exit(1)
2741
- })
2742
- })
2743
- return
2744
- }
2745
-
2746
- // DEV-ONLY cycle1 chunking bench. Gated by env MIXDOG_DEV_BENCH=1 so
2747
- // production is untouched (route returns 404 when unset). Mirrors cycle1's
2748
- // exact fetch query + per-session windowing, then runs each window through
2749
- // buildCycle1ChunkPrompt + callBridgeLlm + parseCycle1LineFormat. STRICT
2750
- // read-only — no UPDATE, no transaction, no commit.
2751
- if (req.method === 'POST' && req.url === '/dev/cycle1-bench') {
2752
- // Gate: env MIXDOG_DEV_BENCH=1 OR a runtime flag file, so it can be
2753
- // toggled without restarting Claude Code (env only reaches the worker
2754
- // on a full CC restart, not via dev-sync full-restart).
2755
- const _devBenchOn = process.env.MIXDOG_DEV_BENCH === '1'
2756
- || (DATA_DIR && fs.existsSync(path.join(DATA_DIR, '.dev-bench-enabled')))
2757
- if (!_devBenchOn) {
2758
- sendJson(res, { error: 'not found' }, 404)
2759
- return
2760
- }
2761
- if (!isLocalOrigin(req)) {
2762
- sendJson(res, { ok: false, error: 'forbidden: cross-origin' }, 403)
2763
- return
2764
- }
2765
- try {
2766
- const body = await readBody(req)
2767
- const sets = Math.max(1, Number(body?.sets ?? 5))
2768
- const repeat = Math.max(1, Number(body?.repeat ?? 1))
2769
- // Optional variant matrix. Each variant: {name, rules}. rules=null → default prompt.
2770
- const rawVariants = Array.isArray(body?.variants) ? body.variants : null
2771
- const variants = rawVariants && rawVariants.length > 0
2772
- ? rawVariants.map((v, i) => ({
2773
- name: typeof v?.name === 'string' && v.name ? v.name : `variant-${i + 1}`,
2774
- rules: Array.isArray(v?.rules) ? v.rules : null,
2775
- }))
2776
- : null
2777
-
2778
- // Lazy-load LLM + chunking helpers so production boot pays nothing.
2779
- const [{ buildCycle1ChunkPrompt, parseCycle1LineFormat }, { callBridgeLlm }, { resolveMaintenancePreset }] = await Promise.all([
2780
- import('./lib/memory-cycle1.mjs'),
2781
- import('./lib/agent-ipc.mjs'),
2782
- import('../shared/llm/index.mjs'),
2783
- ])
2784
-
2785
- const CYCLE1_MIN_BATCH = 3
2786
- const CYCLE1_SESSION_CAP = 10
2787
- const BATCH_SIZE = 100
2788
- const TIMEOUT_MS = 180_000
2789
- const fetchLimit = CYCLE1_SESSION_CAP * BATCH_SIZE
2790
-
2791
- const fetchResult = await db.query(
2792
- `SELECT id, ts, role, content, session_id, source_ref, project_id
2793
- FROM entries
2794
- WHERE chunk_root IS NULL AND session_id IS NOT NULL
2795
- ORDER BY ts DESC, id DESC
2796
- LIMIT $1`,
2797
- [fetchLimit],
2798
- )
2799
- const rowsDesc = fetchResult.rows
2800
-
2801
- if (rowsDesc.length < CYCLE1_MIN_BATCH) {
2802
- sendJson(res, {
2803
- ok: true,
2804
- sets, repeat,
2805
- windowsAvailable: 0,
2806
- note: `not enough pending rows (need >= ${CYCLE1_MIN_BATCH}, got ${rowsDesc.length})`,
2807
- results: [],
2808
- })
2809
- return
2810
- }
2811
-
2812
- // Partition by session_id — same as memory-cycle1.mjs _runCycle1Impl L207-233.
2813
- const sessionMap = new Map()
2814
- for (const row of rowsDesc.slice().reverse()) {
2815
- const sid = row.session_id
2816
- if (!sessionMap.has(sid)) sessionMap.set(sid, [])
2817
- sessionMap.get(sid).push(row)
2818
- }
2819
- const windows = []
2820
- for (const [sid, sessionRows] of sessionMap) {
2821
- if (sessionRows.length < CYCLE1_MIN_BATCH) continue
2822
- const windowCount = Math.max(1, Math.ceil(sessionRows.length / BATCH_SIZE))
2823
- const baseSize = Math.floor(sessionRows.length / windowCount)
2824
- const remainder = sessionRows.length % windowCount
2825
- let _offset = 0
2826
- for (let i = 0; i < windowCount; i++) {
2827
- const size = baseSize + (i < remainder ? 1 : 0)
2828
- windows.push({ sid, rows: sessionRows.slice(_offset, _offset + size) })
2829
- _offset += size
2830
- }
2831
- }
2832
- const chosen = windows.slice(0, sets)
2833
-
2834
- const preset = resolveMaintenancePreset('memory')
2835
-
2836
- function summariseChunks(chunks, totalEntries) {
2837
- const usedIdx = new Set()
2838
- for (const c of chunks) for (const i of (c._idxList || [])) usedIdx.add(i)
2839
- const omitted = []
2840
- for (let i = 1; i <= totalEntries; i++) if (!usedIdx.has(i)) omitted.push(i)
2841
- return { covered: usedIdx.size, omitted }
2842
- }
2843
-
2844
- // When variants are absent, fall back to a single implicit baseline so the
2845
- // pre-variant call shape (single rows × repeat) keeps producing the same
2846
- // {runs:[…]} payload the trigger already knows how to print.
2847
- const variantList = variants ?? [{ name: 'baseline', rules: null }]
2848
-
2849
- async function runOnce(rows, customRules) {
2850
- const userMessage = buildCycle1ChunkPrompt(rows, customRules)
2851
- const t0 = Date.now()
2852
- let raw, error
2853
- try {
2854
- raw = await callBridgeLlm({
2855
- role: 'cycle1-agent',
2856
- taskType: 'maintenance',
2857
- mode: 'cycle1',
2858
- preset,
2859
- timeout: TIMEOUT_MS,
2860
- cwd: null,
2861
- }, userMessage)
2862
- } catch (e) {
2863
- error = e?.message ?? String(e)
2864
- }
2865
- const llmMs = Date.now() - t0
2866
- if (error) return { ok: false, llmMs, error }
2867
- const parsed = parseCycle1LineFormat(raw)
2868
- const chunks = Array.isArray(parsed?.chunks) ? parsed.chunks : []
2869
- const { covered, omitted } = summariseChunks(chunks, rows.length)
2870
- const ratio = chunks.length > 0
2871
- ? parseFloat((rows.length / chunks.length).toFixed(2))
2872
- : null
2873
- return {
2874
- ok: true,
2875
- llmMs,
2876
- entries: rows.length,
2877
- chunks: chunks.length,
2878
- ratio,
2879
- covered,
2880
- omitted,
2881
- chunkList: chunks.map(c => ({
2882
- idx: c._idxList,
2883
- element: c.element,
2884
- category: c.category,
2885
- summary: c.summary,
2886
- })),
2887
- }
2888
- }
2889
-
2890
- const results = []
2891
- for (let s = 0; s < chosen.length; s++) {
2892
- const { sid, rows } = chosen[s]
2893
- const sidShort = String(sid).slice(0, 8)
2894
- if (variants) {
2895
- // Variant mode: same rows, one run per variant per repeat.
2896
- const variantResults = []
2897
- for (const v of variantList) {
2898
- const runs = []
2899
- for (let r = 0; r < repeat; r++) {
2900
- const run = await runOnce(rows, v.rules)
2901
- runs.push({ repIdx: r + 1, ...run })
2902
- }
2903
- variantResults.push({ name: v.name, runs })
2904
- }
2905
- results.push({
2906
- setIdx: s + 1,
2907
- sessionIdShort: sidShort,
2908
- entries: rows.length,
2909
- variants: variantResults,
2910
- })
2911
- } else {
2912
- // Legacy single-baseline payload shape.
2913
- const runs = []
2914
- for (let r = 0; r < repeat; r++) {
2915
- const run = await runOnce(rows, null)
2916
- runs.push({ repIdx: r + 1, ...run })
2917
- }
2918
- results.push({
2919
- setIdx: s + 1,
2920
- sessionIdShort: sidShort,
2921
- entries: rows.length,
2922
- runs,
2923
- })
2924
- }
2925
- }
2926
- sendJson(res, {
2927
- ok: true,
2928
- sets, repeat,
2929
- windowsAvailable: windows.length,
2930
- variants: variants ? variantList.map(v => v.name) : null,
2931
- results,
2932
- })
2933
- } catch (e) {
2934
- sendError(res, e?.message || String(e))
2935
- }
2936
- return
2937
- }
2938
-
2939
- if (req.method === 'POST' && req.url === '/session-start/recap') {
2940
- try {
2941
- const body = await readBody(req)
2942
- const projectId = resolveProjectScope(typeof body.cwd === 'string' && body.cwd ? body.cwd : null)
2943
- const rows = projectId !== null
2944
- ? (await db.query(`
2945
- SELECT id, ts, summary FROM entries
2946
- WHERE is_root = 1 AND (project_id IS NULL OR project_id = $1)
2947
- ORDER BY ts DESC, id DESC LIMIT 20
2948
- `, [projectId])).rows
2949
- : (await db.query(`
2950
- SELECT id, ts, summary FROM entries
2951
- WHERE is_root = 1
2952
- ORDER BY ts DESC, id DESC LIMIT 20
2953
- `)).rows
2954
- sendJson(res, { ok: true, projectId, rows })
2955
- } catch (e) { sendError(res, e.message) }
2956
- return
2957
- }
2958
-
2959
- if (req.method === 'POST' && req.url === '/api/tool') {
2960
- if (!isLocalOrigin(req)) {
2961
- sendJson(res, { content: [{ type: 'text', text: 'forbidden: cross-origin' }], isError: true }, 403)
2962
- return
2963
- }
2964
- // Owner-side cancel plumbing: the fork-proxy worker forwards parent
2965
- // 'cancel' IPC by issuing POST /api/cancel with the same callId. Track
2966
- // each in-flight /api/tool by its caller-supplied X-Mixdog-Call-Id so
2967
- // the cancel endpoint can abort the AbortSignal threaded into
2968
- // handleToolCall. Without this the proxy-side fetch aborts but the
2969
- // owner keeps running the upstream tool to completion.
2970
- const callId = String(req.headers['x-mixdog-call-id'] || '').trim() || null
2971
- const ac = new AbortController()
2972
- // Abort only on a genuine mid-flight client disconnect. The req 'close'
2973
- // event fires on every normal request once the request body is consumed
2974
- // (before handleToolCall resolves), so gating on it would mark normal
2975
- // completions as aborted. Use the response side instead: when the
2976
- // socket closes, res.writableFinished is true iff the response was
2977
- // fully written — a real client disconnect closes the socket before
2978
- // the response finishes, leaving writableFinished===false.
2979
- res.on('close', () => {
2980
- if (res.writableFinished) return
2981
- try { ac.abort() } catch {}
2982
- })
2983
- if (callId) _ownerInFlightHttpCalls.set(callId, ac)
2984
- try {
2985
- const body = await readBody(req)
2986
- const result = await handleToolCall(body.name, body.arguments ?? {}, ac.signal)
2987
- sendJson(res, result)
2988
- } catch (e) {
2989
- sendJson(res, { content: [{ type: 'text', text: `api/tool error: ${e.message}` }], isError: true }, Number(e?.statusCode) || 500)
2990
- } finally {
2991
- if (callId) _ownerInFlightHttpCalls.delete(callId)
2992
- }
2993
- return
2994
- }
2995
-
2996
- if (req.method === 'POST' && req.url === '/api/cancel') {
2997
- if (!isLocalOrigin(req)) {
2998
- sendJson(res, { ok: false, error: 'forbidden: cross-origin' }, 403)
2999
- return
3000
- }
3001
- try {
3002
- const body = await readBody(req)
3003
- const id = String(body.callId || '').trim()
3004
- if (!id) { sendJson(res, { ok: false, error: 'callId required' }, 400); return }
3005
- const ac = _ownerInFlightHttpCalls.get(id)
3006
- if (ac) {
3007
- try { ac.abort() } catch {}
3008
- _ownerInFlightHttpCalls.delete(id)
3009
- sendJson(res, { ok: true, cancelled: true })
3010
- } else {
3011
- sendJson(res, { ok: true, cancelled: false })
3012
- }
3013
- } catch (e) {
3014
- sendJson(res, { ok: false, error: e.message }, Number(e?.statusCode) || 500)
3015
- }
3016
- return
3017
- }
3018
-
3019
- if (req.url === '/mcp') {
3020
- if (!isLocalOrigin(req)) {
3021
- sendJson(res, { error: 'forbidden: cross-origin' }, 403)
3022
- return
3023
- }
3024
- try {
3025
- if (req.method === 'POST') {
3026
- const httpMcp = createHttpMcpServer()
3027
- const httpTransport = new StreamableHTTPServerTransport({
3028
- sessionIdGenerator: undefined,
3029
- enableJsonResponse: true,
3030
- })
3031
- res.on('close', () => {
3032
- httpTransport.close()
3033
- void httpMcp.close()
3034
- })
3035
- await httpMcp.connect(httpTransport)
3036
- const body = await readBody(req)
3037
- await httpTransport.handleRequest(req, res, body)
3038
- } else {
3039
- sendJson(res, { error: 'Method not allowed' }, 405)
3040
- }
3041
- } catch (e) {
3042
- process.stderr.write(`[memory-service] /mcp error: ${e.stack || e.message}\n`)
3043
- if (!res.headersSent) sendError(res, e.message, Number(e?.statusCode) || 500)
3044
- }
3045
- return
3046
- }
3047
-
3048
- if (req.method !== 'POST') {
3049
- sendJson(res, { error: 'Method not allowed' }, 405)
3050
- return
3051
- }
3052
-
3053
- // Tail block handles /entry and /ingest-transcript — both mutate the DB,
3054
- // so apply the same cross-origin guard as /admin/* routes.
3055
- if (!isLocalOrigin(req)) {
3056
- sendError(res, 'forbidden: cross-origin', 403)
3057
- return
3058
- }
3059
-
3060
- let body
3061
- try { body = await readBody(req) }
3062
- catch (e) { sendError(res, e.message, Number(e?.statusCode) || 500); return }
3063
-
3064
- try {
3065
- if (req.url === '/entry') {
3066
- const role = String(body.role ?? 'user')
3067
- const content = String(body.content ?? '')
3068
- const sourceRef = String(body.sourceRef ?? `manual:${Date.now()}-${process.pid}`)
3069
- const sessionId = body.sessionId ?? null
3070
- const tsMs = parseTsToMs(body.ts ?? Date.now())
3071
- if (!content) { sendJson(res, { error: 'content required' }, 400); return }
3072
- // Run the same scrubber used by ingestTranscriptFile so noise markers
3073
- // like "[Request interrupted by user]" and whitespace-only payloads
3074
- // are rejected before they reach the entries table. Match the
3075
- // existing 400 / { error } convention for invalid payloads.
3076
- const cleaned = cleanMemoryText(content)
3077
- if (!cleaned || !cleaned.trim()) {
3078
- sendJson(res, { error: 'empty after clean' }, 400)
3079
- return
3080
- }
3081
- const entryProjectId = resolveProjectScope(typeof body.cwd === 'string' && body.cwd ? body.cwd : null)
3082
- try {
3083
- const result = await db.query(`
3084
- INSERT INTO entries(ts, role, content, source_ref, session_id, project_id)
3085
- VALUES ($1, $2, $3, $4, $5, $6)
3086
- ON CONFLICT DO NOTHING
3087
- RETURNING id
3088
- `, [tsMs, role, cleaned, sourceRef, sessionId, entryProjectId])
3089
- const insertedId = result.rows[0]?.id ?? null
3090
- sendJson(res, { ok: true, id: insertedId !== null ? Number(insertedId) : null, changes: Number(result.rowCount ?? result.affectedRows ?? 0) })
3091
- } catch (e) {
3092
- sendJson(res, { error: e.message }, 500)
3093
- }
3094
- return
3095
- }
3096
-
3097
- if (req.url === '/ingest-transcript') {
3098
- const filePath = body.filePath
3099
- if (!filePath) { sendJson(res, { error: 'filePath required' }, 400); return }
3100
- try {
3101
- const n = await ingestTranscriptFile(filePath, { cwd: body.cwd })
3102
- sendJson(res, { ok: true, ingested: n })
3103
- } catch (e) {
3104
- sendJson(res, { error: e.message }, 500)
3105
- }
3106
- return
3107
- }
3108
-
3109
- if (req.url === '/transcript/ingest-sync') {
3110
- const filePath = body.path
3111
- if (!filePath || typeof filePath !== 'string') {
3112
- sendJson(res, { error: 'path required' }, 400)
3113
- return
3114
- }
3115
- try {
3116
- let stat
3117
- try { stat = await fs.promises.stat(filePath) } catch {
3118
- sendJson(res, { ok: true, complete: true, fileSize: 0, offsetBytes: 0 })
3119
- return
3120
- }
3121
- const fileSize = stat.size
3122
- await ingestTranscriptFile(filePath, { cwd: body.cwd })
3123
- const off = _transcriptOffsets.get(filePath)
3124
- const offsetBytes = off && Number.isFinite(off.bytes) ? off.bytes : 0
3125
- const complete = offsetBytes >= fileSize
3126
- sendJson(res, { ok: true, offsetBytes, fileSize, complete })
3127
- } catch (e) {
3128
- sendJson(res, { error: e.message }, 500)
3129
- }
3130
- return
3131
- }
3132
-
3133
- sendJson(res, { error: 'Not found' }, 404)
3134
- } catch (e) {
3135
- process.stderr.write(`[memory-service] ${req.url} error: ${e.stack || e.message}\n`)
3136
- sendError(res, e.message)
3137
- }
3138
- })
3139
-
3140
- export { TOOL_DEFS, handleToolCall }
3141
- export { MEMORY_INSTRUCTIONS_TEXT as instructions }
3142
- export { acquireLock, releaseLock }
3143
- export { cwdFromTranscriptPath }
3144
- export async function init() {
3145
- if (_initialized) return
3146
- process.stderr.write(`[boot-time] tag=memory-init-start tMs=${Date.now()}\n`)
3147
- if (process.env.MIXDOG_WORKER_MODE === '1' && process.send) {
3148
- // Single-worker daemon: acquire the owner lock (which reclaims a crashed
3149
- // predecessor's stale, dead-PID lock). If a LIVE peer still holds it — an
3150
- // anomaly, since server-main forks exactly one memory worker — exit so
3151
- // server-main respawns us instead of running a second owner.
3152
- if (!tryAcquireMemoryOwnerLock()) {
3153
- process.stderr.write('[memory-service] live peer holds owner lock — exiting for respawn\n')
3154
- process.exit(0)
3155
- }
3156
- process.on('exit', releaseMemoryOwnerLock)
3157
- }
3158
- const runtimeReady = _beginRuntimeInit()
3159
- const boundPort = await _startHttpServer()
3160
- await runtimeReady
3161
- advertiseMemoryPort(boundPort)
3162
- if (process.env.MIXDOG_WORKER_MODE === '1' && process.send) {
3163
- process.stderr.write(`[boot-time] tag=memory-ready tMs=${Date.now()}\n`)
3164
- process.send({ type: 'ready', port: boundPort })
3165
- }
3166
- process.stderr.write(`[memory-service] init() complete (entries unified mode, version=${PLUGIN_VERSION})\n`)
3167
- }
3168
-
3169
- export async function stop() {
3170
- _stopCycles()
3171
- await stopLlmWorker()
3172
- if (httpServer) await new Promise(resolve => httpServer.close(resolve))
3173
- await closeDatabase(DATA_DIR)
3174
- // Stop the PG postmaster after the connection pool has been drained.
3175
- // closeDatabase() only ends the client pool; without this the child
3176
- // postmaster keeps running after the memory service exits.
3177
- try {
3178
- const { stopPgForShutdown } = await import('./lib/pg/supervisor.mjs')
3179
- await stopPgForShutdown()
3180
- } catch {}
3181
- releaseLock()
3182
- }
3183
-
3184
- let activePort = BASE_PORT
3185
- let _httpReadyPromise = null
3186
- let _httpBoundPort = null
3187
-
3188
- function _startHttpServer() {
3189
- if (_httpBoundPort != null) return Promise.resolve(_httpBoundPort)
3190
- if (_httpReadyPromise) return _httpReadyPromise
3191
- _httpReadyPromise = new Promise((resolve, reject) => {
3192
- function tryListen() {
3193
- httpServer.listen(activePort, '127.0.0.1', () => {
3194
- // Use actual bound port (important when activePort=0, OS assigns a free port).
3195
- const boundPort = httpServer.address().port
3196
- _httpBoundPort = boundPort
3197
- process.stderr.write(`[memory-service] HTTP listening on 127.0.0.1:${boundPort}\n`)
3198
- resolve(boundPort)
3199
- })
3200
- }
3201
- httpServer.on('error', (err) => {
3202
- if (err.code === 'EADDRINUSE' && activePort < MAX_PORT) {
3203
- activePort++
3204
- tryListen()
3205
- } else if (err.code === 'EADDRINUSE') {
3206
- // All fixed ports exhausted; let OS pick a free port.
3207
- activePort = 0
3208
- tryListen()
3209
- } else {
3210
- process.stderr.write(`[memory-service] HTTP fatal: ${err.message}\n`)
3211
- reject(err)
3212
- }
3213
- })
3214
- tryListen()
3215
- })
3216
- return _httpReadyPromise
3217
- }
3218
-
3219
- if (process.env.MIXDOG_WORKER_MODE === '1' && process.send) {
3220
- // SIGTERM/SIGINT handler for worker mode: call stop() (fsyncs,
3221
- // removes port file) then exit(0). Prevents taskkill /F from bypassing
3222
- // graceful shutdown and leaving pgdata in an inconsistent checkpoint state.
3223
- let _stopInFlight = false
3224
- const _workerSignalHandler = (sig) => {
3225
- if (_stopInFlight) {
3226
- process.stderr.write(`[memory-worker] ${sig} — stop already in flight, ignoring\n`)
3227
- return
3228
- }
3229
- _stopInFlight = true
3230
- process.stderr.write(`[memory-worker] received ${sig} — calling stop() for clean shutdown\n`)
3231
- const _exitTimer = setTimeout(() => {
3232
- process.stderr.write(`[memory-worker] stop() timed out after 6000ms — forcing exit(2)\n`)
3233
- process.exit(2)
3234
- }, 6000)
3235
- stop().then(() => {
3236
- clearTimeout(_exitTimer)
3237
- process.stderr.write(`[memory-worker] stop() complete — exiting cleanly\n`)
3238
- process.exit(0)
3239
- }).catch((e) => {
3240
- clearTimeout(_exitTimer)
3241
- process.stderr.write(`[memory-worker] stop() error on ${sig}: ${e && (e.message || e)}\n`)
3242
- process.exit(1)
3243
- })
3244
- }
3245
- process.on('SIGTERM', () => _workerSignalHandler('SIGTERM'))
3246
- process.on('SIGINT', () => _workerSignalHandler('SIGINT'))
3247
-
3248
- // callId → AbortController for in-flight IPC calls (cancel handler uses this).
3249
- const _inFlightCalls = new Map()
3250
-
3251
- process.on('message', async (msg) => {
3252
- // Handle parent-initiated graceful shutdown IPC message.
3253
- if (msg.type === 'shutdown') {
3254
- process.stderr.write('[memory-worker] received IPC shutdown — calling stop()\n')
3255
- _workerSignalHandler('IPC:shutdown')
3256
- return
3257
- }
3258
- if (msg.type === 'cancel' && msg.callId) {
3259
- const entry = _inFlightCalls.get(msg.callId)
3260
- if (entry) {
3261
- // Mark cancelled so the in-flight call's result/error branch below
3262
- // does not double-respond after the AbortController fires.
3263
- entry.cancelled = true
3264
- entry.ac.abort()
3265
- _inFlightCalls.delete(msg.callId)
3266
- process.send({ type: 'result', callId: msg.callId, error: 'cancelled' })
3267
- }
3268
- return
3269
- }
3270
- if (msg.type !== 'call' || !msg.callId) return
3271
- const entry = { ac: new AbortController(), cancelled: false }
3272
- _inFlightCalls.set(msg.callId, entry)
3273
- try {
3274
- let result
3275
- try {
3276
- result = await handleToolCall(msg.name, msg.args || {}, entry.ac.signal)
3277
- } finally {
3278
- _inFlightCalls.delete(msg.callId)
3279
- }
3280
- if (!entry.cancelled) process.send({ type: 'result', callId: msg.callId, result })
3281
- } catch (e) {
3282
- if (!entry.cancelled) process.send({ type: 'result', callId: msg.callId, error: e.message })
3283
- }
3284
- })
3285
- init().catch(e => {
3286
- let detail
3287
- try {
3288
- const parts = []
3289
- if (e?.name) parts.push(`name=${e.name}`)
3290
- if (e?.code) parts.push(`code=${e.code}`)
3291
- if (e?.errno) parts.push(`errno=${e.errno}`)
3292
- if (e?.syscall) parts.push(`syscall=${e.syscall}`)
3293
- if (e?.path) parts.push(`path=${e.path}`)
3294
- if (e?.message) parts.push(`message=${e.message}`)
3295
- let stringified = null
3296
- try { stringified = JSON.stringify(e, Object.getOwnPropertyNames(e || {})) } catch {}
3297
- if (stringified && stringified !== '{}' && stringified !== '"{}"') parts.push(`json=${stringified}`)
3298
- if (e?.stack) parts.push(`\nstack=\n${e.stack}`)
3299
- if (parts.length === 0) parts.push(`raw=${typeof e}:${String(e)}`)
3300
- detail = parts.join(' | ')
3301
- } catch (logErr) {
3302
- detail = `(error formatting failed: ${logErr?.message}) raw=${String(e)}`
3303
- }
3304
- process.stderr.write(`[memory-worker] init failed: ${detail}\n`)
3305
- // Signal degraded state to parent before exiting so it records the failure
3306
- // rather than treating this as a normal pre-ready crash.
3307
- try { process.send({ type: 'ready', degraded: true, error: detail.slice(0, 800) }) } catch {}
3308
- process.exit(1)
3309
- })
3310
- }
3311
-
3312
- // Standalone MCP launcher path. When this module is the entry script AND no
3313
- // MIXDOG_WORKER_MODE flag is set, we own stdio and bring up the full MCP
3314
- // server with acquireLock + StdioServerTransport. Server-main spawnWorker
3315
- // also forks this file with MIXDOG_WORKER_MODE='1'; that path uses the IPC
3316
- // handler block above and acquireLock/init() as the single memory owner.
3317
- if (import.meta.url === pathToFileURL(process.argv[1] || '').href && process.env.MIXDOG_WORKER_MODE !== '1') {
3318
- ;(async () => {
3319
- acquireLock()
3320
- process.on('exit', releaseLock)
3321
- process.on('SIGINT', () => { stop().finally(() => process.exit(0)) })
3322
- process.on('SIGTERM', () => { stop().finally(() => process.exit(0)) })
3323
- await init()
3324
- const transport = new StdioServerTransport()
3325
- await mcp.connect(transport)
3326
- await new Promise((resolve) => { mcp.onclose = resolve })
3327
- await stop()
3328
- })().catch((err) => {
3329
- process.stderr.write(`[memory-service] startup failed: ${err.stack || err.message}\n`)
3330
- process.exit(1)
3331
- })
3332
- }