mixdog 0.9.95 → 0.9.97

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 (282) hide show
  1. package/LICENSES/Apache-2.0.txt +201 -0
  2. package/LICENSES/MIT.txt +56 -0
  3. package/LICENSES/codex-NOTICE.txt +6 -0
  4. package/NOTICE.md +46 -9
  5. package/README.md +0 -17
  6. package/package.json +16 -12
  7. package/scripts/.tmp-coverage-report.mjs +96 -0
  8. package/scripts/build-tui.mjs +17 -1
  9. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  10. package/scripts/fixtures/patch-replay-corpus.json +98 -0
  11. package/src/defaults/mixdog-config.template.json +5 -8
  12. package/src/defaults/skills/setup/SKILL.md +1 -1
  13. package/src/lib/keychain-cjs.cjs +1 -1
  14. package/src/lib/mixdog-debug.cjs +2 -3
  15. package/src/rules/agent/30-explorer.md +7 -4
  16. package/src/rules/lead/01-general.md +1 -2
  17. package/src/rules/lead/lead-tool.md +0 -3
  18. package/src/rules/shared/01-tool.md +21 -29
  19. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
  20. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
  23. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  24. package/src/runtime/agent/orchestrator/config.mjs +246 -67
  25. package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
  26. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
  27. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
  28. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
  29. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
  30. package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
  31. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
  33. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
  36. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
  37. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  39. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
  41. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
  42. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
  43. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +36 -10
  44. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -33
  46. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
  47. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
  48. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
  49. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +21 -29
  50. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
  51. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
  52. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  53. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
  54. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  55. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
  56. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
  57. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  58. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
  59. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
  60. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  61. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
  62. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  63. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  64. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
  65. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -33
  66. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
  67. package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
  70. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +116 -26
  71. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  72. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
  73. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
  74. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
  75. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  76. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
  77. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +12 -26
  78. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
  79. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
  80. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
  81. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -9
  82. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
  83. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
  84. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
  85. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
  87. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
  88. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
  89. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
  90. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +142 -26
  91. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  92. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
  93. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
  94. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  95. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
  96. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
  97. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
  99. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
  100. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
  101. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -11
  102. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
  104. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
  105. package/src/runtime/channels/lib/config.mjs +34 -34
  106. package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
  107. package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
  108. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  109. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  110. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  111. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  112. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  113. package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
  114. package/src/runtime/channels/lib/scheduler.mjs +2 -2
  115. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  116. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  117. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  118. package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
  119. package/src/runtime/channels/lib/worker-main.mjs +38 -38
  120. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  121. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  122. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  123. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/memory/index.mjs +76 -248
  126. package/src/runtime/memory/lib/agent-ipc.mjs +146 -81
  127. package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
  128. package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
  129. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +13 -34
  130. package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
  131. package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
  132. package/src/runtime/memory/lib/http-router.mjs +9 -6
  133. package/src/runtime/memory/lib/ko-morph.mjs +3 -20
  134. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
  135. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  136. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
  137. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
  138. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
  139. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  140. package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
  141. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  142. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  143. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  144. package/src/runtime/memory/lib/memory.mjs +1 -1
  145. package/src/runtime/memory/lib/pg/process.mjs +32 -9
  146. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  147. package/src/runtime/memory/tool-defs.mjs +1 -5
  148. package/src/runtime/search/index.mjs +2 -2
  149. package/src/runtime/search/tool-defs.mjs +2 -2
  150. package/src/runtime/shared/abort-race.mjs +70 -0
  151. package/src/runtime/shared/agent-route-config.mjs +119 -0
  152. package/src/runtime/shared/atomic-file.mjs +80 -4
  153. package/src/runtime/shared/channel-notification-routing.mjs +2 -2
  154. package/src/runtime/shared/child-guardian.mjs +204 -108
  155. package/src/runtime/shared/child-spawn-gate.mjs +139 -72
  156. package/src/runtime/shared/config.mjs +120 -47
  157. package/src/runtime/shared/llm/index.mjs +1 -1
  158. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  159. package/src/runtime/shared/resource-admission.mjs +150 -100
  160. package/src/runtime/shared/stream-progress.mjs +6 -0
  161. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  162. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  163. package/src/runtime/shared/tool-surface.mjs +91 -27
  164. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  165. package/src/runtime/shared/turn-snapshot.mjs +47 -6
  166. package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
  167. package/src/session-runtime/channel-config-api.mjs +5 -5
  168. package/src/session-runtime/config-helpers.mjs +39 -37
  169. package/src/session-runtime/config-lifecycle.mjs +27 -27
  170. package/src/session-runtime/lifecycle-api.mjs +22 -3
  171. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  172. package/src/session-runtime/model-route-api.mjs +7 -5
  173. package/src/session-runtime/prewarm.mjs +1 -1
  174. package/src/session-runtime/provider-auth-api.mjs +0 -7
  175. package/src/session-runtime/provider-models.mjs +1 -1
  176. package/src/session-runtime/remote-control.mjs +6 -7
  177. package/src/session-runtime/runtime-core.mjs +63 -39
  178. package/src/session-runtime/runtime-tunables.mjs +4 -4
  179. package/src/session-runtime/self-update.mjs +1 -1
  180. package/src/session-runtime/session-lifecycle.mjs +45 -17
  181. package/src/session-runtime/session-title.mjs +1 -1
  182. package/src/session-runtime/session-turn-api.mjs +96 -24
  183. package/src/session-runtime/settings-api.mjs +12 -24
  184. package/src/session-runtime/tool-defs.mjs +3 -3
  185. package/src/session-runtime/workflow-agents-api.mjs +55 -81
  186. package/src/session-runtime/workflow.mjs +14 -24
  187. package/src/standalone/agent-dispatch-broker.mjs +198 -0
  188. package/src/standalone/agent-tool/helpers.mjs +2 -2
  189. package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
  190. package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
  191. package/src/standalone/agent-tool/tool-def.mjs +0 -14
  192. package/src/standalone/channel-admin.mjs +53 -64
  193. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
  194. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
  195. package/src/standalone/channel-worker.mjs +124 -647
  196. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
  197. package/src/standalone/explore-tool.mjs +187 -70
  198. package/src/standalone/fair-call-scheduler.mjs +264 -0
  199. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  200. package/src/standalone/session-client.mjs +1134 -0
  201. package/src/standalone/session-protocol.mjs +192 -0
  202. package/src/standalone/session-runtime-pool.mjs +393 -0
  203. package/src/standalone/session-runtime-record.mjs +20 -0
  204. package/src/standalone/session-runtime-worker.mjs +203 -0
  205. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
  206. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
  207. package/src/standalone/session-wire.mjs +55 -0
  208. package/src/tui/App.jsx +34 -21
  209. package/src/tui/app/app-format.mjs +1 -1
  210. package/src/tui/app/app-view.jsx +5 -2
  211. package/src/tui/app/channel-pickers.mjs +41 -41
  212. package/src/tui/app/core-memory-picker.mjs +19 -19
  213. package/src/tui/app/doctor.mjs +5 -11
  214. package/src/tui/app/input-parsers.mjs +2 -2
  215. package/src/tui/app/message-selector.mjs +103 -0
  216. package/src/tui/app/onboarding-steps.mjs +3 -4
  217. package/src/tui/app/project-picker.mjs +80 -56
  218. package/src/tui/app/prompt-submit.mjs +48 -43
  219. package/src/tui/app/resume-picker.mjs +2 -2
  220. package/src/tui/app/route-pickers.mjs +3 -1
  221. package/src/tui/app/settings-picker.mjs +31 -33
  222. package/src/tui/app/transcript-window.mjs +38 -0
  223. package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
  224. package/src/tui/app/use-prompt-handlers.mjs +86 -21
  225. package/src/tui/app/use-prompt-hint.mjs +3 -2
  226. package/src/tui/app/use-prompt-queue-history.mjs +10 -3
  227. package/src/tui/app/use-transcript-scroll.mjs +10 -3
  228. package/src/tui/app/use-transcript-window.mjs +50 -17
  229. package/src/tui/components/PromptInput.jsx +71 -38
  230. package/src/tui/components/Spinner.jsx +1 -1
  231. package/src/tui/components/ToolExecution.jsx +1 -1
  232. package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
  233. package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
  234. package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
  235. package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
  236. package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
  237. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  238. package/src/tui/dist/index.mjs +1031 -1676
  239. package/src/tui/hooks/useSession.mjs +16 -0
  240. package/src/tui/index.jsx +12 -13
  241. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  242. package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
  243. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  244. package/src/tui/{engine → session}/context-state.mjs +3 -3
  245. package/src/tui/{engine → session}/labels.mjs +2 -2
  246. package/src/tui/{engine → session}/live-share.mjs +38 -4
  247. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  248. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  249. package/src/tui/session/oauth-flows.mjs +140 -0
  250. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  251. package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
  252. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  253. package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
  254. package/src/tui/{engine → session}/session-api.mjs +110 -93
  255. package/src/tui/{engine → session}/session-flow.mjs +8 -2
  256. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  257. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  258. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  259. package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
  260. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  261. package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
  262. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  263. package/src/tui/{engine → session}/turn.mjs +38 -176
  264. package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
  265. package/src/tui/session.mjs +22 -0
  266. package/src/tui/spinner-meta.mjs +4 -4
  267. package/src/tui/spinner-verbs.mjs +51 -3
  268. package/src/ui/statusline-segments.mjs +1 -1
  269. package/src/ui/statusline.mjs +1 -1
  270. package/vendor/ink/build/components/App.js +10 -1
  271. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  272. package/src/standalone/engine-daemon-client.mjs +0 -894
  273. package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
  274. package/src/standalone/engine-daemon-protocol.mjs +0 -33
  275. package/src/tui/engine/turn-watchdog.mjs +0 -92
  276. package/src/tui/engine.mjs +0 -22
  277. package/src/tui/hooks/useEngine.mjs +0 -16
  278. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  279. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  280. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  281. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  282. /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
@@ -1,49 +1,136 @@
1
1
  /**
2
- * agent-ipc.mjs — IPC client for agent dispatch calls from the memory worker.
2
+ * Agent-dispatch client for the memory process.
3
3
  *
4
- * The memory worker runs in its own fork, while the agent runtime / provider
5
- * registry lives in-process in the parent server. cycle1 / cycle2 can't
6
- * call makeAgentDispatch() locally (provider map is empty here), so we route
7
- * every LLM call over IPC:
8
- *
9
- * memory → parent : { type: 'agent_ipc_request', callId, tool, params }
10
- * parent → memory : { type: 'agent_ipc_response', callId, ok, result | error }
11
- *
12
- * Uses a module-level pending map keyed by callId. Parent-side handler
13
- * lives in server.mjs spawnWorker's message listener.
4
+ * PG/embedding/recall remain isolated in the memory process, while the heavy
5
+ * provider/session graph lives exactly once in the machine-global daemon.
6
+ * daemon. Calls use its authenticated loopback broker, so the memory process
7
+ * can outlive its original fork parent and reconnect after a daemon restart.
14
8
  */
15
- import { safeIpcSend } from '../../shared/safe-ipc-send.mjs'
9
+ import http from 'node:http'
10
+ import { readFileSync } from 'node:fs'
11
+ import { tmpdir } from 'node:os'
12
+ import { join, resolve } from 'node:path'
16
13
 
17
- const pending = new Map()
18
- let listenerInstalled = false
14
+ const brokerAgent = new http.Agent({
15
+ keepAlive: true,
16
+ maxSockets: 64,
17
+ maxFreeSockets: 8,
18
+ })
19
19
  let _idSeq = 0
20
20
 
21
- function installListener() {
22
- if (listenerInstalled) return
23
- listenerInstalled = true
24
- process.on('message', (msg) => {
25
- if (!msg || msg.type !== 'agent_ipc_response' || !msg.callId) return
26
- const entry = pending.get(msg.callId)
27
- if (!entry) return
28
- pending.delete(msg.callId)
29
- if (msg.ok) entry.resolve(msg.result)
30
- else entry.reject(new Error(msg.error || 'agent_ipc_response error'))
31
- })
32
- process.once('disconnect', () => {
33
- const error = new Error('agent-ipc: parent IPC disconnected')
34
- for (const [, entry] of pending) entry.reject(error)
35
- pending.clear()
36
- })
37
- }
38
-
39
21
  function nextCallId() {
40
22
  _idSeq += 1
41
23
  return `mem-${process.pid}-${Date.now()}-${_idSeq}`
42
24
  }
43
25
 
26
+ function runtimeRoot() {
27
+ return process.env.MIXDOG_RUNTIME_ROOT
28
+ ? resolve(process.env.MIXDOG_RUNTIME_ROOT)
29
+ : join(tmpdir(), 'mixdog')
30
+ }
31
+
32
+ function isPidAlive(value) {
33
+ const pid = Number(value)
34
+ if (!Number.isInteger(pid) || pid <= 0) return false
35
+ try { process.kill(pid, 0); return true } catch (error) { return error?.code === 'EPERM' }
36
+ }
37
+
38
+ function readBrokerDiscovery() {
39
+ try {
40
+ const raw = JSON.parse(readFileSync(join(runtimeRoot(), 'daemon.json'), 'utf8'))
41
+ const endpoint = raw?.endpoints?.channel
42
+ const port = Number(endpoint?.port)
43
+ if (!Number.isInteger(port) || port <= 0 || port >= 65536 || !endpoint?.token || !isPidAlive(raw?.pid)) {
44
+ return null
45
+ }
46
+ return { port, token: String(endpoint.token) }
47
+ } catch {
48
+ return null
49
+ }
50
+ }
51
+
52
+ function abortError(signal) {
53
+ return signal?.reason instanceof Error
54
+ ? signal.reason
55
+ : new Error(String(signal?.reason || 'agent broker request canceled'))
56
+ }
57
+
58
+ function requestBroker(discovery, path, body, { timeoutMs, signal = null } = {}) {
59
+ return new Promise((resolvePromise, rejectPromise) => {
60
+ const payload = JSON.stringify(body || {})
61
+ let settled = false
62
+ let req = null
63
+ const finish = (fn, value) => {
64
+ if (settled) return
65
+ settled = true
66
+ clearTimeout(deadline)
67
+ try { signal?.removeEventListener?.('abort', onAbort) } catch {}
68
+ fn(value)
69
+ }
70
+ const reject = (error) => finish(
71
+ rejectPromise,
72
+ error instanceof Error ? error : new Error(String(error)),
73
+ )
74
+ const onAbort = () => {
75
+ const error = abortError(signal)
76
+ try { req?.destroy?.(error) } catch {}
77
+ reject(error)
78
+ }
79
+ const deadline = setTimeout(() => {
80
+ const error = new Error(`agent broker request timed out after ${timeoutMs}ms`)
81
+ try { req?.destroy?.(error) } catch {}
82
+ reject(error)
83
+ }, timeoutMs)
84
+ deadline.unref?.()
85
+ if (signal?.aborted) {
86
+ onAbort()
87
+ return
88
+ }
89
+ try { signal?.addEventListener?.('abort', onAbort, { once: true }) } catch {}
90
+ req = http.request({
91
+ hostname: '127.0.0.1',
92
+ port: discovery.port,
93
+ path,
94
+ method: 'POST',
95
+ agent: brokerAgent,
96
+ headers: {
97
+ 'X-Mixdog-Daemon-Token': discovery.token,
98
+ 'Content-Type': 'application/json',
99
+ 'Content-Length': Buffer.byteLength(payload),
100
+ },
101
+ }, (res) => {
102
+ let data = ''
103
+ res.setEncoding('utf8')
104
+ res.on('data', (chunk) => { data += chunk })
105
+ res.on('error', reject)
106
+ res.on('end', () => {
107
+ let parsed = null
108
+ try { parsed = data ? JSON.parse(data) : null } catch {}
109
+ if ((res.statusCode || 0) >= 400) {
110
+ reject(new Error(parsed?.error || data || `HTTP ${res.statusCode}`))
111
+ return
112
+ }
113
+ if (parsed?.ok === false) {
114
+ reject(new Error(parsed.error || 'agent broker dispatch failed'))
115
+ return
116
+ }
117
+ finish(resolvePromise, parsed)
118
+ })
119
+ })
120
+ req.on('error', reject)
121
+ req.write(payload)
122
+ req.end()
123
+ })
124
+ }
125
+
126
+ function cancelBrokerCall(discovery, callId, reason) {
127
+ return requestBroker(discovery, '/agent/cancel', { callId, reason }, {
128
+ timeoutMs: 1500,
129
+ }).catch(() => {})
130
+ }
131
+
44
132
  /**
45
- * Send an agent-agent dispatch request to the parent. Throws if IPC is
46
- * unavailable (worker not forked) or the parent reports an error.
133
+ * Dispatch through the singleton daemon provider/session graph.
47
134
  *
48
135
  * @param {object} opts agent-dispatch construction options
49
136
  * @param {string} [opts.agent]
@@ -55,55 +142,33 @@ function nextCallId() {
55
142
  * @param {string} prompt user message
56
143
  * @returns {Promise<string>} raw assistant content
57
144
  */
58
- export function callAgentDispatch(opts = {}, prompt) {
59
- if (!process.send || !process.connected) {
60
- return Promise.reject(new Error('agent-ipc: IPC channel unavailable (no process.send / not connected)'))
145
+ export async function callAgentDispatch(opts = {}, prompt) {
146
+ const discovery = readBrokerDiscovery()
147
+ if (!discovery) {
148
+ throw new Error('agent-broker: daemon unavailable')
61
149
  }
62
- installListener()
63
150
  const callId = nextCallId()
64
151
  const timeoutMs = Math.max(1000, Number(opts.timeout ?? 600000))
65
- return new Promise((resolve, reject) => {
66
- const timer = setTimeout(() => {
67
- if (!pending.has(callId)) return
68
- pending.delete(callId)
69
- if (process.connected) {
70
- safeIpcSend(process, { type: 'agent_ipc_cancel', callId })
71
- }
72
- reject(new Error(`agent-ipc: timed out after ${timeoutMs}ms`))
73
- }, timeoutMs)
74
- pending.set(callId, {
75
- resolve: (v) => { clearTimeout(timer); resolve(v) },
76
- reject: (e) => { clearTimeout(timer); reject(e) },
152
+ try {
153
+ const response = await requestBroker(discovery, '/agent/dispatch', {
154
+ callId,
155
+ params: {
156
+ agent: opts.agent || null,
157
+ taskType: opts.taskType || null,
158
+ mode: opts.mode || null,
159
+ preset: opts.preset || null,
160
+ cwd: opts.cwd || null,
161
+ prompt: String(prompt ?? ''),
162
+ timeout: timeoutMs,
163
+ },
164
+ }, {
165
+ // Small transport grace after the dispatch's own timeout/watchdog.
166
+ timeoutMs: timeoutMs + 5000,
167
+ signal: opts.signal || null,
77
168
  })
78
- if (!process.connected) {
79
- pending.delete(callId)
80
- clearTimeout(timer)
81
- reject(new Error('agent-ipc: IPC channel not connected'))
82
- return
83
- }
84
- // Pass a write callback so a closed-channel write surfaces as an async
85
- // error here (rejecting this Promise) instead of bubbling to the process
86
- // 'error' event and crashing the daemon with ERR_IPC_CHANNEL_CLOSED.
87
- safeIpcSend(process, {
88
- type: 'agent_ipc_request',
89
- callId,
90
- tool: 'agent_dispatch',
91
- params: {
92
- agent: opts.agent || null,
93
- taskType: opts.taskType || null,
94
- mode: opts.mode || null,
95
- preset: opts.preset || null,
96
- cwd: opts.cwd || null,
97
- prompt: String(prompt ?? ''),
98
- timeout: timeoutMs,
99
- },
100
- }, {
101
- onError: (err) => {
102
- if (!pending.has(callId)) return
103
- pending.delete(callId)
104
- clearTimeout(timer)
105
- reject(err)
106
- },
107
- })
108
- })
169
+ return String(response?.result ?? '')
170
+ } catch (error) {
171
+ await cancelBrokerCall(discovery, callId, error?.message || 'agent broker request failed')
172
+ throw error
173
+ }
109
174
  }
@@ -0,0 +1,88 @@
1
+ const DEFAULT_MAX_ENTRIES = 1000
2
+ const DEFAULT_MAX_BYTES = 8 * 1024 * 1024
3
+
4
+ function entryBytes(key, vector) {
5
+ // Vector storage is exact (Float32Array.byteLength). UTF-16 key bytes plus a
6
+ // small fixed allowance bound Map/string/object overhead conservatively.
7
+ return vector.byteLength + String(key).length * 2 + 64
8
+ }
9
+
10
+ /**
11
+ * LRU cache specialized for embedding vectors.
12
+ *
13
+ * Worker results are semantically float32 but arrive over structured clone as
14
+ * JS number[]. Keeping 1,000 such arrays stores every component as a wider JS
15
+ * Number. Compact once on insertion; callers still receive normal arrays, so
16
+ * the embedding provider's public contract and PG serialization stay intact.
17
+ */
18
+ export function createCompactVectorCache({
19
+ maxEntries = DEFAULT_MAX_ENTRIES,
20
+ maxBytes = DEFAULT_MAX_BYTES,
21
+ } = {}) {
22
+ const entryLimit = Math.max(1, Math.floor(Number(maxEntries) || DEFAULT_MAX_ENTRIES))
23
+ const byteLimit = Math.max(1024, Math.floor(Number(maxBytes) || DEFAULT_MAX_BYTES))
24
+ const entries = new Map()
25
+ let bytes = 0
26
+
27
+ function remove(key) {
28
+ const current = entries.get(key)
29
+ if (!current) return false
30
+ entries.delete(key)
31
+ bytes = Math.max(0, bytes - current.bytes)
32
+ return true
33
+ }
34
+
35
+ function trim() {
36
+ while (entries.size > entryLimit || bytes > byteLimit) {
37
+ const oldest = entries.keys().next().value
38
+ if (oldest === undefined) break
39
+ remove(oldest)
40
+ }
41
+ }
42
+
43
+ function set(key, input) {
44
+ if (!Array.isArray(input) && !ArrayBuffer.isView(input)) {
45
+ throw new TypeError('embedding cache vector must be an array or typed array')
46
+ }
47
+ const vector = input instanceof Float32Array ? input.slice() : Float32Array.from(input)
48
+ remove(key)
49
+ const size = entryBytes(key, vector)
50
+ // A single vector larger than the whole budget is useful to its caller but
51
+ // must not permanently violate the cache bound.
52
+ if (size > byteLimit) return false
53
+ entries.set(key, { vector, bytes: size })
54
+ bytes += size
55
+ trim()
56
+ return true
57
+ }
58
+
59
+ function get(key) {
60
+ const current = entries.get(key)
61
+ if (!current) return null
62
+ entries.delete(key)
63
+ entries.set(key, current)
64
+ return current.vector
65
+ }
66
+
67
+ function clear() {
68
+ entries.clear()
69
+ bytes = 0
70
+ }
71
+
72
+ function snapshot() {
73
+ return {
74
+ entries: entries.size,
75
+ bytes,
76
+ maxEntries: entryLimit,
77
+ maxBytes: byteLimit,
78
+ }
79
+ }
80
+
81
+ return {
82
+ get,
83
+ set,
84
+ has: (key) => entries.has(key),
85
+ clear,
86
+ snapshot,
87
+ }
88
+ }
@@ -29,6 +29,20 @@ export const CORE_DEDUP_TOP_K = 5
29
29
 
30
30
  const CORE_ELEMENT_DERIVE_LENGTH = 40
31
31
 
32
+ // Op aliases: the surface verbs are add/edit/delete/list/…, and a caller
33
+ // naming the same intent differently ("update", "remove") used to get a hard
34
+ // rejection. Only unambiguous synonyms map through.
35
+ const CORE_OP_ALIASES = {
36
+ update: 'edit', modify: 'edit', change: 'edit', set: 'edit',
37
+ create: 'add', new: 'add', insert: 'add', store: 'add', save: 'add',
38
+ remove: 'delete', rm: 'delete', del: 'delete', forget: 'delete',
39
+ show: 'list', get: 'list', read: 'list',
40
+ }
41
+ export function normalizeCoreOp(op) {
42
+ const raw = String(op ?? '').trim().toLowerCase()
43
+ return CORE_OP_ALIASES[raw] ?? raw
44
+ }
45
+
32
46
  function trimOrNull(v) {
33
47
  if (v == null) return null
34
48
  const s = String(v).trim()
@@ -36,7 +50,10 @@ function trimOrNull(v) {
36
50
  }
37
51
 
38
52
  export function normalizeCoreInput(input = {}, options = {}) {
39
- const summary = trimOrNull(input.summary ?? input.content)
53
+ // Summary aliases: the field is `summary`, but callers routinely send the
54
+ // synonym they'd use in prose. Accepting them costs nothing and removes a
55
+ // measured class of bounced calls.
56
+ const summary = trimOrNull(input.summary ?? input.content ?? input.text ?? input.value ?? input.note)
40
57
  const element = trimOrNull(input.element) ?? (summary ? summary.slice(0, CORE_ELEMENT_DERIVE_LENGTH) : null)
41
58
  const suppliedCategory = trimOrNull(input.category)
42
59
  const category = (suppliedCategory ?? 'fact').toLowerCase()
@@ -1,43 +1,22 @@
1
1
  // Cycle LLM dispatch adapters, extracted from index.mjs (pass 3).
2
2
  //
3
- // Each cycleN scheduler/handler routes its LLM work through an in-process
4
- // makeAgentDispatch() adapter (the memory daemon runs the provider registry
5
- // locally, so the dead IPC agent path is never used). The factory built per
6
- // cycle is fixed (role/taskType), so it is memoized in a single-slot closure
7
- // and reshaped to the cycle call signature:
3
+ // Each cycleN scheduler/handler routes its LLM work through the provider's
4
+ // process-singleton agent broker. The memory process owns PG/embeddings only;
5
+ // provider/session modules are not duplicated here. The factory fixes the
6
+ // cycle role/taskType and reshapes to the cycle call signature:
8
7
  // callLlm({ role, taskType, mode, preset, timeout, cwd }, userMessage) -> string
9
- // while makeAgentDispatch's function takes a single { prompt } object. The
10
- // adapter maps the two — preset/cwd resolution is handled inside
11
- // makeAgentDispatch via role.
8
+ // while callAgentDispatch takes (opts, prompt).
12
9
  //
13
- // Pure factory: index.mjs injects makeAgentDispatch. No import-time side
10
+ // Pure factory: index.mjs injects callAgentDispatch. No import-time side
14
11
  // effects, no db/config coupling.
15
- export function createCycleLlmAdapters({ makeAgentDispatch }) {
12
+ export function createCycleLlmAdapters({ callAgentDispatch }) {
16
13
  function buildAdapter(agent) {
17
- let dispatch = null
18
14
  return async (opts = {}, userMessage) => {
19
- if (!dispatch) {
20
- dispatch = makeAgentDispatch({
21
- agent,
22
- taskType: 'maintenance',
23
- sourceType: 'memory-cycle',
24
- // The cycle agents parse the full raw line-format response; the
25
- // agent brief cap (12KB) would truncate a large valid response and
26
- // append prose, causing partial parsing / omitted / invalid chunks.
27
- // Opt out so the no-truncation contract is preserved.
28
- brief: false,
29
- })
30
- }
31
- // Preserve the cycle timeout contract: the caller derives opts.timeout
32
- // from config / caller deadline and expects it to bound the call.
33
- // makeAgentDispatch takes it as a per-call idleTimeoutMs (stale
34
- // watchdog). Map it through; omit when absent/0 so agent defaults apply.
35
- const callTimeout = Number(opts?.timeout)
36
- return dispatch({
37
- prompt: String(userMessage ?? ''),
38
- preset: opts?.preset || undefined,
39
- ...(Number.isFinite(callTimeout) && callTimeout > 0 ? { idleTimeoutMs: callTimeout } : {}),
40
- })
15
+ return callAgentDispatch({
16
+ ...opts,
17
+ agent,
18
+ taskType: 'maintenance',
19
+ }, String(userMessage ?? ''))
41
20
  }
42
21
  }
43
22
 
@@ -45,7 +24,7 @@ export function createCycleLlmAdapters({ makeAgentDispatch }) {
45
24
  // treat these as FACTORIES: call once to obtain the callLlm adapter.
46
25
  // Returning the adapter directly here made `getCycle1CallLlm()` execute an
47
26
  // LLM dispatch with no arguments — empty prompt → agent-dispatch throw →
48
- // unhandled rejection that killed the memory daemon. Keep the factory
27
+ // unhandled rejection that killed the memory runtime. Keep the factory
49
28
  // contract: each getter returns the (memoized) adapter function.
50
29
  const cycle1 = buildAdapter('cycle1-agent')
51
30
  const cycle2 = buildAdapter('cycle2-agent')
@@ -8,6 +8,7 @@ import { Worker } from 'worker_threads'
8
8
  import { join } from 'path'
9
9
  import { fileURLToPath } from 'url'
10
10
  import { writeProfilePoint } from './model-profile.mjs'
11
+ import { createCompactVectorCache } from './compact-vector-cache.mjs'
11
12
  import {
12
13
  getConfiguredEmbeddingModelId,
13
14
  getDefaultEmbeddingDtype,
@@ -33,27 +34,21 @@ let _warmupPromise = null
33
34
  let _embedCallCount = 0
34
35
  let _msgId = 0
35
36
  const _pending = new Map()
37
+ const retiringWorkers = new WeakSet()
36
38
  const EMBED_STEADY_SAMPLE_EVERY = 20
37
- const queryEmbeddingCache = new Map()
38
- const QUERY_EMBEDDING_CACHE_LIMIT = 1000
39
+ const queryEmbeddingCache = createCompactVectorCache({
40
+ maxEntries: 1000,
41
+ maxBytes: 8 * 1024 * 1024,
42
+ })
39
43
 
40
44
  const WORKER_PATH = join(fileURLToPath(import.meta.url), '..', 'embedding-worker.mjs')
41
45
 
42
46
  function cacheEmbedding(key, vector) {
43
- if (queryEmbeddingCache.has(key)) queryEmbeddingCache.delete(key)
44
47
  queryEmbeddingCache.set(key, vector)
45
- if (queryEmbeddingCache.size > QUERY_EMBEDDING_CACHE_LIMIT) {
46
- const oldestKey = queryEmbeddingCache.keys().next().value
47
- if (oldestKey) queryEmbeddingCache.delete(oldestKey)
48
- }
49
48
  }
50
49
 
51
50
  function getCachedEmbedding(key) {
52
- if (!queryEmbeddingCache.has(key)) return null
53
- const value = queryEmbeddingCache.get(key)
54
- queryEmbeddingCache.delete(key)
55
- queryEmbeddingCache.set(key, value)
56
- return value
51
+ return queryEmbeddingCache.get(key)
57
52
  }
58
53
 
59
54
  function ensureWorker() {
@@ -68,8 +63,16 @@ function ensureWorker() {
68
63
  }
69
64
  _lastRestartMs = now
70
65
  const execArgv = process.execArgv.filter((arg) => !String(arg).startsWith('--input-type'))
71
- worker = new Worker(WORKER_PATH, { env: { ...process.env }, execArgv })
72
- worker.on('message', (msg) => {
66
+ const created = new Worker(WORKER_PATH, { env: { ...process.env }, execArgv })
67
+ worker = created
68
+ const rejectWorkerPending = (error) => {
69
+ for (const [id, pending] of _pending) {
70
+ if (pending.worker !== created) continue
71
+ _pending.delete(id)
72
+ pending.reject(error)
73
+ }
74
+ }
75
+ created.on('message', (msg) => {
73
76
  if (msg.type === 'log') {
74
77
  // Worker-thread stdio forwarded via IPC (see embedding-worker.mjs
75
78
  // header): route through the parent's guardable stderr so TUI runs
@@ -85,8 +88,25 @@ function ensureWorker() {
85
88
  _modelReady = false
86
89
  _device = 'cpu'
87
90
  const reason = String(msg.reason || 'idle timeout')
88
- __mixdogMemoryLog(`[embed] ${reason} — model disposed\n`)
91
+ __mixdogMemoryLog(`[embed] ${reason} — model disposed; retiring worker thread\n`)
89
92
  writeProfilePoint({ phase: 'post-idle', model: MODEL_ID, device: msg.device, dtype: msg.dtype, note: reason })
93
+ // A request can be posted after the worker's idle timer starts disposal
94
+ // but before this notification reaches the parent. Do not terminate that
95
+ // worker out from under the accepted request; its next idle cycle will
96
+ // retire it after the request finishes.
97
+ const hasAcceptedRequest = [..._pending.values()]
98
+ .some((pending) => pending.worker === created)
99
+ if (hasAcceptedRequest) {
100
+ __mixdogMemoryLog(`[embed] ${reason} — worker retirement deferred for accepted request\n`)
101
+ return
102
+ }
103
+ // ORT's native allocator does not reliably return model pages when only
104
+ // InferenceSession.dispose() runs on Windows. Ending the worker thread is
105
+ // the actual native-memory boundary; the next recall lazily creates a
106
+ // fresh worker while cached dimensions remain available.
107
+ if (worker === created) worker = null
108
+ retiringWorkers.add(created)
109
+ void created.terminate().catch(() => {})
90
110
  return
91
111
  }
92
112
  const pending = _pending.get(msg.id)
@@ -98,28 +118,28 @@ function ensureWorker() {
98
118
  pending.resolve(msg)
99
119
  }
100
120
  })
101
- worker.on('error', (err) => {
121
+ created.on('error', (err) => {
102
122
  __mixdogMemoryLog(`[embed] worker error: ${err?.message || err}\n`)
103
- for (const [, p] of _pending) p.reject(err)
104
- _pending.clear()
105
- worker = null
123
+ rejectWorkerPending(err)
124
+ if (worker === created) worker = null
106
125
  _modelReady = false
107
126
  _restartCount++
108
127
  })
109
- worker.on('exit', (code) => {
128
+ created.on('exit', (code) => {
129
+ const retired = retiringWorkers.has(created)
130
+ retiringWorkers.delete(created)
110
131
  const exitError = new Error(`Worker exited with code ${code}`)
111
- if (code !== 0) {
132
+ if (!retired && code !== 0) {
112
133
  __mixdogMemoryLog(`[embed] worker exited with code ${code}\n`)
113
134
  _restartCount++
114
- } else {
135
+ } else if (!retired) {
115
136
  _restartCount = 0
116
137
  }
117
- for (const [, p] of _pending) p.reject(exitError)
118
- _pending.clear()
119
- worker = null
138
+ rejectWorkerPending(exitError)
139
+ if (worker === created) worker = null
120
140
  _modelReady = false
121
141
  })
122
- return worker
142
+ return created
123
143
  }
124
144
 
125
145
  const EMBED_WORKER_TIMEOUT_MS = 60_000
@@ -140,6 +160,7 @@ function sendToWorker(action, extra = {}, timeoutMs = EMBED_WORKER_TIMEOUT_MS) {
140
160
  reject(new Error(`embed worker ${action} timed out after ${timeoutMs}ms`))
141
161
  }, timeoutMs)
142
162
  _pending.set(id, {
163
+ worker: w,
143
164
  resolve: (v) => { clearTimeout(timer); resolve(v) },
144
165
  reject: (e) => { clearTimeout(timer); reject(e) },
145
166
  })