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,621 @@
1
+ import { buildFtsQuery } from './memory-text-utils.mjs'
2
+ import { VALID_CATEGORY, embeddingToSql } from './memory.mjs'
3
+ import { freshnessFactor } from './memory-score.mjs'
4
+ import { buildRecallScopeFilter } from './memory-recall-scope-filter.mjs'
5
+ import { recallReadQuery } from './memory-recall-read-query.mjs'
6
+
7
+ // Per-db cache of mv_hot_active populated state. The main recall path currently
8
+ // uses entries directly; this guard remains for explicit useHotActive callers.
9
+ const _MV_HOT_ACTIVE_TTL_MS = 60_000
10
+ const _mvHotActiveCache = new WeakMap() // db → { populated: boolean, ts: number }
11
+ const SEMANTIC_ONLY_MIN_SIM = 0.72
12
+ const SHORT_QUERY_TOKEN_MAX = 2
13
+
14
+ function buildExactTerms(query) {
15
+ const clean = String(query ?? '').replace(/\s+/g, ' ').trim()
16
+ if (!clean) return []
17
+ const terms = []
18
+ const add = (value) => {
19
+ const term = String(value ?? '').trim()
20
+ .replace(/^[^\p{L}\p{N}_./:-]+|[^\p{L}\p{N}_./:-]+$/gu, '')
21
+ if (!term) return
22
+ const hasIdentifierShape = /[_./:-]/.test(term)
23
+ if (!hasIdentifierShape && /^[\d\s]+$/.test(term)) return
24
+ const symbolCount = Array.from(term).length
25
+ if (!hasIdentifierShape && symbolCount < 2) return
26
+ terms.push(term.slice(0, 80))
27
+ }
28
+ if (clean.length <= 80) add(clean)
29
+ const tokens = clean.match(/[\p{L}\p{N}_./:-]+/gu) || []
30
+ for (const token of tokens) add(token)
31
+ for (let i = 0; i < tokens.length - 1; i++) {
32
+ add(`${tokens[i]} ${tokens[i + 1]}`)
33
+ }
34
+ return [...new Set(terms.map(t => t.toLowerCase()))].slice(0, 12)
35
+ }
36
+
37
+ function countQueryTokens(query) {
38
+ const clean = String(query ?? '').replace(/\s+/g, ' ').trim()
39
+ if (!clean) return 0
40
+ return (clean.match(/[\p{L}\p{N}_./:-]+/gu) || [])
41
+ .filter(token => String(token ?? '').trim().length > 0)
42
+ .length
43
+ }
44
+
45
+ function hasFullQueryTextMatch(query, row) {
46
+ const clean = String(query ?? '').replace(/\s+/g, ' ').trim().toLowerCase()
47
+ if (!clean || clean.length > 160) return false
48
+ const element = String(row?.element ?? '').toLowerCase()
49
+ const summary = String(row?.summary ?? '').toLowerCase()
50
+ const content = String(row?.content ?? '').toLowerCase()
51
+ return (element && element.includes(clean))
52
+ || (summary && summary.includes(clean))
53
+ || (content && content.includes(clean))
54
+ }
55
+
56
+ function hasQueryTokenCoverage(row, queryTokenCount) {
57
+ if (queryTokenCount <= SHORT_QUERY_TOKEN_MAX) return true
58
+ const hits = Number(row?.exact_hits)
59
+ return Number.isFinite(hits) && hits >= queryTokenCount
60
+ }
61
+
62
+ function exactTextBoost(query, row, exactHits) {
63
+ const clean = String(query ?? '').trim().toLowerCase()
64
+ if (!clean) return 0
65
+ const element = String(row?.element ?? '').toLowerCase()
66
+ const summary = String(row?.summary ?? '').toLowerCase()
67
+ const content = String(row?.content ?? '').toLowerCase()
68
+ let boost = 0
69
+ if (element && element.includes(clean)) boost += 0.035
70
+ if (summary && summary.includes(clean)) boost += 0.025
71
+ if (content && content.includes(clean)) boost += 0.015
72
+ const hits = Number(exactHits)
73
+ if (Number.isFinite(hits) && hits > 0) boost += Math.min(0.03, hits * 0.008)
74
+ return boost
75
+ }
76
+
77
+ async function _checkMvHotActivePopulated(db) {
78
+ const cached = _mvHotActiveCache.get(db)
79
+ const now = Date.now()
80
+ if (cached && now - cached.ts < _MV_HOT_ACTIVE_TTL_MS) return cached.populated
81
+ const r = await recallReadQuery(
82
+ db,
83
+ `SELECT relispopulated FROM pg_class WHERE relname = 'mv_hot_active' LIMIT 1`,
84
+ )
85
+ if (!r.rows?.length) throw new Error('mv_hot_active not found in pg_class')
86
+ const populated = Boolean(r.rows[0].relispopulated)
87
+ _mvHotActiveCache.set(db, { populated, ts: now })
88
+ return populated
89
+ }
90
+
91
+ export async function searchRelevantHybrid(db, query, options = {}) {
92
+ const clean = String(query ?? '').trim()
93
+ if (!clean) return []
94
+ // Numeric-only lookup is too broad for text recall ("1" matches nearly
95
+ // everything through the short ILIKE path). Callers that know an entry id
96
+ // should use recall's `id` mode instead of query search.
97
+ if (/^\d+$/.test(clean)) return []
98
+
99
+ const limit = Math.max(1, Math.floor(Number(options?.limit ?? 8)))
100
+ const candidateWindow = Math.max(40, limit * 8)
101
+ const includeMembers = Boolean(options.includeMembers)
102
+ const writeBackMemberHits = options.writeBackMemberHits !== false
103
+ // Pre-filter knobs. Without them, FTS/vec rank the whole tree and a
104
+ // post-filter time window can wipe the result set.
105
+ const tsFrom = Number.isFinite(Number(options.ts_from)) ? Number(options.ts_from) : null
106
+ const tsTo = Number.isFinite(Number(options.ts_to)) ? Number(options.ts_to) : null
107
+ // Default = empty exclusion. The archive bucket holds the bulk of historical
108
+ // work (active is reserved for permanent invariants in this design; the
109
+ // last-week / last-month / "what did I work on previously" recall pattern
110
+ // depends on archived rows being in the pool). Cycle2 internal sweeps
111
+ // that genuinely want active-only data must pass excludeStatuses
112
+ // explicitly.
113
+ const excludeStatuses = Array.isArray(options.excludeStatuses)
114
+ ? options.excludeStatuses.filter(s => typeof s === 'string' && s.trim()).map(s => s.trim().toLowerCase())
115
+ : []
116
+ // Project scope pre-filter applied to the candidate fetch SQL.
117
+ // 'common' → project_id IS NULL; specific slug → project_id IS NULL OR = slug;
118
+ // 'all' or undefined → no filter.
119
+ const projectScope = typeof options.projectScope === 'string' ? options.projectScope : null
120
+ const categories = (Array.isArray(options.category) ? options.category : [options.category])
121
+ .map(c => String(c ?? '').trim().toLowerCase())
122
+ .filter(c => VALID_CATEGORY.has(c))
123
+ // Caller can disable freshness decay when the period is calendar-bounded
124
+ // (yesterday/today/this_week/last_week/specific date). Inside a fixed
125
+ // window, absolute-age decay misranks early-week vs late-week entries.
126
+ const applyFreshness = options.applyFreshness !== false
127
+
128
+ // ── mv_hot_active fast-path opt-in ──────────────────────────────────────
129
+ // When useHotActive:true, the dense and sparse CTE legs query mv_hot_active
130
+ // instead of the full entries table.
131
+ //
132
+ // WHEN TO USE:
133
+ // - Explicit active-only recall (no archived inclusion, no ts_from/ts_to
134
+ // window). The history-first default recall path should keep useHotActive
135
+ // false so archived roots and fresh pending work remain eligible.
136
+ // - mv_hot_active holds only active roots with embeddings. Its dedicated
137
+ // HNSW (mv_hot_active_hnsw) and GIN (mv_hot_active_tsv) indexes are smaller
138
+ // than the partial indexes on entries, so ANN and FTS scans are faster.
139
+ // - Caller must ensure cycle2 has run at least once. The MV is created WITH NO
140
+ // DATA; a never-refreshed MV silently returns 0 rows — primary risk on fresh
141
+ // deployments.
142
+ //
143
+ // WHEN NOT TO USE:
144
+ // - ts_from / ts_to active: MV lacks the ts column; the filter clause would
145
+ // reference a non-existent column and the query would error.
146
+ // - Archived entries must be included: MV only holds active rows.
147
+ // - trgm is the primary signal: MV lacks content and ts, so the trgm leg
148
+ // always routes to entries regardless of useHotActive.
149
+ //
150
+ // COLUMN GAPS (resolved per CTE leg):
151
+ // ts : missing → trgm short-query ORDER BY ts DESC impossible on MV;
152
+ // also makes ts_from/ts_to filter clauses invalid.
153
+ // content : missing → trgm similarity/ILIKE impossible on MV.
154
+ // Both gaps are intentional; trgm is unconditionally routed to entries.
155
+ //
156
+ // The combined/JOIN fetch after the CTE always queries entries by id, so the
157
+ // final row shape is identical regardless of which path was taken.
158
+ const hasTsFilter = tsFrom != null || tsTo != null
159
+ const hasArchivedInclusion = !excludeStatuses.includes('archived')
160
+ let useHotActive = Boolean(options.useHotActive)
161
+ && !hasTsFilter
162
+ && !hasArchivedInclusion
163
+ // Guard against unrefreshed mv_hot_active (created WITH NO DATA → SQLSTATE
164
+ // 55000 on read). Cheap pg_class check, cached 60 s per db handle to avoid
165
+ // per-recall round-trip cost.
166
+ if (useHotActive) {
167
+ const populated = await _checkMvHotActivePopulated(db)
168
+ if (!populated) useHotActive = false
169
+ }
170
+
171
+ // buildFilterClause: pushes ts/status/scope filters INTO candidate SELECTs.
172
+ // offset = 1-based index of the first bind param it may consume.
173
+ // Returns { clause: string, params: any[] }; clause begins with AND or is ''.
174
+ function buildFilterClause(offset) {
175
+ return buildRecallScopeFilter(offset, {
176
+ ts_from: tsFrom,
177
+ ts_to: tsTo,
178
+ excludeStatuses,
179
+ category: categories,
180
+ projectScope,
181
+ })
182
+ }
183
+
184
+ // Kept for the non-candidate root-lookup inside the member-hit resolution path.
185
+ function buildScopeClause(offset) {
186
+ if (projectScope === 'common') {
187
+ return { clause: 'AND project_id IS NULL', params: [] }
188
+ } else if (projectScope && projectScope !== 'all') {
189
+ return { clause: `AND (project_id IS NULL OR project_id = $${offset})`, params: [projectScope] }
190
+ }
191
+ return { clause: '', params: [] }
192
+ }
193
+
194
+ // ── Single-round-trip hybrid CTE ─────────────────────────────────────────
195
+ // Param layout (fixed prefix):
196
+ // $1 = halfvec literal (NULL when no queryVector)
197
+ // $2 = tsQuery text (NULL when short query)
198
+ // $3 = cleanText (trigram term)
199
+ // $4 = candidateWindow (LIMIT for each CTE leg)
200
+ // $5+ = filter params (ts_from, ts_to, excludeStatuses..., category..., projectScope slug)
201
+ //
202
+ // When a leg is inapplicable its CTE returns no rows; the UNION + LEFT JOINs
203
+ // handle that cleanly. dense/sparse/trgm legs each re-use the same filter
204
+ // params starting at $5 since they live in independent CTE scopes.
205
+
206
+ const vecSql = (Array.isArray(options.queryVector) && options.queryVector.length > 0)
207
+ ? embeddingToSql(options.queryVector)
208
+ : null
209
+
210
+ const ftsQuery = clean.length >= 3 ? (buildFtsQuery(clean) ?? null) : null
211
+ const exactTerms = buildExactTerms(clean)
212
+ const queryTokenCount = countQueryTokens(clean)
213
+ const minExactHits = exactTerms.length >= 8 ? 3 : exactTerms.length >= 4 ? 2 : 1
214
+
215
+ // For very short queries (< 3 chars) the trigram operator still works but
216
+ // we relax the server-side threshold via set_limit() — however that requires
217
+ // a separate round-trip. Instead we fall back to a plain ILIKE scan for
218
+ // short text (rare edge case; sequential scan is acceptable for < 3 chars).
219
+ const isShortQuery = clean.length < 3
220
+
221
+ // $5 onward are the filter params for the entries legs (non-MV path).
222
+ // Each CTE leg duplicates the same positional params because they live in
223
+ // independent SELECT scopes. When useHotActive=true, the trgm leg still uses
224
+ // these params but at adjusted offsets (see activeBindParams below).
225
+ const { clause: filterClause, params: filterParams } = buildFilterClause(5)
226
+
227
+ // MV-specific filter: only category/projectScope matter (status='active' and
228
+ // embedding IS NOT NULL are baked into mv_hot_active; ts_from/ts_to are
229
+ // unavailable since MV lacks the ts column).
230
+ function buildMvFilterClause(offset) {
231
+ const clauses = []
232
+ const params = []
233
+ let next = offset
234
+ if (categories.length > 0) {
235
+ const placeholders = categories.map(() => `$${next++}`).join(', ')
236
+ clauses.push(`category IN (${placeholders})`)
237
+ params.push(...categories)
238
+ }
239
+ if (projectScope === 'common') {
240
+ clauses.push('project_id IS NULL')
241
+ } else if (projectScope && projectScope !== 'all') {
242
+ clauses.push(`(project_id IS NULL OR project_id = $${next++})`)
243
+ params.push(projectScope)
244
+ }
245
+ return { clause: clauses.length > 0 ? `AND ${clauses.join(' AND ')}` : '', params }
246
+ }
247
+ // mvBindParams layout when useHotActive=true:
248
+ // $1–$4 : same prefix (vec, fts, clean, window)
249
+ // $5+ : mvFilterParams (category filters + optional projectScope slug)
250
+ // $5+N+ : trgmFilterParams (ts/status/scope for the entries-only trgm leg)
251
+ //
252
+ // The trgm CTE always targets entries and needs the full filter (excludeStatuses,
253
+ // ts_from, ts_to, category, projectScope). When useHotActive=true, trgm filter params
254
+ // start AFTER mvFilterParams so positional params align correctly in the
255
+ // combined bind array.
256
+ const { clause: mvFilterClause, params: mvFilterParams } = buildMvFilterClause(5)
257
+ // trgm filter: when useHotActive, build starting at offset 5 + mvFilterParams.length.
258
+ const trgmFilterOffset = useHotActive ? 5 + mvFilterParams.length : 5
259
+ const { clause: trgmFilterClause, params: trgmFilterParams } = buildFilterClause(trgmFilterOffset)
260
+ // activeBindParams is the single array passed to db.query for the full hybrid SQL.
261
+ // Non-MV path: [vec,fts,clean,window, ...filterParams] (filterClause == trgmFilterClause).
262
+ // MV path: [vec,fts,clean,window, ...mvFilterParams, ...trgmFilterParams].
263
+ const recallScopeOpts = {
264
+ ts_from: tsFrom,
265
+ ts_to: tsTo,
266
+ excludeStatuses,
267
+ category: categories,
268
+ projectScope,
269
+ }
270
+ const exactTermsParam = useHotActive
271
+ ? 5 + mvFilterParams.length + trgmFilterParams.length
272
+ : 5 + filterParams.length
273
+ const exactFilterClause = buildRecallScopeFilter(
274
+ useHotActive ? trgmFilterOffset : 5,
275
+ recallScopeOpts,
276
+ 'ee',
277
+ ).clause
278
+ const activeBindParams = useHotActive
279
+ ? [vecSql, ftsQuery, clean, candidateWindow, ...mvFilterParams, ...trgmFilterParams, ...(exactTerms.length > 0 ? [exactTerms] : [])]
280
+ : [vecSql, ftsQuery, clean, candidateWindow, ...filterParams, ...(exactTerms.length > 0 ? [exactTerms] : [])]
281
+
282
+ // dense CTE: active only when a query vector is supplied.
283
+ // useHotActive → queries mv_hot_active (smaller HNSW, no ts/content needed).
284
+ const denseCte = vecSql ? (useHotActive ? `
285
+ dense AS (
286
+ SELECT id,
287
+ 1 - (embedding <=> $1::halfvec) AS sim,
288
+ ROW_NUMBER() OVER (ORDER BY embedding <=> $1::halfvec) AS dense_rank
289
+ FROM mv_hot_active
290
+ WHERE true
291
+ ${mvFilterClause}
292
+ ORDER BY embedding <=> $1::halfvec
293
+ LIMIT $4
294
+ ),` : `
295
+ dense AS (
296
+ SELECT id,
297
+ 1 - (embedding <=> $1::halfvec) AS sim,
298
+ ROW_NUMBER() OVER (ORDER BY embedding <=> $1::halfvec) AS dense_rank
299
+ FROM entries
300
+ WHERE embedding IS NOT NULL
301
+ ${filterClause}
302
+ ORDER BY embedding <=> $1::halfvec
303
+ LIMIT $4
304
+ ),`) : `
305
+ dense AS (SELECT NULL::bigint AS id, NULL::float8 AS sim, NULL::bigint AS dense_rank WHERE $1::halfvec IS NOT NULL AND false),`
306
+
307
+ // sparse CTE: active only when ftsQuery is non-null.
308
+ // useHotActive → queries mv_hot_active GIN index (mv_hot_active_tsv).
309
+ const sparseCte = ftsQuery ? (useHotActive ? `
310
+ sparse AS (
311
+ SELECT id,
312
+ ts_rank_cd(search_tsv, websearch_to_tsquery('simple', $2)) AS lex,
313
+ ROW_NUMBER() OVER (ORDER BY ts_rank_cd(search_tsv, websearch_to_tsquery('simple', $2)) DESC) AS sparse_rank
314
+ FROM mv_hot_active
315
+ WHERE search_tsv @@ websearch_to_tsquery('simple', $2)
316
+ ${mvFilterClause}
317
+ ORDER BY lex DESC
318
+ LIMIT $4
319
+ ),` : `
320
+ sparse AS (
321
+ SELECT id,
322
+ ts_rank_cd(search_tsv, websearch_to_tsquery('simple', $2)) AS lex,
323
+ ROW_NUMBER() OVER (ORDER BY ts_rank_cd(search_tsv, websearch_to_tsquery('simple', $2)) DESC) AS sparse_rank
324
+ FROM entries
325
+ WHERE search_tsv @@ websearch_to_tsquery('simple', $2)
326
+ ${filterClause}
327
+ ORDER BY lex DESC
328
+ LIMIT $4
329
+ ),`) : `
330
+ sparse AS (SELECT NULL::bigint AS id, NULL::float8 AS lex, NULL::bigint AS sparse_rank WHERE $2::text IS NOT NULL AND false),`
331
+
332
+ // trgm CTE: pg_trgm similarity path. For short queries (< 3 chars) the %
333
+ // operator is unreliable (trigrams need at least 3 chars); use ILIKE instead.
334
+ // NOTE: trgm always queries entries regardless of useHotActive — mv_hot_active
335
+ // lacks the content column (trgm/ILIKE) and ts column (short-query ORDER BY).
336
+ // Uses trgmFilterClause whose $N offsets are aligned to activeBindParams.
337
+ const trgmCte = isShortQuery ? `
338
+ trgm AS (
339
+ SELECT id,
340
+ 0.5::float8 AS trg_sim,
341
+ ROW_NUMBER() OVER (ORDER BY ts DESC) AS trgm_rank
342
+ FROM entries
343
+ WHERE (content ILIKE '%' || $3 || '%' OR element ILIKE '%' || $3 || '%')
344
+ ${trgmFilterClause}
345
+ ORDER BY ts DESC
346
+ LIMIT $4
347
+ ),` : `
348
+ trgm AS (
349
+ SELECT id,
350
+ GREATEST(
351
+ CASE WHEN content ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(content, $3) END,
352
+ CASE WHEN coalesce(element, '') ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(coalesce(element, ''), $3) END,
353
+ CASE WHEN coalesce(summary, '') ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(coalesce(summary, ''), $3) END
354
+ ) AS trg_sim,
355
+ ROW_NUMBER() OVER (ORDER BY GREATEST(
356
+ CASE WHEN content ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(content, $3) END,
357
+ CASE WHEN coalesce(element, '') ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(coalesce(element, ''), $3) END,
358
+ CASE WHEN coalesce(summary, '') ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(coalesce(summary, ''), $3) END
359
+ ) DESC) AS trgm_rank
360
+ FROM entries
361
+ WHERE (
362
+ content % $3 OR element % $3 OR summary % $3
363
+ OR content ILIKE '%' || $3 || '%'
364
+ OR coalesce(element, '') ILIKE '%' || $3 || '%'
365
+ OR coalesce(summary, '') ILIKE '%' || $3 || '%'
366
+ )
367
+ AND GREATEST(
368
+ CASE WHEN content ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(content, $3) END,
369
+ CASE WHEN coalesce(element, '') ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(coalesce(element, ''), $3) END,
370
+ CASE WHEN coalesce(summary, '') ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(coalesce(summary, ''), $3) END
371
+ ) >= 0.10
372
+ ${trgmFilterClause}
373
+ ORDER BY trg_sim DESC
374
+ LIMIT $4
375
+ ),`
376
+
377
+ const exactCte = exactTerms.length > 0 ? `
378
+ exact AS (
379
+ SELECT ee.id,
380
+ COUNT(*)::float8 AS exact_hits,
381
+ ROW_NUMBER() OVER (ORDER BY COUNT(*) DESC, ee.ts DESC) AS exact_rank
382
+ FROM entries ee
383
+ JOIN LATERAL unnest($${exactTermsParam}::text[]) AS q(term) ON (
384
+ ee.content ILIKE '%' || q.term || '%'
385
+ OR coalesce(ee.element, '') ILIKE '%' || q.term || '%'
386
+ OR coalesce(ee.summary, '') ILIKE '%' || q.term || '%'
387
+ )
388
+ WHERE true
389
+ ${exactFilterClause}
390
+ GROUP BY ee.id, ee.ts
391
+ HAVING COUNT(*) >= ${minExactHits}
392
+ ORDER BY exact_hits DESC, ee.ts DESC
393
+ LIMIT $4
394
+ ),` : `
395
+ exact AS (SELECT NULL::bigint AS id, NULL::float8 AS exact_hits, NULL::bigint AS exact_rank WHERE false),`
396
+
397
+ const hybridSql = `
398
+ WITH
399
+ ${denseCte}
400
+ ${sparseCte}
401
+ ${trgmCte}
402
+ ${exactCte}
403
+ combined AS (
404
+ SELECT id FROM dense WHERE id IS NOT NULL UNION
405
+ SELECT id FROM sparse WHERE id IS NOT NULL UNION
406
+ SELECT id FROM trgm WHERE id IS NOT NULL UNION
407
+ SELECT id FROM exact WHERE id IS NOT NULL
408
+ )
409
+ SELECT
410
+ e.id, e.element, e.summary, e.category, e.status, e.score,
411
+ e.last_seen_at, e.ts, e.project_id, e.session_id, e.source_ref,
412
+ e.source_turn, e.content, e.chunk_root, e.is_root,
413
+ e.role,
414
+ d.sim AS dense_sim,
415
+ d.dense_rank,
416
+ s.lex AS sparse_lex,
417
+ s.sparse_rank,
418
+ t.trg_sim,
419
+ t.trgm_rank,
420
+ x.exact_hits,
421
+ x.exact_rank
422
+ FROM combined c
423
+ JOIN entries e ON e.id = c.id
424
+ LEFT JOIN dense d ON d.id = c.id
425
+ LEFT JOIN sparse s ON s.id = c.id
426
+ LEFT JOIN trgm t ON t.id = c.id
427
+ LEFT JOIN exact x ON x.id = c.id`
428
+
429
+ let rawRows = []
430
+ let denseCount = 0
431
+ let sparseCount = 0
432
+ let trgmCount = 0
433
+ let exactCount = 0
434
+
435
+ try {
436
+ const { rows } = await recallReadQuery(db, hybridSql, activeBindParams)
437
+ rawRows = rows
438
+ // Count how many rows each leg contributed (a row may appear in multiple legs).
439
+ for (const r of rawRows) {
440
+ if (r.dense_rank != null) denseCount++
441
+ if (r.sparse_rank != null) sparseCount++
442
+ if (r.trgm_rank != null) trgmCount++
443
+ if (r.exact_rank != null) exactCount++
444
+ }
445
+ } catch (err) {
446
+ process.stderr.write(`[recall] hybrid CTE failed: ${err.message}\n`)
447
+ return []
448
+ }
449
+
450
+ if (rawRows.length === 0) return []
451
+
452
+ // ── JS-side RRF merge (unchanged logic) ──────────────────────────────────
453
+ // K=60 is the standard RRF constant from Cormack et al. (SIGIR 2009).
454
+ const K = 60
455
+ const nowMs = Date.now()
456
+
457
+ const scoredAll = rawRows.map(row => {
458
+ const id = Number(row.id)
459
+ const denseRank = row.dense_rank != null ? Number(row.dense_rank) : null
460
+ const sparseRank = row.sparse_rank != null ? Number(row.sparse_rank) : null
461
+ const trgmRank = row.trgm_rank != null ? Number(row.trgm_rank) : null
462
+ const exactRank = row.exact_rank != null ? Number(row.exact_rank) : null
463
+ const rrf = (denseRank ? 1 / (K + denseRank) : 0)
464
+ + (sparseRank ? 1 / (K + sparseRank) : 0)
465
+ + (trgmRank ? 1 / (K + trgmRank) : 0)
466
+ + (exactRank ? 1 / (K + exactRank) : 0)
467
+ const freshness = applyFreshness ? freshnessFactor(row.ts, nowMs) : 1.0
468
+ const boost = exactTextBoost(clean, row, row.exact_hits)
469
+ return { id, row, rrf, freshness, retrievalScore: (rrf * freshness) + boost }
470
+ })
471
+ let semanticOnlyDropped = 0
472
+ let weakTextDropped = 0
473
+ const scored = scoredAll.filter(({ row }) => {
474
+ const hasTextSupport = row.sparse_rank != null || row.trgm_rank != null || row.exact_rank != null
475
+ const sim = Number(row.dense_sim)
476
+ const hasSemanticSupport = Number.isFinite(sim) && sim >= SEMANTIC_ONLY_MIN_SIM
477
+ if (!hasTextSupport) {
478
+ if (hasSemanticSupport) return true
479
+ semanticOnlyDropped += 1
480
+ return false
481
+ }
482
+ // A long query that only shares a weak trigram/exact tail with old rows
483
+ // should not fill the page with accidental matches. Accept lexical-only
484
+ // rows when the query is a short keyword/identifier lookup, a full phrase
485
+ // match, or an FTS hit. Otherwise require semantic support as a second
486
+ // independent signal.
487
+ if (categories.length > 0) return true
488
+ const hasFullPhrase = hasFullQueryTextMatch(clean, row)
489
+ if (hasFullPhrase) return true
490
+ if (queryTokenCount <= SHORT_QUERY_TOKEN_MAX) return true
491
+ if (row.sparse_rank != null) return true
492
+ if (hasSemanticSupport && hasQueryTokenCoverage(row, queryTokenCount)) return true
493
+ weakTextDropped += 1
494
+ return false
495
+ })
496
+ if (scored.length === 0) return []
497
+ scored.sort((a, b) => b.retrievalScore - a.retrievalScore || b.rrf - a.rrf)
498
+
499
+ const filtered = scored
500
+
501
+ // ── Root resolution + member-hit write-back ───────────────────────────────
502
+ const byId = new Map(rawRows.map(r => [Number(r.id), r]))
503
+ const memberHitRootIds = new Set()
504
+ const rootIdsForReturn = []
505
+ const seen = new Set()
506
+
507
+ for (const { id, rrf, retrievalScore } of filtered) {
508
+ const row = byId.get(id)
509
+ if (!row) continue
510
+ let targetRow = null
511
+ if (row.is_root === 1) {
512
+ targetRow = row
513
+ } else if (row.chunk_root != null && row.chunk_root !== row.id) {
514
+ // $1 = chunk_root id, scope param (if any) = $2
515
+ const { clause: rootScopeClause, params: rootScopeParams } = buildScopeClause(2)
516
+ const { rows: rootRows } = await recallReadQuery(
517
+ db,
518
+ `SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
519
+ element, category, summary, project_id, status, score, last_seen_at
520
+ FROM entries WHERE id = $1 AND is_root = 1 ${rootScopeClause}`,
521
+ [row.chunk_root, ...rootScopeParams],
522
+ )
523
+ const r = rootRows[0]
524
+ if (!r) continue
525
+ memberHitRootIds.add(r.id)
526
+ targetRow = r
527
+ } else {
528
+ targetRow = row
529
+ }
530
+ if (seen.has(targetRow.id)) continue
531
+ seen.add(targetRow.id)
532
+ rootIdsForReturn.push({
533
+ root: targetRow,
534
+ rrf,
535
+ retrievalScore,
536
+ retrievalRank: rootIdsForReturn.length + 1,
537
+ })
538
+ if (rootIdsForReturn.length >= limit) break
539
+ }
540
+
541
+ let writeBackCount = 0
542
+ if (writeBackMemberHits && memberHitRootIds.size > 0) {
543
+ // Batch UPDATE — single round-trip instead of N (one per member-hit root).
544
+ const validRootIds = []
545
+ for (const rootId of memberHitRootIds) {
546
+ const r = rootIdsForReturn.find(x => x.root.id === rootId)?.root ?? byId.get(rootId)
547
+ if (r) validRootIds.push(Number(rootId))
548
+ }
549
+ if (validRootIds.length > 0) {
550
+ try {
551
+ const { rowCount } = await db.query(
552
+ `UPDATE entries SET last_seen_at = $1::bigint WHERE id = ANY($2::bigint[]) AND is_root = 1
553
+ AND (last_seen_at IS NULL OR last_seen_at < $1::bigint - 3600000)`,
554
+ [String(nowMs), validRootIds],
555
+ )
556
+ writeBackCount = rowCount ?? validRootIds.length
557
+ } catch (err) {
558
+ process.stderr.write(`[recall] writeback batch failed (count=${validRootIds.length}): ${err.message}\n`)
559
+ }
560
+ }
561
+ }
562
+
563
+ // ── Final fetch: full row for each root by id = ANY(bigint[]) ────────────
564
+ const topIds = rootIdsForReturn.map(x => Number(x.root.id))
565
+ const { clause: finalFilter, params: finalFilterParams } = buildFilterClause(2)
566
+ const { rows: finalRows } = await recallReadQuery(
567
+ db,
568
+ `SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
569
+ element, category, summary, project_id, status, score, last_seen_at
570
+ FROM entries
571
+ WHERE id = ANY($1::bigint[])
572
+ ${finalFilter}`,
573
+ [topIds, ...finalFilterParams],
574
+ )
575
+ const finalById = new Map(finalRows.map(r => [Number(r.id), r]))
576
+
577
+ // Members: single batch fetch keyed by chunk_root = ANY($1) — one
578
+ // round-trip vs N. Map to per-root arrays preserving (ts ASC, id ASC).
579
+ let membersByRoot = new Map()
580
+ if (includeMembers) {
581
+ const rootIds = rootIdsForReturn
582
+ .map(x => Number(finalById.get(Number(x.root.id))?.id ?? x.root.id))
583
+ .filter(id => {
584
+ const fr = finalById.get(id) ?? rootIdsForReturn.find(x => Number(x.root.id) === id)?.root
585
+ return fr && fr.is_root === 1
586
+ })
587
+ if (rootIds.length > 0) {
588
+ const { rows: memberRows } = await recallReadQuery(
589
+ db,
590
+ `SELECT id, ts, role, content, session_id, source_turn, project_id, chunk_root
591
+ FROM entries WHERE chunk_root = ANY($1::bigint[]) AND is_root = 0
592
+ ORDER BY ts ASC, id ASC`,
593
+ [rootIds],
594
+ )
595
+ for (const m of memberRows) {
596
+ const k = Number(m.chunk_root)
597
+ if (!membersByRoot.has(k)) membersByRoot.set(k, [])
598
+ membersByRoot.get(k).push(m)
599
+ }
600
+ }
601
+ }
602
+ const results = []
603
+ for (const { root, rrf, retrievalScore, retrievalRank } of rootIdsForReturn) {
604
+ // Roots absent from finalById were excluded by the status/time filter on
605
+ // the final fetch; falling back to the unfiltered `root` would leak
606
+ // archived / out-of-window rows via member-hit resolution.
607
+ const finalRoot = finalById.get(Number(root.id))
608
+ if (!finalRoot) continue
609
+ const out = { ...finalRoot, rrf, retrievalScore, retrievalRank }
610
+ if (includeMembers && finalRoot.is_root === 1) {
611
+ out.members = membersByRoot.get(Number(finalRoot.id)) ?? []
612
+ }
613
+ results.push(out)
614
+ }
615
+
616
+ process.stderr.write(
617
+ `[recall] dense=${denseCount} sparse=${sparseCount} trgm=${trgmCount} exact=${exactCount} semantic_only_dropped=${semanticOnlyDropped} weak_text_dropped=${weakTextDropped} merged=${results.length} write_back=${writeBackCount}\n`,
618
+ )
619
+
620
+ return results
621
+ }