mixdog 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (404) hide show
  1. package/.claude-plugin/marketplace.json +31 -0
  2. package/.claude-plugin/plugin.json +20 -0
  3. package/.gitattributes +34 -0
  4. package/.mcp.json +14 -0
  5. package/ARCHITECTURE.md +77 -0
  6. package/CHANGELOG.md +7 -0
  7. package/CONTRIBUTING.md +45 -0
  8. package/DATA-FLOW.md +79 -0
  9. package/LICENSE +21 -0
  10. package/README.md +389 -0
  11. package/SECURITY.md +138 -0
  12. package/UNINSTALL.md +112 -0
  13. package/agents/maintenance.md +5 -0
  14. package/agents/memory-classification.md +30 -0
  15. package/agents/scheduler-task.md +18 -0
  16. package/agents/webhook-handler.md +27 -0
  17. package/agents/worker.md +24 -0
  18. package/bin/bridge +133 -0
  19. package/bin/statusline-launcher.mjs +78 -0
  20. package/bin/statusline-lib.mjs +550 -0
  21. package/bin/statusline.mjs +607 -0
  22. package/bun.lock +802 -0
  23. package/commands/config.md +16 -0
  24. package/commands/doctor.md +13 -0
  25. package/commands/setup.md +17 -0
  26. package/defaults/cycle3-review-prompt.md +90 -0
  27. package/defaults/hidden-roles.json +65 -0
  28. package/defaults/memory-chunk-prompt.md +63 -0
  29. package/defaults/memory-promote-prompt.md +135 -0
  30. package/defaults/mixdog-config.template.json +27 -0
  31. package/defaults/user-workflow.json +8 -0
  32. package/defaults/user-workflow.md +12 -0
  33. package/hooks/hooks.json +73 -0
  34. package/hooks/lib/active-instance.cjs +77 -0
  35. package/hooks/lib/permission-evaluator.cjs +411 -0
  36. package/hooks/lib/permission-route.cjs +63 -0
  37. package/hooks/lib/permission-rules.cjs +170 -0
  38. package/hooks/lib/settings-loader.cjs +116 -0
  39. package/hooks/post-tool-use.cjs +84 -0
  40. package/hooks/pre-mcp-sandbox.cjs +158 -0
  41. package/hooks/pre-tool-subagent.cjs +253 -0
  42. package/hooks/session-start.cjs +1372 -0
  43. package/hooks/turn-timer.cjs +82 -0
  44. package/lib/claude-md-writer.cjs +386 -0
  45. package/lib/config-cjs.cjs +61 -0
  46. package/lib/hook-pipe-path.cjs +10 -0
  47. package/lib/keychain-cjs.cjs +263 -0
  48. package/lib/plugin-paths.cjs +61 -0
  49. package/lib/rules-builder.cjs +241 -0
  50. package/lib/text-utils.cjs +61 -0
  51. package/native/README.md +117 -0
  52. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  53. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  54. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  55. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  56. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  57. package/package.json +107 -0
  58. package/prompts/code-review.txt +16 -0
  59. package/prompts/security-audit.txt +17 -0
  60. package/rules/bridge/00-common.md +39 -0
  61. package/rules/bridge/20-skip-protocol.md +18 -0
  62. package/rules/bridge/30-explorer.md +33 -0
  63. package/rules/bridge/40-cycle1-agent.md +52 -0
  64. package/rules/bridge/41-cycle2-agent.md +62 -0
  65. package/rules/bridge/42-cycle3-agent.md +44 -0
  66. package/rules/lead/00-tool-lead.md +61 -0
  67. package/rules/lead/01-general.md +23 -0
  68. package/rules/lead/02-channels.md +49 -0
  69. package/rules/lead/03-team.md +27 -0
  70. package/rules/lead/04-workflow.md +20 -0
  71. package/rules/shared/00-language.md +14 -0
  72. package/rules/shared/01-tool.md +138 -0
  73. package/scripts/bootstrap.mjs +184 -0
  74. package/scripts/bridge-unify-smoke.mjs +308 -0
  75. package/scripts/build-runtime-linux.sh +348 -0
  76. package/scripts/build-runtime-macos.sh +217 -0
  77. package/scripts/build-runtime-windows.ps1 +242 -0
  78. package/scripts/builtin-utils-smoke.mjs +392 -0
  79. package/scripts/check-json.mjs +45 -0
  80. package/scripts/check-syntax-changed.mjs +102 -0
  81. package/scripts/check-syntax.mjs +58 -0
  82. package/scripts/code-graph-batch.test.mjs +33 -0
  83. package/scripts/config-preserve-smoke.mjs +180 -0
  84. package/scripts/doctor.mjs +484 -0
  85. package/scripts/edit-normalize-fuzz.mjs +130 -0
  86. package/scripts/edit-normalize-smoke.mjs +401 -0
  87. package/scripts/edit-operation-smoke.mjs +369 -0
  88. package/scripts/edit2-smoke.mjs +63 -0
  89. package/scripts/fuzzy-e2e.mjs +28 -0
  90. package/scripts/fuzzy-smoke.mjs +26 -0
  91. package/scripts/generate-runtime-manifest.mjs +166 -0
  92. package/scripts/guard-smoke.mjs +66 -0
  93. package/scripts/hidden-role-schema-smoke.mjs +162 -0
  94. package/scripts/hook-routing-smoke.mjs +29 -0
  95. package/scripts/inject-input.ps1 +204 -0
  96. package/scripts/io-complex-smoke.mjs +667 -0
  97. package/scripts/io-explore-bench.mjs +424 -0
  98. package/scripts/io-guardrails-smoke.mjs +205 -0
  99. package/scripts/io-mini-bench-baseline.json +11 -0
  100. package/scripts/io-mini-bench.mjs +216 -0
  101. package/scripts/io-route-harness.mjs +933 -0
  102. package/scripts/io-telemetry-report.mjs +691 -0
  103. package/scripts/mutation-bench.mjs +564 -0
  104. package/scripts/mutation-io-smoke.mjs +1081 -0
  105. package/scripts/native-patch-bridge-smoke.mjs +288 -0
  106. package/scripts/native-patch-smoke.mjs +304 -0
  107. package/scripts/patch-interior-context-smoke.mjs +49 -0
  108. package/scripts/patch-newline-utf8-smoke.mjs +157 -0
  109. package/scripts/perf-hook-smoke.mjs +71 -0
  110. package/scripts/permission-eval-smoke.mjs +426 -0
  111. package/scripts/prep-patch.mjs +53 -0
  112. package/scripts/prep-shim.mjs +96 -0
  113. package/scripts/provider-cache-smoke.mjs +687 -0
  114. package/scripts/report-runtime-health.mjs +132 -0
  115. package/scripts/run-mcp.mjs +1547 -0
  116. package/scripts/salvage-v4a-shatter.test.mjs +58 -0
  117. package/scripts/scoped-cache-io-smoke.mjs +103 -0
  118. package/scripts/shell-policy-round3-smoke.mjs +46 -0
  119. package/scripts/smoke-runtime-negative.ps1 +100 -0
  120. package/scripts/smoke-runtime-negative.sh +95 -0
  121. package/scripts/stall-policy-smoke.mjs +50 -0
  122. package/scripts/start-memory-worker.mjs +23 -0
  123. package/scripts/statusline-launcher-smoke.mjs +82 -0
  124. package/scripts/stress-atomic-write.mjs +1028 -0
  125. package/scripts/test-config-rmw-restore.mjs +122 -0
  126. package/scripts/test-fault-inject.mjs +164 -0
  127. package/scripts/test-large-file.mjs +174 -0
  128. package/scripts/tool-edge-smoke.mjs +209 -0
  129. package/scripts/uninstall.mjs +201 -0
  130. package/scripts/webhook-selfheal-smoke.mjs +29 -0
  131. package/scripts/write-overwrite-guard-smoke.mjs +56 -0
  132. package/server-main.mjs +3055 -0
  133. package/server.mjs +468 -0
  134. package/setup/config-merge.mjs +254 -0
  135. package/setup/install.mjs +120 -0
  136. package/setup/launch-core.mjs +507 -0
  137. package/setup/launch.mjs +101 -0
  138. package/setup/setup-server.mjs +3206 -0
  139. package/setup/setup.html +3693 -0
  140. package/skills/retro-skill-proposer/SKILL.md +92 -0
  141. package/skills/schedule-add/SKILL.md +77 -0
  142. package/skills/setup/SKILL.md +346 -0
  143. package/skills/webhook-add/SKILL.md +81 -0
  144. package/src/agent/bridge-stall-watchdog.mjs +337 -0
  145. package/src/agent/index.mjs +2138 -0
  146. package/src/agent/orchestrator/activity-bus.mjs +38 -0
  147. package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
  148. package/src/agent/orchestrator/bridge-retry.mjs +220 -0
  149. package/src/agent/orchestrator/bridge-trace.mjs +583 -0
  150. package/src/agent/orchestrator/cache-mtime.mjs +58 -0
  151. package/src/agent/orchestrator/config.mjs +358 -0
  152. package/src/agent/orchestrator/context/collect.mjs +651 -0
  153. package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
  154. package/src/agent/orchestrator/drain-registry.mjs +50 -0
  155. package/src/agent/orchestrator/explore-validator.mjs +8 -0
  156. package/src/agent/orchestrator/internal-roles.mjs +118 -0
  157. package/src/agent/orchestrator/internal-tools.mjs +88 -0
  158. package/src/agent/orchestrator/jobs.mjs +116 -0
  159. package/src/agent/orchestrator/mcp/client.mjs +364 -0
  160. package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
  161. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
  162. package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
  163. package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
  164. package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
  165. package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
  166. package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
  167. package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
  168. package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
  169. package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
  170. package/src/agent/orchestrator/providers/registry.mjs +192 -0
  171. package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
  172. package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
  173. package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
  174. package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
  175. package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
  176. package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
  177. package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
  178. package/src/agent/orchestrator/session/cache/util.mjs +49 -0
  179. package/src/agent/orchestrator/session/loop.mjs +1478 -0
  180. package/src/agent/orchestrator/session/manager.mjs +1975 -0
  181. package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
  182. package/src/agent/orchestrator/session/result-classification.mjs +65 -0
  183. package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
  184. package/src/agent/orchestrator/session/store.mjs +624 -0
  185. package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
  186. package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
  187. package/src/agent/orchestrator/session/trim.mjs +491 -0
  188. package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
  189. package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
  190. package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
  191. package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
  192. package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
  193. package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
  194. package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
  195. package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
  196. package/src/agent/orchestrator/stall-policy.mjs +195 -0
  197. package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
  198. package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
  199. package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
  200. package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
  201. package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
  202. package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
  203. package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
  204. package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
  205. package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
  206. package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
  207. package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
  208. package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
  209. package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
  210. package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
  211. package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
  212. package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
  213. package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
  214. package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
  215. package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
  216. package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
  217. package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
  218. package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
  219. package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
  220. package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
  221. package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
  222. package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
  223. package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
  224. package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
  225. package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
  226. package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
  227. package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
  228. package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
  229. package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
  230. package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
  231. package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
  232. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
  233. package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
  234. package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
  235. package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
  236. package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
  237. package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
  238. package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
  239. package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
  240. package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
  241. package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
  242. package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
  243. package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
  244. package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
  245. package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
  246. package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
  247. package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
  248. package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
  249. package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
  250. package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
  251. package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
  252. package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
  253. package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
  254. package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
  255. package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
  256. package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
  257. package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
  258. package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
  259. package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
  260. package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
  261. package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
  262. package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
  263. package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
  264. package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
  265. package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
  266. package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
  267. package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
  268. package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
  269. package/src/agent/orchestrator/tools/builtin.mjs +500 -0
  270. package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
  271. package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
  272. package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
  273. package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
  274. package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
  275. package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
  276. package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
  277. package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
  278. package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
  279. package/src/agent/orchestrator/tools/host-input.mjs +204 -0
  280. package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
  281. package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
  282. package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
  283. package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
  284. package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
  285. package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
  286. package/src/agent/orchestrator/tools/patch.mjs +2754 -0
  287. package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
  288. package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
  289. package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
  290. package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
  291. package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
  292. package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
  293. package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
  294. package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
  295. package/src/agent/orchestrator/workflow-store.mjs +93 -0
  296. package/src/agent/tool-defs.mjs +103 -0
  297. package/src/channels/backends/discord.mjs +784 -0
  298. package/src/channels/data/voice-runtime-manifest.json +138 -0
  299. package/src/channels/index.mjs +3229 -0
  300. package/src/channels/lib/cli-worker-host.mjs +12 -0
  301. package/src/channels/lib/config-lock.mjs +13 -0
  302. package/src/channels/lib/config.mjs +292 -0
  303. package/src/channels/lib/drop-trace.mjs +71 -0
  304. package/src/channels/lib/event-pipeline.mjs +81 -0
  305. package/src/channels/lib/event-queue.mjs +345 -0
  306. package/src/channels/lib/executor.mjs +168 -0
  307. package/src/channels/lib/format.mjs +188 -0
  308. package/src/channels/lib/holidays.mjs +138 -0
  309. package/src/channels/lib/hook-pipe-server.mjs +802 -0
  310. package/src/channels/lib/interaction-workflows.mjs +184 -0
  311. package/src/channels/lib/memory-client.mjs +149 -0
  312. package/src/channels/lib/output-forwarder.mjs +765 -0
  313. package/src/channels/lib/runtime-paths.mjs +479 -0
  314. package/src/channels/lib/scheduler.mjs +723 -0
  315. package/src/channels/lib/session-control.mjs +36 -0
  316. package/src/channels/lib/session-discovery.mjs +103 -0
  317. package/src/channels/lib/settings.mjs +11 -0
  318. package/src/channels/lib/state-file.mjs +68 -0
  319. package/src/channels/lib/status-snapshot.mjs +219 -0
  320. package/src/channels/lib/tool-format.mjs +140 -0
  321. package/src/channels/lib/transcript-discovery.mjs +195 -0
  322. package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
  323. package/src/channels/lib/webhook.mjs +1179 -0
  324. package/src/channels/lib/whisper-server.mjs +477 -0
  325. package/src/channels/tool-defs.mjs +170 -0
  326. package/src/daemon/host.mjs +118 -0
  327. package/src/daemon/mcp-transport.mjs +47 -0
  328. package/src/daemon/session.mjs +100 -0
  329. package/src/daemon/thin-client.mjs +71 -0
  330. package/src/daemon/transport.mjs +163 -0
  331. package/src/memory/data/runtime-manifest.json +40 -0
  332. package/src/memory/index.mjs +3305 -0
  333. package/src/memory/lib/agent-ipc.mjs +93 -0
  334. package/src/memory/lib/bridge-trace-queries.mjs +120 -0
  335. package/src/memory/lib/core-memory-store.mjs +330 -0
  336. package/src/memory/lib/embedding-provider.mjs +269 -0
  337. package/src/memory/lib/embedding-worker.mjs +323 -0
  338. package/src/memory/lib/llm-worker-host.mjs +17 -0
  339. package/src/memory/lib/memory-cycle.mjs +11 -0
  340. package/src/memory/lib/memory-cycle1.mjs +641 -0
  341. package/src/memory/lib/memory-cycle2.mjs +1284 -0
  342. package/src/memory/lib/memory-cycle3.mjs +540 -0
  343. package/src/memory/lib/memory-embed.mjs +299 -0
  344. package/src/memory/lib/memory-extraction.mjs +5 -0
  345. package/src/memory/lib/memory-maintenance-store.mjs +32 -0
  346. package/src/memory/lib/memory-ops-policy.mjs +190 -0
  347. package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
  348. package/src/memory/lib/memory-recall-read-query.mjs +7 -0
  349. package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
  350. package/src/memory/lib/memory-recall-store.mjs +621 -0
  351. package/src/memory/lib/memory-retrievers.mjs +112 -0
  352. package/src/memory/lib/memory-score.mjs +71 -0
  353. package/src/memory/lib/memory-text-utils.mjs +58 -0
  354. package/src/memory/lib/memory.mjs +412 -0
  355. package/src/memory/lib/model-profile.mjs +85 -0
  356. package/src/memory/lib/pg/adapter.mjs +308 -0
  357. package/src/memory/lib/pg/process.mjs +360 -0
  358. package/src/memory/lib/pg/supervisor.mjs +396 -0
  359. package/src/memory/lib/project-id-resolver.mjs +86 -0
  360. package/src/memory/lib/runtime-fetcher.mjs +442 -0
  361. package/src/memory/lib/trace-store.mjs +728 -0
  362. package/src/memory/tool-defs.mjs +79 -0
  363. package/src/search/index.mjs +1173 -0
  364. package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
  365. package/src/search/lib/backends/exa.mjs +50 -0
  366. package/src/search/lib/backends/firecrawl.mjs +61 -0
  367. package/src/search/lib/backends/gemini-api.mjs +83 -0
  368. package/src/search/lib/backends/grok-oauth.mjs +86 -0
  369. package/src/search/lib/backends/index.mjs +150 -0
  370. package/src/search/lib/backends/openai-api.mjs +144 -0
  371. package/src/search/lib/backends/openai-oauth.mjs +98 -0
  372. package/src/search/lib/backends/openai-web-search.mjs +76 -0
  373. package/src/search/lib/backends/tavily.mjs +55 -0
  374. package/src/search/lib/backends/xai-api.mjs +113 -0
  375. package/src/search/lib/cache.mjs +131 -0
  376. package/src/search/lib/config.mjs +192 -0
  377. package/src/search/lib/formatter.mjs +115 -0
  378. package/src/search/lib/provider-usage.mjs +67 -0
  379. package/src/search/lib/providers.mjs +47 -0
  380. package/src/search/lib/search-intent.mjs +109 -0
  381. package/src/search/lib/setup-handler.mjs +261 -0
  382. package/src/search/lib/state.mjs +201 -0
  383. package/src/search/lib/web-tools.mjs +1207 -0
  384. package/src/search/tool-defs.mjs +83 -0
  385. package/src/setup/defender-exclusion.mjs +183 -0
  386. package/src/shared/abort-controller.mjs +15 -0
  387. package/src/shared/atomic-file.mjs +420 -0
  388. package/src/shared/config.mjs +350 -0
  389. package/src/shared/daemon-recycle.mjs +108 -0
  390. package/src/shared/disable-claude-builtins.mjs +88 -0
  391. package/src/shared/err-text.mjs +12 -0
  392. package/src/shared/llm/cost.mjs +66 -0
  393. package/src/shared/llm/http-agent.mjs +123 -0
  394. package/src/shared/llm/index.mjs +41 -0
  395. package/src/shared/llm/pid-cleanup.mjs +27 -0
  396. package/src/shared/llm/usage-log.mjs +47 -0
  397. package/src/shared/plugin-paths.mjs +58 -0
  398. package/src/shared/schedules-store.mjs +70 -0
  399. package/src/shared/seed.mjs +119 -0
  400. package/src/shared/user-cwd.mjs +213 -0
  401. package/src/shared/user-data-guard.mjs +238 -0
  402. package/src/status/aggregator.mjs +584 -0
  403. package/src/status/server.mjs +413 -0
  404. package/tools.json +1653 -0
@@ -0,0 +1,112 @@
1
+ import { recallReadQuery } from './memory-recall-read-query.mjs'
2
+
3
+ const VALID_CATEGORIES_SET = new Set([
4
+ 'rule', 'constraint', 'decision', 'fact', 'goal', 'preference', 'task', 'issue',
5
+ ])
6
+ const VALID_STATUS_SET = new Set(['pending', 'active', 'archived'])
7
+
8
+ export async function retrieveEntries(db, filters = {}) {
9
+ const where = []
10
+ const params = []
11
+
12
+ // is_root filter (default: true)
13
+ const isRoot = filters.is_root === undefined ? true : Boolean(filters.is_root)
14
+ where.push(`is_root = $${params.length + 1}`)
15
+ params.push(isRoot ? 1 : 0)
16
+
17
+ if (filters.session_id != null) {
18
+ const sid = String(filters.session_id).trim()
19
+ if (sid) { where.push(`session_id = $${params.length + 1}`); params.push(sid) }
20
+ }
21
+
22
+ // projectScope filter: 'common' → project_id IS NULL only;
23
+ // specific slug → project_id IS NULL OR project_id = slug;
24
+ // 'all' or undefined → no filter (full pool).
25
+ if (filters.projectScope === 'common') {
26
+ where.push(`project_id IS NULL`)
27
+ } else if (typeof filters.projectScope === 'string' && filters.projectScope && filters.projectScope !== 'all') {
28
+ where.push(`(project_id IS NULL OR project_id = $${params.length + 1})`)
29
+ params.push(filters.projectScope)
30
+ }
31
+ // projectScope === 'all' or undefined → no filter
32
+
33
+ const tsFrom = Number(filters.ts_from)
34
+ if (Number.isFinite(tsFrom)) { where.push(`ts >= $${params.length + 1}`); params.push(tsFrom) }
35
+ const tsTo = Number(filters.ts_to)
36
+ if (Number.isFinite(tsTo)) { where.push(`ts <= $${params.length + 1}`); params.push(tsTo) }
37
+
38
+ if (filters.category != null) {
39
+ const cats = (Array.isArray(filters.category) ? filters.category : [filters.category])
40
+ .map(c => String(c).trim().toLowerCase())
41
+ .filter(c => VALID_CATEGORIES_SET.has(c))
42
+ if (cats.length > 0) {
43
+ const ph = cats.map((_, i) => `$${params.length + 1 + i}`).join(',')
44
+ where.push(`category IN (${ph})`)
45
+ params.push(...cats)
46
+ }
47
+ }
48
+
49
+ if (filters.status != null) {
50
+ const statusVal = String(filters.status).trim().toLowerCase()
51
+ if (VALID_STATUS_SET.has(statusVal)) {
52
+ where.push(`status = $${params.length + 1}`)
53
+ params.push(statusVal)
54
+ }
55
+ }
56
+
57
+ // R11 reviewer H2: exclude archived leakage in temporal augment paths.
58
+ if (Array.isArray(filters.excludeStatuses) && filters.excludeStatuses.length > 0) {
59
+ const exc = filters.excludeStatuses
60
+ .map(s => String(s).trim().toLowerCase())
61
+ .filter(s => VALID_STATUS_SET.has(s))
62
+ if (exc.length > 0) {
63
+ const ph = exc.map((_, i) => `$${params.length + 1 + i}`).join(',')
64
+ where.push(`(status IS NULL OR status NOT IN (${ph}))`)
65
+ params.push(...exc)
66
+ }
67
+ }
68
+
69
+ // R11 reviewer M3: orphan raw chunks (chunk_root IS NULL) for narrow-window
70
+ // raw merging — prevents classified-member chunks from duplicating their root.
71
+ if (filters.chunkRootNull === true) {
72
+ where.push(`chunk_root IS NULL`)
73
+ }
74
+
75
+ const limit = Math.max(1, Math.min(500, Number(filters.limit ?? 50)))
76
+ const offset = Math.max(0, Number(filters.offset ?? 0))
77
+ const sort = String(filters.sort ?? 'importance').trim().toLowerCase()
78
+ const orderBy = sort === 'date'
79
+ ? 'ts DESC, id DESC'
80
+ : 'score DESC NULLS LAST, ts DESC, id DESC'
81
+
82
+ params.push(limit, offset)
83
+ const sql = `SELECT id, ts, role, content, source_ref, session_id, source_turn,
84
+ chunk_root, is_root, element, category, summary, project_id,
85
+ status, score, last_seen_at
86
+ FROM entries
87
+ WHERE ${where.join(' AND ')}
88
+ ORDER BY ${orderBy}
89
+ LIMIT $${params.length - 1} OFFSET $${params.length}`
90
+
91
+ const rows = (await recallReadQuery(db, sql, params)).rows
92
+
93
+ if (filters.includeMembers && rows.length > 0) {
94
+ const rootIds = rows.map(r => r.id)
95
+ const memRes = (await recallReadQuery(
96
+ db,
97
+ `SELECT id, ts, role, content, session_id, source_turn, project_id, chunk_root
98
+ FROM entries WHERE chunk_root = ANY($1::bigint[]) AND is_root = 0
99
+ ORDER BY chunk_root, ts ASC, id ASC`,
100
+ [rootIds],
101
+ )).rows
102
+ const byRoot = new Map()
103
+ for (const m of memRes) {
104
+ const rid = Number(m.chunk_root)
105
+ if (!byRoot.has(rid)) byRoot.set(rid, [])
106
+ byRoot.get(rid).push({ id: m.id, ts: m.ts, role: m.role, content: m.content, session_id: m.session_id, source_turn: m.source_turn, project_id: m.project_id })
107
+ }
108
+ for (const r of rows) r.members = byRoot.get(Number(r.id)) || []
109
+ }
110
+
111
+ return rows
112
+ }
@@ -0,0 +1,71 @@
1
+ // Per-category grade (base score ceiling) and decay rate.
2
+ //
3
+ // grade = baseline ceiling for the category (durability of knowledge type).
4
+ // decay = how fast score drops with age. 0 means immune to age-decay
5
+ // (rules never decay), higher means faster drop.
6
+ //
7
+ // Pairing: rules/constraints have high grade + low/zero decay (long-term),
8
+ // issues/tasks have low grade + high decay (transient by nature).
9
+ //
10
+ // score = grade * 1 / (1 + ageDays*rate/30)^0.3
11
+ //
12
+ // At ageDays=0: score = grade.
13
+ // As ageDays → ∞: score → 0.
14
+ // rate=0 disables decay entirely → score stays at grade forever.
15
+ export const CATEGORY_GRADE = {
16
+ rule: 2.0,
17
+ constraint: 1.9,
18
+ decision: 1.8,
19
+ fact: 1.6,
20
+ goal: 1.5,
21
+ preference: 1.4,
22
+ task: 1.1,
23
+ issue: 1.0,
24
+ }
25
+
26
+ export const CATEGORY_DECAY = {
27
+ rule: 0.0,
28
+ constraint: 0.06,
29
+ decision: 0.15,
30
+ fact: 0.25,
31
+ goal: 0.30,
32
+ preference: 0.35,
33
+ task: 0.45,
34
+ issue: 0.50,
35
+ }
36
+
37
+ // Smooth exponential freshness factor — used by hybrid retrieval ranking
38
+ // (separate from computeEntryScore, which is the persisted column score).
39
+ // Returns value in [0.50, 1.60].
40
+ // ageH=0 → 1.60, ageH=6 → ~1.39, ageH=24 → ~1.08, ageH=72 → ~0.85,
41
+ // ageH=168 → ~0.68, ageH=720 → ~0.50.
42
+ export function freshnessFactor(ts, nowMs = Date.now()) {
43
+ const ts_ = Number(ts ?? 0)
44
+ if (!Number.isFinite(ts_) || ts_ <= 0) return 0.85
45
+ const ageH = Math.max(0, (nowMs - ts_) / 3_600_000)
46
+ const raw = 0.50 + 1.10 * Math.exp(-ageH / 55)
47
+ return Math.max(0.50, Math.min(1.60, raw))
48
+ }
49
+
50
+ /**
51
+ * Persisted entry score = grade * decay-curve(ageDays, category-specific rate).
52
+ *
53
+ * Returns null on unknown category or non-finite timestamps.
54
+ * rate=0 (rule) yields score = grade with no time component.
55
+ *
56
+ * @param {string} category
57
+ * @param {number|string} lastSeenAt — ms timestamp
58
+ * @param {number} nowMs
59
+ * @returns {number|null}
60
+ */
61
+ export function computeEntryScore(category, lastSeenAt, nowMs) {
62
+ const grade = CATEGORY_GRADE[String(category ?? '').toLowerCase()]
63
+ const rate = CATEGORY_DECAY[String(category ?? '').toLowerCase()]
64
+ if (grade == null || rate == null) return null
65
+ if (!Number.isFinite(Number(nowMs))) return null
66
+ const anchor = Number.isFinite(Number(lastSeenAt)) ? Number(lastSeenAt) : Number(nowMs)
67
+ const ageDays = Math.max(0, (Number(nowMs) - anchor) / 86_400_000)
68
+ const adjustedAge = ageDays * rate
69
+ const decay = 1 / Math.pow(1 + adjustedAge / 30, 0.3)
70
+ return Math.min(grade, grade * decay)
71
+ }
@@ -0,0 +1,58 @@
1
+ import {
2
+ cleanMemoryText,
3
+ } from './memory-extraction.mjs'
4
+
5
+ const MEMORY_TOKEN_STOPWORDS = new Set([
6
+ 'a', 'an', 'and', 'are', 'as', 'at', 'be', 'but', 'by', 'did', 'do', 'does', 'for', 'from',
7
+ 'how', 'i', 'if', 'in', 'is', 'it', 'me', 'my', 'of', 'on', 'or', 'our', 'so', 'that', 'the',
8
+ 'their', 'them', 'they', 'this', 'to', 'was', 'we', 'were', 'what', 'when', 'who', 'why', 'you',
9
+ 'your', 'unless', 'with',
10
+ 'user', 'assistant', 'requested', 'request', 'asked', 'ask', 'stated', 'state', 'reported', 'report',
11
+ 'mentioned', 'mention', 'clarified', 'clarify', 'explicitly', 'currently',
12
+ '사용자', '유저', '요청', '질문', '답변', '언급', '말씀', '설명', '보고', '무슨', '뭐야', '했지', 'user', 'asks', 'asked', 'request', 'requested', 'question', 'answer', 'reply', 'said', 'mentioned', 'explained', 'reported', 'what', 'huh',
13
+ ])
14
+
15
+ export function normalizeMemoryToken(token) {
16
+ let normalized = String(token ?? '').trim().toLowerCase()
17
+ if (!normalized) return ''
18
+
19
+ // Korean suffix stripping: basic particles + compound endings
20
+ if (/[\uAC00-\uD7AF]/.test(normalized) && normalized.length > 2) {
21
+ const stripped = normalized
22
+ .replace(/(했었지|했더라|됐었나|됐던가|했는지|였는지|인건가|하려면|에서는|이라서|였더라|에서도|이었지|으로도|거였지|한건지|이었나)$/u, '')
23
+ .replace(/(했던|했지|됐던|됐지|하게|되던|이라|에서|으로|하는|없는|있는|었던|하자|않게|할때|인지|인데|인건|이고|보다|처럼|까지|부터|마다|밖에|없이)$/u, '')
24
+ .replace(/(은|는|이|가|을|를|랑|과|와|도|에|의|로|만|며|나|고|서|자|요)$/u, '')
25
+ if (stripped.length >= 2) normalized = stripped
26
+ }
27
+
28
+ if (/^[a-z][a-z0-9_-]+$/i.test(normalized)) {
29
+ if (normalized.length > 5 && normalized.endsWith('ing')) normalized = normalized.slice(0, -3)
30
+ else if (normalized.length > 4 && normalized.endsWith('ed')) normalized = normalized.slice(0, -2)
31
+ else if (normalized.length > 4 && normalized.endsWith('es')) normalized = normalized.slice(0, -2)
32
+ else if (normalized.length > 3 && normalized.endsWith('s')) normalized = normalized.slice(0, -1)
33
+ }
34
+
35
+ return normalized
36
+ }
37
+
38
+ export function tokenizeMemoryText(text) {
39
+ return cleanMemoryText(text)
40
+ .toLowerCase()
41
+ .split(/[^\p{L}\p{N}_]+/u)
42
+ .map(token => normalizeMemoryToken(token))
43
+ .filter(token => token.length >= 2)
44
+ .filter(token => !MEMORY_TOKEN_STOPWORDS.has(token))
45
+ .slice(0, 24)
46
+ }
47
+
48
+ export function buildFtsQuery(text) {
49
+ const tokens = tokenizeMemoryText(text)
50
+ if (tokens.length === 0) return ''
51
+ // Include 2-char Korean tokens (they carry meaning unlike 2-char English)
52
+ const ftsTokens = [...new Set(tokens)].filter(t => t.length >= 3 || (t.length === 2 && /[\uAC00-\uD7AF]/.test(t)))
53
+ if (ftsTokens.length === 0) return ''
54
+ // websearch_to_tsquery handles tokenization + OR/AND/quoting itself; pass plain tokens space-joined.
55
+ return ftsTokens.map(t => t.replace(/["']/g, '')).filter(t => t.length > 0).join(' ')
56
+ }
57
+
58
+
@@ -0,0 +1,412 @@
1
+ // Native-PG-backed memory store. Schema, helpers, and lifecycle.
2
+
3
+ import { ensurePgInstance, closePgInstance, withSchemaBootstrapLock } from './pg/adapter.mjs'
4
+ import { mkdirSync } from 'fs'
5
+ import { resolve } from 'path'
6
+ import { cleanMemoryText } from './memory-extraction.mjs'
7
+
8
+ const dbs = new Map()
9
+ const opening = new Map()
10
+
11
+ export { cleanMemoryText }
12
+
13
+ export const VALID_CATEGORY = new Set([
14
+ 'rule', 'constraint', 'decision', 'fact', 'goal', 'preference', 'task', 'issue',
15
+ ])
16
+
17
+ export async function init(db, dims) {
18
+ const dimCount = Number(dims)
19
+ if (!Number.isInteger(dimCount) || dimCount <= 0) {
20
+ throw new Error(`init: dims must be a positive integer, got ${dims}`)
21
+ }
22
+
23
+ // Extensions are created once by pg-adapter.bootstrapInstance; skip here.
24
+
25
+ // Status as a real ENUM type — DB-level enforcement, B-tree friendly.
26
+ // PG has no CREATE TYPE IF NOT EXISTS; guard via pg_type lookup so a partial
27
+ // bootstrap (crash after CREATE TYPE but before boot.schema_bootstrap_complete)
28
+ // can re-run init() on the next boot without colliding on the existing type.
29
+ await db.exec(`
30
+ DO $$
31
+ BEGIN
32
+ IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'entry_status') THEN
33
+ CREATE TYPE entry_status AS ENUM ('pending', 'active', 'archived');
34
+ END IF;
35
+ END
36
+ $$
37
+ `)
38
+
39
+ // Per-category score parameters (lookup table for the score function).
40
+ await db.exec(`
41
+ CREATE TABLE IF NOT EXISTS category_score_params (
42
+ category TEXT PRIMARY KEY,
43
+ grade REAL NOT NULL,
44
+ decay REAL NOT NULL
45
+ )
46
+ `)
47
+ await db.query(`
48
+ INSERT INTO category_score_params(category, grade, decay) VALUES
49
+ ('rule', 2.0, 0.0),
50
+ ('constraint', 1.9, 0.06),
51
+ ('decision', 1.8, 0.15),
52
+ ('fact', 1.6, 0.25),
53
+ ('goal', 1.5, 0.30),
54
+ ('preference', 1.4, 0.35),
55
+ ('task', 1.1, 0.45),
56
+ ('issue', 1.0, 0.50)
57
+ ON CONFLICT (category) DO NOTHING
58
+ `)
59
+
60
+ // SQL function mirrors src/memory/lib/memory-score.mjs computeEntryScore.
61
+ // STABLE (not IMMUTABLE) because the function reads category_score_params.
62
+ // IMMUTABLE would let the planner cache results across rows where params
63
+ // could legitimately differ if the table is updated.
64
+ await db.exec(`
65
+ CREATE OR REPLACE FUNCTION compute_entry_score(
66
+ category_p TEXT,
67
+ last_seen_at_p BIGINT,
68
+ now_ms_p BIGINT
69
+ ) RETURNS REAL LANGUAGE sql STABLE AS $$
70
+ SELECT CASE
71
+ WHEN p.grade IS NULL OR last_seen_at_p IS NULL OR now_ms_p IS NULL THEN NULL::REAL
72
+ WHEN p.decay = 0 THEN p.grade
73
+ ELSE LEAST(
74
+ p.grade,
75
+ p.grade / POWER(
76
+ 1 + (GREATEST(0, (now_ms_p - last_seen_at_p)) / 86400000.0) * p.decay / 30,
77
+ 0.3
78
+ )
79
+ )::REAL
80
+ END
81
+ FROM category_score_params p
82
+ WHERE p.category = category_p
83
+ $$
84
+ `)
85
+
86
+ await db.exec(`
87
+ CREATE TABLE IF NOT EXISTS entries (
88
+ id BIGSERIAL PRIMARY KEY,
89
+ ts BIGINT NOT NULL,
90
+ role TEXT NOT NULL,
91
+ content TEXT NOT NULL,
92
+ source_ref TEXT NOT NULL UNIQUE,
93
+ session_id TEXT,
94
+ project_id TEXT,
95
+ source_turn INTEGER,
96
+ chunk_root BIGINT REFERENCES entries(id) ON DELETE SET NULL,
97
+ is_root SMALLINT NOT NULL DEFAULT 0,
98
+ element TEXT,
99
+ category TEXT,
100
+ summary TEXT,
101
+ core_summary TEXT,
102
+ status entry_status,
103
+ score REAL,
104
+ last_seen_at BIGINT,
105
+ reviewed_at BIGINT,
106
+ promoted_at BIGINT,
107
+ error_count INTEGER NOT NULL DEFAULT 0,
108
+ embedding halfvec(${dimCount}),
109
+ summary_hash TEXT,
110
+ search_tsv tsvector GENERATED ALWAYS AS (
111
+ setweight(to_tsvector('simple', coalesce(element, '')), 'A') ||
112
+ setweight(to_tsvector('simple', coalesce(summary, '')), 'B') ||
113
+ setweight(to_tsvector('simple', coalesce(content, '')), 'C') ||
114
+ setweight(to_tsvector('english', coalesce(element, '')), 'A') ||
115
+ setweight(to_tsvector('english', coalesce(summary, '')), 'B') ||
116
+ setweight(to_tsvector('english', coalesce(content, '')), 'C')
117
+ ) STORED
118
+ )
119
+ `)
120
+
121
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_chunk_root ON entries(chunk_root) WHERE chunk_root IS NOT NULL`)
122
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_ts_desc ON entries(ts DESC)`)
123
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_session_ts ON entries(session_id, ts DESC) WHERE session_id IS NOT NULL`)
124
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_root_status_score ON entries(status, score DESC) WHERE is_root = 1`)
125
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_root_category ON entries(category, status) WHERE is_root = 1`)
126
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_pending ON entries(ts DESC, id DESC) WHERE chunk_root IS NULL AND session_id IS NOT NULL`)
127
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_roots_active ON entries(status, last_seen_at ASC, score DESC) WHERE is_root = 1 AND status = 'active'`)
128
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_project ON entries(project_id) WHERE project_id IS NOT NULL`)
129
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_reviewed_at ON entries(reviewed_at ASC) WHERE is_root = 1`)
130
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_phase_sweep ON entries(status, is_root, error_count, reviewed_at, id)`)
131
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_promoted_at ON entries(promoted_at) WHERE promoted_at IS NOT NULL`)
132
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_tsv ON entries USING GIN (search_tsv)`)
133
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_content_trgm ON entries USING GIN (content gin_trgm_ops) WHERE is_root = 1`)
134
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_element_trgm ON entries USING GIN (element gin_trgm_ops) WHERE is_root = 1 AND element IS NOT NULL`)
135
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_embedding_hnsw ON entries USING hnsw (embedding halfvec_cosine_ops) WHERE is_root = 1 AND embedding IS NOT NULL`)
136
+
137
+ // BEFORE INSERT/UPDATE trigger keeps score in sync with category + last_seen_at
138
+ // automatically; cycle code no longer needs to UPDATE entries SET score = ...
139
+ await db.exec(`
140
+ CREATE OR REPLACE FUNCTION trg_entry_score_recalc() RETURNS trigger LANGUAGE plpgsql AS $$
141
+ BEGIN
142
+ IF NEW.is_root = 1 AND NEW.category IS NOT NULL THEN
143
+ -- NOW()-to-ms conversion is intentional schema-level work; the
144
+ -- "no EXTRACT(EPOCH …)" rule applies to ms-stored BIGINT timestamp
145
+ -- COLUMNS, not to the trigger reading the current wall clock.
146
+ NEW.score := compute_entry_score(
147
+ NEW.category,
148
+ COALESCE(NEW.last_seen_at, NEW.ts),
149
+ (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT
150
+ );
151
+ END IF;
152
+ RETURN NEW;
153
+ END;
154
+ $$
155
+ `)
156
+ await db.exec(`DROP TRIGGER IF EXISTS trg_entries_score ON entries`)
157
+ await db.exec(`
158
+ CREATE TRIGGER trg_entries_score
159
+ BEFORE INSERT OR UPDATE OF category, last_seen_at, promoted_at, is_root ON entries
160
+ FOR EACH ROW
161
+ EXECUTE FUNCTION trg_entry_score_recalc()
162
+ `)
163
+
164
+ await db.exec(`
165
+ CREATE OR REPLACE FUNCTION trg_entry_embedding_invalidate() RETURNS trigger LANGUAGE plpgsql AS $$
166
+ BEGIN
167
+ IF NEW.is_root = 1 AND (
168
+ NEW.content IS DISTINCT FROM OLD.content OR
169
+ NEW.summary IS DISTINCT FROM OLD.summary OR
170
+ NEW.element IS DISTINCT FROM OLD.element
171
+ ) THEN
172
+ NEW.embedding := NULL;
173
+ NEW.summary_hash := NULL;
174
+ END IF;
175
+ RETURN NEW;
176
+ END;
177
+ $$
178
+ `)
179
+ await db.exec(`DROP TRIGGER IF EXISTS trg_entries_embedding_invalidate ON entries`)
180
+ await db.exec(`
181
+ CREATE TRIGGER trg_entries_embedding_invalidate
182
+ BEFORE UPDATE OF content, summary, element ON entries
183
+ FOR EACH ROW EXECUTE FUNCTION trg_entry_embedding_invalidate()
184
+ `)
185
+
186
+ await db.exec(`
187
+ CREATE TABLE IF NOT EXISTS core_entries (
188
+ id BIGSERIAL PRIMARY KEY,
189
+ element TEXT NOT NULL,
190
+ summary TEXT NOT NULL,
191
+ category TEXT NOT NULL,
192
+ project_id TEXT,
193
+ embedding halfvec(${dimCount}),
194
+ created_at BIGINT NOT NULL,
195
+ updated_at BIGINT NOT NULL
196
+ )
197
+ `)
198
+ await db.exec(`CREATE INDEX IF NOT EXISTS core_entries_project_idx ON core_entries(project_id)`)
199
+ await db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS core_entries_unique_proj_elem ON core_entries (project_id, element) NULLS NOT DISTINCT`)
200
+ await db.exec(`CREATE INDEX IF NOT EXISTS core_entries_embedding_hnsw ON core_entries USING hnsw (embedding halfvec_cosine_ops) WHERE embedding IS NOT NULL`)
201
+
202
+ await db.exec(`
203
+ CREATE TABLE IF NOT EXISTS meta (
204
+ key TEXT PRIMARY KEY,
205
+ value JSONB NOT NULL
206
+ )
207
+ `)
208
+
209
+ // Operational view — used by /health and dashboards. One round-trip,
210
+ // covers the metrics that previously needed 6+ COUNT queries.
211
+ await db.exec(`
212
+ CREATE OR REPLACE VIEW v_cycle_state AS
213
+ SELECT
214
+ COUNT(*) FILTER (WHERE is_root = 1) AS roots,
215
+ COUNT(*) FILTER (WHERE is_root = 1 AND status = 'pending') AS pending,
216
+ COUNT(*) FILTER (WHERE is_root = 1 AND status = 'active') AS active,
217
+ COUNT(*) FILTER (WHERE is_root = 1 AND status = 'archived') AS archived,
218
+ COUNT(*) FILTER (WHERE chunk_root IS NULL) AS unclassified,
219
+ COUNT(*) AS total
220
+ FROM entries
221
+ `)
222
+
223
+ // Hot active set — recall hot path uses the materialized copy. Refresh hook
224
+ // is owned by cycle2 (after promotion/archival). Created WITH NO DATA so
225
+ // bootstrap is fast; first refresh happens on the first cycle2 run.
226
+ await db.exec(`
227
+ CREATE MATERIALIZED VIEW IF NOT EXISTS mv_hot_active AS
228
+ SELECT id, element, summary, category, status, score, last_seen_at, promoted_at,
229
+ project_id, embedding, search_tsv
230
+ FROM entries
231
+ WHERE is_root = 1 AND status = 'active' AND embedding IS NOT NULL
232
+ WITH NO DATA
233
+ `)
234
+ await db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS mv_hot_active_id ON mv_hot_active(id)`)
235
+ await db.exec(`CREATE INDEX IF NOT EXISTS mv_hot_active_hnsw ON mv_hot_active USING hnsw (embedding halfvec_cosine_ops)`)
236
+ await db.exec(`CREATE INDEX IF NOT EXISTS mv_hot_active_tsv ON mv_hot_active USING GIN (search_tsv)`)
237
+ await db.exec(`CREATE INDEX IF NOT EXISTS mv_hot_active_score ON mv_hot_active(score DESC)`)
238
+
239
+ await db.query(
240
+ `INSERT INTO meta(key, value) VALUES ($1, $2::jsonb)
241
+ ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
242
+ ['embedding.current_dims', JSON.stringify(dimCount)],
243
+ )
244
+ await db.query(
245
+ `INSERT INTO meta(key, value) VALUES ($1, $2::jsonb)
246
+ ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
247
+ ['boot.schema_bootstrap_complete', JSON.stringify('1')],
248
+ )
249
+ }
250
+
251
+ // Validate that the halfvec column dimension stored in the DB matches
252
+ // dimCount from the current model config. Call after schema is confirmed
253
+ // complete and before any embedding operations.
254
+ export async function validateEmbeddingDims(db, dimCount) {
255
+ const r = await db.query(`
256
+ SELECT atttypmod
257
+ FROM pg_attribute a
258
+ JOIN pg_class c ON c.oid = a.attrelid
259
+ WHERE c.relname = 'entries'
260
+ AND a.attname = 'embedding'
261
+ AND a.attnum > 0
262
+ AND NOT a.attisdropped
263
+ `)
264
+ const row = r.rows[0]
265
+ if (!row) return // column absent — pre-schema DB; bootstrapSchema will handle
266
+ // pgvector halfvec stores dimension as atttypmod directly (unlike varchar which uses dims+4).
267
+ const colDims = row.atttypmod
268
+ if (colDims !== dimCount) {
269
+ throw new Error(
270
+ `Embedding dimension mismatch: DB column halfvec(${colDims}) vs model config ${dimCount} dims. ` +
271
+ `Reconfigure the embedding model or rebuild the memory store before booting.`
272
+ )
273
+ }
274
+ }
275
+
276
+ export async function ensureCurrentSchemaExtensions(db, dims) {
277
+ // core_entries gained an embedding column for cross-table semantic dedup
278
+ // between user-curated rows and cycle2-promoted entries. ALTER + index are
279
+ // idempotent and define the current runtime schema.
280
+ if (Number.isInteger(dims) && dims > 0) {
281
+ await db.exec(`ALTER TABLE core_entries ADD COLUMN IF NOT EXISTS embedding halfvec(${dims})`)
282
+ await db.exec(`CREATE INDEX IF NOT EXISTS core_entries_embedding_hnsw ON core_entries USING hnsw (embedding halfvec_cosine_ops) WHERE embedding IS NOT NULL`)
283
+ }
284
+ await db.exec(`ALTER TABLE entries ADD COLUMN IF NOT EXISTS core_summary text`)
285
+
286
+
287
+
288
+ // Dedupe core_entries before creating the unique index — keeps the row with
289
+ // the most recent updated_at (id breaks ties), drops the rest.
290
+ const dedupe = await db.query(`
291
+ WITH ranked AS (
292
+ SELECT id,
293
+ row_number() OVER (
294
+ PARTITION BY project_id, element
295
+ ORDER BY updated_at DESC NULLS LAST, id DESC
296
+ ) AS rn
297
+ FROM core_entries
298
+ ), deleted AS (
299
+ DELETE FROM core_entries c
300
+ USING ranked r
301
+ WHERE c.id = r.id AND r.rn > 1
302
+ RETURNING c.id
303
+ )
304
+ SELECT count(*)::int AS n FROM deleted
305
+ `)
306
+ const deduped = Number(dedupe.rows?.[0]?.n ?? 0)
307
+ if (deduped > 0) {
308
+ process.stderr.write(`[memory] ensureCurrentSchemaExtensions: removed ${deduped} duplicate core_entries before unique index creation\n`)
309
+ }
310
+ try {
311
+ await db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS core_entries_unique_proj_elem ON core_entries (project_id, element) NULLS NOT DISTINCT`)
312
+ } catch (err) {
313
+ process.stderr.write(`[memory] ensureCurrentSchemaExtensions: core_entries_unique_proj_elem creation failed — duplicate rows must be deduplicated before this index can be created: ${err?.message || err}\n`)
314
+ throw err
315
+ }
316
+ }
317
+
318
+ export async function openDatabase(dataDir, dims) {
319
+ const key = resolve(dataDir)
320
+
321
+ // Fast path — already resolved.
322
+ if (dbs.get(key)) return dbs.get(key)
323
+
324
+ // Dedupe concurrent callers — return the in-flight Promise if one exists.
325
+ if (opening.has(key)) return opening.get(key)
326
+
327
+ const promise = (async () => {
328
+ mkdirSync(key, { recursive: true })
329
+
330
+ const { db, pool } = await ensurePgInstance(dataDir, { schema: 'memory' })
331
+
332
+ if (!(await isBootstrapComplete(db))) {
333
+ // Serialize the schema/CREATE TYPE bootstrap across concurrent first-boot
334
+ // processes with a cluster-global advisory lock. Re-check completion once
335
+ // the lock is held (double-checked locking) so a worker that lost the
336
+ // race skips the redundant DDL instead of re-running init().
337
+ await withSchemaBootstrapLock(pool, async () => {
338
+ if (!(await isBootstrapComplete(db))) {
339
+ await init(db, dims)
340
+ }
341
+ })
342
+ }
343
+ await ensureCurrentSchemaExtensions(db, Number(dims))
344
+ await validateEmbeddingDims(db, Number(dims))
345
+
346
+ dbs.set(key, db)
347
+ return db
348
+ })()
349
+
350
+ opening.set(key, promise)
351
+ try {
352
+ return await promise
353
+ } finally {
354
+ opening.delete(key)
355
+ }
356
+ }
357
+
358
+ export function getDatabase(dataDir) {
359
+ if (!dataDir) return null
360
+ const key = resolve(dataDir)
361
+ return dbs.get(key) ?? null
362
+ }
363
+
364
+ export async function closeDatabase(dataDir) {
365
+ const key = resolve(dataDir)
366
+ const db = dbs.get(key)
367
+ if (!db) return
368
+ try { await db.close() } catch {}
369
+ dbs.delete(key)
370
+ // Evict pg-adapter's instance cache too: db.close() ends the pool, but the
371
+ // adapter still holds `instances.get(key)` pointing at the ended pool. A
372
+ // same-process reopen would then return the dead handle. closePgInstance
373
+ // drops the cache entry (and re-ends the pool, which is a safe no-op on
374
+ // an already-ended pool) so the next ensurePgInstance rebuilds fresh.
375
+ try { await closePgInstance(dataDir, { schema: 'memory' }) } catch {}
376
+ }
377
+
378
+ export async function isBootstrapComplete(db) {
379
+ try {
380
+ const r = await db.query(`SELECT 1 FROM meta WHERE key = 'boot.schema_bootstrap_complete'`)
381
+ return r.rows.length > 0
382
+ } catch {
383
+ return false
384
+ }
385
+ }
386
+
387
+ // Returns the raw JSON-encoded string stored in meta.value. Callers JSON.parse
388
+ // it themselves; preserves API parity with the prior TEXT column.
389
+ export async function getMetaValue(db, key, fallback = null) {
390
+ try {
391
+ const r = await db.query(`SELECT value::text AS v FROM meta WHERE key = $1`, [key])
392
+ if (r.rows.length === 0) return fallback
393
+ return r.rows[0].v ?? fallback
394
+ } catch {
395
+ return fallback
396
+ }
397
+ }
398
+
399
+ // Caller passes a JSON-encoded string (e.g. JSON.stringify(obj) or a quoted
400
+ // scalar like '"v1"'). Stored verbatim into the JSONB column.
401
+ export async function setMetaValue(db, key, value) {
402
+ await db.query(
403
+ `INSERT INTO meta(key, value) VALUES ($1, $2::jsonb)
404
+ ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
405
+ [key, value == null ? 'null' : String(value)],
406
+ )
407
+ }
408
+
409
+ export function embeddingToSql(arr) {
410
+ if (!arr || !Array.isArray(arr)) return null
411
+ return `[${arr.map((n) => Number(n).toFixed(6)).join(',')}]`
412
+ }