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
@@ -1,9 +1,9 @@
1
1
  /**
2
- * agent-ipc.mjs — IPC client for bridge LLM calls from the memory worker.
2
+ * agent-ipc.mjs — IPC client for agent dispatch calls from the memory worker.
3
3
  *
4
- * The memory worker runs in its own fork, while the bridge / provider
4
+ * The memory worker runs in its own fork, while the agent runtime / provider
5
5
  * registry lives in-process in the parent server. cycle1 / cycle2 can't
6
- * call makeBridgeLlm() locally (provider map is empty here), so we route
6
+ * call makeAgentDispatch() locally (provider map is empty here), so we route
7
7
  * every LLM call over IPC:
8
8
  *
9
9
  * memory → parent : { type: 'agent_ipc_request', callId, tool, params }
@@ -36,10 +36,10 @@ function nextCallId() {
36
36
  }
37
37
 
38
38
  /**
39
- * Send an agent-bridge LLM request to the parent. Throws if IPC is
39
+ * Send an agent-agent dispatch request to the parent. Throws if IPC is
40
40
  * unavailable (worker not forked) or the parent reports an error.
41
41
  *
42
- * @param {object} opts bridge-llm construction options
42
+ * @param {object} opts agent-dispatch construction options
43
43
  * @param {string} [opts.role]
44
44
  * @param {string} [opts.taskType]
45
45
  * @param {string} [opts.mode]
@@ -49,9 +49,9 @@ function nextCallId() {
49
49
  * @param {string} prompt user message
50
50
  * @returns {Promise<string>} raw assistant content
51
51
  */
52
- export function callBridgeLlm(opts = {}, prompt) {
53
- if (!process.send) {
54
- return Promise.reject(new Error('agent-ipc: process.send unavailable (not running as worker)'))
52
+ export function callAgentDispatch(opts = {}, prompt) {
53
+ if (!process.send || !process.connected) {
54
+ return Promise.reject(new Error('agent-ipc: IPC channel unavailable (no process.send / not connected)'))
55
55
  }
56
56
  installListener()
57
57
  const callId = nextCallId()
@@ -60,20 +60,31 @@ export function callBridgeLlm(opts = {}, prompt) {
60
60
  const timer = setTimeout(() => {
61
61
  if (!pending.has(callId)) return
62
62
  pending.delete(callId)
63
- try {
64
- process.send({ type: 'agent_ipc_cancel', callId })
65
- } catch {}
63
+ if (process.connected) {
64
+ try {
65
+ process.send({ type: 'agent_ipc_cancel', callId }, undefined, {}, () => {})
66
+ } catch {}
67
+ }
66
68
  reject(new Error(`agent-ipc: timed out after ${timeoutMs}ms`))
67
69
  }, timeoutMs)
68
70
  pending.set(callId, {
69
71
  resolve: (v) => { clearTimeout(timer); resolve(v) },
70
72
  reject: (e) => { clearTimeout(timer); reject(e) },
71
73
  })
74
+ if (!process.connected) {
75
+ pending.delete(callId)
76
+ clearTimeout(timer)
77
+ reject(new Error('agent-ipc: IPC channel not connected'))
78
+ return
79
+ }
80
+ // Pass a write callback so a closed-channel write surfaces as an async
81
+ // error here (rejecting this Promise) instead of bubbling to the process
82
+ // 'error' event and crashing the daemon with ERR_IPC_CHANNEL_CLOSED.
72
83
  try {
73
84
  process.send({
74
85
  type: 'agent_ipc_request',
75
86
  callId,
76
- tool: 'bridge_llm',
87
+ tool: 'agent_dispatch',
77
88
  params: {
78
89
  role: opts.role || null,
79
90
  taskType: opts.taskType || null,
@@ -83,6 +94,12 @@ export function callBridgeLlm(opts = {}, prompt) {
83
94
  prompt: String(prompt ?? ''),
84
95
  timeout: timeoutMs,
85
96
  },
97
+ }, undefined, {}, (err) => {
98
+ if (!err) return
99
+ if (!pending.has(callId)) return
100
+ pending.delete(callId)
101
+ clearTimeout(timer)
102
+ reject(err)
86
103
  })
87
104
  } catch (e) {
88
105
  pending.delete(callId)
@@ -16,7 +16,7 @@ function __mixdogMemoryLog(...args) {
16
16
 
17
17
  import { getDatabase, embeddingToSql } from './memory.mjs'
18
18
  import { cachedEmbedTextBatch } from './memory-embed.mjs'
19
- import { callBridgeLlm } from './agent-ipc.mjs'
19
+ import { callAgentDispatch } from './agent-ipc.mjs'
20
20
  import { resolveMaintenancePreset } from '../../shared/llm/index.mjs'
21
21
  import { checkedConnect } from './pg/adapter.mjs'
22
22
 
@@ -169,7 +169,7 @@ async function _llmJudgeMerge(existing, incoming) {
169
169
  `EXISTING: ${existing.element} — ${String(existing.summary || '')}\n` +
170
170
  `INCOMING: ${incoming.element} — ${String(incoming.summary || '')}`
171
171
  try {
172
- const raw = await callBridgeLlm({
172
+ const raw = await callAgentDispatch({
173
173
  role: 'cycle2-agent',
174
174
  taskType: 'maintenance',
175
175
  mode: 'core-merge-judge',
@@ -0,0 +1,55 @@
1
+ const DEFAULT_MODEL_ID = 'ibm-granite/granite-embedding-97m-multilingual-r2'
2
+
3
+ const MODEL_PROFILES = Object.freeze({
4
+ [DEFAULT_MODEL_ID]: Object.freeze({
5
+ dims: 384,
6
+ defaultDtype: 'fp32',
7
+ defaultDevice: 'cpu',
8
+ modelFileName: 'model_quint8_avx2',
9
+ supportedDtypes: Object.freeze(['fp32']),
10
+ }),
11
+ 'Xenova/bge-m3': Object.freeze({
12
+ dims: 1024,
13
+ defaultDtype: 'q4',
14
+ defaultDevice: 'auto',
15
+ supportedDtypes: Object.freeze(['fp32', 'fp16', 'q8', 'q4']),
16
+ }),
17
+ })
18
+
19
+ function clean(value) {
20
+ return String(value ?? '').trim()
21
+ }
22
+
23
+ export function getConfiguredEmbeddingModelId() {
24
+ return clean(process.env.MIXDOG_EMBED_MODEL) || DEFAULT_MODEL_ID
25
+ }
26
+
27
+ export function getEmbeddingModelProfile(modelId = getConfiguredEmbeddingModelId()) {
28
+ return MODEL_PROFILES[clean(modelId)] || null
29
+ }
30
+
31
+ export function getKnownEmbeddingDims(modelId = getConfiguredEmbeddingModelId()) {
32
+ return getEmbeddingModelProfile(modelId)?.dims ?? null
33
+ }
34
+
35
+ export function normalizeEmbeddingDtype(modelId, dtype) {
36
+ const profile = getEmbeddingModelProfile(modelId)
37
+ const fallback = profile?.defaultDtype || 'fp32'
38
+ const requested = clean(dtype).toLowerCase()
39
+ if (!requested) return fallback
40
+ const supported = new Set(profile?.supportedDtypes || ['fp32', 'fp16', 'q8', 'q4'])
41
+ return supported.has(requested) ? requested : fallback
42
+ }
43
+
44
+ export function getDefaultEmbeddingDtype(modelId = getConfiguredEmbeddingModelId()) {
45
+ return normalizeEmbeddingDtype(modelId, process.env.MIXDOG_EMBED_DTYPE)
46
+ }
47
+
48
+ export function getDefaultEmbeddingDevice(modelId = getConfiguredEmbeddingModelId()) {
49
+ return getEmbeddingModelProfile(modelId)?.defaultDevice || 'auto'
50
+ }
51
+
52
+ export function getEmbeddingModelLoadOptions(modelId = getConfiguredEmbeddingModelId()) {
53
+ const profile = getEmbeddingModelProfile(modelId)
54
+ return profile?.modelFileName ? { model_file_name: profile.modelFileName } : {}
55
+ }
@@ -12,12 +12,18 @@ import { Worker } from 'worker_threads'
12
12
  import { join } from 'path'
13
13
  import { fileURLToPath } from 'url'
14
14
  import { writeProfilePoint } from './model-profile.mjs'
15
+ import {
16
+ getConfiguredEmbeddingModelId,
17
+ getDefaultEmbeddingDtype,
18
+ getKnownEmbeddingDims,
19
+ normalizeEmbeddingDtype,
20
+ } from './embedding-model-config.mjs'
15
21
 
16
- const MODEL_ID = 'Xenova/bge-m3'
22
+ const MODEL_ID = getConfiguredEmbeddingModelId()
17
23
 
18
24
  // Static dims registry — bypasses first-boot measurement for registered models.
19
25
  // Validated against measured dims inside warmupEmbeddingProvider; mismatch throws.
20
- const KNOWN_MODEL_DIMS = { 'Xenova/bge-m3': 1024 }
26
+ const KNOWN_MODEL_DIMS = { [MODEL_ID]: getKnownEmbeddingDims(MODEL_ID) }
21
27
 
22
28
  let worker = null
23
29
  let _restartCount = 0
@@ -26,6 +32,8 @@ const MAX_RESTART_BACKOFF_MS = 30_000
26
32
  let cachedDims = null
27
33
  let _modelReady = false
28
34
  let _device = 'cpu'
35
+ let _configuredDtype = getDefaultEmbeddingDtype(MODEL_ID)
36
+ let _warmupPromise = null
29
37
  let _embedCallCount = 0
30
38
  let _msgId = 0
31
39
  const _pending = new Map()
@@ -145,9 +153,10 @@ export function configureEmbedding(config = {}) {
145
153
  cachedDims = null
146
154
  _modelReady = false
147
155
  _device = 'cpu'
156
+ _configuredDtype = normalizeEmbeddingDtype(MODEL_ID, config.dtype ?? process.env.MIXDOG_EMBED_DTYPE)
148
157
  queryEmbeddingCache.clear()
149
158
  if (worker) {
150
- sendToWorker('configure', { dtype: config.dtype }).catch((err) => {
159
+ sendToWorker('configure', { dtype: _configuredDtype }).catch((err) => {
151
160
  // Silent .catch hid worker reconfigure failures (dtype mismatch,
152
161
  // worker crash, IPC closed). At least one log line so cycle1 /
153
162
  // cycle2 root-cause investigation can see the upstream failure
@@ -166,16 +175,24 @@ export function getEmbeddingModelId() {
166
175
  return MODEL_ID
167
176
  }
168
177
 
178
+ export function getEmbeddingDtype() {
179
+ return _configuredDtype
180
+ }
181
+
169
182
  export function getKnownDimsForCurrentModel() {
170
183
  return KNOWN_MODEL_DIMS[MODEL_ID] ?? null
171
184
  }
172
185
 
186
+ export function isEmbeddingModelReady() {
187
+ return _modelReady && Boolean(cachedDims)
188
+ }
189
+
173
190
  export function getEmbeddingDims() {
174
191
  if (!cachedDims) throw new Error('embedding dims not yet measured — warmup required')
175
192
  return cachedDims
176
193
  }
177
194
 
178
- export async function warmupEmbeddingProvider() {
195
+ async function runEmbeddingWarmup() {
179
196
  if (_modelReady && cachedDims) return true
180
197
  const result = await sendToWorker('warmup')
181
198
  if (!result.dims) throw new Error('warmup returned no dims — model output missing')
@@ -191,6 +208,16 @@ export async function warmupEmbeddingProvider() {
191
208
  return true
192
209
  }
193
210
 
211
+ export function warmupEmbeddingProvider() {
212
+ if (_modelReady && cachedDims) return Promise.resolve(true)
213
+ if (!_warmupPromise) {
214
+ _warmupPromise = runEmbeddingWarmup().finally(() => {
215
+ _warmupPromise = null
216
+ })
217
+ }
218
+ return _warmupPromise
219
+ }
220
+
194
221
  export async function embedText(text) {
195
222
  const clean = String(text ?? '').trim()
196
223
  if (!clean) return []
@@ -7,14 +7,23 @@ function __mixdogMemoryLog(...args) {
7
7
  import { parentPort } from 'worker_threads'
8
8
  import { createRequire } from 'module'
9
9
  import { join } from 'path'
10
- import { pathToFileURL } from 'url'
11
10
  import { mkdirSync } from 'fs'
12
11
  import os from 'os'
13
12
  import { execFile } from 'child_process'
14
13
  import { promisify } from 'util'
14
+ import { resolvePluginData } from '../../shared/plugin-paths.mjs'
15
+ import {
16
+ getConfiguredEmbeddingModelId,
17
+ getDefaultEmbeddingDevice,
18
+ getDefaultEmbeddingDtype,
19
+ getEmbeddingModelLoadOptions,
20
+ normalizeEmbeddingDtype,
21
+ } from './embedding-model-config.mjs'
15
22
 
16
- const MODEL_ID = 'Xenova/bge-m3'
17
- const DEFAULT_DTYPE = 'q4'
23
+ const MODEL_ID = getConfiguredEmbeddingModelId()
24
+ const DEFAULT_DEVICE = getDefaultEmbeddingDevice(MODEL_ID)
25
+ const DEFAULT_DTYPE = getDefaultEmbeddingDtype(MODEL_ID)
26
+ const MODEL_LOAD_OPTIONS = getEmbeddingModelLoadOptions(MODEL_ID)
18
27
  const INTRA_OP_THREADS = 1
19
28
  const INTER_OP_THREADS = 1
20
29
  // Session-create graph optimization. ORT defaults to 'all' (full node fusion),
@@ -31,7 +40,7 @@ const execFileAsync = promisify(execFile)
31
40
  // MIXDOG_EMBED_LOAD_CORES overrides (default 1 = single core).
32
41
  const _envLoadCores = Number(process.env.MIXDOG_EMBED_LOAD_CORES)
33
42
  const LOAD_AFFINITY_CORES = Number.isInteger(_envLoadCores) && _envLoadCores >= 1 ? _envLoadCores : 1
34
- const MODEL_CACHE_DIR = join(process.env.HOME || process.env.USERPROFILE, '.cache', 'mixdog-memory', 'models')
43
+ const MODEL_CACHE_DIR = join(resolvePluginData(), 'memory-models')
35
44
  // Idle dispose was previously 15 min. Production profiling showed the model
36
45
  // re-load cost (~3 s DirectML cold start) repeating ~880×/4 h because cycle1
37
46
  // gaps exceed 15 min in normal use. Default to keep-alive (0) so the model
@@ -92,8 +101,8 @@ function patchOrtThreads() {
92
101
  const require = createRequire(import.meta.url)
93
102
  let ort = null
94
103
  try {
95
- const transformersPkg = require.resolve('@huggingface/transformers/package.json')
96
- const transformersRequire = createRequire(pathToFileURL(transformersPkg))
104
+ const transformersEntry = require.resolve('@huggingface/transformers')
105
+ const transformersRequire = createRequire(transformersEntry)
97
106
  ort = transformersRequire('onnxruntime-node')
98
107
  } catch {
99
108
  ort = require('onnxruntime-node')
@@ -129,12 +138,13 @@ async function loadExtractor() {
129
138
  env.cacheDir = MODEL_CACHE_DIR
130
139
  try { env.backends.onnx.wasm.numThreads = INTRA_OP_THREADS } catch {}
131
140
  const opts = {}
132
- if (configuredDtype && configuredDtype !== 'fp32') {
141
+ Object.assign(opts, MODEL_LOAD_OPTIONS)
142
+ if (configuredDtype) {
133
143
  opts.dtype = configuredDtype
134
144
  }
135
145
  const startMs = Date.now()
136
146
  let extractor
137
- const requestedDevice = String(process.env.MIXDOG_MEMORY_EMBED_DEVICE || 'auto').trim().toLowerCase()
147
+ const requestedDevice = String(process.env.MIXDOG_MEMORY_EMBED_DEVICE || DEFAULT_DEVICE).trim().toLowerCase()
138
148
  const preferGpu = requestedDevice === 'dml'
139
149
  || requestedDevice === 'gpu'
140
150
  || (requestedDevice === 'auto' && process.platform === 'win32')
@@ -268,8 +278,7 @@ async function processMessage(msg) {
268
278
  _embedInFlight = true
269
279
  if (_idleTimer) { clearTimeout(_idleTimer); _idleTimer = null }
270
280
  if (msg.dtype != null) {
271
- const dt = String(msg.dtype).trim().toLowerCase()
272
- configuredDtype = ['fp32', 'fp16', 'q8', 'q4'].includes(dt) ? dt : DEFAULT_DTYPE
281
+ configuredDtype = normalizeEmbeddingDtype(MODEL_ID, msg.dtype)
273
282
  }
274
283
  if (extractorPromise) {
275
284
  try {
@@ -6,12 +6,8 @@
6
6
  * Each task spawns a child process directly and communicates via stdio.
7
7
  */
8
8
 
9
- let active = false
10
-
11
9
  export function startLlmWorker() {
12
- active = true
13
10
  }
14
11
 
15
12
  export async function stopLlmWorker() {
16
- active = false
17
13
  }
@@ -6,9 +6,9 @@ function __mixdogMemoryLog(...args) {
6
6
 
7
7
  import { cleanMemoryText } from './memory.mjs'
8
8
  import { resolveMaintenancePreset } from '../../shared/llm/index.mjs'
9
- import { callBridgeLlm } from './agent-ipc.mjs'
9
+ import { callAgentDispatch } from './agent-ipc.mjs'
10
10
  import {
11
- flushEmbeddingDirty, inferChunkProjectId,
11
+ flushEmbeddingDirty, inferChunkProjectId, syncRootEmbedding,
12
12
  } from './memory-embed.mjs'
13
13
  import { markCycleRequest, consumeCycleRequests, resolveCoalesceMaxDrains, scheduleCoalescedCycleRetry, makeCycleRequestSignature, resolveCoalesceMaxRetries } from './memory-cycle-requests.mjs'
14
14
 
@@ -380,9 +380,21 @@ async function _runCycle1Impl(db, config = {}, options = {}, _dataDir = null) {
380
380
  const pendingRowsAtStart = await countPendingRows(db)
381
381
  throwIfAborted(signal)
382
382
  const batchSize = Math.max(1, Number(config.batch_size ?? 100))
383
+ const windowSize = Math.max(1, Number(config.window_size ?? config.windowSize ?? batchSize))
384
+ const rowsPerSession = Math.max(windowSize, Number(
385
+ config.rows_per_session
386
+ ?? config.rowsPerSession
387
+ ?? config.max_rows_per_session
388
+ ?? config.maxRowsPerSession
389
+ ?? batchSize,
390
+ ) || batchSize)
383
391
  // Fallback chain handles flat config + nested cycle1 wrap shapes.
384
392
  const minBatch = Math.max(1, Number(config?.min_batch ?? config?.cycle1?.min_batch ?? CYCLE1_MIN_BATCH))
385
393
  const sessionCap = Math.max(1, Number(config?.session_cap ?? config?.cycle1?.session_cap ?? CYCLE1_SESSION_CAP))
394
+ const onlySessionId = String(config.session_id ?? config.sessionId ?? '').trim()
395
+ const sessionFilterSql = onlySessionId ? 'AND session_id = $4' : ''
396
+ const queryParams = [sessionCap, Date.now() - CYCLE1_OMITTED_COOLDOWN_MS, rowsPerSession]
397
+ if (onlySessionId) queryParams.push(onlySessionId)
386
398
  const preset = options.preset || resolveMaintenancePreset('memory')
387
399
  // Inner LLM timeout aligns to caller deadline -1s so the channel side can ack gracefully.
388
400
  const callerDeadlineMs = Number(options.callerDeadlineMs ?? 0)
@@ -400,6 +412,7 @@ async function _runCycle1Impl(db, config = {}, options = {}, _dataDir = null) {
400
412
  WHERE chunk_root IS NULL
401
413
  AND NULLIF(btrim(session_id), '') IS NOT NULL
402
414
  AND (reviewed_at IS NULL OR reviewed_at < $2)
415
+ ${sessionFilterSql}
403
416
  GROUP BY session_id
404
417
  ORDER BY latest_ts DESC, latest_id DESC
405
418
  LIMIT $1
@@ -416,7 +429,7 @@ async function _runCycle1Impl(db, config = {}, options = {}, _dataDir = null) {
416
429
  FROM ranked
417
430
  WHERE rn <= $3
418
431
  ORDER BY latest_ts DESC, latest_id DESC, session_id, ts DESC, id DESC`,
419
- [sessionCap, Date.now() - CYCLE1_OMITTED_COOLDOWN_MS, batchSize],
432
+ queryParams,
420
433
  )
421
434
  throwIfAborted(signal)
422
435
  const rowsDesc = fetchResult.rows
@@ -463,9 +476,9 @@ async function _runCycle1Impl(db, config = {}, options = {}, _dataDir = null) {
463
476
  const windows = []
464
477
  for (const sessionRowsDesc of rowsBySession.values()) {
465
478
  const rowsAsc = sessionRowsDesc.slice().reverse()
466
- for (let offset = 0; offset < rowsAsc.length; offset += batchSize) {
479
+ for (let offset = 0; offset < rowsAsc.length; offset += windowSize) {
467
480
  throwIfAborted(signal)
468
- windows.push(rowsAsc.slice(offset, offset + batchSize))
481
+ windows.push(rowsAsc.slice(offset, offset + windowSize))
469
482
  }
470
483
  }
471
484
 
@@ -501,7 +514,7 @@ async function _runCycle1Impl(db, config = {}, options = {}, _dataDir = null) {
501
514
  }
502
515
 
503
516
  const userMessage = buildCycle1ChunkPrompt(rows)
504
- const llmCall = typeof options?.callLlm === 'function' ? options.callLlm : callBridgeLlm
517
+ const llmCall = typeof options?.callLlm === 'function' ? options.callLlm : callAgentDispatch
505
518
 
506
519
  let raw
507
520
  const _tLlm = Date.now()
@@ -512,7 +525,7 @@ async function _runCycle1Impl(db, config = {}, options = {}, _dataDir = null) {
512
525
  mode: 'cycle1',
513
526
  preset,
514
527
  timeout,
515
- // Pin cwd to null so every memory cycle call hits the same bridge cache shard.
528
+ // Pin cwd to null so every memory cycle call hits the same agent cache shard.
516
529
  cwd: null,
517
530
  }, userMessage)
518
531
  } catch (err) {
@@ -646,6 +659,14 @@ async function _runCycle1Impl(db, config = {}, options = {}, _dataDir = null) {
646
659
  usedIds.add(mid)
647
660
  committedRowIds.add(mid)
648
661
  }
662
+ // Real-time embedding: embed this episode the moment it is committed so
663
+ // dense recall sees fresh roots without waiting for the end-of-cycle flush.
664
+ // Fire-and-forget on a separate pool connection (independent of the
665
+ // just-finished chunk transaction); never await — the chunk loop must
666
+ // not block. The end-of-cycle flushEmbeddingDirty remains as a safety
667
+ // net that sweeps any NULL embeddings this per-root path raced/missed.
668
+ syncRootEmbedding(db, rootId, { signal })
669
+ .catch((err) => __mixdogMemoryLog(`[cycle1] realtime embed failed (root=${rootId}): ${err.message}\n`))
649
670
  } catch (err) {
650
671
  __mixdogMemoryLog(`[cycle1] chunk commit failed (root=${rootId}): ${err.message}\n`)
651
672
  skippedChunks += 1
@@ -8,7 +8,7 @@ import { existsSync, readFileSync } from 'fs'
8
8
  import { join } from 'path'
9
9
  import { fileURLToPath } from 'url'
10
10
  import { resolveMaintenancePreset } from '../../shared/llm/index.mjs'
11
- import { callBridgeLlm } from './agent-ipc.mjs'
11
+ import { callAgentDispatch } from './agent-ipc.mjs'
12
12
  import {
13
13
  syncRootEmbedding, deleteRootEmbedding, flushEmbeddingDirty,
14
14
  } from './memory-embed.mjs'
@@ -45,7 +45,7 @@ function resourceDir() {
45
45
  return process.env.MIXDOG_ROOT || fileURLToPath(new URL('../../../..', import.meta.url))
46
46
  }
47
47
 
48
- async function invokeLlm(prompt, mode, preset, timeout, llmCall = callBridgeLlm) {
48
+ async function invokeLlm(prompt, mode, preset, timeout, llmCall = callAgentDispatch) {
49
49
  return await llmCall({
50
50
  role: 'cycle2-agent',
51
51
  taskType: 'maintenance',
@@ -339,7 +339,7 @@ function _pickKeeper(a, b) {
339
339
  async function _llmJudgePair(summaryA, summaryB, siblingContext = [], options = {}) {
340
340
  const signal = options?.signal
341
341
  throwIfAborted(signal)
342
- const llmCall = typeof options?.callLlm === 'function' ? options.callLlm : callBridgeLlm
342
+ const llmCall = typeof options?.callLlm === 'function' ? options.callLlm : callAgentDispatch
343
343
  const siblings = Array.isArray(siblingContext) && siblingContext.length > 0
344
344
  ? `\n\nSibling near-matches (recall context only — do not absorb these into the verdict):\n${siblingContext.slice(0, 5).map((p, i) => `${i + 1}. ${String(p.a?.summary ?? '')} ↔ ${String(p.b?.summary ?? '')}`).join('\n')}`
345
345
  : ''
@@ -521,7 +521,6 @@ export function loadCurrentRulesDigest() {
521
521
  const now = Date.now()
522
522
  if (_currentRulesDigest && now - _currentRulesDigestTs < 60_000) return _currentRulesDigest
523
523
  const sources = [
524
- join(resourceDir(), 'rules', 'shared', '00-language.md'),
525
524
  join(resourceDir(), 'rules', 'shared', '01-general.md'),
526
525
  join(resourceDir(), 'rules', 'shared', '01-tool.md'),
527
526
  join(resourceDir(), 'rules', 'shared', '04-memory.md'),
@@ -805,7 +804,7 @@ async function sonnetCascade(candidates, rulesDigest, options = {}) {
805
804
  // when no binding exists, which would defeat the cascade. SONNET HIGH
806
805
  // matches the worker pool's default preset id from agent-config.
807
806
  const preset = options.cascadePreset || 'SONNET HIGH'
808
- const llmCall = typeof options?.callLlm === 'function' ? options.callLlm : callBridgeLlm
807
+ const llmCall = typeof options?.callLlm === 'function' ? options.callLlm : callAgentDispatch
809
808
  let raw
810
809
  try {
811
810
  raw = await llmCall({
@@ -23,7 +23,7 @@ import { existsSync, readFileSync } from 'fs'
23
23
  import { join } from 'path'
24
24
  import { fileURLToPath } from 'url'
25
25
  import { resolveMaintenancePreset } from '../../shared/llm/index.mjs'
26
- import { callBridgeLlm } from './agent-ipc.mjs'
26
+ import { callAgentDispatch } from './agent-ipc.mjs'
27
27
  import { listCore, editCore, deleteCore, CORE_SUMMARY_MAX } from './core-memory-store.mjs'
28
28
  import { loadCurrentRulesDigest } from './memory-cycle2.mjs'
29
29
  import { embedText } from './embedding-provider.mjs'
@@ -34,7 +34,7 @@ function resourceDir() {
34
34
  return process.env.MIXDOG_ROOT || fileURLToPath(new URL('../../../..', import.meta.url))
35
35
  }
36
36
 
37
- async function invokeLlm(prompt, mode, preset, timeout, llmCall = callBridgeLlm) {
37
+ async function invokeLlm(prompt, mode, preset, timeout, llmCall = callAgentDispatch) {
38
38
  return await llmCall({
39
39
  role: 'cycle3-agent',
40
40
  taskType: 'maintenance',
@@ -5,8 +5,8 @@ function __mixdogMemoryLog(...args) {
5
5
  }
6
6
 
7
7
  import fs from 'node:fs'
8
- import os from 'node:os'
9
8
  import path from 'node:path'
9
+ import { mixdogHome } from '../../shared/plugin-paths.mjs'
10
10
 
11
11
  function normalizeBackfillWindow(value) {
12
12
  const normalized = String(value ?? 'all').trim().toLowerCase()
@@ -44,7 +44,7 @@ export async function countUnclassified(db) {
44
44
  }
45
45
 
46
46
  export function selectBackfillTranscripts({ sinceMs = null, limit = null, projectsRoot = null } = {}) {
47
- const root = projectsRoot || path.join(os.homedir(), '.mixdog', 'projects')
47
+ const root = projectsRoot || path.join(mixdogHome(), 'projects')
48
48
  if (!fs.existsSync(root)) return []
49
49
  const files = []
50
50
  for (const d of fs.readdirSync(root)) {
@@ -0,0 +1,38 @@
1
+ // Pure, DB-free merge of session-fallback recall rows into the already-ranked
2
+ // global hybrid rows. Extracted into its own module so the starve-prevention +
3
+ // dedupe contract can be unit-tested without booting the memory worker (which
4
+ // has heavy top-level side effects: http server, pg, onnx embedding).
5
+ //
6
+ // Contract:
7
+ // - APPEND session rows, never prepend. Blind prepend lets session rows
8
+ // (limit+) starve the global first page and distorts sort/offset/limit.
9
+ // - Re-apply the SAME sort policy as the global path so global hybrid hits
10
+ // (carrying a real retrievalScore) keep their rank; session rows
11
+ // (retrievalScore 0 under importance, ts-ordered under date) only fill
12
+ // genuinely open slots.
13
+ // - Dedupe drops any session row whose id already appears as a global row OR
14
+ // as a global root's inlined chunk member (prevents member/leaf double
15
+ // output).
16
+ export function mergeSessionRowsIntoGlobal(globalRows, sessionRows, { sort = 'importance' } = {}) {
17
+ const filtered = Array.isArray(globalRows) ? [...globalRows] : []
18
+ if (!Array.isArray(sessionRows) || sessionRows.length === 0) return filtered
19
+ const seen = new Set(filtered.map(r => Number(r.id)))
20
+ for (const r of filtered) {
21
+ if (Array.isArray(r.members)) {
22
+ for (const m of r.members) seen.add(Number(m.id))
23
+ }
24
+ }
25
+ const merged = sessionRows.filter(r => !seen.has(Number(r.id)))
26
+ if (merged.length === 0) return filtered
27
+ const out = [...filtered, ...merged]
28
+ const sa = (v) => { const n = Number(v); return Number.isFinite(n) ? n : 0 }
29
+ if (sort === 'date') {
30
+ out.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0))
31
+ } else {
32
+ out.sort((a, b) => (sa(b.retrievalScore ?? b.rrf ?? 0) - sa(a.retrievalScore ?? a.rrf ?? 0))
33
+ || (sa(b.score ?? 0) - sa(a.score ?? 0))
34
+ || (sa(b.ts ?? 0) - sa(a.ts ?? 0))
35
+ || (Number(a.id ?? 0) - Number(b.id ?? 0)))
36
+ }
37
+ return out
38
+ }