mixdog 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +344 -0
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1223 -95
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2325 -450
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +118 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
  114. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  116. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  117. package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
  118. package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
  119. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  120. package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
  121. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  123. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  124. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  125. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  126. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  128. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  129. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  130. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  132. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  133. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  139. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  140. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  141. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  142. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  143. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  144. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
  145. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  146. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  147. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  148. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  149. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  150. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  151. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  152. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  155. package/src/runtime/channels/index.mjs +152 -24
  156. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  157. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  158. package/src/runtime/channels/lib/tool-format.mjs +0 -2
  159. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  160. package/src/runtime/channels/lib/webhook.mjs +1 -1
  161. package/src/runtime/channels/tool-defs.mjs +29 -29
  162. package/src/runtime/memory/index.mjs +635 -107
  163. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  164. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  165. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  166. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  167. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  168. package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
  169. package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
  170. package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
  171. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  172. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  173. package/src/runtime/memory/lib/memory.mjs +88 -9
  174. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  175. package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  177. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  178. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  179. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  180. package/src/runtime/memory/tool-defs.mjs +46 -37
  181. package/src/runtime/search/index.mjs +102 -466
  182. package/src/runtime/search/lib/web-tools.mjs +45 -25
  183. package/src/runtime/search/tool-defs.mjs +16 -23
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/atomic-file.mjs +4 -3
  186. package/src/runtime/shared/background-tasks.mjs +122 -11
  187. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  188. package/src/runtime/shared/config.mjs +7 -4
  189. package/src/runtime/shared/err-text.mjs +131 -4
  190. package/src/runtime/shared/llm/cost.mjs +2 -2
  191. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  192. package/src/runtime/shared/llm/index.mjs +34 -11
  193. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  194. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  195. package/src/runtime/shared/singleton-owner.mjs +104 -0
  196. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  197. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  198. package/src/runtime/shared/tool-surface.mjs +624 -98
  199. package/src/runtime/shared/user-data-guard.mjs +0 -2
  200. package/src/standalone/agent-task-status.mjs +203 -0
  201. package/src/standalone/agent-task-status.test.mjs +76 -0
  202. package/src/standalone/agent-tool.mjs +1913 -0
  203. package/src/standalone/channel-worker.mjs +370 -14
  204. package/src/standalone/explore-tool.mjs +165 -70
  205. package/src/standalone/folder-dialog.mjs +314 -0
  206. package/src/standalone/hook-bus.mjs +898 -22
  207. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  208. package/src/standalone/projects.mjs +226 -0
  209. package/src/standalone/provider-admin.mjs +41 -24
  210. package/src/standalone/seeds.mjs +2 -69
  211. package/src/standalone/usage-dashboard.mjs +96 -8
  212. package/src/tui/App.jsx +4798 -2153
  213. package/src/tui/components/AnsiText.jsx +39 -28
  214. package/src/tui/components/ContextPanel.jsx +87 -29
  215. package/src/tui/components/Markdown.jsx +43 -77
  216. package/src/tui/components/MarkdownTable.jsx +9 -184
  217. package/src/tui/components/Message.jsx +28 -11
  218. package/src/tui/components/Picker.jsx +95 -56
  219. package/src/tui/components/PromptInput.jsx +367 -239
  220. package/src/tui/components/QueuedCommands.jsx +1 -1
  221. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  222. package/src/tui/components/Spinner.jsx +67 -38
  223. package/src/tui/components/StatusLine.jsx +606 -38
  224. package/src/tui/components/TextEntryPanel.jsx +128 -9
  225. package/src/tui/components/ToolExecution.jsx +617 -368
  226. package/src/tui/components/TurnDone.jsx +3 -3
  227. package/src/tui/components/UsagePanel.jsx +3 -5
  228. package/src/tui/components/tool-output-format.mjs +365 -0
  229. package/src/tui/components/tool-output-format.test.mjs +220 -0
  230. package/src/tui/dist/index.mjs +8915 -2418
  231. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  232. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  233. package/src/tui/engine.mjs +1455 -279
  234. package/src/tui/figures.mjs +21 -40
  235. package/src/tui/index.jsx +75 -31
  236. package/src/tui/input-editing.mjs +25 -0
  237. package/src/tui/markdown/format-token.mjs +511 -68
  238. package/src/tui/markdown/format-token.test.mjs +216 -0
  239. package/src/tui/markdown/render-ansi.mjs +94 -0
  240. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  241. package/src/tui/markdown/stream-fence.mjs +34 -0
  242. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  243. package/src/tui/markdown/table-layout.mjs +250 -0
  244. package/src/tui/paste-attachments.mjs +0 -7
  245. package/src/tui/spinner-verbs.mjs +1 -2
  246. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  247. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  248. package/src/tui/theme.mjs +746 -24
  249. package/src/tui/time-format.mjs +1 -1
  250. package/src/tui/transcript-tool-failures.mjs +67 -0
  251. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  252. package/src/ui/ansi.mjs +1 -2
  253. package/src/ui/markdown.mjs +85 -26
  254. package/src/ui/markdown.test.mjs +70 -0
  255. package/src/ui/model-display.mjs +121 -0
  256. package/src/ui/session-stats.mjs +44 -0
  257. package/src/ui/statusline-context-label.test.mjs +15 -0
  258. package/src/ui/statusline.mjs +386 -178
  259. package/src/ui/tool-card.mjs +3 -16
  260. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  261. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  262. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  263. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  264. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  265. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  266. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  267. package/src/workflows/default/WORKFLOW.md +12 -5
  268. package/src/workflows/default/workflow.json +0 -1
  269. package/src/workflows/solo/WORKFLOW.md +15 -0
  270. package/src/workflows/solo/workflow.json +7 -0
  271. package/vendor/ink/build/output.js +6 -1
  272. package/src/agents/scheduler-task.md +0 -3
  273. package/src/agents/web-researcher/AGENT.md +0 -3
  274. package/src/agents/web-researcher/agent.json +0 -6
  275. package/src/agents/webhook-handler.md +0 -3
  276. package/src/rules/bridge/00-common.md +0 -5
  277. package/src/rules/bridge/30-explorer.md +0 -4
  278. package/src/rules/lead/00-tool-lead.md +0 -5
  279. package/src/rules/shared/00-language.md +0 -3
  280. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  281. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  282. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  283. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  284. package/src/standalone/bridge-tool.mjs +0 -1414
  285. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -1,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,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
 
@@ -277,7 +277,7 @@ export async function runCycle1(db, config = {}, options = {}, dataDir = null) {
277
277
  )
278
278
  if (_runCycle1InFlight.has(db)) {
279
279
  if (!coalescedRetry) await markCycleRequest(db, 'cycle1', 'in-flight', requestSignature)
280
- scheduleRetry()
280
+ if (!coalescedRetry || retryAttempt < maxRetries) scheduleRetry()
281
281
  logCycle1Throttled('in-flight', '[cycle1] skipped: already in flight for this db\n')
282
282
  return {
283
283
  processed: 0, chunks: 0, skipped: 0, sessions: 0,
@@ -301,7 +301,7 @@ export async function runCycle1(db, config = {}, options = {}, dataDir = null) {
301
301
  if (!gotLock) {
302
302
  client.release()
303
303
  if (!coalescedRetry) await markCycleRequest(db, 'cycle1', 'advisory-lock', requestSignature)
304
- scheduleRetry()
304
+ if (!coalescedRetry || retryAttempt < maxRetries) scheduleRetry()
305
305
  logCycle1Throttled('advisory-lock', '[cycle1] skipped: advisory lock held by another worker\n')
306
306
  return { processed: 0, chunks: 0, skipped: 0, sessions: 0, skippedInFlight: true, pendingRows: await countPendingRows(db) }
307
307
  }
@@ -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)
@@ -390,23 +402,24 @@ async function _runCycle1Impl(db, config = {}, options = {}, _dataDir = null) {
390
402
  const timeout = callerDeadlineMs > 0
391
403
  ? Math.min(baseTimeout, Math.max(5000, callerDeadlineMs - 1000))
392
404
  : baseTimeout
393
- // Select oldest-pending sessions first, then fetch oldest rows per selected
394
- // session. This keeps classifier windows session-isolated and prevents hot
395
- // sessions/new rows from starving older pending work.
405
+ // Select closest/recent sessions first, then fetch closest/recent rows per
406
+ // selected session. Memory fill is recency-first; session isolation below
407
+ // keeps unrelated episodes out of the same classifier prompt.
396
408
  const fetchResult = await db.query(
397
409
  `WITH selected_sessions AS (
398
- SELECT session_id, MIN(ts) AS oldest_ts, MIN(id) AS oldest_id
410
+ SELECT session_id, MAX(ts) AS latest_ts, MAX(id) AS latest_id
399
411
  FROM entries
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
- ORDER BY oldest_ts ASC, oldest_id ASC
417
+ ORDER BY latest_ts DESC, latest_id DESC
405
418
  LIMIT $1
406
419
  ), ranked AS (
407
420
  SELECT e.id, e.ts, e.role, e.content, e.session_id, e.source_ref, e.project_id,
408
- s.oldest_ts, s.oldest_id,
409
- ROW_NUMBER() OVER (PARTITION BY e.session_id ORDER BY e.ts ASC, e.id ASC) AS rn
421
+ s.latest_ts, s.latest_id,
422
+ ROW_NUMBER() OVER (PARTITION BY e.session_id ORDER BY e.ts DESC, e.id DESC) AS rn
410
423
  FROM entries e
411
424
  JOIN selected_sessions s ON s.session_id = e.session_id
412
425
  WHERE e.chunk_root IS NULL
@@ -415,8 +428,8 @@ async function _runCycle1Impl(db, config = {}, options = {}, _dataDir = null) {
415
428
  SELECT id, ts, role, content, session_id, source_ref, project_id
416
429
  FROM ranked
417
430
  WHERE rn <= $3
418
- ORDER BY oldest_ts ASC, oldest_id ASC, session_id, ts DESC, id DESC`,
419
- [sessionCap, Date.now() - CYCLE1_OMITTED_COOLDOWN_MS, batchSize],
431
+ ORDER BY latest_ts DESC, latest_id DESC, session_id, ts DESC, id DESC`,
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({
@@ -901,7 +900,7 @@ export async function runCycle2(db, config = {}, options = {}, dataDir = null) {
901
900
  }
902
901
  if (_runCycle2InFlight.has(db)) {
903
902
  if (!coalescedRetry) await markCycleRequest(db, 'cycle2', 'in-flight', requestSignature)
904
- scheduleRetry()
903
+ if (!coalescedRetry || retryAttempt < maxRetries) scheduleRetry()
905
904
  __mixdogMemoryLog('[cycle2] skipped: already in flight for this db\n')
906
905
  return { ok: true, ...partial, skippedInFlight: true }
907
906
  }
@@ -921,7 +920,7 @@ export async function runCycle2(db, config = {}, options = {}, dataDir = null) {
921
920
  if (!gotLock) {
922
921
  client.release()
923
922
  if (!coalescedRetry) await markCycleRequest(db, 'cycle2', 'advisory-lock', requestSignature)
924
- scheduleRetry()
923
+ if (!coalescedRetry || retryAttempt < maxRetries) scheduleRetry()
925
924
  __mixdogMemoryLog('[cycle2] skipped: advisory lock held by another worker\n')
926
925
  return { ok: true, ...partial, skippedInFlight: true }
927
926
  }
@@ -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',
@@ -272,7 +272,7 @@ export async function runCycle3(db, config, dataDir, options = {}) {
272
272
  }
273
273
  if (_runCycle3InFlight.has(db)) {
274
274
  if (!coalescedRetry) await markCycleRequest(db, 'cycle3', 'in-flight', requestSignature)
275
- scheduleRetry()
275
+ if (!coalescedRetry || retryAttempt < maxRetries) scheduleRetry()
276
276
  __mixdogMemoryLog('[cycle3] skipped: already in flight for this db\n')
277
277
  return { ...partial, skippedInFlight: true }
278
278
  }
@@ -291,7 +291,7 @@ export async function runCycle3(db, config, dataDir, options = {}) {
291
291
  if (!gotLock) {
292
292
  client.release()
293
293
  if (!coalescedRetry) await markCycleRequest(db, 'cycle3', 'advisory-lock', requestSignature)
294
- scheduleRetry()
294
+ if (!coalescedRetry || retryAttempt < maxRetries) scheduleRetry()
295
295
  __mixdogMemoryLog('[cycle3] skipped: advisory lock held by another worker\n')
296
296
  return { ...partial, skippedInFlight: true }
297
297
  }
@@ -7,6 +7,7 @@ function __mixdogMemoryLog(...args) {
7
7
  import fs from 'node:fs'
8
8
  import os from 'node:os'
9
9
  import path from 'node:path'
10
+ import { mixdogHome } from '../../shared/plugin-paths.mjs'
10
11
 
11
12
  function normalizeBackfillWindow(value) {
12
13
  const normalized = String(value ?? 'all').trim().toLowerCase()
@@ -44,7 +45,7 @@ export async function countUnclassified(db) {
44
45
  }
45
46
 
46
47
  export function selectBackfillTranscripts({ sinceMs = null, limit = null, projectsRoot = null } = {}) {
47
- const root = projectsRoot || path.join(os.homedir(), '.mixdog', 'projects')
48
+ const root = projectsRoot || path.join(mixdogHome(), 'projects')
48
49
  if (!fs.existsSync(root)) return []
49
50
  const files = []
50
51
  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
+ }