hippo-memory 1.44.0 → 1.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (528) hide show
  1. package/README.md +12 -10
  2. package/bin/hippo.js +3 -1
  3. package/dist/api.d.ts +21 -21
  4. package/dist/api.js +69 -42
  5. package/dist/audit.d.ts +1 -1
  6. package/dist/audit.js +9 -0
  7. package/dist/autolearn.d.ts +1 -1
  8. package/dist/autolearn.js +2 -1
  9. package/dist/cli.d.ts +3 -0
  10. package/dist/cli.js +151 -72
  11. package/dist/client.d.ts +4 -21
  12. package/dist/client.js +3 -112
  13. package/dist/config.js +2 -1
  14. package/dist/consolidate.d.ts +3 -0
  15. package/dist/consolidate.js +44 -42
  16. package/dist/dag.d.ts +1 -0
  17. package/dist/dag.js +10 -5
  18. package/dist/dashboard.js +7 -3
  19. package/dist/db.js +12 -23
  20. package/dist/dedupe.d.ts +1 -0
  21. package/dist/dedupe.js +4 -7
  22. package/dist/embedding-provider.js +2 -1
  23. package/dist/embeddings.js +81 -8
  24. package/dist/extract.d.ts +2 -0
  25. package/dist/extract.js +9 -4
  26. package/dist/mcp/server.js +21 -36
  27. package/dist/memory.d.ts +3 -0
  28. package/dist/memory.js +7 -1
  29. package/dist/refine-llm.js +3 -2
  30. package/dist/rerankers/jev.js +3 -2
  31. package/dist/rerankers/llm.js +3 -2
  32. package/dist/search.js +3 -3
  33. package/dist/server.d.ts +5 -0
  34. package/dist/server.js +39 -29
  35. package/dist/shared.js +1 -1
  36. package/dist/store.d.ts +18 -11
  37. package/dist/store.js +144 -63
  38. package/dist/version.d.ts +4 -6
  39. package/dist/version.js +4 -7
  40. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  41. package/extensions/openclaw-plugin/package.json +1 -1
  42. package/openclaw.plugin.json +1 -1
  43. package/package.json +6 -3
  44. package/dist/ablation.d.ts.map +0 -1
  45. package/dist/ablation.js.map +0 -1
  46. package/dist/ambient.d.ts.map +0 -1
  47. package/dist/ambient.js.map +0 -1
  48. package/dist/api.d.ts.map +0 -1
  49. package/dist/api.js.map +0 -1
  50. package/dist/audit-prune.d.ts.map +0 -1
  51. package/dist/audit-prune.js.map +0 -1
  52. package/dist/audit.d.ts.map +0 -1
  53. package/dist/audit.js.map +0 -1
  54. package/dist/auth.d.ts.map +0 -1
  55. package/dist/auth.js.map +0 -1
  56. package/dist/autolearn.d.ts.map +0 -1
  57. package/dist/autolearn.js.map +0 -1
  58. package/dist/availability.d.ts.map +0 -1
  59. package/dist/availability.js.map +0 -1
  60. package/dist/benchmarks/e1.3/incident-recall-eval.js +0 -78
  61. package/dist/benchmarks/e1.3/incident-recall-eval.js.map +0 -1
  62. package/dist/benchmarks/e1.3/slack-1000-event-smoke.js +0 -103
  63. package/dist/benchmarks/e1.3/slack-1000-event-smoke.js.map +0 -1
  64. package/dist/capture.d.ts.map +0 -1
  65. package/dist/capture.js.map +0 -1
  66. package/dist/card-detail.d.ts.map +0 -1
  67. package/dist/card-detail.js.map +0 -1
  68. package/dist/card.d.ts.map +0 -1
  69. package/dist/card.js.map +0 -1
  70. package/dist/cli.d.ts.map +0 -1
  71. package/dist/cli.js.map +0 -1
  72. package/dist/client.d.ts.map +0 -1
  73. package/dist/client.js.map +0 -1
  74. package/dist/compare.d.ts.map +0 -1
  75. package/dist/compare.js.map +0 -1
  76. package/dist/config.d.ts.map +0 -1
  77. package/dist/config.js.map +0 -1
  78. package/dist/connectors/github/backfill.d.ts.map +0 -1
  79. package/dist/connectors/github/backfill.js.map +0 -1
  80. package/dist/connectors/github/cli-impl.d.ts.map +0 -1
  81. package/dist/connectors/github/cli-impl.js.map +0 -1
  82. package/dist/connectors/github/deletion.d.ts.map +0 -1
  83. package/dist/connectors/github/deletion.js.map +0 -1
  84. package/dist/connectors/github/dlq.d.ts.map +0 -1
  85. package/dist/connectors/github/dlq.js.map +0 -1
  86. package/dist/connectors/github/idempotency.d.ts.map +0 -1
  87. package/dist/connectors/github/idempotency.js.map +0 -1
  88. package/dist/connectors/github/ingest.d.ts.map +0 -1
  89. package/dist/connectors/github/ingest.js.map +0 -1
  90. package/dist/connectors/github/octokit-client.d.ts.map +0 -1
  91. package/dist/connectors/github/octokit-client.js.map +0 -1
  92. package/dist/connectors/github/ratelimit.d.ts.map +0 -1
  93. package/dist/connectors/github/ratelimit.js.map +0 -1
  94. package/dist/connectors/github/scope.d.ts.map +0 -1
  95. package/dist/connectors/github/scope.js.map +0 -1
  96. package/dist/connectors/github/signature.d.ts.map +0 -1
  97. package/dist/connectors/github/signature.js.map +0 -1
  98. package/dist/connectors/github/tenant-routing.d.ts.map +0 -1
  99. package/dist/connectors/github/tenant-routing.js.map +0 -1
  100. package/dist/connectors/github/transform.d.ts.map +0 -1
  101. package/dist/connectors/github/transform.js.map +0 -1
  102. package/dist/connectors/github/types.d.ts.map +0 -1
  103. package/dist/connectors/github/types.js.map +0 -1
  104. package/dist/connectors/slack/backfill.d.ts.map +0 -1
  105. package/dist/connectors/slack/backfill.js.map +0 -1
  106. package/dist/connectors/slack/deletion.d.ts.map +0 -1
  107. package/dist/connectors/slack/deletion.js.map +0 -1
  108. package/dist/connectors/slack/dlq.d.ts.map +0 -1
  109. package/dist/connectors/slack/dlq.js.map +0 -1
  110. package/dist/connectors/slack/idempotency.d.ts.map +0 -1
  111. package/dist/connectors/slack/idempotency.js.map +0 -1
  112. package/dist/connectors/slack/ingest.d.ts.map +0 -1
  113. package/dist/connectors/slack/ingest.js.map +0 -1
  114. package/dist/connectors/slack/ratelimit.d.ts.map +0 -1
  115. package/dist/connectors/slack/ratelimit.js.map +0 -1
  116. package/dist/connectors/slack/scope.d.ts.map +0 -1
  117. package/dist/connectors/slack/scope.js.map +0 -1
  118. package/dist/connectors/slack/signature.d.ts.map +0 -1
  119. package/dist/connectors/slack/signature.js.map +0 -1
  120. package/dist/connectors/slack/tenant-routing.d.ts.map +0 -1
  121. package/dist/connectors/slack/tenant-routing.js.map +0 -1
  122. package/dist/connectors/slack/transform.d.ts.map +0 -1
  123. package/dist/connectors/slack/transform.js.map +0 -1
  124. package/dist/connectors/slack/types.d.ts.map +0 -1
  125. package/dist/connectors/slack/types.js.map +0 -1
  126. package/dist/connectors/slack/web-client.d.ts.map +0 -1
  127. package/dist/connectors/slack/web-client.js.map +0 -1
  128. package/dist/connectors/slack/workspaces.d.ts.map +0 -1
  129. package/dist/connectors/slack/workspaces.js.map +0 -1
  130. package/dist/consolidate.d.ts.map +0 -1
  131. package/dist/consolidate.js.map +0 -1
  132. package/dist/correction-latency.d.ts.map +0 -1
  133. package/dist/correction-latency.js.map +0 -1
  134. package/dist/customer-notes.d.ts.map +0 -1
  135. package/dist/customer-notes.js.map +0 -1
  136. package/dist/dag.d.ts.map +0 -1
  137. package/dist/dag.js.map +0 -1
  138. package/dist/dashboard.d.ts.map +0 -1
  139. package/dist/dashboard.js.map +0 -1
  140. package/dist/db.d.ts.map +0 -1
  141. package/dist/db.js.map +0 -1
  142. package/dist/decisions.d.ts.map +0 -1
  143. package/dist/decisions.js.map +0 -1
  144. package/dist/dedupe.d.ts.map +0 -1
  145. package/dist/dedupe.js.map +0 -1
  146. package/dist/embedding-provider.d.ts.map +0 -1
  147. package/dist/embedding-provider.js.map +0 -1
  148. package/dist/embeddings.d.ts.map +0 -1
  149. package/dist/embeddings.js.map +0 -1
  150. package/dist/eval-suite.d.ts.map +0 -1
  151. package/dist/eval-suite.js.map +0 -1
  152. package/dist/eval.d.ts.map +0 -1
  153. package/dist/eval.js.map +0 -1
  154. package/dist/extensions/openclaw-plugin/index.js.map +0 -1
  155. package/dist/extract.d.ts.map +0 -1
  156. package/dist/extract.js.map +0 -1
  157. package/dist/forward-claim-detector.d.ts.map +0 -1
  158. package/dist/forward-claim-detector.js.map +0 -1
  159. package/dist/goals.d.ts.map +0 -1
  160. package/dist/goals.js.map +0 -1
  161. package/dist/graph-extract.d.ts.map +0 -1
  162. package/dist/graph-extract.js.map +0 -1
  163. package/dist/graph-recall.d.ts.map +0 -1
  164. package/dist/graph-recall.js.map +0 -1
  165. package/dist/graph-stream.d.ts.map +0 -1
  166. package/dist/graph-stream.js.map +0 -1
  167. package/dist/graph-view.d.ts.map +0 -1
  168. package/dist/graph-view.js.map +0 -1
  169. package/dist/graph.d.ts.map +0 -1
  170. package/dist/graph.js.map +0 -1
  171. package/dist/handoff.d.ts.map +0 -1
  172. package/dist/handoff.js.map +0 -1
  173. package/dist/hooks.d.ts.map +0 -1
  174. package/dist/hooks.js.map +0 -1
  175. package/dist/importers.d.ts.map +0 -1
  176. package/dist/importers.js.map +0 -1
  177. package/dist/incidents.d.ts.map +0 -1
  178. package/dist/incidents.js.map +0 -1
  179. package/dist/index.d.ts.map +0 -1
  180. package/dist/index.js.map +0 -1
  181. package/dist/invalidation.d.ts.map +0 -1
  182. package/dist/invalidation.js.map +0 -1
  183. package/dist/mcp/framing.d.ts.map +0 -1
  184. package/dist/mcp/framing.js.map +0 -1
  185. package/dist/mcp/server.d.ts.map +0 -1
  186. package/dist/mcp/server.js.map +0 -1
  187. package/dist/memory-value-weights.d.ts.map +0 -1
  188. package/dist/memory-value-weights.js.map +0 -1
  189. package/dist/memory-value.d.ts.map +0 -1
  190. package/dist/memory-value.js.map +0 -1
  191. package/dist/memory.d.ts.map +0 -1
  192. package/dist/memory.js.map +0 -1
  193. package/dist/multihop.d.ts.map +0 -1
  194. package/dist/multihop.js.map +0 -1
  195. package/dist/owner-validation.d.ts.map +0 -1
  196. package/dist/owner-validation.js.map +0 -1
  197. package/dist/path-context.d.ts.map +0 -1
  198. package/dist/path-context.js.map +0 -1
  199. package/dist/physics-config.d.ts.map +0 -1
  200. package/dist/physics-config.js.map +0 -1
  201. package/dist/physics-state.d.ts.map +0 -1
  202. package/dist/physics-state.js.map +0 -1
  203. package/dist/physics.d.ts.map +0 -1
  204. package/dist/physics.js.map +0 -1
  205. package/dist/policies.d.ts.map +0 -1
  206. package/dist/policies.js.map +0 -1
  207. package/dist/postinstall.d.ts.map +0 -1
  208. package/dist/postinstall.js.map +0 -1
  209. package/dist/predictions.d.ts.map +0 -1
  210. package/dist/predictions.js.map +0 -1
  211. package/dist/processes.d.ts.map +0 -1
  212. package/dist/processes.js.map +0 -1
  213. package/dist/project-briefs.d.ts.map +0 -1
  214. package/dist/project-briefs.js.map +0 -1
  215. package/dist/project-identity.d.ts.map +0 -1
  216. package/dist/project-identity.js.map +0 -1
  217. package/dist/provenance-coverage.d.ts.map +0 -1
  218. package/dist/provenance-coverage.js.map +0 -1
  219. package/dist/rate-limit.d.ts.map +0 -1
  220. package/dist/rate-limit.js.map +0 -1
  221. package/dist/raw-archive-mirror-cleanup.d.ts.map +0 -1
  222. package/dist/raw-archive-mirror-cleanup.js.map +0 -1
  223. package/dist/raw-archive.d.ts.map +0 -1
  224. package/dist/raw-archive.js.map +0 -1
  225. package/dist/recall-history.d.ts.map +0 -1
  226. package/dist/recall-history.js.map +0 -1
  227. package/dist/recall-scope.d.ts.map +0 -1
  228. package/dist/recall-scope.js.map +0 -1
  229. package/dist/recall-trace.d.ts.map +0 -1
  230. package/dist/recall-trace.js.map +0 -1
  231. package/dist/refine-llm.d.ts.map +0 -1
  232. package/dist/refine-llm.js.map +0 -1
  233. package/dist/reject-flow.d.ts.map +0 -1
  234. package/dist/reject-flow.js.map +0 -1
  235. package/dist/rejection.d.ts.map +0 -1
  236. package/dist/rejection.js.map +0 -1
  237. package/dist/replay.d.ts.map +0 -1
  238. package/dist/replay.js.map +0 -1
  239. package/dist/rerankers/cross-encoder.d.ts.map +0 -1
  240. package/dist/rerankers/cross-encoder.js.map +0 -1
  241. package/dist/rerankers/index.d.ts.map +0 -1
  242. package/dist/rerankers/index.js.map +0 -1
  243. package/dist/rerankers/jev.d.ts.map +0 -1
  244. package/dist/rerankers/jev.js.map +0 -1
  245. package/dist/rerankers/llm.d.ts.map +0 -1
  246. package/dist/rerankers/llm.js.map +0 -1
  247. package/dist/rerankers/types.d.ts.map +0 -1
  248. package/dist/rerankers/types.js.map +0 -1
  249. package/dist/rrf.d.ts.map +0 -1
  250. package/dist/rrf.js.map +0 -1
  251. package/dist/salience.d.ts.map +0 -1
  252. package/dist/salience.js.map +0 -1
  253. package/dist/scheduler.d.ts.map +0 -1
  254. package/dist/scheduler.js.map +0 -1
  255. package/dist/scope.d.ts.map +0 -1
  256. package/dist/scope.js.map +0 -1
  257. package/dist/search.d.ts.map +0 -1
  258. package/dist/search.js.map +0 -1
  259. package/dist/secret-detect.d.ts.map +0 -1
  260. package/dist/secret-detect.js.map +0 -1
  261. package/dist/server-detect.d.ts.map +0 -1
  262. package/dist/server-detect.js.map +0 -1
  263. package/dist/server.d.ts.map +0 -1
  264. package/dist/server.js.map +0 -1
  265. package/dist/shared.d.ts.map +0 -1
  266. package/dist/shared.js.map +0 -1
  267. package/dist/skills.d.ts.map +0 -1
  268. package/dist/skills.js.map +0 -1
  269. package/dist/sleep-redact.d.ts +0 -58
  270. package/dist/sleep-redact.d.ts.map +0 -1
  271. package/dist/sleep-redact.js +0 -80
  272. package/dist/sleep-redact.js.map +0 -1
  273. package/dist/src/ablation.js +0 -138
  274. package/dist/src/ablation.js.map +0 -1
  275. package/dist/src/ambient.js +0 -148
  276. package/dist/src/ambient.js.map +0 -1
  277. package/dist/src/api.js +0 -2109
  278. package/dist/src/api.js.map +0 -1
  279. package/dist/src/audit-prune.js +0 -109
  280. package/dist/src/audit-prune.js.map +0 -1
  281. package/dist/src/audit.js +0 -213
  282. package/dist/src/audit.js.map +0 -1
  283. package/dist/src/auth.js +0 -97
  284. package/dist/src/auth.js.map +0 -1
  285. package/dist/src/autolearn.js +0 -174
  286. package/dist/src/autolearn.js.map +0 -1
  287. package/dist/src/availability.js +0 -94
  288. package/dist/src/availability.js.map +0 -1
  289. package/dist/src/capture.js +0 -1346
  290. package/dist/src/capture.js.map +0 -1
  291. package/dist/src/card-detail.js +0 -15
  292. package/dist/src/card-detail.js.map +0 -1
  293. package/dist/src/card.js +0 -19
  294. package/dist/src/card.js.map +0 -1
  295. package/dist/src/cli.js +0 -9387
  296. package/dist/src/cli.js.map +0 -1
  297. package/dist/src/client.js +0 -237
  298. package/dist/src/client.js.map +0 -1
  299. package/dist/src/compare.js +0 -122
  300. package/dist/src/compare.js.map +0 -1
  301. package/dist/src/config.js +0 -135
  302. package/dist/src/config.js.map +0 -1
  303. package/dist/src/connectors/github/backfill.js +0 -281
  304. package/dist/src/connectors/github/backfill.js.map +0 -1
  305. package/dist/src/connectors/github/cli-impl.js +0 -234
  306. package/dist/src/connectors/github/cli-impl.js.map +0 -1
  307. package/dist/src/connectors/github/deletion.js +0 -85
  308. package/dist/src/connectors/github/deletion.js.map +0 -1
  309. package/dist/src/connectors/github/dlq.js +0 -190
  310. package/dist/src/connectors/github/dlq.js.map +0 -1
  311. package/dist/src/connectors/github/idempotency.js +0 -27
  312. package/dist/src/connectors/github/idempotency.js.map +0 -1
  313. package/dist/src/connectors/github/ingest.js +0 -156
  314. package/dist/src/connectors/github/ingest.js.map +0 -1
  315. package/dist/src/connectors/github/octokit-client.js +0 -70
  316. package/dist/src/connectors/github/octokit-client.js.map +0 -1
  317. package/dist/src/connectors/github/ratelimit.js +0 -31
  318. package/dist/src/connectors/github/ratelimit.js.map +0 -1
  319. package/dist/src/connectors/github/scope.js +0 -13
  320. package/dist/src/connectors/github/scope.js.map +0 -1
  321. package/dist/src/connectors/github/signature.js +0 -81
  322. package/dist/src/connectors/github/signature.js.map +0 -1
  323. package/dist/src/connectors/github/tenant-routing.js +0 -69
  324. package/dist/src/connectors/github/tenant-routing.js.map +0 -1
  325. package/dist/src/connectors/github/transform.js +0 -103
  326. package/dist/src/connectors/github/transform.js.map +0 -1
  327. package/dist/src/connectors/github/types.js +0 -100
  328. package/dist/src/connectors/github/types.js.map +0 -1
  329. package/dist/src/connectors/slack/backfill.js +0 -78
  330. package/dist/src/connectors/slack/backfill.js.map +0 -1
  331. package/dist/src/connectors/slack/deletion.js +0 -59
  332. package/dist/src/connectors/slack/deletion.js.map +0 -1
  333. package/dist/src/connectors/slack/dlq.js +0 -224
  334. package/dist/src/connectors/slack/dlq.js.map +0 -1
  335. package/dist/src/connectors/slack/idempotency.js +0 -31
  336. package/dist/src/connectors/slack/idempotency.js.map +0 -1
  337. package/dist/src/connectors/slack/ingest.js +0 -109
  338. package/dist/src/connectors/slack/ingest.js.map +0 -1
  339. package/dist/src/connectors/slack/ratelimit.js +0 -18
  340. package/dist/src/connectors/slack/ratelimit.js.map +0 -1
  341. package/dist/src/connectors/slack/scope.js +0 -13
  342. package/dist/src/connectors/slack/scope.js.map +0 -1
  343. package/dist/src/connectors/slack/signature.js +0 -27
  344. package/dist/src/connectors/slack/signature.js.map +0 -1
  345. package/dist/src/connectors/slack/tenant-routing.js +0 -41
  346. package/dist/src/connectors/slack/tenant-routing.js.map +0 -1
  347. package/dist/src/connectors/slack/transform.js +0 -47
  348. package/dist/src/connectors/slack/transform.js.map +0 -1
  349. package/dist/src/connectors/slack/types.js +0 -35
  350. package/dist/src/connectors/slack/types.js.map +0 -1
  351. package/dist/src/connectors/slack/web-client.js +0 -43
  352. package/dist/src/connectors/slack/web-client.js.map +0 -1
  353. package/dist/src/connectors/slack/workspaces.js +0 -62
  354. package/dist/src/connectors/slack/workspaces.js.map +0 -1
  355. package/dist/src/consolidate.js +0 -978
  356. package/dist/src/consolidate.js.map +0 -1
  357. package/dist/src/correction-latency.js +0 -74
  358. package/dist/src/correction-latency.js.map +0 -1
  359. package/dist/src/customer-notes.js +0 -325
  360. package/dist/src/customer-notes.js.map +0 -1
  361. package/dist/src/dag.js +0 -352
  362. package/dist/src/dag.js.map +0 -1
  363. package/dist/src/dashboard.js +0 -258
  364. package/dist/src/dashboard.js.map +0 -1
  365. package/dist/src/db.js +0 -2731
  366. package/dist/src/db.js.map +0 -1
  367. package/dist/src/decisions.js +0 -304
  368. package/dist/src/decisions.js.map +0 -1
  369. package/dist/src/dedupe.js +0 -141
  370. package/dist/src/dedupe.js.map +0 -1
  371. package/dist/src/embedding-provider.js +0 -314
  372. package/dist/src/embedding-provider.js.map +0 -1
  373. package/dist/src/embeddings.js +0 -543
  374. package/dist/src/embeddings.js.map +0 -1
  375. package/dist/src/eval-suite.js +0 -294
  376. package/dist/src/eval-suite.js.map +0 -1
  377. package/dist/src/eval.js +0 -187
  378. package/dist/src/eval.js.map +0 -1
  379. package/dist/src/extract.js +0 -117
  380. package/dist/src/extract.js.map +0 -1
  381. package/dist/src/forward-claim-detector.js +0 -117
  382. package/dist/src/forward-claim-detector.js.map +0 -1
  383. package/dist/src/goals.js +0 -399
  384. package/dist/src/goals.js.map +0 -1
  385. package/dist/src/graph-extract.js +0 -314
  386. package/dist/src/graph-extract.js.map +0 -1
  387. package/dist/src/graph-recall.js +0 -277
  388. package/dist/src/graph-recall.js.map +0 -1
  389. package/dist/src/graph-stream.js +0 -176
  390. package/dist/src/graph-stream.js.map +0 -1
  391. package/dist/src/graph-view.js +0 -310
  392. package/dist/src/graph-view.js.map +0 -1
  393. package/dist/src/graph.js +0 -762
  394. package/dist/src/graph.js.map +0 -1
  395. package/dist/src/handoff.js +0 -65
  396. package/dist/src/handoff.js.map +0 -1
  397. package/dist/src/hooks.js +0 -926
  398. package/dist/src/hooks.js.map +0 -1
  399. package/dist/src/importers.js +0 -881
  400. package/dist/src/importers.js.map +0 -1
  401. package/dist/src/incidents.js +0 -336
  402. package/dist/src/incidents.js.map +0 -1
  403. package/dist/src/index.js +0 -32
  404. package/dist/src/index.js.map +0 -1
  405. package/dist/src/invalidation.js +0 -129
  406. package/dist/src/invalidation.js.map +0 -1
  407. package/dist/src/mcp/framing.js +0 -45
  408. package/dist/src/mcp/framing.js.map +0 -1
  409. package/dist/src/mcp/server.js +0 -1298
  410. package/dist/src/mcp/server.js.map +0 -1
  411. package/dist/src/memory-value-weights.js +0 -31
  412. package/dist/src/memory-value-weights.js.map +0 -1
  413. package/dist/src/memory-value.js +0 -261
  414. package/dist/src/memory-value.js.map +0 -1
  415. package/dist/src/memory.js +0 -425
  416. package/dist/src/memory.js.map +0 -1
  417. package/dist/src/multihop.js +0 -35
  418. package/dist/src/multihop.js.map +0 -1
  419. package/dist/src/owner-validation.js +0 -56
  420. package/dist/src/owner-validation.js.map +0 -1
  421. package/dist/src/path-context.js +0 -48
  422. package/dist/src/path-context.js.map +0 -1
  423. package/dist/src/physics-config.js +0 -26
  424. package/dist/src/physics-config.js.map +0 -1
  425. package/dist/src/physics-state.js +0 -172
  426. package/dist/src/physics-state.js.map +0 -1
  427. package/dist/src/physics.js +0 -378
  428. package/dist/src/physics.js.map +0 -1
  429. package/dist/src/policies.js +0 -414
  430. package/dist/src/policies.js.map +0 -1
  431. package/dist/src/postinstall.js +0 -101
  432. package/dist/src/postinstall.js.map +0 -1
  433. package/dist/src/predictions.js +0 -629
  434. package/dist/src/predictions.js.map +0 -1
  435. package/dist/src/processes.js +0 -349
  436. package/dist/src/processes.js.map +0 -1
  437. package/dist/src/project-briefs.js +0 -492
  438. package/dist/src/project-briefs.js.map +0 -1
  439. package/dist/src/project-identity.js +0 -200
  440. package/dist/src/project-identity.js.map +0 -1
  441. package/dist/src/provenance-coverage.js +0 -23
  442. package/dist/src/provenance-coverage.js.map +0 -1
  443. package/dist/src/rate-limit.js +0 -60
  444. package/dist/src/rate-limit.js.map +0 -1
  445. package/dist/src/raw-archive-mirror-cleanup.js +0 -55
  446. package/dist/src/raw-archive-mirror-cleanup.js.map +0 -1
  447. package/dist/src/raw-archive.js +0 -91
  448. package/dist/src/raw-archive.js.map +0 -1
  449. package/dist/src/recall-history.js +0 -235
  450. package/dist/src/recall-history.js.map +0 -1
  451. package/dist/src/recall-scope.js +0 -89
  452. package/dist/src/recall-scope.js.map +0 -1
  453. package/dist/src/recall-trace.js +0 -186
  454. package/dist/src/recall-trace.js.map +0 -1
  455. package/dist/src/refine-llm.js +0 -156
  456. package/dist/src/refine-llm.js.map +0 -1
  457. package/dist/src/reject-flow.js +0 -209
  458. package/dist/src/reject-flow.js.map +0 -1
  459. package/dist/src/rejection.js +0 -160
  460. package/dist/src/rejection.js.map +0 -1
  461. package/dist/src/replay.js +0 -122
  462. package/dist/src/replay.js.map +0 -1
  463. package/dist/src/rerankers/cross-encoder.js +0 -137
  464. package/dist/src/rerankers/cross-encoder.js.map +0 -1
  465. package/dist/src/rerankers/index.js +0 -20
  466. package/dist/src/rerankers/index.js.map +0 -1
  467. package/dist/src/rerankers/jev.js +0 -119
  468. package/dist/src/rerankers/jev.js.map +0 -1
  469. package/dist/src/rerankers/llm.js +0 -80
  470. package/dist/src/rerankers/llm.js.map +0 -1
  471. package/dist/src/rerankers/types.js +0 -2
  472. package/dist/src/rerankers/types.js.map +0 -1
  473. package/dist/src/rrf.js +0 -61
  474. package/dist/src/rrf.js.map +0 -1
  475. package/dist/src/salience.js +0 -74
  476. package/dist/src/salience.js.map +0 -1
  477. package/dist/src/scheduler.js +0 -77
  478. package/dist/src/scheduler.js.map +0 -1
  479. package/dist/src/scope.js +0 -35
  480. package/dist/src/scope.js.map +0 -1
  481. package/dist/src/search.js +0 -1003
  482. package/dist/src/search.js.map +0 -1
  483. package/dist/src/secret-detect.js +0 -95
  484. package/dist/src/secret-detect.js.map +0 -1
  485. package/dist/src/server-detect.js +0 -231
  486. package/dist/src/server-detect.js.map +0 -1
  487. package/dist/src/server.js +0 -3255
  488. package/dist/src/server.js.map +0 -1
  489. package/dist/src/shared.js +0 -502
  490. package/dist/src/shared.js.map +0 -1
  491. package/dist/src/skills.js +0 -346
  492. package/dist/src/skills.js.map +0 -1
  493. package/dist/src/sleep-redact.js +0 -80
  494. package/dist/src/sleep-redact.js.map +0 -1
  495. package/dist/src/sso.js +0 -22
  496. package/dist/src/sso.js.map +0 -1
  497. package/dist/src/stdin.js +0 -41
  498. package/dist/src/stdin.js.map +0 -1
  499. package/dist/src/store.js +0 -3749
  500. package/dist/src/store.js.map +0 -1
  501. package/dist/src/tenant.js +0 -17
  502. package/dist/src/tenant.js.map +0 -1
  503. package/dist/src/trace.js +0 -72
  504. package/dist/src/trace.js.map +0 -1
  505. package/dist/src/version.js +0 -40
  506. package/dist/src/version.js.map +0 -1
  507. package/dist/src/working-memory.js +0 -157
  508. package/dist/src/working-memory.js.map +0 -1
  509. package/dist/src/yaml.js +0 -107
  510. package/dist/src/yaml.js.map +0 -1
  511. package/dist/sso.d.ts +0 -13
  512. package/dist/sso.d.ts.map +0 -1
  513. package/dist/sso.js +0 -22
  514. package/dist/sso.js.map +0 -1
  515. package/dist/stdin.d.ts.map +0 -1
  516. package/dist/stdin.js.map +0 -1
  517. package/dist/store.d.ts.map +0 -1
  518. package/dist/store.js.map +0 -1
  519. package/dist/tenant.d.ts.map +0 -1
  520. package/dist/tenant.js.map +0 -1
  521. package/dist/trace.d.ts.map +0 -1
  522. package/dist/trace.js.map +0 -1
  523. package/dist/version.d.ts.map +0 -1
  524. package/dist/version.js.map +0 -1
  525. package/dist/working-memory.d.ts.map +0 -1
  526. package/dist/working-memory.js.map +0 -1
  527. package/dist/yaml.d.ts.map +0 -1
  528. package/dist/yaml.js.map +0 -1
@@ -1,1298 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Hippo Memory MCP Server
4
- *
5
- * Exposes hippo memory as MCP tools over stdio transport.
6
- * Uses the programmatic API directly (no child process spawning).
7
- *
8
- * Usage: hippo mcp (or npx hippo-memory mcp)
9
- */
10
- import * as fs from 'fs';
11
- import * as path from 'path';
12
- import { createMemory, Layer, calculateStrength, } from '../memory.js';
13
- import { hybridSearch, physicsSearch, markRetrieved } from '../search.js';
14
- import { isRecallBoostAblated, evalNow } from '../ablation.js';
15
- import { loadAllEntries, writeEntry, readEntry, loadFreshActiveTaskSnapshot, listMemoryConflicts, resolveConflict, RECALL_DEFAULT_DENY_SCOPES } from '../store.js';
16
- import { shareMemory, listPeers, getGlobalRoot } from '../shared.js';
17
- import { consolidate } from '../consolidate.js';
18
- import { execSync } from 'child_process';
19
- import { fetchGitLog, extractLessons, partitionLessons, deduplicateLesson, isGitRepo } from '../autolearn.js';
20
- import { loadConfig } from '../config.js';
21
- import { confidenceLabel } from '../memory.js';
22
- import { resolveTenantId } from '../tenant.js';
23
- import { recall as apiRecall, remember as apiRemember, outcome as apiOutcome, drillDown as apiDrillDown, assemble as apiAssemble, isPrivateScope, passesScopeFilterForRecall, adminActor, buildSuppressionSummary, ambientSecretAdmit } from '../api.js';
24
- import { resolveProjectIdentity, classifyOriginProject, findHippoStoreDir } from '../project-identity.js';
25
- import { computePredictionBaserate } from '../predictions.js';
26
- import { appendAuditEvent } from '../audit.js';
27
- import { RejectedValueError } from '../rejection.js';
28
- import { createHash } from 'node:crypto';
29
- import { detectAnchoring, hashQueryText, buildSessionKey, getOrCreateRing, appendRecall, snapshotRing, } from '../recall-history.js';
30
- import { detectAvailabilityBias } from '../availability.js';
31
- // v0.33 / J1 — Module-level per-(tenant, session) recall-history ring map
32
- // for the MCP pipeline. Separate from CLI/HTTP rings per plan v3
33
- // architecture (per-pipeline rings; no IPC).
34
- const sessionRecallHistoryMcp = new Map();
35
- /** Test-only: reset the module-level recall-history Map. Call from beforeEach. */
36
- export function __resetSessionRecallHistoryMcp() {
37
- sessionRecallHistoryMcp.clear();
38
- }
39
- import { applyGoalStackBoost } from '../goals.js';
40
- import { openHippoDb, closeHippoDb } from '../db.js';
41
- import { PACKAGE_VERSION } from '../version.js';
42
- // ── Find hippo root ──
43
- /** Same bounded walk as the CLI (ends at home, so HIPPO_HOME wins over ~/.hippo); cwd/opts are the test seam. */
44
- export function findHippoRoot(cwd = process.cwd(), opts) {
45
- const local = findHippoStoreDir(cwd, opts);
46
- if (local !== null)
47
- return local;
48
- // Global fallback (respects $HIPPO_HOME / $XDG_DATA_HOME)
49
- const global = getGlobalRoot();
50
- return fs.existsSync(global) ? global : null;
51
- }
52
- // MCP stdio transport spec: messages are newline-delimited JSON-RPC, no embedded newlines.
53
- // https://modelcontextprotocol.io/specification/.../basic/transports#stdio
54
- function send(msg) {
55
- process.stdout.write(JSON.stringify(msg) + '\n');
56
- }
57
- function isJsonString(v) {
58
- return typeof v === 'string';
59
- }
60
- function isJsonBoolean(v) {
61
- return typeof v === 'boolean';
62
- }
63
- function isJsonObjectRecord(v) {
64
- return v !== undefined && v !== null && typeof v === 'object' && !Array.isArray(v);
65
- }
66
- import { formatHandoffEvidenceLine } from '../handoff.js';
67
- function formatContinuityBlock(block) {
68
- const lines = ['## Continuity'];
69
- if (block.activeSnapshot) {
70
- lines.push('');
71
- lines.push('### Active Task Snapshot');
72
- lines.push(`- Task: ${block.activeSnapshot.task}`);
73
- lines.push(`- Summary: ${block.activeSnapshot.summary}`);
74
- lines.push(`- Next: ${block.activeSnapshot.next_step}`);
75
- }
76
- if (block.sessionHandoff) {
77
- lines.push('');
78
- lines.push('### Session Handoff');
79
- lines.push(`- Summary: ${block.sessionHandoff.summary}`);
80
- if (block.sessionHandoff.nextAction) {
81
- lines.push(`- Next action: ${block.sessionHandoff.nextAction}`);
82
- }
83
- if ((block.sessionHandoff.artifacts ?? []).length > 0) {
84
- lines.push(`- Artifacts: ${(block.sessionHandoff.artifacts ?? []).join(', ')}`);
85
- }
86
- if (block.sessionHandoff.outcome) {
87
- lines.push(`- Outcome: ${block.sessionHandoff.outcome}`);
88
- }
89
- if (block.sessionHandoff.targetRuntime) {
90
- lines.push(`- Target runtime: ${block.sessionHandoff.targetRuntime}`);
91
- }
92
- if (block.sessionHandoff.cardId) {
93
- lines.push(`- Card: ${block.sessionHandoff.cardId}`);
94
- }
95
- if ((block.sessionHandoff.constraints ?? []).length > 0) {
96
- lines.push(`- Constraints: ${(block.sessionHandoff.constraints ?? []).join(', ')}`);
97
- }
98
- if (block.sessionHandoff.evidence) {
99
- lines.push(`- Evidence: ${formatHandoffEvidenceLine(block.sessionHandoff.evidence)}`);
100
- }
101
- }
102
- if (block.recentSessionEvents.length > 0) {
103
- lines.push('');
104
- lines.push('### Recent Session Trail');
105
- for (const e of block.recentSessionEvents) {
106
- const preview = e.content.length > 200 ? e.content.slice(0, 200) + '…' : e.content;
107
- lines.push(`- [${e.event_type}] ${preview}`);
108
- }
109
- }
110
- if (lines.length === 1) {
111
- lines.push('');
112
- lines.push('(no active task snapshot, handoff, or recent events for this tenant)');
113
- }
114
- return lines.join('\n');
115
- }
116
- function formatMemories(results, hippoRoot) {
117
- if (results.length === 0)
118
- return 'No relevant memories found.';
119
- const config = loadConfig(hippoRoot);
120
- const lines = [`Found ${results.length} memories:\n`];
121
- for (const r of results) {
122
- const conf = confidenceLabel(r.entry).text;
123
- const tags = r.entry.tags.length > 0 ? ` tags: ${r.entry.tags.join(', ')}` : '';
124
- lines.push(`[${conf}]${tags} (strength=${r.entry.strength.toFixed(2)})`);
125
- lines.push(r.entry.content);
126
- lines.push('');
127
- }
128
- return lines.join('\n');
129
- }
130
- // ── Tool definitions ──
131
- const TOOLS = [
132
- {
133
- name: 'hippo_recall',
134
- description: 'Retrieve relevant memories from the project memory store. Returns memories ranked by relevance, strength, and recency within the token budget. Use at session start or when you need context about a topic. Pass include_continuity=true to also surface the active task snapshot, latest matching session handoff, and recent session events as a "## Continuity" appendix.',
135
- inputSchema: {
136
- type: 'object',
137
- properties: {
138
- query: { type: 'string', description: 'What to search for in memory (natural language)' },
139
- budget: { type: 'number', description: 'Max tokens to return (default: 1500)' },
140
- include_continuity: {
141
- type: 'boolean',
142
- description: 'Append continuity context (active snapshot + handoff + last 5 session events) below the memory results. Useful at session boot.',
143
- },
144
- scope: {
145
- type: 'string',
146
- description: 'Restrict results and continuity to memories/rows matching this scope exactly. When omitted, default-deny applies to ANY <source>:private:* (slack, github, ...) and unknown-legacy rows.',
147
- },
148
- fresh_tail_count: {
149
- type: 'number',
150
- description: 'When > 0, surface the last N kind=raw rows tagged isFreshTail=true regardless of query match. Useful for "what did I just see" continuity. Capped at 200.',
151
- },
152
- fresh_tail_session_id: {
153
- type: 'string',
154
- description: 'Restrict the fresh-tail window to a specific session. Without this, fresh-tail is tenant-wide (legacy v1.5.2 behaviour, pre-v1.6.3 default).',
155
- },
156
- summarize_overflow: {
157
- type: 'boolean',
158
- description: 'When true (default), entries that overflow the limit and share a level-2 parent summary cause that summary to be appended in their place. Set false for strict-limit behaviour.',
159
- },
160
- scorer_window: {
161
- type: 'number',
162
- description: 'Candidate pool size that api.recall evaluates. Affects fresh-tail / summarize-overflow appendix paths and continuity hits. Note: the primary ranked block over MCP is driven by a separate physics/hybrid scorer over the full tenant store, so scorer_window does NOT narrow the main results — only the appendix. Default 200. Rejected as RecallContractError code=invalid_scorer_window if 0/negative/non-finite/non-numeric.',
163
- },
164
- session_id: {
165
- type: 'string',
166
- maxLength: 256,
167
- description: 'Optional session id (v1.7.4). When set AND (tenant, session) has active goals, applies the dlPFC goal-stack boost to the primary physics/hybrid result band before formatting AND to api.recall\'s primary BM25 band (so the audit + appendix paths see the same session). Mirrors fresh_tail_session_id shape (256-char cap).',
168
- },
169
- },
170
- required: ['query'],
171
- },
172
- },
173
- {
174
- name: 'hippo_assemble',
175
- description: 'Build a chronologically-ordered context window for a session. Returns ordered items: fresh-tail raw rows + level-2 summary substitutions for older rows + budget-fit. Hippo-additive vs lossless-claw: eviction picks lowest-strength non-fresh-tail items first instead of oldest-first. Tenant-scoped; default-deny on private scopes.',
176
- inputSchema: {
177
- type: 'object',
178
- properties: {
179
- session_id: {
180
- type: 'string',
181
- description: 'Session identifier. Returns clean empty result if no kind=raw rows match.',
182
- },
183
- budget: {
184
- type: 'number',
185
- description: 'Token budget for the assembled context (default 4000). Eviction kicks in over budget.',
186
- },
187
- fresh_tail_count: {
188
- type: 'number',
189
- description: 'Recent raw rows always kept verbatim (default 10). These are never evicted.',
190
- },
191
- summarize_older: {
192
- type: 'boolean',
193
- description: 'When true (default), older raws sharing a level-2 parent summary get substituted. Set false to keep every older raw as-is.',
194
- },
195
- scope: {
196
- type: 'string',
197
- description: 'Restrict to memories whose scope matches exactly. When omitted, default-deny applies to ANY <source>:private:* scope and unknown:legacy rows. Pass an explicit scope to assemble a private session with consent.',
198
- },
199
- },
200
- required: ['session_id'],
201
- },
202
- },
203
- {
204
- name: 'hippo_drill',
205
- description: 'Walk one step down the DAG from a level-2+ topic summary to its direct children. Companion to hippo_recall: when recall returns an item with isSummary=true and substitutedFor=[ids], pass the summary id here to recover the original detail. Tenant-scoped; default-deny on private scopes.',
206
- inputSchema: {
207
- type: 'object',
208
- properties: {
209
- summary_id: {
210
- type: 'string',
211
- description: 'ID of the level-2 (or higher) summary to drill into. Must be a summary, not a leaf — leaves are not drillable.',
212
- },
213
- limit: {
214
- type: 'number',
215
- description: 'Max children to return (default 50).',
216
- },
217
- budget: {
218
- type: 'number',
219
- description: 'Max total token cost (~ chars/4) of returned children. Truncates chronologically.',
220
- },
221
- depth: {
222
- type: 'integer',
223
- minimum: 1,
224
- maximum: 10,
225
- description: 'v0.30 / E5: walk N levels down (default 1 = direct children only). Higher values include children of children. Token budget remains GLOBAL across levels. Hard cap 10.',
226
- },
227
- },
228
- required: ['summary_id'],
229
- },
230
- },
231
- {
232
- name: 'hippo_remember',
233
- description: 'Store a new memory. Use when you learn something non-obvious, hit an error, or discover a useful pattern. Memories decay over time unless retrieved. Errors get 2x half-life.',
234
- inputSchema: {
235
- type: 'object',
236
- properties: {
237
- text: {
238
- type: 'string',
239
- description: 'The memory to store (1-2 sentences, specific and concrete)',
240
- },
241
- error: { type: 'boolean', description: 'Mark as error memory (doubles half-life)' },
242
- pin: { type: 'boolean', description: 'Pin memory (never decays)' },
243
- tag: { type: 'string', description: 'Optional tag for categorization' },
244
- },
245
- required: ['text'],
246
- },
247
- },
248
- {
249
- name: 'hippo_outcome',
250
- description: 'Report whether recalled memories were useful. Strengthens good memories (+5 days half-life) and weakens bad ones (-3 days). Call after completing work.',
251
- inputSchema: {
252
- type: 'object',
253
- properties: {
254
- good: {
255
- type: 'boolean',
256
- description: 'true = memories helped, false = memories were irrelevant',
257
- },
258
- },
259
- required: ['good'],
260
- },
261
- },
262
- {
263
- name: 'hippo_context',
264
- description: 'Smart context injection: auto-detects current task from git state and returns relevant memories plus the active task snapshot. Use at the start of any session. Memories and snapshot are scope-filtered: a no-scope caller does NOT see ANY <source>:private:* (slack, github, ...) or legacy-quarantine rows.',
265
- inputSchema: {
266
- type: 'object',
267
- properties: {
268
- budget: { type: 'number', minimum: 0, description: 'Max tokens (default: 1500)' },
269
- scope: {
270
- type: 'string',
271
- description: 'Restrict memories and snapshot to this scope exactly. When omitted, default-deny applies to ANY <source>:private:* (slack, github, ...) and unknown-legacy rows.',
272
- },
273
- },
274
- },
275
- },
276
- {
277
- name: 'hippo_status',
278
- description: 'Check memory health: counts, strengths, at-risk memories, last consolidation time.',
279
- inputSchema: {
280
- type: 'object',
281
- properties: {},
282
- },
283
- },
284
- {
285
- name: 'hippo_learn',
286
- description: 'Scan recent git commits for lessons from fix/revert/bug/refactor/perf patterns. Run after coding sessions.',
287
- inputSchema: {
288
- type: 'object',
289
- properties: {
290
- days: { type: 'number', description: 'Days to scan back (default: 7)' },
291
- },
292
- },
293
- },
294
- {
295
- name: 'hippo_conflicts',
296
- description: 'List open memory conflicts — contradictory memories that need resolution.',
297
- inputSchema: {
298
- type: 'object',
299
- properties: {},
300
- },
301
- },
302
- {
303
- name: 'hippo_resolve',
304
- description: 'Resolve a memory conflict by keeping one memory and weakening or deleting the other.',
305
- inputSchema: {
306
- type: 'object',
307
- properties: {
308
- conflict_id: { type: 'number', description: 'The conflict ID to resolve' },
309
- keep: { type: 'string', description: 'ID of the memory to keep' },
310
- forget: { type: 'boolean', description: 'Delete the loser instead of weakening (default: false)' },
311
- rejectLoser: { type: 'boolean', description: 'Tombstone the loser\'s value too, so it refuses re-ingestion (implies removal; default: false)' },
312
- reason: { type: 'string', description: 'Reason recorded on the tombstone when rejectLoser is set (default: a conflict-context string)' },
313
- },
314
- required: ['conflict_id', 'keep'],
315
- },
316
- },
317
- {
318
- name: 'hippo_share',
319
- description: 'Share a memory to the global store for cross-project use with transfer scoring.',
320
- inputSchema: {
321
- type: 'object',
322
- properties: {
323
- id: { type: 'string', description: 'Memory ID to share' },
324
- force: { type: 'boolean', description: 'Share even if transfer score is low' },
325
- },
326
- required: ['id'],
327
- },
328
- },
329
- {
330
- name: 'hippo_peers',
331
- description: 'List all projects that have contributed memories to the global shared store.',
332
- inputSchema: {
333
- type: 'object',
334
- properties: {},
335
- },
336
- },
337
- {
338
- name: 'hippo_predict_baserate',
339
- description: 'J3 reference-class / planning-fallacy detector. Get base-rate stats for closed predictions in a class. Call this when you make a forward-looking claim (effort estimate, rollout risk, deadline) to anchor on the past track record rather than the inside view. Returns count + mean estimate + mean actual + mean ratio + median ratio + MAE + a human-readable summary. Tenant-scoped.',
340
- inputSchema: {
341
- type: 'object',
342
- properties: {
343
- class_tag: {
344
- type: 'string',
345
- description: 'Cohort label, e.g. "migration-effort", "rollout-risk", "deadline-week". Must match the class_tag used when the predictions were created via hippo_predict (or `hippo predict ...`).',
346
- },
347
- },
348
- required: ['class_tag'],
349
- },
350
- },
351
- ];
352
- // ── Track last recalled IDs for outcome feedback ──
353
- //
354
- // Keyed per-client so two HTTP-MCP clients hitting the same tenant cannot
355
- // poison each other's outcome feedback. The key is `ctx.clientKey` when the
356
- // transport supplies one (HTTP-MCP via src/server.ts builds
357
- // hash(bearer+remoteAddr)); stdio and any caller without a clientKey falls
358
- // back to `'stdio-${pid}'` (one process = one client) or
359
- // `${tenantId}:default` if a McpContext is constructed in tests without a
360
- // pid-bound transport.
361
- const lastRecalledIds = new Map();
362
- function resolveClientKey(ctx) {
363
- if (ctx?.clientKey)
364
- return ctx.clientKey;
365
- if (ctx?.tenantId)
366
- return `stdio-${process.pid}:${ctx.tenantId}`;
367
- return `stdio-${process.pid}:default`;
368
- }
369
- // ── Tool execution ──
370
- async function executeTool(name, args, ctx) {
371
- // When a transport hands us a context (HTTP path), trust it: the HTTP
372
- // server already resolved hippoRoot from its bound opts and tenantId
373
- // from the Bearer token (or the loopback fallback). The stdio path
374
- // continues to walk from cwd / fall back to the global root, and to
375
- // resolve tenant from HIPPO_TENANT.
376
- const hippoRoot = ctx?.hippoRoot ?? findHippoRoot();
377
- if (!hippoRoot)
378
- return 'No .hippo/ store found. Run: hippo init';
379
- const config = loadConfig(hippoRoot);
380
- // A5: every loadAllEntries() in this server returns to the caller and is
381
- // tenant-isolated. Resolved once per tool call: prefer the transport's
382
- // ctx.tenantId so an HTTP Bearer for tenant B doesn't drop to HIPPO_TENANT.
383
- const tenantId = ctx?.tenantId ?? resolveTenantId({});
384
- switch (name) {
385
- case 'hippo_recall': {
386
- const query = String(args.query || '');
387
- const budget = Number(args.budget) || config.defaultBudget;
388
- const includeContinuity = Boolean(args.include_continuity);
389
- const explicitScope = isJsonString(args.scope) && args.scope.length > 0
390
- ? args.scope
391
- : undefined;
392
- const freshTailCountArg = Number(args.fresh_tail_count);
393
- const freshTailCount = Number.isFinite(freshTailCountArg) && freshTailCountArg > 0
394
- ? freshTailCountArg
395
- : undefined;
396
- const freshTailSessionId = isJsonString(args.fresh_tail_session_id) && args.fresh_tail_session_id.length > 0
397
- ? args.fresh_tail_session_id
398
- : undefined;
399
- const summarizeOverflow = isJsonBoolean(args.summarize_overflow)
400
- ? args.summarize_overflow
401
- : undefined;
402
- // v1.7.2 T4 — scorer_window: Number-coerce so non-numeric input
403
- // (string 'abc', boolean, etc.) reaches api.recall() and produces
404
- // the same typed RecallContractError(code='invalid_scorer_window')
405
- // as HTTP. Codex CRITICAL[2]: do NOT use `typeof === 'number'` — that
406
- // would silently default-200 on string `"5"` while HTTP 400s on the
407
- // same value. Both transports must agree.
408
- const scorerWindow = args.scorer_window === undefined
409
- ? undefined
410
- : Number(args.scorer_window);
411
- // v1.7.4 -- session_id for the dlPFC goal-stack boost. Mirrors
412
- // fresh_tail_session_id shape: trim, 256-char cap. When set and the
413
- // (tenant, session) has active goals, the boost is applied (a) inside
414
- // api.recall on its primary BM25 band (so the audit + fresh-tail /
415
- // summary appendix paths see consistent ranking), and (b) below on the
416
- // physics/hybrid result list before formatMemories (since MCP's
417
- // user-visible primary ordering does NOT come from api.recall).
418
- const sessionIdRaw = isJsonString(args.session_id) ? args.session_id.trim() : '';
419
- const sessionId = sessionIdRaw.length > 0 && sessionIdRaw.length <= 256
420
- ? sessionIdRaw
421
- : undefined;
422
- const apiCtx = {
423
- hippoRoot,
424
- tenantId,
425
- actor: adminActor(ctx?.actor ?? 'mcp'),
426
- };
427
- // Route through api.recall for audit + (when requested) continuity block.
428
- // api.recall already applies the same default-deny / exact-match rules
429
- // we want here, so its continuity output is the source of truth.
430
- // RecallContractError throws propagate raw to the MCP caller (per the
431
- // v1.6.5 F5 contract documented in mcp-recall-fresh-tail-policy.test.ts).
432
- const recallExtra = {};
433
- if (freshTailCount !== undefined)
434
- recallExtra.freshTailCount = freshTailCount;
435
- if (freshTailSessionId !== undefined)
436
- recallExtra.freshTailSessionId = freshTailSessionId;
437
- if (summarizeOverflow !== undefined)
438
- recallExtra.summarizeOverflow = summarizeOverflow;
439
- if (scorerWindow !== undefined)
440
- recallExtra.scorerWindow = scorerWindow;
441
- if (sessionId !== undefined)
442
- recallExtra.sessionId = sessionId;
443
- const apiResult = apiRecall(apiCtx, {
444
- query,
445
- limit: 50,
446
- scope: explicitScope,
447
- includeContinuity,
448
- // v1.13.x / J2 — MCP computes its OWN availability hint over the
449
- // physics/hybrid result set below; suppress api.recall's BM25-band copy
450
- // so one MCP recall does not emit recall_availability_detected twice.
451
- suppressAvailabilityHint: true,
452
- // LC1 F2 fix — MCP's user-visible primary ordering comes from the
453
- // physics/hybrid scorer below, NOT this api.recall call's BM25 band
454
- // (see the comment above apiRecall). Tracing this call as pipeline
455
- // 'api' would mislabel training data with ids/ranks/scores the user
456
- // never actually saw. Real MCP tracing is the reserved 'mcp'
457
- // pipeline value (schema v40) — a follow-up, not v1 scope.
458
- suppressRecallTrace: true,
459
- ...recallExtra,
460
- });
461
- // Existing physics/hybrid scorer continues to drive user-visible
462
- // ordering and the strength bump on retrieval. Apply the same scope
463
- // rule as api.recall: explicit scope = exact match; no scope =
464
- // default-deny on ANY `<source>:private:*` AND 'unknown:legacy'.
465
- // v1.2.1: generic-private check via api.isPrivateScope.
466
- const allEntries = loadAllEntries(hippoRoot, tenantId);
467
- // v1.12.13 / C5 — WYSIATI counters for the MCP physics/hybrid pipeline.
468
- // Per the plan-eng-critic round 1 CRIT resolution: MCP's user-visible
469
- // memory list comes from THIS pipeline (loadAllEntries -> scope filter
470
- // -> physicsSearch/hybridSearch), NOT from apiResult. The MCP
471
- // suppressionSummary must describe what the user actually sees, so we
472
- // track filter activity here and replace apiResult.suppressionSummary
473
- // in the user-facing response.
474
- const totalCandidatesCountMcp = allEntries.length;
475
- const entries = explicitScope
476
- ? allEntries.filter((e) => e.scope === explicitScope)
477
- : allEntries.filter((e) => {
478
- const s = e.scope ?? null;
479
- if (s === null)
480
- return true;
481
- if (isPrivateScope(s))
482
- return false;
483
- // v1.7.2: read from RECALL_DEFAULT_DENY_SCOPES (single source of truth
484
- // shared with SQL clause + passesScopeFilterForRecall).
485
- if (RECALL_DEFAULT_DENY_SCOPES.some((deny) => deny === s))
486
- return false;
487
- return true;
488
- });
489
- const droppedPreRankCountMcp = allEntries.length - entries.length;
490
- const usePhysics = config.physics?.enabled !== false;
491
- let results = usePhysics
492
- ? await physicsSearch(query, entries, { budget, hippoRoot, physicsConfig: config.physics })
493
- : await hybridSearch(query, entries, { budget, hippoRoot });
494
- // v1.12.13 / C5 — droppedByBudget for MCP is an UPPER BOUND. The
495
- // difference (entries.length - results.length) lumps three things
496
- // together: rows hybridSearch/physicsSearch internally dropped because
497
- // they scored zero (didn't match the query at all), rows the search
498
- // engine filtered internally (e.g. superseded when --include-
499
- // superseded isn't set), and rows that genuinely didn't fit the
500
- // `budget` token cap. The honest fix needs hybridSearch/physicsSearch
501
- // to expose their pre-budget-cut scored-count. Until then this is an
502
- // upper bound that conflates "not relevant" with "no budget" on
503
- // no-match / sparse-match queries. Plan-eng-critic round 1 MED and
504
- // codex-review-critic P2 both flagged this; documented + tracked as
505
- // a v1.12.14 follow-up. Independent-review-critic and code-review-
506
- // critic both graded as non-blocking for v1.12.13 ship.
507
- // TODO(c5.1): expose scoredCount from hybridSearch/physicsSearch and
508
- // compute droppedByBudget = scoredCount - results.length, with the
509
- // remainder (entries.length - scoredCount) attributed to
510
- // droppedPreRank or a new "noQueryMatch" counter.
511
- const droppedByBudgetCountMcp = Math.max(0, entries.length - results.length);
512
- // v1.7.4 -- dlPFC goal-stack boost on the MCP physics/hybrid result
513
- // list BEFORE formatMemories. MCP's user-visible primary ordering does
514
- // NOT come from api.recall (apiResult above), so the boost has to run
515
- // here too. Helper signature accepts any { entry, score } shape; the
516
- // physics/hybrid result rows are already in that shape.
517
- if (sessionId !== undefined) {
518
- const dbForBoost = openHippoDb(hippoRoot);
519
- try {
520
- results = applyGoalStackBoost(dbForBoost, results, {
521
- sessionId,
522
- tenantId,
523
- limit: results.length,
524
- });
525
- }
526
- finally {
527
- closeHippoDb(dbForBoost);
528
- }
529
- }
530
- // Mark retrieved and persist
531
- const retrieved = markRetrieved(results.map((r) => r.entry));
532
- // EVAL-ONLY ablation (see ablation.ts): skip persistence under the recall
533
- // flag; ids below stay populated for outcome attribution.
534
- if (!isRecallBoostAblated()) {
535
- for (const entry of retrieved)
536
- writeEntry(hippoRoot, entry, { actor: ctx?.actor ?? 'mcp' });
537
- }
538
- lastRecalledIds.set(resolveClientKey(ctx), retrieved.map((e) => e.id));
539
- // v0.33 / J1 — MCP per-pipeline anchoring detector. UNLIKE J3.2's
540
- // planningFallacyHint (which is pipeline-invariant because it
541
- // depends only on queryText + predictions table state), the
542
- // anchoring hint depends on (a) per-pipeline top-1 ranking (MCP's
543
- // physics/hybrid winner can differ from api.recall's BM25 winner)
544
- // and (b) per-pipeline ring buffer. So MCP computes its OWN hint
545
- // against MCP's own top-1, mirroring the C5 per-pipeline rule.
546
- let mcpAnchoringHint = null;
547
- if (process.env.HIPPO_ANCHORING !== 'off') {
548
- if (sessionId) {
549
- const ringKey = buildSessionKey(tenantId, sessionId);
550
- const ring = getOrCreateRing(sessionRecallHistoryMcp, ringKey);
551
- const queryHash = hashQueryText(query);
552
- const topId = results[0]?.entry.id ?? null;
553
- mcpAnchoringHint = detectAnchoring(snapshotRing(ring), queryHash, topId);
554
- appendRecall(ring, queryHash, topId, mcpAnchoringHint?.memoryId);
555
- // Pipeline-local audit emission (lockstep with CLI / api.recall).
556
- if (mcpAnchoringHint?.reason === 'memory_dominance') {
557
- const dbForAudit = openHippoDb(hippoRoot);
558
- try {
559
- appendAuditEvent(dbForAudit, {
560
- tenantId,
561
- actor: ctx?.actor ?? 'mcp',
562
- op: 'recall_anchor_detected_memory_dominance',
563
- targetId: mcpAnchoringHint.memoryId,
564
- metadata: {
565
- memory_id: mcpAnchoringHint.memoryId,
566
- query_count: mcpAnchoringHint.queryCount ?? null,
567
- },
568
- });
569
- }
570
- finally {
571
- closeHippoDb(dbForAudit);
572
- }
573
- }
574
- else if (mcpAnchoringHint?.reason === 'query_repeat') {
575
- const dbForAudit = openHippoDb(hippoRoot);
576
- try {
577
- appendAuditEvent(dbForAudit, {
578
- tenantId,
579
- actor: ctx?.actor ?? 'mcp',
580
- op: 'recall_anchor_detected_query_repeat',
581
- targetId: mcpAnchoringHint.memoryId,
582
- metadata: { memory_id: mcpAnchoringHint.memoryId },
583
- });
584
- }
585
- finally {
586
- closeHippoDb(dbForAudit);
587
- }
588
- }
589
- }
590
- else {
591
- // Telemetry: caller had no sessionId so ring tracking skipped.
592
- // Per the recall-audit convention at api.ts:854, use SHA-256/16
593
- // for prompt hashing (NOT hashQueryText which is FNV-1a 32-bit
594
- // for recall matching; brute-force trivial for low-entropy
595
- // queries). Codex round-2 P2 catch.
596
- const dbForAudit = openHippoDb(hippoRoot);
597
- try {
598
- appendAuditEvent(dbForAudit, {
599
- tenantId,
600
- actor: ctx?.actor ?? 'mcp',
601
- op: 'recall_anchor_skipped_no_session',
602
- targetId: undefined,
603
- metadata: {
604
- query_hash: createHash('sha256').update(query).digest('hex').slice(0, 16),
605
- query_length: query.length,
606
- },
607
- });
608
- }
609
- finally {
610
- closeHippoDb(dbForAudit);
611
- }
612
- }
613
- }
614
- // v0.32 / J3.2 — auto-injection of reference-class baserate hint
615
- // when the query carries a forward-prediction phrase. Read from
616
- // apiResult.planningFallacyHint (already computed inside api.recall
617
- // with the caller identity threaded through ctx.actor.subject -
618
- // auth-resolved actor under HTTP-MCP, 'mcp' for stdio). The hint is
619
- // pipeline-INVARIANT — same (hippoRoot, tenantId, query) inputs
620
- // produce the same hint regardless of which downstream search
621
- // pipeline (api.recall band vs physics/hybrid) renders the memory
622
- // list, so re-computing here would double the audit emission for
623
- // identical telemetry. C5 per-pipeline rule does NOT apply here
624
- // because the hint depends on queryText, not on the matched memory
625
- // set. Prepend BEFORE the memory list so the agent sees it first.
626
- // v0.33 / J1: Anchoring hint goes ABOVE planning-fallacy hint
627
- // (anchoring is the stronger cognitive-pull warning).
628
- // v1.13.3 / C5 follow-up — Build MCP-pipeline suppressionSummary BEFORE
629
- // the response is assembled so the Cutoff block can render at TOP
630
- // alongside the other Track J hints. The dogfood
631
- // (docs/dogfood/2026-05-27-track-j-warnings.md) showed the v1.13.0-v1.13.2
632
- // bottom-placement was dark: a fresh sub-agent summarised the visible
633
- // memories with zero mention of the dropped pool. Top-placement + plain-
634
- // English rewrite fixes the read-rate without any system-prompt addendum.
635
- const physicsIds = new Set(results.map((r) => r.entry.id));
636
- const tailOrSummary = apiResult.results.filter((r) => (r.isFreshTail || r.isSummary) && !physicsIds.has(r.id));
637
- const freshTailAddedMcp = tailOrSummary.filter((r) => r.isFreshTail && !r.isSummary).length;
638
- const summarySubsAddedMcp = tailOrSummary.filter((r) => r.isSummary).length;
639
- // v0.33 / J1: suppressedByInterference bumped on MCP's R2 fire.
640
- const mcpSuppressedByInterference = mcpAnchoringHint?.reason === 'memory_dominance' ? 1 : 0;
641
- const mcpSuppressionSummary = buildSuppressionSummary({
642
- totalCandidates: totalCandidatesCountMcp,
643
- droppedPreRank: droppedPreRankCountMcp,
644
- droppedByBudget: droppedByBudgetCountMcp,
645
- summarySubstitutionsAdded: summarySubsAddedMcp,
646
- freshTailAdded: freshTailAddedMcp,
647
- suppressedByInterference: mcpSuppressedByInterference,
648
- });
649
- // v1.13.x / J2 — MCP per-pipeline availability/recency-bias detector.
650
- // Like the anchoring hint above (and unlike J3.2's pipeline-invariant
651
- // planningFallacyHint), this depends on MCP's OWN returned top-K and the
652
- // scope-filtered candidate pool (entries) it was drawn from, so MCP
653
- // computes its own hint here. Soft warning only. Gated by
654
- // HIPPO_AVAILABILITY=off; audit emission is pipeline-local (actor =
655
- // auth-resolved ctx.actor under HTTP-MCP, 'mcp' for stdio).
656
- let mcpAvailabilityHint = null;
657
- if (process.env.HIPPO_AVAILABILITY !== 'off') {
658
- mcpAvailabilityHint = detectAvailabilityBias({
659
- topK: results.map((r) => ({ id: r.entry.id, created: r.entry.created })),
660
- pool: entries.map((e) => ({ id: e.id, created: e.created })),
661
- });
662
- if (mcpAvailabilityHint) {
663
- const dbForAudit = openHippoDb(hippoRoot);
664
- try {
665
- appendAuditEvent(dbForAudit, {
666
- tenantId,
667
- actor: ctx?.actor ?? 'mcp',
668
- op: 'recall_availability_detected',
669
- metadata: {
670
- recent_fraction: mcpAvailabilityHint.recentFraction,
671
- older_passed_over: mcpAvailabilityHint.olderCandidatesPassedOver,
672
- returned_count: mcpAvailabilityHint.returnedCount,
673
- },
674
- });
675
- }
676
- finally {
677
- closeHippoDb(dbForAudit);
678
- }
679
- }
680
- }
681
- let response = '';
682
- if (mcpAnchoringHint) {
683
- response =
684
- `## Anchoring hint\n` +
685
- `${mcpAnchoringHint.summary}\n` +
686
- `[anchored_on: ${mcpAnchoringHint.memoryId}]\n` +
687
- `\n---\n\n`;
688
- }
689
- // v1.13.x / J2 — availability/recency-bias hint, rendered below the
690
- // anchoring hint and above the planning-fallacy hint. Soft warning only.
691
- if (mcpAvailabilityHint) {
692
- response += `## Availability bias\n${mcpAvailabilityHint.summary}\n\n---\n\n`;
693
- }
694
- if (apiResult.planningFallacyHint) {
695
- const h = apiResult.planningFallacyHint;
696
- const safePhrase = JSON.stringify(h.detectedPhrase);
697
- response +=
698
- `## Planning fallacy hint\n` +
699
- `Class: ${h.classTag}\n` +
700
- `${h.baserateSummary}\n` +
701
- `(detected: ${safePhrase})\n` +
702
- `\n---\n\n`;
703
- }
704
- else if (apiResult.planningFallacyWatching) {
705
- // v1.13.4 / J3.2 follow-up — surface the watching variant when
706
- // the regex matched but no baserate could be produced
707
- // (no_class_match / tiebreak). Mutually exclusive with the hint
708
- // block above. Suggestion text directs the user toward an action
709
- // (typically: tag a prediction class) that would unblock the
710
- // hint next time.
711
- const w = apiResult.planningFallacyWatching;
712
- const safePhrase = JSON.stringify(w.detectedPhrase);
713
- response +=
714
- `## Planning fallacy watch\n` +
715
- `Reason: ${w.reason}\n` +
716
- `${w.suggestion}\n` +
717
- `(detected: ${safePhrase})\n` +
718
- `\n---\n\n`;
719
- }
720
- // v1.13.3 / C5 follow-up — Cutoff block (was "WYSIATI:" line at bottom
721
- // in v1.13.0-v1.13.2). Top placement so the agent reads the cutoff
722
- // before scrolling the result list. "Cutoff" is plain English; the old
723
- // "WYSIATI:" acronym was opaque to agents without Kahneman context per
724
- // the 2026-05-27 dogfood Trial 1.
725
- const sMcp = mcpSuppressionSummary;
726
- const cutoffClauses = [];
727
- if (sMcp.droppedByBudget > 0)
728
- cutoffClauses.push(`${sMcp.droppedByBudget} dropped to fit limit`);
729
- if (sMcp.droppedPreRank > 0)
730
- cutoffClauses.push(`${sMcp.droppedPreRank} filtered pre-rank`);
731
- if (sMcp.summarySubstitutionsAdded > 0)
732
- cutoffClauses.push(`${sMcp.summarySubstitutionsAdded} summary substitutions added`);
733
- if (sMcp.freshTailAdded > 0)
734
- cutoffClauses.push(`${sMcp.freshTailAdded} fresh-tail added`);
735
- if (sMcp.suppressedByInterference > 0)
736
- cutoffClauses.push(`${sMcp.suppressedByInterference} suppressed by interference`);
737
- if (cutoffClauses.length > 0) {
738
- response +=
739
- `## Cutoff\n` +
740
- `Showing ${results.length} of ${sMcp.totalCandidates} candidates; ${cutoffClauses.join('; ')}.\n` +
741
- `\n---\n\n`;
742
- }
743
- response += formatMemories(results, hippoRoot);
744
- // v1.6.3 codex P2 fix. The physics/hybrid scorer drives the primary
745
- // ranked block above, so user-visible ordering is preserved. But
746
- // when the v1.5.0+/v1.5.2 RecallOpts are passed, we MUST also surface
747
- // the fresh-tail and substituted-summary items apiRecall produced —
748
- // otherwise the advertised MCP fields are silently ignored. Append
749
- // them as their own section, deduplicated against the physics ranking.
750
- if (tailOrSummary.length > 0) {
751
- const lines = ['', '## Fresh tail / substituted summaries'];
752
- for (const r of tailOrSummary) {
753
- const tag = r.isSummary ? '[summary]' : '[tail]';
754
- const head = r.content.length > 200 ? r.content.slice(0, 200) + '…' : r.content;
755
- if (r.isSummary && r.substitutedFor && r.substitutedFor.length > 0) {
756
- lines.push(`- ${tag} ${r.id} (covers ${r.substitutedFor.length} rows): ${head}`);
757
- }
758
- else {
759
- lines.push(`- ${tag} ${r.id}: ${head}`);
760
- }
761
- }
762
- response += '\n' + lines.join('\n');
763
- }
764
- if (includeContinuity && apiResult.continuity) {
765
- response += '\n\n' + formatContinuityBlock(apiResult.continuity);
766
- }
767
- return response;
768
- }
769
- case 'hippo_assemble': {
770
- const sessionId = String(args.session_id || '');
771
- if (!sessionId)
772
- return 'No session_id provided.';
773
- const budget = Number(args.budget);
774
- const freshTailCount = Number(args.fresh_tail_count);
775
- const summarizeOlder = args.summarize_older !== false;
776
- const apiCtx = {
777
- hippoRoot,
778
- tenantId,
779
- actor: adminActor(ctx?.actor ?? 'mcp'),
780
- };
781
- const explicitScope = isJsonString(args.scope) && args.scope.length > 0
782
- ? args.scope
783
- : undefined;
784
- const assembleExtra = {};
785
- if (Number.isFinite(budget) && budget > 0)
786
- assembleExtra.budget = budget;
787
- if (Number.isFinite(freshTailCount) && freshTailCount >= 0)
788
- assembleExtra.freshTailCount = freshTailCount;
789
- if (explicitScope !== undefined)
790
- assembleExtra.scope = explicitScope;
791
- const r = apiAssemble(apiCtx, sessionId, {
792
- summarizeOlder,
793
- ...assembleExtra,
794
- });
795
- const lines = [];
796
- lines.push(`Session ${r.sessionId} — ${r.items.length} items, ${r.tokens} tokens (raw=${r.totalRaw}, summarized=${r.summarized}, evicted=${r.evicted})`);
797
- for (const it of r.items) {
798
- const prefix = it.isSummary ? '[summary]' : it.isFreshTail ? '[tail]' : '[older]';
799
- lines.push(` ${prefix} ${it.createdAt} ${it.id} - ${it.content}`);
800
- }
801
- return lines.join('\n');
802
- }
803
- case 'hippo_drill': {
804
- const summaryId = String(args.summary_id || '');
805
- if (!summaryId)
806
- return 'No summary_id provided.';
807
- const limit = Number(args.limit);
808
- const budget = Number(args.budget);
809
- // v0.30 / E5: depth walks N levels (default 1, hard cap 10).
810
- // independent-review MED #5 fold: reject out-of-range explicitly
811
- // (no silent clamp) so MCP callers see the constraint at their layer.
812
- let depth;
813
- if (args.depth !== undefined) {
814
- const depthRaw = Number(args.depth);
815
- if (!Number.isInteger(depthRaw) || depthRaw < 1 || depthRaw > 10) {
816
- return `depth must be an integer between 1 and 10 (got ${args.depth})`;
817
- }
818
- depth = depthRaw;
819
- }
820
- const apiCtx = {
821
- hippoRoot,
822
- tenantId,
823
- actor: adminActor(ctx?.actor ?? 'mcp'),
824
- };
825
- const drillExtra = {};
826
- if (Number.isFinite(limit) && limit > 0)
827
- drillExtra.limit = limit;
828
- if (Number.isFinite(budget) && budget > 0)
829
- drillExtra.budget = budget;
830
- if (depth !== undefined)
831
- drillExtra.depth = depth;
832
- const r = apiDrillDown(apiCtx, summaryId, { ...drillExtra });
833
- if ('failure' in r) {
834
- // v1.6.4: only not_drillable is caller-actionable. not_found
835
- // intentionally collapses cross-tenant + scope-blocked + missing
836
- // (codex round 3 P1: distinguishing scope_blocked would leak
837
- // private-row existence on this surface).
838
- if (r.failure === 'not_drillable') {
839
- return `Id ${summaryId} is a leaf row, not a level-2+ summary; nothing to drill into.`;
840
- }
841
- return `No drillable summary at id=${summaryId}.`;
842
- }
843
- const lines = [];
844
- lines.push(`Summary ${r.summary.id} — ${r.summary.descendantCount} descendants${r.summary.earliestAt ? ` (${r.summary.earliestAt} -> ${r.summary.latestAt})` : ''}`);
845
- lines.push(` ${r.summary.content}`);
846
- lines.push('');
847
- lines.push(`Children (${r.children.length}/${r.totalChildren}${r.truncated ? ', truncated' : ''}):`);
848
- for (const c of r.children) {
849
- lines.push(` [L${c.dagLevel}] ${c.id} - ${c.content}`);
850
- }
851
- return lines.join('\n');
852
- }
853
- case 'hippo_predict_baserate': {
854
- // J3 reference-class / planning-fallacy detector. Reads from the E2
855
- // predictions table; returns text-only response matching the existing
856
- // MCP tool convention (no structured JSON over the wire). Direct call
857
- // to computePredictionBaserate; helper opens its own db + emits audit
858
- // (single source of truth, no caller-site drift).
859
- const classTag = String(args.class_tag || '').trim();
860
- if (!classTag)
861
- return 'No class_tag provided. Usage: pass class_tag matching a class used in past predictions (e.g. "migration-effort").';
862
- const baserate = computePredictionBaserate(hippoRoot, tenantId, classTag, ctx?.actor ?? 'mcp');
863
- if (baserate.nClosed === 0) {
864
- return `No closed predictions in class "${classTag}" yet. Create one via hippo_predict (or 'hippo predict ...' CLI) and close it with hippo_predict_close once the actual outcome is known. Base rates need closed predictions with numeric actual_value to compute.`;
865
- }
866
- const lines = [baserate.summary, ''];
867
- lines.push(`n_closed: ${baserate.nClosed}`);
868
- lines.push(`n_ratio_eligible: ${baserate.nRatioEligible}`);
869
- if (baserate.meanEstimate !== null)
870
- lines.push(`mean_estimate: ${baserate.meanEstimate.toFixed(3)}`);
871
- if (baserate.meanActual !== null)
872
- lines.push(`mean_actual: ${baserate.meanActual.toFixed(3)}`);
873
- if (baserate.meanRatio !== null)
874
- lines.push(`mean_ratio: ${baserate.meanRatio.toFixed(3)}x`);
875
- if (baserate.p50Ratio !== null)
876
- lines.push(`p50_ratio: ${baserate.p50Ratio.toFixed(3)}x`);
877
- if (baserate.mae !== null)
878
- lines.push(`mae: ${baserate.mae.toFixed(3)}`);
879
- return lines.join('\n');
880
- }
881
- case 'hippo_remember': {
882
- const text = String(args.text || '');
883
- if (!text)
884
- return 'No text provided.';
885
- const tags = [];
886
- if (args.error)
887
- tags.push('error');
888
- if (args.tag)
889
- tags.push(String(args.tag));
890
- // Route through api.ts so audit_log captures the caller identity
891
- // uniformly with CLI/REST: the auth-resolved ctx.actor under HTTP-MCP,
892
- // 'mcp' for stdio (no ctx). api.ts.remember writes the memory + audit
893
- // row in one transaction-friendly path; we re-read the entry to surface
894
- // the half-life used in the MCP human-readable response.
895
- const apiCtx = {
896
- hippoRoot,
897
- tenantId,
898
- actor: adminActor(ctx?.actor ?? 'mcp'),
899
- };
900
- const result = apiRemember(apiCtx, {
901
- content: text,
902
- tags,
903
- });
904
- const entry = readEntry(hippoRoot, result.id, tenantId);
905
- // Auto-sleep check
906
- if (config.autoSleep.enabled) {
907
- const allEntries = loadAllEntries(hippoRoot, tenantId);
908
- const recentCount = allEntries.filter((e) => {
909
- const age = (Date.now() - new Date(e.created).getTime()) / (1000 * 60 * 60);
910
- return age < 24; // created in last 24 hours
911
- }).length;
912
- if (recentCount >= config.autoSleep.threshold) {
913
- // Fire-and-forget by design (never block the remember response), but
914
- // an unhandled rejection here can kill the MCP server process — any
915
- // consolidate error (DB contention, or the memoryValue fail-loud
916
- // throw) must land as a logged line, not a crash.
917
- consolidate(hippoRoot).catch((err) => {
918
- console.error(`auto-sleep consolidate failed: ${err instanceof Error ? err.message : err}`);
919
- });
920
- }
921
- }
922
- const halfLife = entry?.half_life_days ?? config.defaultHalfLifeDays;
923
- const tagStr = entry?.tags.join(', ') || tags.join(', ') || 'none';
924
- return `Remembered [${result.id}] (half-life: ${halfLife}d, tags: ${tagStr})`;
925
- }
926
- case 'hippo_outcome': {
927
- const good = Boolean(args.good);
928
- const clientKey = resolveClientKey(ctx);
929
- const ids = lastRecalledIds.get(clientKey) ?? [];
930
- if (ids.length === 0)
931
- return 'No recent recalls to apply outcome to.';
932
- // Route through src/api.ts so audit_log captures the caller identity
933
- // (auth-resolved ctx.actor under HTTP-MCP, 'mcp' for stdio) and tenant
934
- // scoping is enforced uniformly (same surface as recall/remember).
935
- // outcome() also handles cross-tenant id skip silently.
936
- const apiCtx = {
937
- hippoRoot,
938
- tenantId,
939
- actor: adminActor(ctx?.actor ?? 'mcp'),
940
- };
941
- const { applied } = apiOutcome(apiCtx, ids, good);
942
- return `Applied ${good ? 'positive' : 'negative'} outcome to ${applied} memories`;
943
- }
944
- case 'hippo_context': {
945
- const budget = args.budget === undefined
946
- ? config.defaultContextBudget
947
- : Number(args.budget);
948
- if (!Number.isFinite(budget) || budget < 0)
949
- return 'budget must be a non-negative number.';
950
- if (budget === 0)
951
- return '';
952
- const explicitScope = isJsonString(args.scope) && args.scope.length > 0
953
- ? args.scope
954
- : undefined;
955
- // Auto-detect query from git
956
- let query = '';
957
- try {
958
- const branch = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', { encoding: 'utf-8' }).trim();
959
- const diff = execSync('git diff --cached --stat 2>/dev/null', { encoding: 'utf-8' }).trim();
960
- const log = execSync('git log -1 --pretty=format:"%s" 2>/dev/null', { encoding: 'utf-8' }).trim();
961
- query = [branch, log, diff].filter(Boolean).join(' ');
962
- }
963
- catch { /* not a git repo */ }
964
- if (!query)
965
- query = 'project context general';
966
- // v1.2 codex audit: same scope filter as hippo_recall on BOTH the memory
967
- // results and the snapshot. Pre-v1.2 this surface returned all memories
968
- // and the snapshot unfiltered, which would have leaked private-channel
969
- // content to no-scope MCP callers once scope writers shipped.
970
- const allEntries = loadAllEntries(hippoRoot, tenantId);
971
- // v39 memory scope isolation: this surface reads the LOCAL store only,
972
- // but synced-down or legacy rows can still carry another project's
973
- // origin, and secrets must never ambient-inject outside their owner.
974
- // Same policy as api.getContext; the scope filter above keeps this
975
- // surface's own explicit-scope exact-match semantics.
976
- //
977
- // Identity resolution handles both transports (codex rounds 4+5):
978
- // - The SERVED store is authoritative when it is a project store -
979
- // an HTTP /mcp daemon launched from anywhere still isolates the
980
- // project it serves.
981
- // - When the served store is the global root (stdio in a git repo
982
- // with no local .hippo falls back to it), dirname(store) is home
983
- // ('' would admit everything), so fall back to the launch cwd -
984
- // stdio servers launch in the project they serve.
985
- const mcpStoreIdentity = resolveProjectIdentity(path.dirname(path.resolve(hippoRoot)));
986
- const mcpProjectName = mcpStoreIdentity.name !== ''
987
- ? mcpStoreIdentity.name
988
- : resolveProjectIdentity(process.cwd()).name;
989
- const isolationOff = config.contextProjectIsolation === false;
990
- const entries = allEntries.filter((e) => {
991
- if (!passesScopeFilterForRecall(e.scope ?? null, explicitScope))
992
- return false;
993
- if (!ambientSecretAdmit(e, mcpProjectName))
994
- return false;
995
- if (isolationOff)
996
- return true;
997
- return classifyOriginProject(e.origin_project, mcpProjectName) !== 'cross-project';
998
- });
999
- const usePhysicsCtx = config.physics?.enabled !== false;
1000
- const results = usePhysicsCtx
1001
- ? await physicsSearch(query, entries, { budget, hippoRoot, physicsConfig: config.physics })
1002
- : await hybridSearch(query, entries, { budget, hippoRoot });
1003
- const retrieved = markRetrieved(results.map((r) => r.entry));
1004
- // EVAL-ONLY ablation (see ablation.ts): skip persistence under the recall
1005
- // flag; ids below stay populated for outcome attribution.
1006
- if (!isRecallBoostAblated()) {
1007
- for (const entry of retrieved)
1008
- writeEntry(hippoRoot, entry, { actor: ctx?.actor ?? 'mcp' });
1009
- }
1010
- lastRecalledIds.set(resolveClientKey(ctx), retrieved.map((e) => e.id));
1011
- // DF1 (docs/plans/2026-08-23-df1-snapshot-lifecycle.md, T2): bounded
1012
- // read, no session id available on this surface (freshness bound
1013
- // only) — an orphaned snapshot must age out here too, not just on the
1014
- // UserPromptSubmit path.
1015
- const rawSnapshot = loadFreshActiveTaskSnapshot(hippoRoot, tenantId);
1016
- const snapshot = rawSnapshot && passesScopeFilterForRecall(rawSnapshot.scope, explicitScope)
1017
- ? rawSnapshot
1018
- : null;
1019
- const snapshotText = snapshot
1020
- ? [
1021
- '## Active Task Snapshot',
1022
- `- Task: ${snapshot.task}`,
1023
- `- Status: ${snapshot.status}`,
1024
- `- Updated: ${snapshot.updated_at}`,
1025
- '',
1026
- '### Summary',
1027
- snapshot.summary,
1028
- '',
1029
- '### Next step',
1030
- snapshot.next_step,
1031
- '',
1032
- ].join('\n')
1033
- : '';
1034
- const memoryText = formatMemories(results, hippoRoot);
1035
- return snapshotText ? `${snapshotText}\n${memoryText}` : memoryText;
1036
- }
1037
- case 'hippo_status': {
1038
- const entries = loadAllEntries(hippoRoot, tenantId);
1039
- const now = evalNow(); // honors HIPPO_FAKE_NOW (eval-only; see ablation.ts)
1040
- let atRisk = 0;
1041
- let totalStrength = 0;
1042
- for (const e of entries) {
1043
- const s = calculateStrength(e, now);
1044
- totalStrength += s;
1045
- if (s < 0.1 && !e.pinned)
1046
- atRisk++;
1047
- }
1048
- const avgStrength = entries.length > 0 ? (totalStrength / entries.length).toFixed(2) : '0';
1049
- const pinned = entries.filter((e) => e.pinned).length;
1050
- const errors = entries.filter((e) => e.tags.includes('error')).length;
1051
- const conflicts = listMemoryConflicts(hippoRoot, 'open', tenantId).length;
1052
- return [
1053
- `Memories: ${entries.length} (${pinned} pinned, ${errors} errors)`,
1054
- `Avg strength: ${avgStrength}`,
1055
- `At risk (<0.1): ${atRisk}`,
1056
- `Open conflicts: ${conflicts}`,
1057
- `Half-life default: ${config.defaultHalfLifeDays}d`,
1058
- ].join('\n');
1059
- }
1060
- case 'hippo_learn': {
1061
- const days = Number(args.days) || 7;
1062
- if (!isGitRepo(process.cwd()))
1063
- return 'No git history found.';
1064
- const gitLog = fetchGitLog(process.cwd(), days);
1065
- if (!gitLog.trim())
1066
- return 'No fix/revert/bug commits found in the specified period.';
1067
- const parsedLessons = extractLessons(gitLog, config.gitLearnPatterns);
1068
- // DF4: admission gate lives at the write path, not in extractLessons
1069
- // (a published API surface that only parses). Bare subjects like
1070
- // "fixed signals" are dropped here, before they ever become a memory.
1071
- // Gating the loop INPUT is correct here, unlike the CLI path: this
1072
- // loop only writes. The CLI's loop also runs invalidation, so there
1073
- // the gate has to sit on the write alone or a migration subject stops
1074
- // superseding stale memories. Same predicate, different placement,
1075
- // because the loops do different work.
1076
- const { kept: lessons, dropped } = partitionLessons(parsedLessons);
1077
- const lowInfo = dropped.length;
1078
- let added = 0;
1079
- let skipped = 0;
1080
- let rejected = 0;
1081
- for (const lesson of lessons) {
1082
- if (deduplicateLesson(hippoRoot, lesson, 0.7, tenantId)) {
1083
- skipped++;
1084
- continue;
1085
- }
1086
- const entry = createMemory(lesson, {
1087
- layer: Layer.Episodic,
1088
- tags: ['git-learned'],
1089
- source: 'git',
1090
- confidence: 'observed',
1091
- baseHalfLifeDays: config.defaultHalfLifeDays,
1092
- tenantId,
1093
- });
1094
- // AT1 (plan §3 containment): a refused lesson must not crash the
1095
- // MCP learn call or lose the rest of the git log scan.
1096
- try {
1097
- writeEntry(hippoRoot, entry, { actor: ctx?.actor ?? 'mcp' });
1098
- }
1099
- catch (err) {
1100
- if (err instanceof RejectedValueError) {
1101
- rejected++;
1102
- continue;
1103
- }
1104
- throw err;
1105
- }
1106
- added++;
1107
- }
1108
- const rejectedSuffix = rejected > 0 ? `, ${rejected} rejected values skipped` : '';
1109
- const lowInfoSuffix = lowInfo > 0 ? `, ${lowInfo} low-information subjects dropped` : '';
1110
- return `Git learn: ${added} new, ${skipped} duplicates skipped${rejectedSuffix}${lowInfoSuffix} (scanned ${days} days)`;
1111
- }
1112
- case 'hippo_conflicts': {
1113
- const conflicts = listMemoryConflicts(hippoRoot, 'open', tenantId);
1114
- if (conflicts.length === 0)
1115
- return 'No open conflicts.';
1116
- return conflicts.map((c) => `conflict_${c.id}: ${c.memory_a_id} <-> ${c.memory_b_id} (score=${c.score.toFixed(2)}) — ${c.reason}`).join('\n');
1117
- }
1118
- case 'hippo_resolve': {
1119
- const conflictId = Number(args.conflict_id);
1120
- const keepId = String(args.keep || '');
1121
- const forget = Boolean(args.forget);
1122
- // AT1: optional rejectLoser + reason, threaded straight through to
1123
- // resolveConflict's opts (plan §5 — mirrors the CLI's --reject-loser).
1124
- const rejectLoser = Boolean(args.rejectLoser);
1125
- const reason = isJsonString(args.reason) ? args.reason : undefined;
1126
- if (isNaN(conflictId) || !keepId)
1127
- return 'Required: conflict_id and keep.';
1128
- const result = resolveConflict(hippoRoot, conflictId, keepId, forget, tenantId, {
1129
- rejectLoserValue: rejectLoser,
1130
- reason,
1131
- // P2 fix: resolveConflict's opts.rejectedBy defaults to 'cli' when
1132
- // omitted — this call site never passed it, so the tombstone's
1133
- // rejected_by AND the conflict_resolve audit's actor both landed as
1134
- // 'cli' even though the caller was MCP. ctx.actor carries the
1135
- // auth-resolved actor for HTTP-MCP (see McpContext above); stdio
1136
- // callers pass no ctx, so 'mcp' is the honest fallback there.
1137
- rejectedBy: ctx?.actor ?? 'mcp',
1138
- });
1139
- if (!result)
1140
- return 'Could not resolve. Check the conflict ID and --keep value.';
1141
- const action = rejectLoser ? 'rejected (tombstoned) and removed' : forget ? 'deleted' : 'weakened';
1142
- return `Resolved conflict ${conflictId}: kept ${keepId}, ${action} ${result.loserId}`;
1143
- }
1144
- case 'hippo_share': {
1145
- const shareId = String(args.id || '');
1146
- if (!shareId)
1147
- return 'Required: id (memory ID to share).';
1148
- const force = Boolean(args.force);
1149
- // Pass tenantId so shareMemory's readEntry filters by tenant. Without
1150
- // this, a Bearer for tenant A could call hippo_share with tenant B's
1151
- // id and copy the row to the global store. The 'Memory not found'
1152
- // error matches the cross-tenant deny shape elsewhere in the code.
1153
- const shared = shareMemory(hippoRoot, shareId, { force, tenantId });
1154
- if (!shared)
1155
- return 'Transfer score too low. Use force=true to override.';
1156
- return `Shared [${shared.id}] to global store. Source: ${shared.source}`;
1157
- }
1158
- case 'hippo_peers': {
1159
- // D4 v1.12.10: tenant-scope the cross-project peer discovery.
1160
- // tenantId is the caller's tenant (matches hippo_share above);
1161
- // passing undefined would restore the pre-D4 host-wide behaviour.
1162
- const peers = listPeers(undefined, tenantId);
1163
- if (peers.length === 0)
1164
- return 'No peers found.';
1165
- return peers.map((p) => `${p.project}: ${p.count} memories (latest: ${p.latest.slice(0, 10)})`).join('\n');
1166
- }
1167
- default:
1168
- return `Unknown tool: ${name}`;
1169
- }
1170
- }
1171
- // ── Request handling ──
1172
- /**
1173
- * Transport-agnostic MCP dispatcher. Both the stdio loop (below) and the
1174
- * HTTP/SSE transport in src/server.ts route every incoming JSON-RPC message
1175
- * through this single function. Returns null for notifications (no response
1176
- * expected) and a McpResponse otherwise. Errors thrown by `executeTool` are
1177
- * the caller's problem — wrap with try/catch on the transport side.
1178
- */
1179
- export async function handleMcpRequest(req, ctx) {
1180
- const { id, method, params } = req;
1181
- switch (method) {
1182
- case 'initialize':
1183
- return {
1184
- jsonrpc: '2.0',
1185
- id,
1186
- result: {
1187
- protocolVersion: '2024-11-05',
1188
- capabilities: { tools: {} },
1189
- serverInfo: { name: 'hippo-memory', version: PACKAGE_VERSION },
1190
- },
1191
- };
1192
- case 'notifications/initialized':
1193
- return null;
1194
- case 'tools/list':
1195
- return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
1196
- case 'tools/call': {
1197
- const nameValue = params?.name;
1198
- const toolName = isJsonString(nameValue) ? nameValue : '';
1199
- const argumentsValue = params?.arguments;
1200
- const toolArgs = isJsonObjectRecord(argumentsValue) ? argumentsValue : {};
1201
- const output = await executeTool(toolName, toolArgs, ctx);
1202
- return {
1203
- jsonrpc: '2.0',
1204
- id,
1205
- result: {
1206
- content: [{ type: 'text', text: output || 'Done.' }],
1207
- },
1208
- };
1209
- }
1210
- default:
1211
- // Notifications (no id) must not receive a response
1212
- if (method.startsWith('notifications/'))
1213
- return null;
1214
- return {
1215
- jsonrpc: '2.0',
1216
- id,
1217
- error: { code: -32601, message: `Method not found: ${method}` },
1218
- };
1219
- }
1220
- }
1221
- // ── Stdio transport ──
1222
- import { parseFrame } from './framing.js';
1223
- let buffer = Buffer.alloc(0);
1224
- function dispatch(body) {
1225
- let req;
1226
- try {
1227
- // SAFETY: malformed JSON is caught below and the frame is skipped; the
1228
- // JSON-RPC shape itself is validated field-by-field next (req.method
1229
- // truthiness check), matching the src/server.ts HTTP transport's own
1230
- // `JSON.parse(raw) as McpRequest` boundary cast.
1231
- req = JSON.parse(body);
1232
- }
1233
- catch {
1234
- return; // skip malformed
1235
- }
1236
- if (!req.method)
1237
- return;
1238
- if (req.method.startsWith('notifications/')) {
1239
- handleMcpRequest(req).catch(() => { });
1240
- return;
1241
- }
1242
- handleMcpRequest(req).then((resp) => { if (resp)
1243
- send(resp); }).catch((err) => {
1244
- send({ jsonrpc: '2.0', id: req.id, error: { code: -32603, message: err?.message ?? 'Internal error' } });
1245
- });
1246
- }
1247
- /**
1248
- * Wire stdin/stdout to the dispatcher. Idempotent — only the entrypoint
1249
- * (cli.ts `hippo mcp`, or running this file directly) should call this.
1250
- * src/server.ts imports `handleMcpRequest` without invoking this, so the
1251
- * HTTP daemon does not steal stdin or exit when its parent closes a pipe.
1252
- */
1253
- export function startStdioLoop() {
1254
- process.stdin.on('data', (chunk) => {
1255
- buffer = Buffer.concat([buffer, chunk]);
1256
- while (true) {
1257
- const result = parseFrame(buffer);
1258
- if (result.kind === 'incomplete')
1259
- break;
1260
- buffer = result.rest;
1261
- if (result.kind === 'message')
1262
- dispatch(result.body);
1263
- }
1264
- });
1265
- process.stdin.on('end', () => process.exit(0));
1266
- process.on('uncaughtException', (err) => {
1267
- process.stderr.write(`hippo-mcp uncaught: ${err?.message ?? err}\n`);
1268
- });
1269
- process.on('unhandledRejection', (err) => {
1270
- process.stderr.write(`hippo-mcp unhandled: ${err instanceof Error ? err.message : String(err)}\n`);
1271
- });
1272
- }
1273
- // Auto-start when invoked as the main module (node dist/mcp/server.js or via
1274
- // the cli's `import('./mcp/server.js')`). Importing this file from another
1275
- // module (e.g. src/server.ts wiring up the HTTP/SSE transport) will NOT
1276
- // trigger the stdio loop. The cli imports this file specifically to start
1277
- // stdio; that import is also `import.meta.url === main`-equivalent because
1278
- // it's executed as the program, so we keep a fallback: if HIPPO_MCP_STDIO=1
1279
- // or argv1 ends in /mcp/server.js we start.
1280
- const isMainModule = (() => {
1281
- try {
1282
- const argv1 = process.argv[1] ?? '';
1283
- if (argv1.endsWith('mcp/server.js') || argv1.endsWith('mcp\\server.js'))
1284
- return true;
1285
- if (process.env.HIPPO_MCP_STDIO === '1')
1286
- return true;
1287
- // ESM main-module check
1288
- const mainUrl = `file://${argv1.replace(/\\/g, '/')}`;
1289
- return import.meta.url === mainUrl || import.meta.url === `file:///${argv1.replace(/\\/g, '/')}`;
1290
- }
1291
- catch {
1292
- return false;
1293
- }
1294
- })();
1295
- if (isMainModule) {
1296
- startStdioLoop();
1297
- }
1298
- //# sourceMappingURL=server.js.map