mixdog 0.8.1 → 0.9.1

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 (299) hide show
  1. package/README.md +47 -23
  2. package/package.json +34 -28
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +205 -33
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1222 -90
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -29
  62. package/src/lib/plugin-paths.cjs +1 -7
  63. package/src/lib/rules-builder.cjs +242 -98
  64. package/src/lib/text-utils.cjs +1 -1
  65. package/src/mixdog-session-runtime.mjs +2211 -446
  66. package/src/output-styles/default.md +12 -28
  67. package/src/output-styles/extreme-simple.md +9 -6
  68. package/src/output-styles/simple.md +22 -9
  69. package/src/repl.mjs +108 -59
  70. package/src/rules/agent/00-common.md +15 -0
  71. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  72. package/src/rules/agent/30-explorer.md +22 -0
  73. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  75. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  76. package/src/rules/lead/01-general.md +9 -5
  77. package/src/rules/lead/04-workflow.md +51 -12
  78. package/src/rules/lead/lead-tool.md +6 -0
  79. package/src/rules/shared/01-tool.md +12 -1
  80. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  81. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  85. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  86. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  87. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  88. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  89. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  90. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  92. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  93. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  94. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  95. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
  97. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  98. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  100. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  101. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  102. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  103. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  104. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  107. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
  109. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
  110. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  111. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
  113. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  114. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  115. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  116. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  117. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  118. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  119. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  120. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  121. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  122. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  123. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
  124. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
  125. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
  126. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  127. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  138. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  143. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  146. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  147. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  148. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  149. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  150. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  151. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  152. package/src/runtime/channels/index.mjs +149 -51
  153. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  154. package/src/runtime/channels/lib/config.mjs +0 -1
  155. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  156. package/src/runtime/channels/lib/executor.mjs +0 -3
  157. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  158. package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
  159. package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
  160. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  161. package/src/runtime/channels/lib/session-discovery.mjs +3 -6
  162. package/src/runtime/channels/lib/tool-format.mjs +0 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
  164. package/src/runtime/channels/lib/webhook.mjs +1 -1
  165. package/src/runtime/channels/tool-defs.mjs +29 -29
  166. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  167. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  168. package/src/runtime/memory/index.mjs +519 -118
  169. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  170. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  171. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  172. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  173. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  174. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  175. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  176. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  177. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  178. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
  179. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  180. package/src/runtime/memory/lib/memory.mjs +88 -9
  181. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  182. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  183. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  184. package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
  185. package/src/runtime/memory/lib/session-ingest.mjs +196 -0
  186. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  187. package/src/runtime/memory/tool-defs.mjs +46 -37
  188. package/src/runtime/search/index.mjs +104 -473
  189. package/src/runtime/search/lib/config.mjs +0 -4
  190. package/src/runtime/search/lib/state.mjs +1 -15
  191. package/src/runtime/search/lib/web-tools.mjs +44 -25
  192. package/src/runtime/search/tool-defs.mjs +16 -23
  193. package/src/runtime/shared/abort-controller.mjs +1 -1
  194. package/src/runtime/shared/atomic-file.mjs +4 -3
  195. package/src/runtime/shared/background-tasks.mjs +122 -11
  196. package/src/runtime/shared/child-spawn-gate.mjs +139 -0
  197. package/src/runtime/shared/config.mjs +7 -4
  198. package/src/runtime/shared/err-text.mjs +131 -4
  199. package/src/runtime/shared/llm/cost.mjs +2 -2
  200. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  201. package/src/runtime/shared/llm/index.mjs +34 -11
  202. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  203. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  204. package/src/runtime/shared/singleton-owner.mjs +104 -0
  205. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  206. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  207. package/src/runtime/shared/tool-surface.mjs +624 -95
  208. package/src/runtime/shared/user-data-guard.mjs +0 -2
  209. package/src/standalone/agent-task-status.mjs +203 -0
  210. package/src/standalone/agent-task-status.test.mjs +76 -0
  211. package/src/standalone/agent-tool.mjs +1913 -0
  212. package/src/standalone/channel-worker.mjs +370 -14
  213. package/src/standalone/explore-tool.mjs +165 -70
  214. package/src/standalone/folder-dialog.mjs +314 -0
  215. package/src/standalone/hook-bus.mjs +898 -22
  216. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  217. package/src/standalone/projects.mjs +226 -0
  218. package/src/standalone/provider-admin.mjs +41 -24
  219. package/src/standalone/seeds.mjs +1 -78
  220. package/src/standalone/usage-dashboard.mjs +96 -8
  221. package/src/tui/App.jsx +4800 -2140
  222. package/src/tui/components/AnsiText.jsx +39 -28
  223. package/src/tui/components/ContextPanel.jsx +14 -4
  224. package/src/tui/components/Markdown.jsx +43 -77
  225. package/src/tui/components/MarkdownTable.jsx +9 -184
  226. package/src/tui/components/Message.jsx +28 -11
  227. package/src/tui/components/Picker.jsx +95 -56
  228. package/src/tui/components/PromptInput.jsx +428 -239
  229. package/src/tui/components/QueuedCommands.jsx +1 -1
  230. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  231. package/src/tui/components/Spinner.jsx +67 -38
  232. package/src/tui/components/StatusLine.jsx +606 -38
  233. package/src/tui/components/TextEntryPanel.jsx +128 -9
  234. package/src/tui/components/ToolExecution.jsx +597 -362
  235. package/src/tui/components/TurnDone.jsx +3 -3
  236. package/src/tui/components/UsagePanel.jsx +3 -5
  237. package/src/tui/components/tool-output-format.mjs +499 -0
  238. package/src/tui/components/tool-output-format.test.mjs +312 -0
  239. package/src/tui/dist/index.mjs +9192 -2399
  240. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  241. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  242. package/src/tui/engine.mjs +1455 -279
  243. package/src/tui/figures.mjs +21 -40
  244. package/src/tui/index.jsx +75 -31
  245. package/src/tui/input-editing.mjs +25 -0
  246. package/src/tui/markdown/format-token.mjs +671 -69
  247. package/src/tui/markdown/format-token.test.mjs +312 -0
  248. package/src/tui/markdown/render-ansi.mjs +94 -0
  249. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  250. package/src/tui/markdown/stream-fence.mjs +34 -0
  251. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  252. package/src/tui/markdown/table-layout.mjs +250 -0
  253. package/src/tui/paste-attachments.mjs +0 -7
  254. package/src/tui/spinner-verbs.mjs +1 -2
  255. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  256. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  257. package/src/tui/theme.mjs +756 -24
  258. package/src/tui/time-format.mjs +1 -1
  259. package/src/tui/transcript-tool-failures.mjs +67 -0
  260. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  261. package/src/ui/ansi.mjs +1 -2
  262. package/src/ui/markdown.mjs +85 -26
  263. package/src/ui/markdown.test.mjs +70 -0
  264. package/src/ui/model-display.mjs +121 -0
  265. package/src/ui/session-stats.mjs +44 -0
  266. package/src/ui/statusline-context-label.test.mjs +15 -0
  267. package/src/ui/statusline.mjs +386 -178
  268. package/src/ui/tool-card.mjs +2 -13
  269. package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
  270. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  271. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  272. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  273. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  274. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  275. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  276. package/src/workflows/default/WORKFLOW.md +12 -5
  277. package/src/workflows/default/workflow.json +0 -1
  278. package/src/workflows/solo/WORKFLOW.md +15 -0
  279. package/src/workflows/solo/workflow.json +7 -0
  280. package/vendor/ink/build/ink.js +54 -8
  281. package/vendor/ink/build/output.js +6 -1
  282. package/src/agents/scheduler-task.md +0 -3
  283. package/src/agents/web-researcher/AGENT.md +0 -3
  284. package/src/agents/web-researcher/agent.json +0 -6
  285. package/src/agents/webhook-handler.md +0 -3
  286. package/src/hooks/lib/permission-rules.cjs +0 -170
  287. package/src/hooks/lib/settings-loader.cjs +0 -112
  288. package/src/lib/hook-pipe-path.cjs +0 -10
  289. package/src/rules/bridge/00-common.md +0 -5
  290. package/src/rules/bridge/30-explorer.md +0 -4
  291. package/src/rules/lead/00-tool-lead.md +0 -5
  292. package/src/rules/shared/00-language.md +0 -3
  293. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  294. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  295. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  296. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  297. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  298. package/src/standalone/bridge-tool.mjs +0 -1414
  299. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -81,11 +81,22 @@ import {
81
81
  isBootstrapComplete,
82
82
  getMetaValue,
83
83
  setMetaValue,
84
+ mergeMetaValue,
84
85
  cleanMemoryText,
85
86
  } from './lib/memory.mjs'
86
- import { configureEmbedding, embedText, embedTexts, getEmbeddingDims, getEmbeddingModelId, getKnownDimsForCurrentModel, primeEmbeddingDims, warmupEmbeddingProvider } from './lib/embedding-provider.mjs'
87
+ import {
88
+ normalizeIngestRole,
89
+ firstTextContent,
90
+ stableSessionSourceRef,
91
+ sessionMessageContent,
92
+ createIngestTurnAllocator,
93
+ } from './lib/session-ingest.mjs'
94
+ import { configureEmbedding, embedText, embedTexts, getEmbeddingDims, getEmbeddingDtype, getEmbeddingModelId, getKnownDimsForCurrentModel, isEmbeddingModelReady, primeEmbeddingDims, warmupEmbeddingProvider } from './lib/embedding-provider.mjs'
87
95
  import { startLlmWorker, stopLlmWorker } from './lib/llm-worker-host.mjs'
88
96
  import { runCycle1, runCycle2, runCycle3, runUnifiedGate, parseInterval, syncRootEmbedding, applySimpleStatus, applyUpdate, applyMerge, CYCLE2_ACTIVE_TARGET_CAP } from './lib/memory-cycle.mjs'
97
+ import { loadConfig as loadAgentConfig } from '../agent/orchestrator/config.mjs'
98
+ import { initProviders } from '../agent/orchestrator/providers/registry.mjs'
99
+ import { makeAgentDispatch } from '../agent/orchestrator/agent-runtime/agent-dispatch.mjs'
89
100
  import { getInFlightCycle1 } from './lib/memory-cycle1.mjs'
90
101
  import { claimAndMarkScheduledCycle, makeCycleRequestSignature, resolveCoalesceMaxRetries, scheduleCoalescedCycleRetry } from './lib/memory-cycle-requests.mjs'
91
102
  import { searchRelevantHybrid } from './lib/memory-recall-store.mjs'
@@ -96,10 +107,12 @@ import { computeEntryScore } from './lib/memory-score.mjs'
96
107
  import { runFullBackfill } from './lib/memory-ops-policy.mjs'
97
108
  import { listCore, addCore, editCore, deleteCore, compactCoreIds, CORE_SUMMARY_MAX } from './lib/core-memory-store.mjs'
98
109
  import { resolveProjectId, resolveProjectScope } from './lib/project-id-resolver.mjs'
99
- import { openTraceDatabase, insertTraceEvents, enqueueTraceEvents, insertBridgeCalls, registerTraceExitDrain } from './lib/trace-store.mjs'
110
+ import { openTraceDatabase, closeTraceDatabase, insertTraceEvents, enqueueTraceEvents, insertAgentCalls, registerTraceExitDrain } from './lib/trace-store.mjs'
100
111
  import { updateJsonAtomicSync, writeJsonAtomicSync } from '../shared/atomic-file.mjs'
101
- import { resolvePluginData } from '../shared/plugin-paths.mjs'
102
- const DATA_DIR = process.argv[2] || resolvePluginData()
112
+ import { resolvePluginData, mixdogHome } from '../shared/plugin-paths.mjs'
113
+ const IS_MEMORY_ENTRY = !!process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
114
+ const USE_ARG_DATA_DIR = IS_MEMORY_ENTRY || process.env.MIXDOG_WORKER_MODE === '1'
115
+ const DATA_DIR = process.env.MIXDOG_DATA_DIR || (USE_ARG_DATA_DIR ? process.argv[2] : '') || resolvePluginData()
103
116
  if (!DATA_DIR) {
104
117
  __mixdogMemoryLog('[memory-service] memory data dir not set and no explicit data dir provided\n')
105
118
  process.exit(1)
@@ -113,6 +126,7 @@ const RUNTIME_ROOT = process.env.MIXDOG_RUNTIME_ROOT
113
126
  : path.join(os.tmpdir(), 'mixdog')
114
127
 
115
128
  let _periodicAdvertiseInstalled = false
129
+ let _periodicAdvertiseTimer = null
116
130
  // Track the most recently advertised port so the periodic tick re-reads it
117
131
  // every interval. Without this the setInterval closure binds the FIRST port
118
132
  // (the upstream we proxied to) and keeps re-advertising the dead upstream
@@ -124,7 +138,28 @@ function parsePositivePid(value) {
124
138
  return Number.isFinite(pid) && pid > 0 ? pid : null
125
139
  }
126
140
 
127
- const MEMORY_SERVER_PID = parsePositivePid(process.env.MIXDOG_SERVER_PID)
141
+ const MEMORY_SERVER_PID = parsePositivePid(process.env.MIXDOG_SERVER_PID) ?? process.pid
142
+ const MEMORY_DAEMON_MODE = process.env.MIXDOG_MEMORY_DAEMON === '1'
143
+ const MEMORY_IDLE_TTL_MS = Math.max(0, Number(process.env.MIXDOG_MEMORY_IDLE_TTL_MS) || 10 * 60_000)
144
+ let _idleShutdownTimer = null
145
+
146
+ function touchDaemonIdleTimer(reason = 'activity') {
147
+ if (!MEMORY_DAEMON_MODE || MEMORY_IDLE_TTL_MS <= 0) return
148
+ if (_idleShutdownTimer) {
149
+ try { clearTimeout(_idleShutdownTimer) } catch {}
150
+ _idleShutdownTimer = null
151
+ }
152
+ _idleShutdownTimer = setTimeout(() => {
153
+ __mixdogMemoryLog(`[memory-service] daemon idle TTL elapsed after ${reason}; shutting down\n`)
154
+ stop()
155
+ .then(() => process.exit(0))
156
+ .catch((e) => {
157
+ __mixdogMemoryLog(`[memory-service] daemon idle shutdown failed: ${e?.message || e}\n`)
158
+ process.exit(1)
159
+ })
160
+ }, MEMORY_IDLE_TTL_MS)
161
+ _idleShutdownTimer.unref?.()
162
+ }
128
163
 
129
164
  function advertiseMemoryPort(boundPort, attempt = 0) {
130
165
  if (!Number.isFinite(boundPort) || boundPort <= 0) return
@@ -156,18 +191,20 @@ function advertiseMemoryPort(boundPort, attempt = 0) {
156
191
  }, { compact: true, fsyncDir: true })
157
192
  if (!_periodicAdvertiseInstalled) {
158
193
  _periodicAdvertiseInstalled = true
159
- setInterval(() => {
194
+ _periodicAdvertiseTimer = setInterval(() => {
160
195
  try {
161
196
  if (_currentAdvertisedPort != null) {
162
197
  advertiseMemoryPort(_currentAdvertisedPort)
163
198
  }
164
199
  } catch {}
165
- }, 30_000).unref()
200
+ }, 30_000)
201
+ _periodicAdvertiseTimer.unref?.()
166
202
  }
167
203
  } catch (e) {
168
204
  const transient = e?.code === 'EPERM' || e?.code === 'EBUSY' || e?.code === 'EACCES'
169
205
  if (transient && attempt < 3) {
170
- setTimeout(() => advertiseMemoryPort(boundPort, attempt + 1), 50 * (attempt + 1))
206
+ const retryTimer = setTimeout(() => advertiseMemoryPort(boundPort, attempt + 1), 50 * (attempt + 1))
207
+ retryTimer.unref?.()
171
208
  return
172
209
  }
173
210
  __mixdogMemoryLog(`[memory-service] active-instance memory_port advertise failed: ${e?.message || e}\n`)
@@ -363,8 +400,12 @@ let _startupTimeout = null
363
400
  let _cycle1InFlight = null // shared cycle1 promise (outer coalesce layer)
364
401
  let _initialized = false
365
402
  let _initPromise = null
403
+ let _stopPromise = null
366
404
  let _bootTimestamp = null
367
405
  let _transcriptOffsets = new Map()
406
+ /** @type {Map<string, Promise<unknown>>} */
407
+ const _ingestTranscriptTails = new Map()
408
+ let _transcriptOffsetsPersistTail = Promise.resolve()
368
409
  // Boot-edge background warmup. ONNX session creation on the embedding worker
369
410
  // thread is CPU-heavy, so it must not overlap the worker's own init (DB open,
370
411
  // schema, cycle wiring). Previously this was gated behind a fixed setTimeout —
@@ -373,6 +414,7 @@ let _transcriptOffsets = new Map()
373
414
  // so it starts the instant boot's CPU-heavy work is done — no magic-number
374
415
  // delay. MIXDOG_EMBED_WARMUP=0 disables it (model loads lazily on first use).
375
416
  let _pendingEmbeddingWarmup = null
417
+ let _embeddingColdRecallLogAt = 0
376
418
 
377
419
  const TRANSCRIPT_OFFSETS_KEY = 'state.transcript_offsets'
378
420
  const CYCLE_LAST_RUN_KEY = 'state.cycle_last_run'
@@ -382,8 +424,50 @@ function embeddingWarmupEnabled() {
382
424
  return !(raw === '0' || raw === 'false' || raw === 'off' || raw === 'no')
383
425
  }
384
426
 
427
+ function envFlagEnabled(name) {
428
+ const raw = String(process.env[name] ?? '').trim().toLowerCase()
429
+ return raw === '1' || raw === 'true' || raw === 'on' || raw === 'yes'
430
+ }
431
+
432
+ function memorySecondaryMode() {
433
+ return envFlagEnabled('MIXDOG_MEMORY_SECONDARY')
434
+ }
435
+
436
+ function embeddingWarmupCanStart() {
437
+ return embeddingWarmupEnabled() && !memorySecondaryMode()
438
+ }
439
+
440
+ function memoryLlmWorkerEnabled() {
441
+ return !memorySecondaryMode() && !envFlagEnabled('MIXDOG_MEMORY_DISABLE_LLM_WORKER')
442
+ }
443
+
444
+ function memoryCyclesEnabled() {
445
+ return !memorySecondaryMode() && !envFlagEnabled('MIXDOG_MEMORY_DISABLE_CYCLES')
446
+ }
447
+
448
+ function secondaryPgAdvertised() {
449
+ if (!memorySecondaryMode()) return true
450
+ const runtimeRoot = process.env.MIXDOG_RUNTIME_ROOT
451
+ ? path.resolve(process.env.MIXDOG_RUNTIME_ROOT)
452
+ : path.join(os.tmpdir(), 'mixdog')
453
+ try {
454
+ const cur = JSON.parse(fs.readFileSync(path.join(runtimeRoot, 'active-instance.json'), 'utf8'))
455
+ const port = Number(cur?.pg_port)
456
+ const pgdata = cur?.pg_pgdata ? path.resolve(String(cur.pg_pgdata)) : ''
457
+ return Number.isInteger(port) && port > 0 && pgdata === path.resolve(path.join(DATA_DIR, 'pgdata'))
458
+ } catch {
459
+ return false
460
+ }
461
+ }
462
+
463
+ function assertSecondaryPgAttachable() {
464
+ if (!secondaryPgAdvertised()) {
465
+ throw new Error('memory-service: secondary mode requires an existing primary PG instance')
466
+ }
467
+ }
468
+
385
469
  function scheduleBackgroundEmbeddingWarmup(metaPath, metaKey) {
386
- if (!embeddingWarmupEnabled()) return
470
+ if (!embeddingWarmupCanStart()) return
387
471
  // Queue the warmup; _initRuntime fires it once boot completes.
388
472
  _pendingEmbeddingWarmup = () => {
389
473
  warmupEmbeddingProvider()
@@ -397,7 +481,6 @@ function scheduleBackgroundEmbeddingWarmup(metaPath, metaKey) {
397
481
  })
398
482
  .catch(err => {
399
483
  __mixdogMemoryLog(`[memory-service] background warmup failed: ${err?.message || err}\n`)
400
- process.exit(1)
401
484
  })
402
485
  }
403
486
  }
@@ -429,7 +512,7 @@ async function _initStore() {
429
512
  const metaKey = {
430
513
  provider: embeddingConfig?.provider ?? null,
431
514
  model: getEmbeddingModelId(),
432
- dtype: embeddingConfig?.dtype ?? null,
515
+ dtype: getEmbeddingDtype(),
433
516
  }
434
517
  let dimsResolved = null
435
518
  try {
@@ -450,12 +533,17 @@ async function _initStore() {
450
533
 
451
534
  if (dimsResolved) {
452
535
  primeEmbeddingDims(dimsResolved)
536
+ assertSecondaryPgAttachable()
453
537
  db = await openDatabase(DATA_DIR, dimsResolved)
454
538
  scheduleBackgroundEmbeddingWarmup(EMBEDDING_META_PATH, metaKey)
455
539
  } else {
540
+ if (!embeddingWarmupCanStart()) {
541
+ throw new Error('memory-service: embedding dims unavailable while warmup is disabled')
542
+ }
456
543
  // Cold path: meta missed AND model not registered. Sequential.
457
544
  await warmupEmbeddingProvider()
458
545
  dimsResolved = Number(getEmbeddingDims())
546
+ assertSecondaryPgAttachable()
459
547
  db = await openDatabase(DATA_DIR, dimsResolved)
460
548
  try {
461
549
  writeJsonAtomicSync(EMBEDDING_META_PATH, { ...metaKey, dims: dimsResolved }, { lock: true })
@@ -467,7 +555,32 @@ async function _initStore() {
467
555
  if (!await isBootstrapComplete(db)) {
468
556
  throw new Error('memory-service: bootstrap not complete after openDatabase')
469
557
  }
470
- startLlmWorker()
558
+ if (memoryLlmWorkerEnabled()) {
559
+ startLlmWorker()
560
+ } else {
561
+ __mixdogMemoryLog('[memory-service] secondary mode; skipping llm worker\n')
562
+ }
563
+ // Initialize the in-process provider registry so cycle1 can run the agent dispatch
564
+ // LLM locally (makeAgentDispatch → session manager → provider.send). In
565
+ // standalone the memory worker runs as a detached HTTP daemon whose parent
566
+ // has disconnected IPC, so the legacy callAgentDispatch() IPC path is dead on
567
+ // arrival. Mirror the channels worker boot (channels/index.mjs:
568
+ // loadAgentConfig() + initProviders) so the registry is populated before any
569
+ // cycle1 dispatch. The gate MUST match _startCycle1Run's makeAgentDispatch
570
+ // injection condition: cycle1 may dispatch in-process whenever cycles are
571
+ // enabled OR the llm worker is enabled (both exclude secondary mode), so
572
+ // registering only under the llm-worker gate would leave a hole where
573
+ // MIXDOG_MEMORY_DISABLE_LLM_WORKER=1 + cycles enabled hits an empty registry
574
+ // and fails with "Provider not found". Non-fatal: a failure here is logged
575
+ // and cycle1's own callLlm surfaces the unresolved-provider error per call.
576
+ if (memoryCyclesEnabled() || memoryLlmWorkerEnabled()) {
577
+ try {
578
+ const agentCfg = loadAgentConfig()
579
+ await initProviders(agentCfg.providers || {})
580
+ } catch (e) {
581
+ process.stderr.write(`[memory-service] initProviders failed (non-fatal): ${e instanceof Error ? e.message : String(e)}\n`)
582
+ }
583
+ }
471
584
  _bootTimestamp = Date.now()
472
585
  await loadTranscriptOffsets()
473
586
  }
@@ -483,12 +596,16 @@ async function loadTranscriptOffsets() {
483
596
  }
484
597
 
485
598
  async function persistTranscriptOffsets() {
486
- try {
487
- const obj = Object.fromEntries(_transcriptOffsets)
488
- await setMetaValue(db, TRANSCRIPT_OFFSETS_KEY, JSON.stringify(obj))
489
- } catch (e) {
490
- __mixdogMemoryLog(`[memory] persist transcript offsets failed: ${e.message}\n`)
491
- }
599
+ const run = _transcriptOffsetsPersistTail.catch(() => {}).then(async () => {
600
+ try {
601
+ const obj = Object.fromEntries(_transcriptOffsets)
602
+ await setMetaValue(db, TRANSCRIPT_OFFSETS_KEY, JSON.stringify(obj))
603
+ } catch (e) {
604
+ __mixdogMemoryLog(`[memory] persist transcript offsets failed: ${e.message}\n`)
605
+ }
606
+ })
607
+ _transcriptOffsetsPersistTail = run.catch(() => {})
608
+ return run
492
609
  }
493
610
 
494
611
  async function getCycleLastRun() {
@@ -522,9 +639,7 @@ async function getCycleLastRun() {
522
639
  }
523
640
 
524
641
  async function setCycleLastRun(kind, ts) {
525
- const cur = await getCycleLastRun()
526
- cur[kind] = ts
527
- await setMetaValue(db, CYCLE_LAST_RUN_KEY, JSON.stringify(cur))
642
+ await mergeMetaValue(db, CYCLE_LAST_RUN_KEY, { [kind]: ts })
528
643
  }
529
644
 
530
645
  // Raw-row priority lookup for narrow-window queries. Raw rows (is_root=0,
@@ -590,7 +705,7 @@ async function recallSessionRows(args = {}) {
590
705
  where.push(`(${clauses.join(' OR ')})`)
591
706
  }
592
707
  params.push(limit)
593
- const rows = (await db.query(`
708
+ let rows = (await db.query(`
594
709
  SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
595
710
  element, category, summary, status, score, last_seen_at, project_id
596
711
  FROM entries
@@ -598,40 +713,132 @@ async function recallSessionRows(args = {}) {
598
713
  ORDER BY ts DESC, id DESC
599
714
  LIMIT $${params.length}
600
715
  `, params)).rows
716
+ if (rows.length < limit) {
717
+ const seen = new Set(rows.map((row) => Number(row.id)).filter((id) => Number.isFinite(id)))
718
+ const fillLimit = Math.max(0, limit - rows.length)
719
+ const fillRows = fillLimit > 0
720
+ ? (await db.query(`
721
+ SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
722
+ element, category, summary, status, score, last_seen_at, project_id
723
+ FROM entries
724
+ WHERE session_id = $1
725
+ AND id <> ALL($2::bigint[])
726
+ ORDER BY ts DESC, id DESC
727
+ LIMIT $3
728
+ `, [sessionId, [...seen], fillLimit])).rows
729
+ : []
730
+ if (fillRows.length > 0) rows = [...rows, ...fillRows]
731
+ }
732
+ if (args.includeMembers === true) {
733
+ const rootIds = rows
734
+ .filter((row) => Number(row.is_root) === 1)
735
+ .map((row) => Number(row.id))
736
+ .filter((id) => Number.isFinite(id))
737
+ if (rootIds.length > 0) {
738
+ const members = (await db.query(`
739
+ SELECT id, ts, role, content, session_id, source_turn, project_id, chunk_root
740
+ FROM entries
741
+ WHERE chunk_root = ANY($1::bigint[]) AND is_root = 0
742
+ ORDER BY chunk_root ASC, COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
743
+ `, [rootIds])).rows
744
+ const byRoot = new Map(rootIds.map((id) => [id, []]))
745
+ for (const member of members) {
746
+ const root = Number(member.chunk_root)
747
+ if (byRoot.has(root)) byRoot.get(root).push(member)
748
+ }
749
+ for (const row of rows) {
750
+ const id = Number(row.id)
751
+ if (byRoot.has(id)) row.members = byRoot.get(id)
752
+ }
753
+ }
754
+ }
601
755
  return { text: renderEntryLines(rows) }
602
756
  }
603
757
 
604
758
  async function ingestSessionMessages(args = {}) {
605
759
  const sessionId = String(args.sessionId || args.session_id || `session-${Date.now()}`).trim()
606
760
  const messages = Array.isArray(args.messages) ? args.messages : []
607
- const limit = Math.max(1, Math.min(500, Number(args.limit) || 200))
761
+ // Recall fast-track hydrates the current session before compaction; allow
762
+ // callers to ingest the full in-memory transcript instead of silently
763
+ // clipping long sessions at 500 turns. Default remains conservative.
764
+ const limit = Math.max(1, Math.min(5000, Number(args.limit) || 200))
608
765
  const start = Math.max(0, messages.length - limit)
609
766
  const projectId = resolveProjectScope(typeof args.cwd === 'string' && args.cwd ? args.cwd : null)
610
767
  let considered = 0
611
768
  let inserted = 0
769
+ // Monotonic ingest order, independent of the current (post-compaction)
770
+ // array index. source_turn used to be `i+1`, but after compaction shrinks /
771
+ // reindexes session.messages a NEWLY appended turn gets a LOW i and thus a
772
+ // LOW source_turn — and since dump_session_roots / recall order by
773
+ // source_turn first, it would sort BEFORE older pre-compaction rows. Seed a
774
+ // running counter from the current max source_turn for this session so every
775
+ // new row is assigned a turn strictly greater than all previously-ingested
776
+ // ones (true continuation order). Re-ingested (ON CONFLICT) rows keep their
777
+ // original turn and do not consume a new one.
778
+ let prevMaxTurn = 0
779
+ try {
780
+ const maxRow = await db.query(
781
+ `SELECT COALESCE(MAX(source_turn), 0) AS max_turn FROM entries WHERE session_id = $1`,
782
+ [sessionId],
783
+ )
784
+ prevMaxTurn = Number(maxRow.rows?.[0]?.max_turn) || 0
785
+ } catch { prevMaxTurn = 0 }
786
+ const turnAllocator = createIngestTurnAllocator(prevMaxTurn)
612
787
  for (let i = start; i < messages.length; i += 1) {
613
788
  const m = messages[i]
614
- if (!m || (m.role !== 'user' && m.role !== 'assistant')) continue
615
- const content = cleanMemoryText(firstTextContent(m.content))
789
+ if (!m || typeof m !== 'object') continue
790
+ const role = normalizeIngestRole(m.role)
791
+ // ingest_session persists user/assistant only; system/developer/tool rows
792
+ // are dropped so recall-fasttrack summaries stay conversation-focused and
793
+ // do not re-inject content already in the protected system prefix.
794
+ if (!role) continue
795
+ const content = cleanMemoryText(sessionMessageContent(m))
616
796
  if (!content || !content.trim()) continue
617
797
  considered += 1
618
798
  const tsMs = parseTsToMs(m.ts ?? m.timestamp ?? (Date.now() - (messages.length - i)))
619
- const sourceRef = `session:${sessionId}#${i + 1}`
799
+ // Stable per-message identity. The previous `session:${id}#${i+1}` key was
800
+ // positional, so after compaction shrinks/reindexes session.messages a
801
+ // later turn could reuse an old index and be silently skipped by
802
+ // ON CONFLICT DO NOTHING. stableSessionSourceRef hashes only durable
803
+ // fields (role, original ts if present, tool ids, content) — never the
804
+ // synthesized tsMs fallback or the loop index.
805
+ const sourceRef = stableSessionSourceRef(sessionId, m, role, content)
806
+ // Assign the next monotonic turn; only consume it when the row is actually
807
+ // inserted (a conflicting re-ingest keeps its original source_turn).
808
+ const assignedTurn = turnAllocator.peekNext()
620
809
  const result = await db.query(`
621
810
  INSERT INTO entries(ts, role, content, source_ref, session_id, source_turn, project_id)
622
811
  VALUES ($1, $2, $3, $4, $5, $6, $7)
623
812
  ON CONFLICT DO NOTHING
624
- `, [tsMs, m.role, content, sourceRef, sessionId, i + 1, projectId])
625
- inserted += Number(result.rowCount ?? result.affectedRows ?? 0) || 0
813
+ `, [tsMs, role, content, sourceRef, sessionId, assignedTurn, projectId])
814
+ const rowInserted = Number(result.rowCount ?? result.affectedRows ?? 0) || 0
815
+ if (rowInserted > 0) {
816
+ inserted += rowInserted
817
+ turnAllocator.next()
818
+ }
626
819
  }
627
820
  return { text: `ingest_session: considered=${considered} inserted=${inserted} session=${sessionId}` }
628
821
  }
629
822
 
630
- async function ingestTranscriptFile(transcriptPath, { cwd } = {}) {
823
+ function runTranscriptIngestSerialized(transcriptPath, fn) {
824
+ const key = path.resolve(transcriptPath)
825
+ const prev = _ingestTranscriptTails.get(key) ?? Promise.resolve()
826
+ const run = prev.catch(() => {}).then(fn)
827
+ _ingestTranscriptTails.set(key, run.catch(() => {}))
828
+ return run
829
+ }
830
+
831
+ function snapshotTranscriptOffset(transcriptPath) {
832
+ const stored = _transcriptOffsets.get(transcriptPath)
833
+ if (!stored) return { bytes: 0, lineIndex: 0 }
834
+ return { bytes: Number(stored.bytes) || 0, lineIndex: Number(stored.lineIndex) || 0 }
835
+ }
836
+
837
+ async function ingestTranscriptFileImpl(transcriptPath, { cwd } = {}) {
631
838
  let stat
632
839
  try { stat = await fs.promises.stat(transcriptPath) } catch { return 0 }
633
840
  const sessionUuid = path.basename(transcriptPath, '.jsonl')
634
- const prev = _transcriptOffsets.get(transcriptPath) ?? { bytes: 0, lineIndex: 0 }
841
+ const prev = snapshotTranscriptOffset(transcriptPath)
635
842
  if (stat.size < prev.bytes) {
636
843
  prev.bytes = 0
637
844
  prev.lineIndex = 0
@@ -721,21 +928,16 @@ async function ingestTranscriptFile(transcriptPath, { cwd } = {}) {
721
928
  break
722
929
  }
723
930
  }
724
- prev.bytes = lastGoodBytes
725
- prev.lineIndex = lastGoodLineIndex
726
- _transcriptOffsets.set(transcriptPath, prev)
931
+ _transcriptOffsets.set(transcriptPath, {
932
+ bytes: lastGoodBytes,
933
+ lineIndex: lastGoodLineIndex,
934
+ })
727
935
  await persistTranscriptOffsets()
728
936
  return count
729
937
  }
730
938
 
731
- function firstTextContent(content) {
732
- if (typeof content === 'string') return content
733
- if (!Array.isArray(content)) return ''
734
- for (const item of content) {
735
- if (typeof item === 'string') return item
736
- if (item?.type === 'text' && typeof item.text === 'string') return item.text
737
- }
738
- return ''
939
+ async function ingestTranscriptFile(transcriptPath, options = {}) {
940
+ return runTranscriptIngestSerialized(transcriptPath, () => ingestTranscriptFileImpl(transcriptPath, options))
739
941
  }
740
942
 
741
943
  function parseTsToMs(value) {
@@ -776,7 +978,7 @@ function cwdFromTranscriptPath(fp) {
776
978
  }
777
979
 
778
980
  function _initTranscriptWatcher() {
779
- const projectsRoot = path.join(os.homedir(), '.mixdog', 'projects')
981
+ const projectsRoot = path.join(mixdogHome(), 'projects')
780
982
  const SAFETY_POLL_MS = 5 * 60_000
781
983
  const DEBOUNCE_MS = 500
782
984
  const watchedFiles = new Map()
@@ -963,6 +1165,83 @@ function _initTranscriptWatcher() {
963
1165
  const CYCLE1_HEALTH_OVERDUE_MS = 5 * 60_000
964
1166
  const CYCLE1_AUTO_RESTART_COOLDOWN_MS = 5 * 60_000
965
1167
 
1168
+ // In-process cycle1 LLM adapter. The memory daemon runs makeAgentDispatch()
1169
+ // locally (provider registry is initialized in _initStore), so cycle1 never
1170
+ // has to route over the dead IPC agent path (agent-ipc.mjs callAgentDispatch). The
1171
+ // factory is built once (role/taskType are fixed) and the returned function
1172
+ // is reshaped to cycle1's call signature: cycle1 invokes
1173
+ // `callLlm({ role, taskType, mode, preset, timeout, cwd }, userMessage)` and
1174
+ // expects a raw string, while makeAgentDispatch's function takes a single
1175
+ // `{ prompt }` object. The adapter maps the two — preset/cwd resolution is
1176
+ // handled inside makeAgentDispatch via role (cycle1-agent → maint.memory slot).
1177
+ let _cycle1AgentDispatch = null
1178
+ function getCycle1CallLlm() {
1179
+ if (!_cycle1AgentDispatch) {
1180
+ _cycle1AgentDispatch = makeAgentDispatch({
1181
+ role: 'cycle1-agent',
1182
+ taskType: 'maintenance',
1183
+ sourceType: 'memory-cycle',
1184
+ // cycle1 parses the full raw line-format response; the agent brief cap
1185
+ // (12KB) would truncate a large valid response and append prose, causing
1186
+ // partial parsing / omitted / invalid chunks. Opt out so the cycle1
1187
+ // no-truncation contract is preserved through makeAgentDispatch.
1188
+ brief: false,
1189
+ })
1190
+ }
1191
+ return async (opts = {}, userMessage) => {
1192
+ // Preserve cycle1's timeout contract: cycle1 derives `opts.timeout` from
1193
+ // config / caller deadline and expects it to bound the call. makeAgentDispatch
1194
+ // takes it as a per-call `idleTimeoutMs` (stale watchdog). Map it through;
1195
+ // omit when absent/0 so agent defaults apply.
1196
+ const callTimeout = Number(opts?.timeout)
1197
+ return _cycle1AgentDispatch({
1198
+ prompt: String(userMessage ?? ''),
1199
+ preset: opts?.preset || undefined,
1200
+ ...(Number.isFinite(callTimeout) && callTimeout > 0 ? { idleTimeoutMs: callTimeout } : {}),
1201
+ })
1202
+ }
1203
+ }
1204
+
1205
+ let _cycle2AgentDispatch = null
1206
+ function getCycle2CallLlm() {
1207
+ if (!_cycle2AgentDispatch) {
1208
+ _cycle2AgentDispatch = makeAgentDispatch({
1209
+ role: 'cycle2-agent',
1210
+ taskType: 'maintenance',
1211
+ sourceType: 'memory-cycle',
1212
+ brief: false,
1213
+ })
1214
+ }
1215
+ return async (opts = {}, userMessage) => {
1216
+ const callTimeout = Number(opts?.timeout)
1217
+ return _cycle2AgentDispatch({
1218
+ prompt: String(userMessage ?? ''),
1219
+ preset: opts?.preset || undefined,
1220
+ ...(Number.isFinite(callTimeout) && callTimeout > 0 ? { idleTimeoutMs: callTimeout } : {}),
1221
+ })
1222
+ }
1223
+ }
1224
+
1225
+ let _cycle3AgentDispatch = null
1226
+ function getCycle3CallLlm() {
1227
+ if (!_cycle3AgentDispatch) {
1228
+ _cycle3AgentDispatch = makeAgentDispatch({
1229
+ role: 'cycle3-agent',
1230
+ taskType: 'maintenance',
1231
+ sourceType: 'memory-cycle',
1232
+ brief: false,
1233
+ })
1234
+ }
1235
+ return async (opts = {}, userMessage) => {
1236
+ const callTimeout = Number(opts?.timeout)
1237
+ return _cycle3AgentDispatch({
1238
+ prompt: String(userMessage ?? ''),
1239
+ preset: opts?.preset || undefined,
1240
+ ...(Number.isFinite(callTimeout) && callTimeout > 0 ? { idleTimeoutMs: callTimeout } : {}),
1241
+ })
1242
+ }
1243
+ }
1244
+
966
1245
  async function recordCycle1Result(result) {
967
1246
  const now = Date.now()
968
1247
  await setCycleLastRun('cycle1_heartbeat', now)
@@ -978,6 +1257,13 @@ async function recordCycle1Result(result) {
978
1257
  }
979
1258
 
980
1259
  function _startCycle1Run(config = {}, options = {}) {
1260
+ // Default to the in-process agent dispatch so every cycle1 path — scheduled,
1261
+ // auto-restart, periodic, manual (action:cycle1), backfill drain, rebuild —
1262
+ // dispatches locally and the dead IPC fallback in memory-cycle1.mjs is never
1263
+ // reached. Explicit options.callLlm (if a caller ever passes one) wins.
1264
+ if (typeof options?.callLlm !== 'function') {
1265
+ options = { ...options, callLlm: getCycle1CallLlm() }
1266
+ }
981
1267
  _cycle1InFlight = (async () => {
982
1268
  try {
983
1269
  const result = await runCycle1(db, config, options, DATA_DIR)
@@ -1142,10 +1428,14 @@ function scheduleScheduledCycle2(config, signature, attempt = 0) {
1142
1428
  }
1143
1429
  _cycle2InFlight = true
1144
1430
  try {
1145
- const result = await runCycle2(db, config, {
1431
+ let c2Options = {
1146
1432
  coalescedRetry: true,
1147
1433
  onCoalescedSuccess: _finalizeCycle2Run,
1148
- }, DATA_DIR)
1434
+ }
1435
+ if (typeof c2Options?.callLlm !== 'function') {
1436
+ c2Options = { ...c2Options, callLlm: getCycle2CallLlm() }
1437
+ }
1438
+ const result = await runCycle2(db, config, c2Options, DATA_DIR)
1149
1439
  if (result?.skippedInFlight) {
1150
1440
  scheduleScheduledCycle2(config, signature, attempt + 1)
1151
1441
  } else if (result?.coalescedRetryNoop) {
@@ -1175,10 +1465,14 @@ function scheduleScheduledCycle3(config, signature, attempt = 0) {
1175
1465
  }
1176
1466
  _cycle3InFlight = true
1177
1467
  try {
1178
- const result = await runCycle3(db, config, DATA_DIR, {
1468
+ let c3Options = {
1179
1469
  coalescedRetry: true,
1180
1470
  onCoalescedSuccess: () => setCycleLastRun('cycle3', Date.now()),
1181
- })
1471
+ }
1472
+ if (typeof c3Options?.callLlm !== 'function') {
1473
+ c3Options = { ...c3Options, callLlm: getCycle3CallLlm() }
1474
+ }
1475
+ const result = await runCycle3(db, config, DATA_DIR, c3Options)
1182
1476
  if (result?.skippedInFlight) {
1183
1477
  scheduleScheduledCycle3(config, signature, attempt + 1)
1184
1478
  } else if (result?.coalescedRetryNoop) {
@@ -1335,8 +1629,12 @@ async function _initRuntime() {
1335
1629
  // increments, so deleted rows leave permanent gaps. Fast no-op when already
1336
1630
  // contiguous (or empty). Runs only here — never in cycle2/addCore/deleteCore.
1337
1631
  await compactCoreIds(DATA_DIR)
1338
- _transcriptWatcher = _initTranscriptWatcher()
1339
- _startCycles()
1632
+ if (memoryCyclesEnabled()) {
1633
+ _transcriptWatcher = _initTranscriptWatcher()
1634
+ _startCycles()
1635
+ } else {
1636
+ __mixdogMemoryLog('[memory-service] secondary mode; skipping background cycles\n')
1637
+ }
1340
1638
  _initialized = true
1341
1639
  // Boot complete — continue straight into the deferred embedding warmup.
1342
1640
  // Fire-and-forget on the embedding worker thread; never awaited so it does
@@ -1349,6 +1647,7 @@ function _beginRuntimeInit() {
1349
1647
  if (!_initPromise) {
1350
1648
  _initPromise = _initRuntime().catch((e) => {
1351
1649
  __mixdogMemoryLog(`[memory-service] runtime init failed: ${e?.stack || e?.message || e}\n`)
1650
+ _initPromise = null
1352
1651
  throw e
1353
1652
  })
1354
1653
  }
@@ -1613,15 +1912,20 @@ async function handleSearch(args, signal) {
1613
1912
  })
1614
1913
  let settled
1615
1914
  try {
1616
- // Pre-warm the per-query embedding cache with one batched ONNX run so
1617
- // each sub-search lands an embedText cache hit (~0ms) instead of
1618
- // serially queueing through the worker's single-flight inference lock.
1619
- // Replaces N sequential ~130ms inferences with one ~150-200ms batch.
1620
- //
1621
- // Race against the same deadline as the fan-out itself: a stuck
1622
- // embedding worker would previously park here indefinitely because
1623
- // the timer hadn't been started yet from the fan-out's perspective.
1624
- await Promise.race([embedTexts(queries), deadlineRace])
1915
+ // Pre-warm only when the embedding model is already resident. If the
1916
+ // process is still cold, keep recall responsive and let the background
1917
+ // warmup finish independently instead of making the first query pay the
1918
+ // ONNX session-create cost.
1919
+ if (isEmbeddingModelReady()) {
1920
+ // Race against the same deadline as the fan-out itself: a stuck
1921
+ // embedding worker would previously park here indefinitely because
1922
+ // the timer hadn't been started yet from the fan-out's perspective.
1923
+ await Promise.race([embedTexts(queries), deadlineRace])
1924
+ } else if (embeddingWarmupCanStart()) {
1925
+ void warmupEmbeddingProvider().catch((err) => {
1926
+ __mixdogMemoryLog(`[memory-service] embedding warmup after cold fan-out skipped dense search: ${err?.message || err}\n`)
1927
+ })
1928
+ }
1625
1929
  settled = await Promise.race([
1626
1930
  Promise.all(queries.map(async (q) => {
1627
1931
  if (fanOutAbort.signal.aborted) throw fanOutAbort.signal.reason
@@ -1700,7 +2004,21 @@ async function handleSearch(args, signal) {
1700
2004
  if (query) {
1701
2005
  const _t0 = Date.now()
1702
2006
  if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1703
- const queryVector = await embedText(query)
2007
+ let queryVector = null
2008
+ if (isEmbeddingModelReady()) {
2009
+ queryVector = await embedText(query)
2010
+ } else {
2011
+ const now = Date.now()
2012
+ if (now - _embeddingColdRecallLogAt > 10_000) {
2013
+ _embeddingColdRecallLogAt = now
2014
+ __mixdogMemoryLog('[recall] embedding model cold; returning lexical results while background warmup continues\n')
2015
+ }
2016
+ if (embeddingWarmupCanStart()) {
2017
+ void warmupEmbeddingProvider().catch((err) => {
2018
+ __mixdogMemoryLog(`[memory-service] embedding warmup after cold recall failed: ${err?.message || err}\n`)
2019
+ })
2020
+ }
2021
+ }
1704
2022
  if (signal?.aborted) throw signal.reason ?? new Error('aborted')
1705
2023
  const _t1 = Date.now()
1706
2024
  if (process.env.MIXDOG_DEBUG_MEMORY) {
@@ -1791,7 +2109,7 @@ async function handleSearch(args, signal) {
1791
2109
  // Emit a recall trace event so getTraceWithEntries() can correlate
1792
2110
  // this search with the top-ranked memory entry. One event per
1793
2111
  // handleSearch call (not per returned row) — cheapest meaningful link.
1794
- // parent_span_id left null: the bridge-side span id is only known after
2112
+ // parent_span_id left null: the agent-side span id is only known after
1795
2113
  // the DB insert of the loop/tool events, which happens async on the
1796
2114
  // client side and is not available here.
1797
2115
  if (_traceDb && filtered.length > 0) {
@@ -1937,6 +2255,7 @@ async function dumpSessionRootChunks(args = {}) {
1937
2255
  chunks.push({
1938
2256
  id: Number(raw.id),
1939
2257
  kind: 'raw',
2258
+ chunkRoot: raw.chunk_root ?? null,
1940
2259
  ts: Number(raw.ts) || 0,
1941
2260
  sourceTurn: raw.source_turn ?? null,
1942
2261
  category: null,
@@ -1954,7 +2273,7 @@ async function dumpSessionRootChunks(args = {}) {
1954
2273
  ? chunks.map((chunk, idx) => {
1955
2274
  const label = chunk.kind === 'root'
1956
2275
  ? `# chunk ${idx + 1} root=${chunk.id}${chunk.category ? ` category=${chunk.category}` : ''}`
1957
- : `# raw ${idx + 1} id=${chunk.id}`
2276
+ : `${chunk.chunkRoot == null ? '# raw_pending' : '# raw_terminal'} ${idx + 1} id=${chunk.id}`
1958
2277
  const summary = chunk.summary ? `summary: ${chunk.summary}\n` : ''
1959
2278
  return `${label}\n${summary}${chunk.text}`.trim()
1960
2279
  }).join('\n\n')
@@ -1997,7 +2316,10 @@ async function _handleMemCycle1(args, config, signal) {
1997
2316
  const minBatchOverride = Number(args?.min_batch)
1998
2317
  const sessionCapOverride = Number(args?.session_cap)
1999
2318
  const batchSizeOverride = Number(args?.batch_size)
2319
+ const windowSizeOverride = Number(args?.window_size ?? args?.windowSize)
2320
+ const rowsPerSessionOverride = Number(args?.rows_per_session ?? args?.rowsPerSession ?? args?.max_rows_per_session ?? args?.maxRowsPerSession)
2000
2321
  const concurrencyOverride = Number(args?.concurrency)
2322
+ const sessionIdOverride = String(args?.sessionId ?? args?.session_id ?? '').trim()
2001
2323
  const baseCycle1 = config?.cycle1 || {}
2002
2324
  let cycle1Config = baseCycle1
2003
2325
  // _runCycle1Impl reads `config?.min_batch ?? config?.cycle1?.min_batch ??
@@ -2011,12 +2333,24 @@ async function _handleMemCycle1(args, config, signal) {
2011
2333
  if (Number.isFinite(batchSizeOverride) && batchSizeOverride > 0) {
2012
2334
  cycle1Config = { ...cycle1Config, batch_size: batchSizeOverride }
2013
2335
  }
2336
+ if (Number.isFinite(windowSizeOverride) && windowSizeOverride > 0) {
2337
+ cycle1Config = { ...cycle1Config, window_size: windowSizeOverride }
2338
+ }
2339
+ if (Number.isFinite(rowsPerSessionOverride) && rowsPerSessionOverride > 0) {
2340
+ cycle1Config = { ...cycle1Config, rows_per_session: rowsPerSessionOverride }
2341
+ }
2342
+ if (sessionIdOverride) {
2343
+ cycle1Config = { ...cycle1Config, session_id: sessionIdOverride }
2344
+ }
2014
2345
  if (Number.isFinite(concurrencyOverride) && concurrencyOverride > 0) {
2015
2346
  cycle1Config = { ...cycle1Config, concurrency: Math.min(8, Math.floor(concurrencyOverride)) }
2016
2347
  }
2017
2348
  const callerDeadlineMs = Number(args?._callerDeadlineMs) || 0
2018
2349
  if (signal?.aborted) throw signal.reason ?? new Error('aborted')
2019
2350
  const cycle1Options = callerDeadlineMs > 0 ? { callerDeadlineMs, signal } : { signal }
2351
+ if (typeof args?._callLlm === 'function') {
2352
+ cycle1Options.callLlm = args._callLlm
2353
+ }
2020
2354
  const result = await _awaitCycle1Run(
2021
2355
  cycle1Config,
2022
2356
  cycle1Options,
@@ -2030,6 +2364,7 @@ async function _handleMemCycle1(args, config, signal) {
2030
2364
  const failedRows = Array.isArray(result?.failed_row_ids) ? result.failed_row_ids.length : Number(result?.quality?.failed_rows || 0)
2031
2365
  const invalidChunks = Array.isArray(result?.invalid_chunks) ? result.invalid_chunks.length : Number(result?.quality?.invalid_chunks || 0)
2032
2366
  return {
2367
+ ...result,
2033
2368
  text: `cycle1: chunks=${result.chunks} processed=${result.processed} skipped_chunks=${result.skipped}` +
2034
2369
  ` omitted=${omitted} prefiltered=${prefiltered} failed_rows=${failedRows} invalid_chunks=${invalidChunks}` +
2035
2370
  ` pending=${pendingStr} inFlight=${inFlightStr}${timedOutPart}`,
@@ -2038,7 +2373,11 @@ async function _handleMemCycle1(args, config, signal) {
2038
2373
 
2039
2374
  async function _handleMemCycle2(args, config, signal) {
2040
2375
  if (signal?.aborted) throw signal.reason ?? new Error('aborted')
2041
- const result = await runCycle2(db, config?.cycle2 || {}, { signal }, DATA_DIR)
2376
+ let c2Options = { signal }
2377
+ if (typeof c2Options?.callLlm !== 'function') {
2378
+ c2Options = { ...c2Options, callLlm: getCycle2CallLlm() }
2379
+ }
2380
+ const result = await runCycle2(db, config?.cycle2 || {}, c2Options, DATA_DIR)
2042
2381
  if (signal?.aborted) throw signal.reason ?? new Error('aborted')
2043
2382
  await _finalizeCycle2Run(result)
2044
2383
  const counts = {
@@ -2075,7 +2414,11 @@ async function _handleMemCycle3(args, config, signal) {
2075
2414
  : (requestedMode === 'proposal' || requestedMode === 'dry-run' || requestedMode === 'dryrun')
2076
2415
  ? 'proposal'
2077
2416
  : 'conservative'
2078
- const result = await runCycle3(db, config || {}, DATA_DIR, { signal, apply: confirmed ? true : undefined, applyMode })
2417
+ let c3Options = { signal, apply: confirmed ? true : undefined, applyMode }
2418
+ if (typeof c3Options?.callLlm !== 'function') {
2419
+ c3Options = { ...c3Options, callLlm: getCycle3CallLlm() }
2420
+ }
2421
+ const result = await runCycle3(db, config || {}, DATA_DIR, c3Options)
2079
2422
  if (signal?.aborted) throw signal.reason ?? new Error('aborted')
2080
2423
  const parts = ['reviewed', 'kept', 'updated', 'merged', 'deleted']
2081
2424
  .map(k => `${k}=${result?.[k] || 0}`)
@@ -2100,7 +2443,11 @@ async function _handleMemFlush(args, config, signal) {
2100
2443
  if (signal?.aborted) throw signal.reason ?? new Error('aborted')
2101
2444
  const r1 = await _awaitCycle1Run(config?.cycle1 || {}, { signal })
2102
2445
  if (signal?.aborted) throw signal.reason ?? new Error('aborted')
2103
- const r2 = await runCycle2(db, config?.cycle2 || {}, { signal }, DATA_DIR)
2446
+ let flushC2Options = { signal }
2447
+ if (typeof flushC2Options?.callLlm !== 'function') {
2448
+ flushC2Options = { ...flushC2Options, callLlm: getCycle2CallLlm() }
2449
+ }
2450
+ const r2 = await runCycle2(db, config?.cycle2 || {}, flushC2Options, DATA_DIR)
2104
2451
  if (signal?.aborted) throw signal.reason ?? new Error('aborted')
2105
2452
  await _finalizeCycle2Run(r2)
2106
2453
  return { text: `flush: cycle1 chunks=${r1.chunks} processed=${r1.processed}, cycle2 ${JSON.stringify(r2)}` }
@@ -2204,7 +2551,11 @@ async function _handleMemRebuild(args, config, signal) {
2204
2551
  // inside _awaitCycle1Run and guarantees the newly demoted rows are read.
2205
2552
  const r1 = await _startCycle1Run(config?.cycle1 || {}, { signal })
2206
2553
  if (signal?.aborted) throw signal.reason ?? new Error('aborted')
2207
- const r2 = await runCycle2(db, config?.cycle2 || {}, { signal }, DATA_DIR)
2554
+ let rebuildC2Options = { signal }
2555
+ if (typeof rebuildC2Options?.callLlm !== 'function') {
2556
+ rebuildC2Options = { ...rebuildC2Options, callLlm: getCycle2CallLlm() }
2557
+ }
2558
+ const r2 = await runCycle2(db, config?.cycle2 || {}, rebuildC2Options, DATA_DIR)
2208
2559
  await _finalizeCycle2Run(r2)
2209
2560
  return { text: `rebuild: cycle1 chunks=${r1.chunks} processed=${r1.processed}, cycle2 ${JSON.stringify(r2)}` }
2210
2561
  }
@@ -2254,7 +2605,11 @@ async function _handleMemBackfill(args, config, signal) {
2254
2605
  },
2255
2606
  runCycle2: async (dbArg, c2Config, c2Options, c2DataDir) => {
2256
2607
  if (signal?.aborted) throw signal.reason ?? new Error('aborted')
2257
- const r2 = await runCycle2(dbArg, c2Config, { ...c2Options, signal }, c2DataDir)
2608
+ let backfillC2Options = { ...c2Options, signal }
2609
+ if (typeof backfillC2Options?.callLlm !== 'function') {
2610
+ backfillC2Options = { ...backfillC2Options, callLlm: getCycle2CallLlm() }
2611
+ }
2612
+ const r2 = await runCycle2(dbArg, c2Config, backfillC2Options, c2DataDir)
2258
2613
  _capturedCycle2 = r2
2259
2614
  return r2
2260
2615
  },
@@ -2676,7 +3031,7 @@ async function handleToolCall(name, args, signal) {
2676
3031
  try {
2677
3032
  if (name === 'search_memories') {
2678
3033
  const result = await handleSearch(args || {}, signal)
2679
- return { content: [{ type: 'text', text: result.text }], isError: result.isError || false }
3034
+ return { ...result, content: [{ type: 'text', text: result.text }], isError: result.isError || false }
2680
3035
  }
2681
3036
  if (name === 'recall') {
2682
3037
  // recall is aiWrapped in the unified build; in standalone mode map it to
@@ -2710,11 +3065,11 @@ async function handleToolCall(name, args, signal) {
2710
3065
  ...(a.currentSession !== undefined ? { currentSession: a.currentSession } : {}),
2711
3066
  }
2712
3067
  const result = await handleSearch(searchArgs, signal)
2713
- return { content: [{ type: 'text', text: result.text }], isError: result.isError || false }
3068
+ return { ...result, content: [{ type: 'text', text: result.text }], isError: result.isError || false }
2714
3069
  }
2715
3070
  if (name === 'memory') {
2716
3071
  const result = await handleMemoryAction(args || {}, signal)
2717
- return { content: [{ type: 'text', text: result.text }], isError: result.isError || false }
3072
+ return { ...result, content: [{ type: 'text', text: result.text }], isError: result.isError || false }
2718
3073
  }
2719
3074
  return { content: [{ type: 'text', text: `unknown tool: ${name}` }], isError: true }
2720
3075
  } catch (err) {
@@ -2854,6 +3209,7 @@ let _backfillInFlight = null
2854
3209
  const _ownerInFlightHttpCalls = new Map()
2855
3210
 
2856
3211
  const httpServer = http.createServer(async (req, res) => {
3212
+ touchDaemonIdleTimer(`${req.method || 'HTTP'} ${req.url || '/'}`)
2857
3213
  if (req.method === 'POST' && req.url === '/session-reset') {
2858
3214
  _bootTimestamp = Date.now()
2859
3215
  sendJson(res, { ok: true, bootTimestamp: _bootTimestamp })
@@ -3075,9 +3431,9 @@ const httpServer = http.createServer(async (req, res) => {
3075
3431
  enqueueTraceEvents(_traceDb, body.events)
3076
3432
  // Use `queued` — events are async; `inserted` would imply durability.
3077
3433
  sendJson(res, { ok: true, queued: body.events.length })
3078
- // Fire-and-forget into focused bridge analytic tables.
3079
- insertBridgeCalls(_traceDb, body.events).catch(e =>
3080
- __mixdogMemoryLog(`[trace] insertBridgeCalls error: ${e?.message}\n`)
3434
+ // Fire-and-forget into focused agent analytic tables.
3435
+ insertAgentCalls(_traceDb, body.events).catch(e =>
3436
+ __mixdogMemoryLog(`[trace] insertAgentCalls error: ${e?.message}\n`)
3081
3437
  )
3082
3438
  } catch (e) {
3083
3439
  sendJson(res, { ok: false, error: e.message }, 500)
@@ -3120,11 +3476,11 @@ const httpServer = http.createServer(async (req, res) => {
3120
3476
  // DEV-ONLY cycle1 chunking bench. Gated by env MIXDOG_DEV_BENCH=1 so
3121
3477
  // production is untouched (route returns 404 when unset). Mirrors cycle1's
3122
3478
  // exact fetch query + per-session windowing, then runs each window through
3123
- // buildCycle1ChunkPrompt + callBridgeLlm + parseCycle1LineFormat. STRICT
3479
+ // buildCycle1ChunkPrompt + callAgentDispatch + parseCycle1LineFormat. STRICT
3124
3480
  // read-only — no UPDATE, no transaction, no commit.
3125
3481
  if (req.method === 'POST' && req.url === '/dev/cycle1-bench') {
3126
3482
  // Gate: env MIXDOG_DEV_BENCH=1 OR a runtime flag file, so it can be
3127
- // toggled without restarting Claude Code (env only reaches the worker
3483
+ // toggled without restarting the host agent (env only reaches the worker
3128
3484
  // on a full CC restart, not via dev-sync full-restart).
3129
3485
  const _devBenchOn = process.env.MIXDOG_DEV_BENCH === '1'
3130
3486
  || (DATA_DIR && fs.existsSync(path.join(DATA_DIR, '.dev-bench-enabled')))
@@ -3150,11 +3506,14 @@ const httpServer = http.createServer(async (req, res) => {
3150
3506
  : null
3151
3507
 
3152
3508
  // Lazy-load LLM + chunking helpers so production boot pays nothing.
3153
- const [{ buildCycle1ChunkPrompt, parseCycle1LineFormat }, { callBridgeLlm }, { resolveMaintenancePreset }] = await Promise.all([
3509
+ // Use the same in-process agent dispatch adapter as real cycle1 the legacy
3510
+ // agent-ipc callAgentDispatch() path is dead in the detached standalone
3511
+ // memory daemon (no connected IPC), so the dev bench must mirror prod.
3512
+ const [{ buildCycle1ChunkPrompt, parseCycle1LineFormat }, { resolveMaintenancePreset }] = await Promise.all([
3154
3513
  import('./lib/memory-cycle1.mjs'),
3155
- import('./lib/agent-ipc.mjs'),
3156
3514
  import('../shared/llm/index.mjs'),
3157
3515
  ])
3516
+ const benchCallLlm = getCycle1CallLlm()
3158
3517
 
3159
3518
  const CYCLE1_MIN_BATCH = 3
3160
3519
  const CYCLE1_SESSION_CAP = 10
@@ -3225,13 +3584,9 @@ const httpServer = http.createServer(async (req, res) => {
3225
3584
  const t0 = Date.now()
3226
3585
  let raw, error
3227
3586
  try {
3228
- raw = await callBridgeLlm({
3229
- role: 'cycle1-agent',
3230
- taskType: 'maintenance',
3231
- mode: 'cycle1',
3587
+ raw = await benchCallLlm({
3232
3588
  preset,
3233
3589
  timeout: TIMEOUT_MS,
3234
- cwd: null,
3235
3590
  }, userMessage)
3236
3591
  } catch (e) {
3237
3592
  error = e?.message ?? String(e)
@@ -3310,26 +3665,6 @@ const httpServer = http.createServer(async (req, res) => {
3310
3665
  return
3311
3666
  }
3312
3667
 
3313
- if (req.method === 'POST' && req.url === '/session-start/recap') {
3314
- try {
3315
- const body = await readBody(req)
3316
- const projectId = resolveProjectScope(typeof body.cwd === 'string' && body.cwd ? body.cwd : null)
3317
- const rows = projectId !== null
3318
- ? (await db.query(`
3319
- SELECT id, ts, summary FROM entries
3320
- WHERE is_root = 1 AND (project_id IS NULL OR project_id = $1)
3321
- ORDER BY ts DESC, id DESC LIMIT 20
3322
- `, [projectId])).rows
3323
- : (await db.query(`
3324
- SELECT id, ts, summary FROM entries
3325
- WHERE is_root = 1
3326
- ORDER BY ts DESC, id DESC LIMIT 20
3327
- `)).rows
3328
- sendJson(res, { ok: true, projectId, rows })
3329
- } catch (e) { sendError(res, e.message) }
3330
- return
3331
- }
3332
-
3333
3668
  if (req.method === 'POST' && req.url === '/api/tool') {
3334
3669
  if (!isLocalOrigin(req)) {
3335
3670
  sendJson(res, { content: [{ type: 'text', text: 'forbidden: cross-origin' }], isError: true }, 403)
@@ -3530,34 +3865,94 @@ export async function init() {
3530
3865
  process.on('exit', releaseMemoryOwnerLock)
3531
3866
  }
3532
3867
  const runtimeReady = _beginRuntimeInit()
3533
- const boundPort = await _startHttpServer()
3534
- await runtimeReady
3535
- advertiseMemoryPort(boundPort)
3868
+ let boundPort = null
3869
+ if (!memorySecondaryMode()) {
3870
+ boundPort = await _startHttpServer()
3871
+ await runtimeReady
3872
+ advertiseMemoryPort(boundPort)
3873
+ } else {
3874
+ await runtimeReady
3875
+ }
3536
3876
  if (process.env.MIXDOG_WORKER_MODE === '1' && process.send) {
3537
3877
  __mixdogMemoryLog(`[boot-time] tag=memory-ready tMs=${Date.now()}\n`)
3538
3878
  process.send({ type: 'ready', port: boundPort })
3539
3879
  }
3540
3880
  __mixdogMemoryLog(`[memory-service] init() complete (entries unified mode, version=${PLUGIN_VERSION})\n`)
3881
+ touchDaemonIdleTimer('init')
3541
3882
  }
3542
3883
 
3543
3884
  export async function stop() {
3544
- _stopCycles()
3545
- await stopLlmWorker()
3546
- if (httpServer) await new Promise(resolve => httpServer.close(resolve))
3547
- await closeDatabase(DATA_DIR)
3548
- // Stop the PG postmaster after the connection pool has been drained.
3549
- // closeDatabase() only ends the client pool; without this the child
3550
- // postmaster keeps running after the memory service exits.
3551
- try {
3552
- const { stopPgForShutdown } = await import('./lib/pg/supervisor.mjs')
3553
- await stopPgForShutdown()
3554
- } catch {}
3555
- releaseLock()
3885
+ if (_stopPromise) return _stopPromise
3886
+ _stopPromise = (async () => {
3887
+ _stopCycles()
3888
+ if (_periodicAdvertiseTimer) {
3889
+ try { clearInterval(_periodicAdvertiseTimer) } catch {}
3890
+ _periodicAdvertiseTimer = null
3891
+ }
3892
+ _periodicAdvertiseInstalled = false
3893
+ _currentAdvertisedPort = null
3894
+ _pendingEmbeddingWarmup = null
3895
+ if (_idleShutdownTimer) {
3896
+ try { clearTimeout(_idleShutdownTimer) } catch {}
3897
+ _idleShutdownTimer = null
3898
+ }
3899
+ await stopLlmWorker()
3900
+ resetHttpListenErrorHandler()
3901
+ if (_httpBoundPort != null || _httpReadyPromise) {
3902
+ await new Promise(resolve => {
3903
+ try {
3904
+ httpServer.close(() => resolve())
3905
+ } catch {
3906
+ resolve()
3907
+ }
3908
+ })
3909
+ }
3910
+ _httpReadyPromise = null
3911
+ _httpBoundPort = null
3912
+ activePort = BASE_PORT
3913
+ if (_traceDb) {
3914
+ try { await closeTraceDatabase(DATA_DIR) } catch {}
3915
+ _traceDb = null
3916
+ }
3917
+ await closeDatabase(DATA_DIR)
3918
+ // Stop the PG postmaster after the connection pools have been drained.
3919
+ // closeDatabase() only ends the client pool; without this the child
3920
+ // postmaster keeps running after the memory service exits.
3921
+ if (!memorySecondaryMode()) {
3922
+ try {
3923
+ const { stopPgForShutdown } = await import('./lib/pg/supervisor.mjs')
3924
+ await stopPgForShutdown()
3925
+ } catch {}
3926
+ } else {
3927
+ __mixdogMemoryLog('[memory-service] secondary mode; leaving shared PG running\n')
3928
+ }
3929
+ db = null
3930
+ mainConfig = null
3931
+ _initialized = false
3932
+ _initPromise = null
3933
+ _bootTimestamp = null
3934
+ _transcriptOffsets = new Map()
3935
+ _cycle1InFlight = null
3936
+ _cycle2InFlight = false
3937
+ _cycle3InFlight = false
3938
+ _checkCyclesInFlight = false
3939
+ releaseLock()
3940
+ })().finally(() => {
3941
+ _stopPromise = null
3942
+ })
3943
+ return _stopPromise
3556
3944
  }
3557
3945
 
3558
3946
  let activePort = BASE_PORT
3559
3947
  let _httpReadyPromise = null
3560
3948
  let _httpBoundPort = null
3949
+ let _httpListenErrorHandler = null
3950
+
3951
+ function resetHttpListenErrorHandler() {
3952
+ if (!_httpListenErrorHandler) return
3953
+ try { httpServer.off('error', _httpListenErrorHandler) } catch {}
3954
+ _httpListenErrorHandler = null
3955
+ }
3561
3956
 
3562
3957
  function _startHttpServer() {
3563
3958
  if (_httpBoundPort != null) return Promise.resolve(_httpBoundPort)
@@ -3572,7 +3967,11 @@ function _startHttpServer() {
3572
3967
  resolve(boundPort)
3573
3968
  })
3574
3969
  }
3575
- httpServer.on('error', (err) => {
3970
+ _httpListenErrorHandler = (err) => {
3971
+ if (_httpBoundPort != null) {
3972
+ __mixdogMemoryLog(`[memory-service] HTTP error: ${err?.message || err}\n`)
3973
+ return
3974
+ }
3576
3975
  if (err.code === 'EADDRINUSE' && activePort < MAX_PORT) {
3577
3976
  activePort++
3578
3977
  tryListen()
@@ -3582,9 +3981,11 @@ function _startHttpServer() {
3582
3981
  tryListen()
3583
3982
  } else {
3584
3983
  __mixdogMemoryLog(`[memory-service] HTTP fatal: ${err.message}\n`)
3984
+ resetHttpListenErrorHandler()
3585
3985
  reject(err)
3586
3986
  }
3587
- })
3987
+ }
3988
+ httpServer.on('error', _httpListenErrorHandler)
3588
3989
  tryListen()
3589
3990
  })
3590
3991
  return _httpReadyPromise
@@ -3688,7 +4089,7 @@ if (process.env.MIXDOG_WORKER_MODE === '1' && process.send) {
3688
4089
  // server with acquireLock + StdioServerTransport. Server-main spawnWorker
3689
4090
  // also forks this file with MIXDOG_WORKER_MODE='1'; that path uses the IPC
3690
4091
  // handler block above and acquireLock/init() as the single memory owner.
3691
- if (import.meta.url === pathToFileURL(process.argv[1] || '').href && process.env.MIXDOG_WORKER_MODE !== '1') {
4092
+ if (IS_MEMORY_ENTRY && process.env.MIXDOG_WORKER_MODE !== '1') {
3692
4093
  ;(async () => {
3693
4094
  acquireLock()
3694
4095
  process.on('exit', releaseLock)