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
@@ -57,15 +57,11 @@ const execFileAsync = promisify(execFile)
57
57
  const _envLoadCores = Number(process.env.MIXDOG_EMBED_LOAD_CORES)
58
58
  const LOAD_AFFINITY_CORES = Number.isInteger(_envLoadCores) && _envLoadCores >= 1 ? _envLoadCores : 1
59
59
  const MODEL_CACHE_DIR = join(resolvePluginData(), 'memory-models')
60
- // Reclaim the model after a short idle window. The worker remains available,
61
- // so the next recall reloads on demand without keeping hundreds of MB resident
62
- // throughout an otherwise idle desktop session. Set 0 to opt into keep-alive.
60
+ // Reclaim the model after a short idle window. The parent retires this worker
61
+ // thread so the next recall reloads on demand without keeping hundreds of MB
62
+ // resident throughout an otherwise idle desktop session.
63
63
  const _envIdleMs = Number(process.env.MIXDOG_EMBED_IDLE_TIMEOUT_MS)
64
- const IDLE_TIMEOUT_MS = Number.isFinite(_envIdleMs) && _envIdleMs >= 0 ? _envIdleMs : 3 * 60_000
65
- const _envPressureMb = Number(process.env.MIXDOG_EMBED_PRESSURE_MIN_FREE_MB)
66
- const PRESSURE_MIN_FREE_BYTES = (
67
- Number.isFinite(_envPressureMb) && _envPressureMb >= 0 ? _envPressureMb : 1536
68
- ) * 1024 * 1024
64
+ const IDLE_TIMEOUT_MS = Number.isFinite(_envIdleMs) && _envIdleMs >= 0 ? _envIdleMs : 60_000
69
65
 
70
66
  // Defensive belt against giant model inputs. Callers should already bound text
71
67
  // (see memory-embed truncateForEmbed), but the worker is the last line before
@@ -87,6 +83,7 @@ let configuredDtype = DEFAULT_DTYPE
87
83
  let _device = 'cpu'
88
84
  let _idleTimer = null
89
85
  let _embedInFlight = false
86
+ let _reclaiming = false
90
87
  const _msgQueue = []
91
88
  let ortPatched = false
92
89
  // Control ops must never be overtaken by a priority embed: crossing a queued
@@ -155,44 +152,41 @@ function enqueue(msg) {
155
152
  // reset / ext.dispose() while the prior extractor is still being torn down.
156
153
  const GUARDED_ACTIONS = new Set(['embed', 'embed-batch', 'warmup', 'configure', 'dispose'])
157
154
 
158
- function disposeLoadedExtractor(reason) {
159
- if (!extractorPromise) return false
155
+ async function disposeLoadedExtractor(reason) {
156
+ if (!extractorPromise || _reclaiming) return false
157
+ _reclaiming = true
160
158
  const loaded = extractorPromise
161
159
  extractorPromise = null
162
160
  const prevDevice = _device
163
161
  _device = 'cpu'
164
- loaded.then(ext => { try { ext.dispose() } catch {} }).catch(() => {})
165
- __mixdogMemoryLog(`[embed-worker] ${reason} — model disposed\n`)
166
- parentPort.postMessage({
167
- type: 'idle-dispose',
168
- reason,
169
- device: prevDevice,
170
- dtype: configuredDtype,
171
- })
172
- return true
162
+ try {
163
+ try {
164
+ const ext = await loaded
165
+ try { await Promise.resolve(ext.dispose()) } catch {}
166
+ } catch {}
167
+ __mixdogMemoryLog(`[embed-worker] ${reason} — model disposed\n`)
168
+ parentPort.postMessage({
169
+ type: 'idle-dispose',
170
+ reason,
171
+ device: prevDevice,
172
+ dtype: configuredDtype,
173
+ })
174
+ return true
175
+ } finally {
176
+ _reclaiming = false
177
+ }
173
178
  }
174
179
 
175
180
  function resetIdleTimer() {
176
181
  if (_idleTimer) clearTimeout(_idleTimer)
177
182
  if (IDLE_TIMEOUT_MS <= 0) return
178
183
  _idleTimer = setTimeout(() => {
179
- if (!_embedInFlight) disposeLoadedExtractor('idle timeout')
184
+ if (!_embedInFlight) void disposeLoadedExtractor('idle timeout')
180
185
  _idleTimer = null
181
186
  }, IDLE_TIMEOUT_MS)
182
187
  _idleTimer.unref?.()
183
188
  }
184
189
 
185
- const _pressureTimer = setInterval(() => {
186
- if (PRESSURE_MIN_FREE_BYTES <= 0 || _embedInFlight || !extractorPromise) return
187
- let freeBytes = Number.POSITIVE_INFINITY
188
- try { freeBytes = Number(os.freemem()) } catch {}
189
- if (Number.isFinite(freeBytes) && freeBytes < PRESSURE_MIN_FREE_BYTES) {
190
- if (_idleTimer) { clearTimeout(_idleTimer); _idleTimer = null }
191
- disposeLoadedExtractor('host memory pressure')
192
- }
193
- }, 5_000)
194
- _pressureTimer.unref?.()
195
-
196
190
  // Set this process's CPU affinity to `mask` (bitmask of allowed logical
197
191
  // processors). Windows-only and best-effort: there is no Node API, so go
198
192
  // through PowerShell; a process may set its own affinity without elevation.
@@ -243,9 +237,6 @@ function patchOrtThreads() {
243
237
  async function loadExtractor() {
244
238
  if (!extractorPromise) {
245
239
  extractorPromise = (async () => {
246
- if (PRESSURE_MIN_FREE_BYTES > 0 && os.freemem() < PRESSURE_MIN_FREE_BYTES) {
247
- throw new Error('embedding model load deferred under host memory pressure')
248
- }
249
240
  parentPort.postMessage({ type: 'profile', record: { phase: 'baseline', model: MODEL_ID, device: _device, dtype: configuredDtype, note: 'pre-load' } })
250
241
  patchOrtThreads()
251
242
  const { pipeline, env } = await import('@huggingface/transformers')
@@ -441,6 +432,11 @@ async function drainQueue() {
441
432
  await processMessage(next)
442
433
  }
443
434
  _embedInFlight = false
435
+ // Idle means time since the LAST completed inference, not time since it
436
+ // started. A cold model load can outlive a short idle window; without this
437
+ // re-arm that timer fires while busy, is discarded, and the loaded ORT
438
+ // session then remains resident indefinitely.
439
+ if (extractorPromise && !_reclaiming) resetIdleTimer()
444
440
  }
445
441
 
446
442
  parentPort.on('message', async (msg) => {
@@ -121,10 +121,7 @@ export function createHttpRouter({
121
121
  const _ownerInFlightHttpCalls = new Map()
122
122
 
123
123
  const requestHandler = async (req, res) => {
124
- touchDaemonIdleTimer(`${req.method || 'HTTP'} ${req.url || '/'}`)
125
- // Connected-client lifecycle. Proxies register on first use and
126
- // deregister on CLI shutdown; the daemon reaps itself shortly after the
127
- // last client leaves (see registerClient/deregisterClient in index.mjs).
124
+ touchDaemonIdleTimer?.(`${req.method || 'HTTP'} ${req.url || '/'}`)
128
125
  if (req.method === 'POST' && (req.url === '/client/register' || req.url === '/client/deregister')) {
129
126
  if (!isLocalOrigin(req)) {
130
127
  sendJson(res, { ok: false, error: 'forbidden: cross-origin' }, 403)
@@ -136,8 +133,6 @@ export function createHttpRouter({
136
133
  if (req.url === '/client/register') {
137
134
  const accepted = registerClient?.(clientPid)
138
135
  if (accepted === false) {
139
- // Daemon is draining — distinct signal so the proxy respawns a fresh
140
- // daemon and retries its pending RPC instead of binding to this one.
141
136
  sendJson(res, { ok: false, draining: true, error: 'memory worker draining' }, 503)
142
137
  return
143
138
  }
@@ -171,6 +166,7 @@ export function createHttpRouter({
171
166
  try {
172
167
  const db = getDb()
173
168
  const stats = await entryStats()
169
+ const memory = process.memoryUsage()
174
170
  sendJson(res, {
175
171
  status: 'ok',
176
172
  worker_pid: process.pid,
@@ -192,6 +188,13 @@ export function createHttpRouter({
192
188
  cycle_running: cycleScheduler.getCycleRunning(),
193
189
  cycle_health: cycleScheduler.getCycleHealth(),
194
190
  cycle_backlog: cycleScheduler.getCycleBacklogSnapshot(),
191
+ memory: {
192
+ rssBytes: memory.rss,
193
+ heapTotalBytes: memory.heapTotal,
194
+ heapUsedBytes: memory.heapUsed,
195
+ externalBytes: memory.external,
196
+ arrayBufferBytes: memory.arrayBuffers,
197
+ },
195
198
  })
196
199
  } catch (e) { sendError(res, e.message) }
197
200
  return
@@ -15,7 +15,6 @@
15
15
  // analyze(text) — sync; returns TokenInfo[] or null when not ready.
16
16
  // stems(text) — sync; content-morpheme stems (NNG/NNP/VV/VA/XR/SL) or null.
17
17
  import fs from 'node:fs'
18
- import os from 'node:os'
19
18
  import path from 'node:path'
20
19
  import zlib from 'node:zlib'
21
20
 
@@ -54,11 +53,7 @@ let _idleTimer = null
54
53
  const _envIdleMs = Number(process.env.MIXDOG_KO_MORPH_IDLE_TIMEOUT_MS)
55
54
  const IDLE_TIMEOUT_MS = Number.isFinite(_envIdleMs) && _envIdleMs >= 0
56
55
  ? _envIdleMs
57
- : 2 * 60_000
58
- const _envPressureMb = Number(process.env.MIXDOG_KO_MORPH_PRESSURE_MIN_FREE_MB)
59
- const PRESSURE_MIN_FREE_BYTES = (
60
- Number.isFinite(_envPressureMb) && _envPressureMb >= 0 ? _envPressureMb : 1536
61
- ) * 1024 * 1024
56
+ : 60_000
62
57
 
63
58
  export function isReady() { return _state === 'ready' && _kiwi != null }
64
59
  export function state() { return _state }
@@ -78,14 +73,8 @@ function releaseLoaded(reason = '') {
78
73
 
79
74
  function touchIdleTimer() {
80
75
  if (_idleTimer) clearTimeout(_idleTimer)
81
- let delay = IDLE_TIMEOUT_MS
82
- try {
83
- if (PRESSURE_MIN_FREE_BYTES > 0 && os.freemem() < PRESSURE_MIN_FREE_BYTES) delay = 1
84
- } catch {}
85
- if (delay <= 0) return
86
- _idleTimer = setTimeout(() => releaseLoaded(
87
- delay <= 1 ? 'host memory pressure' : 'idle timeout',
88
- ), delay)
76
+ if (IDLE_TIMEOUT_MS <= 0) return
77
+ _idleTimer = setTimeout(() => releaseLoaded('idle timeout'), IDLE_TIMEOUT_MS)
89
78
  _idleTimer.unref?.()
90
79
  }
91
80
 
@@ -166,12 +155,6 @@ export async function init(dataDir, log = () => {}) {
166
155
  return true
167
156
  }
168
157
  if (_initPromise) return _initPromise
169
- try {
170
- if (PRESSURE_MIN_FREE_BYTES > 0 && os.freemem() < PRESSURE_MIN_FREE_BYTES) {
171
- _log('[memory-service] kiwi morph load deferred under host memory pressure\n')
172
- return false
173
- }
174
- } catch {}
175
158
  _state = 'loading'
176
159
  const t0 = Date.now()
177
160
  _log(`[memory-service] kiwi morph init start (model ${KIWI_MODEL_VERSION})\n`)
@@ -33,6 +33,7 @@ import {
33
33
  promoteCoreCandidate,
34
34
  dismissCoreCandidate,
35
35
  normalizeCoreInput,
36
+ normalizeCoreOp,
36
37
  } from './core-memory-store.mjs'
37
38
  import { resolveProjectId, resolveProjectScope } from './project-id-resolver.mjs'
38
39
  import { resolvePluginData } from '../../shared/plugin-paths.mjs'
@@ -602,7 +603,7 @@ export function createMemoryActionHandlers({
602
603
  }
603
604
 
604
605
  if (action === 'core') {
605
- const op = String(args.op ?? '').trim().toLowerCase()
606
+ const op = normalizeCoreOp(args.op)
606
607
  if (!['add', 'edit', 'delete', 'list', 'candidates', 'promote', 'dismiss'].includes(op)) {
607
608
  return { text: 'core requires op: "add" | "edit" | "delete" | "list" | "candidates" | "promote" | "dismiss"', isError: true }
608
609
  }
@@ -14,7 +14,7 @@ export function readMainConfig() {
14
14
 
15
15
  // Recap toggle lives in the `agent` config section (agent.recap.enabled,
16
16
  // default true) — the same file the session runtime persists via
17
- // saveConfigAndAdopt. The memory daemon runs as a detached cross-process HTTP
17
+ // saveConfigAndAdopt. The memory runtime is hosted by the unified daemon
18
18
  // worker, so it re-reads this section from disk each cycle tick (poll-on-use)
19
19
  // rather than relying on IPC.
20
20
 
@@ -634,6 +634,7 @@ async function _runCycle1Impl(db, config = {}, options = {}, _dataDir = null) {
634
634
  timeout,
635
635
  // Pin cwd to null so every memory cycle call hits the same agent cache shard.
636
636
  cwd: null,
637
+ signal,
637
638
  }, userMessage)
638
639
  throwIfAborted(signal)
639
640
  __mixdogMemoryLog(`[cycle1-time] window=${windowIdx} llmMs=${Date.now() - _tLlm}\n`)
@@ -32,7 +32,7 @@ const isShiftFollowToken = (tok) => {
32
32
  return ALL_PRIMARY_VERBS.has(v) || v === 'why' || v === 'core'
33
33
  }
34
34
 
35
- async function invokeLlm(prompt, mode, preset, timeout, llmCall = callAgentDispatch) {
35
+ async function invokeLlm(prompt, mode, preset, timeout, llmCall = callAgentDispatch, signal = null) {
36
36
  return await llmCall({
37
37
  agent: 'cycle2-agent',
38
38
  taskType: 'maintenance',
@@ -40,6 +40,7 @@ async function invokeLlm(prompt, mode, preset, timeout, llmCall = callAgentDispa
40
40
  preset,
41
41
  timeout,
42
42
  cwd: null,
43
+ signal,
43
44
  }, prompt)
44
45
  }
45
46
 
@@ -357,7 +358,7 @@ export async function runUnifiedGate(db, rows, activeContext, config = {}, optio
357
358
  const callOnce = async (extraTag) => {
358
359
  throwIfAborted(signal)
359
360
  const p = extraTag ? `${prompt}\n\n[retry:${extraTag}]` : prompt
360
- const raw = await invokeLlm(p, mode, preset, timeout, options.callLlm)
361
+ const raw = await invokeLlm(p, mode, preset, timeout, options.callLlm, signal)
361
362
  throwIfAborted(signal)
362
363
  return raw
363
364
  }
@@ -518,6 +519,7 @@ export async function sonnetCascade(candidates, rulesDigest, options = {}) {
518
519
  preset,
519
520
  timeout: 600000,
520
521
  cwd: null,
522
+ signal,
521
523
  }, prompt)
522
524
  } catch (err) {
523
525
  if (signal?.aborted) throw signal.reason ?? err
@@ -322,6 +322,7 @@ async function _llmJudgePair(summaryA, summaryB, siblingContext = [], options =
322
322
  preset: options.preset || resolveMaintenancePreset('memory'),
323
323
  timeout: 30000,
324
324
  cwd: null,
325
+ signal,
325
326
  }, prompt)
326
327
  throwIfAborted(signal)
327
328
  return String(raw ?? '').trim().toLowerCase().startsWith('merge')
@@ -2,6 +2,7 @@
2
2
  // memory-cycle2.mjs). Logging shim, abort check, and resource-dir resolution.
3
3
  // No cycle2 business logic; safe to import from any cycle2 sub-module.
4
4
  import { fileURLToPath } from 'url'
5
+ import { join } from 'path'
5
6
 
6
7
  import { __mixdogMemoryLog } from './memory-log.mjs'
7
8
  export { __mixdogMemoryLog }
@@ -11,5 +12,7 @@ export function throwIfAborted(signal) {
11
12
  }
12
13
 
13
14
  export function resourceDir() {
14
- return process.env.MIXDOG_ROOT || fileURLToPath(new URL('../../../..', import.meta.url))
15
+ return process.env.MIXDOG_ROOT
16
+ ? join(process.env.MIXDOG_ROOT, 'src')
17
+ : fileURLToPath(new URL('../../..', import.meta.url))
15
18
  }
@@ -36,7 +36,7 @@ function resourceDir() {
36
36
  return process.env.MIXDOG_ROOT || fileURLToPath(new URL('../../../..', import.meta.url))
37
37
  }
38
38
 
39
- async function invokeLlm(prompt, mode, preset, timeout, llmCall = callAgentDispatch) {
39
+ async function invokeLlm(prompt, mode, preset, timeout, llmCall = callAgentDispatch, signal = null) {
40
40
  return await llmCall({
41
41
  agent: 'cycle3-agent',
42
42
  taskType: 'maintenance',
@@ -44,6 +44,7 @@ async function invokeLlm(prompt, mode, preset, timeout, llmCall = callAgentDispa
44
44
  preset,
45
45
  timeout,
46
46
  cwd: null,
47
+ signal,
47
48
  }, prompt)
48
49
  }
49
50
 
@@ -55,6 +56,7 @@ export async function invokeCycle3Maintenance(prompt, options = {}) {
55
56
  preset,
56
57
  Number(options.timeout ?? 600000),
57
58
  options.callLlm,
59
+ options.signal,
58
60
  )
59
61
  }
60
62
 
@@ -24,6 +24,11 @@ export function createMemoryDaemonLifecycle({
24
24
  _idleShutdownTimer = null
25
25
  }
26
26
  _idleShutdownTimer = setTimeout(() => {
27
+ pruneDeadClients()
28
+ if (_connectedClients.size > 0) {
29
+ touchDaemonIdleTimer('live client retained')
30
+ return
31
+ }
27
32
  log(`[memory-service] daemon idle TTL elapsed after ${reason}; shutting down\n`)
28
33
  stop()
29
34
  .then(() => process.exit(0))
@@ -1,4 +1,4 @@
1
- // Shared memory-daemon stderr logger. The write is captured at import time so
1
+ // Shared memory-runtime stderr logger. The write is captured at import time so
2
2
  // memory output stays visible even when a TUI log guard swaps
3
3
  // process.stderr.write later; MIXDOG_QUIET_MEMORY_LOG mutes it entirely.
4
4
  const __mixdogMemoryStderrWrite = process.stderr.write.bind(process.stderr);
@@ -1,6 +1,5 @@
1
- // Extracted from index.mjs — single/multi-instance process-lock + owner-election
2
- // helpers. Pure functions parameterized on lock-file paths so index.mjs retains
3
- // ownership of DATA_DIR-derived constants and the daemon logger.
1
+ // Process lock for the standalone memory MCP entry. Product mode is hosted
2
+ // A direct entry (including the isolated daemon child) owns this lock.
4
3
  import fs from 'node:fs'
5
4
  import { execFileSync } from 'node:child_process'
6
5
 
@@ -15,55 +14,7 @@ export function isPidAliveLocal(pid) {
15
14
  catch (e) { return e.code !== 'ESRCH' }
16
15
  }
17
16
 
18
- export function tryAcquireMemoryOwnerLock(ownerLockFile, log = () => {}) {
19
- // Returns true on success (this process now owns memory worker for the data
20
- // dir), false when a live peer holds the lock. Stale locks (dead PID) are
21
- // unlinked and retried atomically. Throws on unexpected fs errors so callers
22
- // surface lock-system corruption rather than silently downgrading.
23
- //
24
- // EPERM/EBUSY/EACCES at openSync are transient — AV scanners (SignKorea /
25
- // SKCert / ezPDFWS etc) briefly lock newly-created files during inspection.
26
- // The 0.1.x baseline threw immediately and the worker promoted to
27
- // permanentlyDegraded, killing memory tools for the rest of the session.
28
- // Treat the AV error codes as retryable with bounded backoff (~750ms total)
29
- // before giving up and rethrowing.
30
- for (let attempt = 0; attempt < 5; attempt++) {
31
- try {
32
- const fd = fs.openSync(ownerLockFile, 'wx')
33
- fs.writeSync(fd, String(process.pid))
34
- fs.closeSync(fd)
35
- return true
36
- } catch (e) {
37
- if (e.code === 'EEXIST') {
38
- let ownerPid = NaN
39
- try { ownerPid = Number(fs.readFileSync(ownerLockFile, 'utf8').trim()) } catch {}
40
- if (isPidAliveLocal(ownerPid)) return false
41
- // Stale lock: dead owner — unlink and retry exclusive create.
42
- try { fs.unlinkSync(ownerLockFile) } catch {}
43
- continue
44
- }
45
- const transient = e.code === 'EPERM' || e.code === 'EBUSY' || e.code === 'EACCES'
46
- if (transient && attempt < 4) {
47
- // Sync busy-wait acceptable here: this runs on memory worker boot
48
- // path, once per process; the parent handler is not blocked.
49
- const end = Date.now() + 50 * (attempt + 1)
50
- while (Date.now() < end) {}
51
- continue
52
- }
53
- throw e
54
- }
55
- }
56
- return false
57
- }
58
-
59
- export function releaseMemoryOwnerLock(ownerLockFile) {
60
- try {
61
- const ownerPid = Number(fs.readFileSync(ownerLockFile, 'utf8').trim())
62
- if (ownerPid === process.pid) fs.unlinkSync(ownerLockFile)
63
- } catch {}
64
- }
65
-
66
- export function killPreviousServer(pid, log = () => {}) {
17
+ function killPreviousServer(pid, log = () => {}) {
67
18
  if (pid <= 0 || pid === process.pid) return false
68
19
  if (process.platform === 'win32') {
69
20
  try {
@@ -117,14 +68,8 @@ export function killPreviousServer(pid, log = () => {}) {
117
68
  }
118
69
 
119
70
  export function acquireLock(lockFile, log = () => {}) {
120
- // Multi-instance guard. In multi-terminal mode the lock owner is a *peer*
121
- // memory worker serving recall for another CC session. killPreviousServer
122
- // would taskkill /F that healthy peer mid-flight, then this fork-proxy
123
- // mode wouldn't even need a lock anyway. Skip the entire kill-the-previous
124
- // protocol; fork-proxy detection in init() takes priority. If neither
125
- // proxy nor lock-owner path applies (race window during simultaneous
126
- // boot), the worker simply continues without the lock — server-main /
127
- // PG / port-listen handle the actual conflict cases.
71
+ // Multi-instance mode is coordinated by the unified daemon, so a direct MCP
72
+ // entry does not evict an already-running peer from another client.
128
73
  if (process.env.MIXDOG_MULTI_INSTANCE === '1') return
129
74
  try {
130
75
  if (fs.existsSync(lockFile)) {
@@ -417,7 +417,7 @@ async function _migrateRecallIndexesIfStale(db) {
417
417
 
418
418
  export async function ensureCurrentSchemaExtensions(db, dims) {
419
419
  // One-time cleanup: attachment-only placeholder rows ('(attachment)' user
420
- // content, e.g. Discord backend discord.mjs:724) predate the
420
+ // content, e.g. Discord provider discord.mjs:724) predate the
421
421
  // shouldExcludeIngestMessage() ingest-time filter (session-ingest.mjs).
422
422
  // Delete any already-persisted rows so they stop polluting recall/cycle1.
423
423
  // Idempotent (no-op once cleaned); best-effort so a failure never blocks boot.
@@ -107,6 +107,7 @@ async function findFreePort(preferred) {
107
107
  // ---------------------------------------------------------------------------
108
108
 
109
109
  const MIXDOG_CONF_V2_MARKER = '# mixdog overrides v2 — bgwriter / checkpoint distribution'
110
+ const MIXDOG_CONF_V3_MARKER = '# mixdog overrides v3 — bounded local-app resources'
110
111
 
111
112
  // Lines emitted into postgresql.conf for v2. effective_io_concurrency requires
112
113
  // posix_fadvise; PostgreSQL rejects non-zero values on Windows and macOS.
@@ -124,6 +125,21 @@ function buildV2Block() {
124
125
  return lines.join('\n') + '\n'
125
126
  }
126
127
 
128
+ function buildV3Block() {
129
+ return [
130
+ '',
131
+ MIXDOG_CONF_V3_MARKER,
132
+ // The only long-lived pools are memory(max 5) + trace(max 5). Keep ample
133
+ // recovery/bootstrap headroom without reserving backend slots for 100
134
+ // connections this single-user local store can never use.
135
+ 'max_connections = 32',
136
+ // Mixdog queries are short indexed lookups / bounded batches. LLVM compile
137
+ // setup adds latency and transient memory; parallel query workers remain
138
+ // untouched for genuinely large scans.
139
+ 'jit = off',
140
+ ].join('\n') + '\n'
141
+ }
142
+
127
143
  // Migration: an earlier v2 emitted effective_io_concurrency on every POSIX
128
144
  // platform. Strip it anywhere except Linux so old macOS clusters can start.
129
145
  function stripPlatformInvalidLines(conf) {
@@ -136,18 +152,25 @@ function ensureConfV2(pgdataDir) {
136
152
  if (!existsSync(confPath)) return false
137
153
  const original = readFileSync(confPath, 'utf8')
138
154
  let conf = stripPlatformInvalidLines(original)
139
- const stripped = conf !== original
140
- const hasMarker = conf.includes(MIXDOG_CONF_V2_MARKER)
141
- if (hasMarker && !stripped) return false
142
- if (!hasMarker) conf = conf + buildV2Block()
155
+ let changed = conf !== original
156
+ if (!conf.includes(MIXDOG_CONF_V2_MARKER)) {
157
+ conf += buildV2Block()
158
+ changed = true
159
+ }
160
+ if (!conf.includes(MIXDOG_CONF_V3_MARKER)) {
161
+ conf += buildV3Block()
162
+ changed = true
163
+ }
164
+ if (!changed) return false
143
165
  writeFileSync(confPath, conf)
144
166
  return true
145
167
  }
146
168
 
147
- // Public reconcile: idempotent conf v2 append + pg_ctl reload on a running
148
- // instance. Single source of truth for v2 application — supervisor and other
169
+ // Public reconcile: idempotent current override append + pg_ctl reload on a
170
+ // running instance. Single source of truth — supervisor and other
149
171
  // callers route through this rather than re-implementing the conf/reload pair.
150
- // All v2 settings are reload-applicable (no restart required).
172
+ // Reload-compatible settings apply immediately; postmaster settings such as
173
+ // max_connections apply on the next normal PG start (never restart live work).
151
174
  export function reconcileConfV2(runtimeDir, pgdataDir) {
152
175
  const applied = ensureConfV2(pgdataDir)
153
176
  if (!applied) return { applied: false, reloaded: false }
@@ -156,7 +179,7 @@ export function reconcileConfV2(runtimeDir, pgdataDir) {
156
179
  })
157
180
  const reloaded = reload.status === 0
158
181
  if (reloaded) {
159
- __mixdogMemoryLog('[pg-process] postgresql.conf v2 overrides applied via pg_ctl reload\n')
182
+ __mixdogMemoryLog('[pg-process] postgresql.conf overrides reconciled via pg_ctl reload\n')
160
183
  } else {
161
184
  __mixdogMemoryLog(`[pg-process] pg_ctl reload after v2 append failed (non-fatal): ${reload.stderr?.toString() || ''}\n`)
162
185
  }
@@ -252,7 +275,7 @@ export async function startPg({
252
275
  'wal_compression = on', // smaller WAL segments; fewer bytes for Defender to scan per segment
253
276
  'wal_init_zero = off', // skips zero-fill on new WAL segment; cuts Defender contact at segment creation
254
277
  'wal_recycle = off', // disables WAL rename/recycle loop; eliminates rename-storm EPERM pattern in pgdata
255
- ].join('\n') + '\n' + buildV2Block()
278
+ ].join('\n') + '\n' + buildV2Block() + buildV3Block()
256
279
 
257
280
  try {
258
281
  const existing = readFileSync(confPath, 'utf8')
@@ -305,7 +305,7 @@ export function shouldExcludeIngestMessage(m) {
305
305
  if (role === 'user' && typeof raw === 'string' && /^Reference files:\s*/i.test(raw.trimStart())) {
306
306
  return true
307
307
  }
308
- // Attachment-only placeholder rows (e.g. Discord backend discord.mjs:724
308
+ // Attachment-only placeholder rows (e.g. Discord provider discord.mjs:724
309
309
  // `"(attachment)"` fallback when a message carries no text, only files).
310
310
  // Carries zero retrievable content — excluded so recall isn't polluted
311
311
  // with bare "(attachment)" memory rows.
@@ -10,11 +10,7 @@ export const TOOL_DEFS = [
10
10
  name: 'memory',
11
11
  title: 'Memory Cycle',
12
12
  annotations: { title: 'Memory Cycle', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
13
- description: [
14
- 'Core-memory mutation/status; recall retrieves.',
15
- 'Mutations use action=core with op; status uses action=status.',
16
- 'Store only durable compact ENGLISH facts/rules/preferences.',
17
- ].join(' '),
13
+ description: 'Core-memory mutation/status.',
18
14
  inputSchema: {
19
15
  type: 'object',
20
16
  properties: {
@@ -286,9 +286,9 @@ function normalizeNativeSearchPayload(result, args, startedAt) {
286
286
 
287
287
  async function _searchCore(args, { cacheState, nativeSearch, signal }) {
288
288
  const cacheArgs = searchArgsForCacheKey(args)
289
- const backend = 'native-web-search'
289
+ const provider = 'native-web-search'
290
290
  const searchCacheKey = buildCacheKey('search', {
291
- provider: backend,
291
+ provider,
292
292
  ...cacheArgs,
293
293
  })
294
294
  const cachedSearch = getCachedEntry(cacheState, searchCacheKey)
@@ -8,7 +8,7 @@ export const TOOL_DEFS = [
8
8
  {
9
9
  name: 'search',
10
10
  title: 'Mixdog Web Search',
11
- description: `Web/docs/current-info search. ${TOOL_SYNC_EXECUTION_CONTRACT} Cached; query supports array fan-out.`,
11
+ description: `Cached web/docs/current-info search. ${TOOL_SYNC_EXECUTION_CONTRACT}`,
12
12
  inputSchema: {
13
13
  type: 'object',
14
14
  properties: {
@@ -27,7 +27,7 @@ export const TOOL_DEFS = [
27
27
  {
28
28
  name: 'web_fetch',
29
29
  title: 'Mixdog Web Fetch',
30
- description: 'Use after search. Fetch page/docs body from URL.',
30
+ description: 'Fetch page/docs body from URL.',
31
31
  inputSchema: {
32
32
  type: 'object',
33
33
  properties: {
@@ -0,0 +1,70 @@
1
+ export function abortReason(signal, fallback = 'Operation aborted') {
2
+ const reason = signal?.reason;
3
+ if (reason instanceof Error) return reason;
4
+ if (reason !== undefined && reason !== null && reason !== '') {
5
+ return new Error(String(reason));
6
+ }
7
+ return new Error(fallback);
8
+ }
9
+
10
+ export function throwIfAborted(signal, fallback) {
11
+ if (signal?.aborted) throw abortReason(signal, fallback);
12
+ }
13
+
14
+ /**
15
+ * Await one operation while making the caller's settlement independent from
16
+ * an implementation that ignores AbortSignal. The detached operation remains
17
+ * observed so a later rejection can never become unhandled.
18
+ */
19
+ export async function runAbortable(signal, task, fallback) {
20
+ if (typeof task !== 'function') throw new TypeError('runAbortable requires a task function');
21
+ if (!signal) return await task();
22
+ throwIfAborted(signal, fallback);
23
+
24
+ const underlying = Promise.resolve().then(task);
25
+ underlying.catch(() => {});
26
+
27
+ return await new Promise((resolve, reject) => {
28
+ let settled = false;
29
+ const finish = (callback, value) => {
30
+ if (settled) return;
31
+ settled = true;
32
+ try { signal.removeEventListener('abort', onAbort); } catch {}
33
+ callback(value);
34
+ };
35
+ const onAbort = () => finish(reject, abortReason(signal, fallback));
36
+ signal.addEventListener('abort', onAbort, { once: true });
37
+ if (signal.aborted) {
38
+ onAbort();
39
+ return;
40
+ }
41
+ underlying.then(
42
+ (value) => finish(resolve, value),
43
+ (error) => finish(reject, error),
44
+ );
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Bound best-effort cleanup without cancelling the underlying work. The
50
+ * detached promise remains observed and may still finish in the background.
51
+ */
52
+ export async function settleWithin(promise, timeoutMs) {
53
+ const budget = Number(timeoutMs);
54
+ if (!(Number.isFinite(budget) && budget > 0)) {
55
+ return { settled: true, value: await promise };
56
+ }
57
+ const underlying = Promise.resolve(promise).then((value) => ({ settled: true, value }));
58
+ underlying.catch(() => {});
59
+ let timer = null;
60
+ try {
61
+ return await Promise.race([
62
+ underlying,
63
+ new Promise((resolve) => {
64
+ timer = setTimeout(() => resolve({ settled: false, value: undefined }), budget);
65
+ }),
66
+ ]);
67
+ } finally {
68
+ if (timer) clearTimeout(timer);
69
+ }
70
+ }