hippo-memory 1.60.0 → 1.61.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 (585) hide show
  1. package/README.md +1 -1
  2. package/dist/ablation.js +9 -27
  3. package/dist/agent-memories/apply.js +4 -1
  4. package/dist/agent-memories/claude-code.js +1 -1
  5. package/dist/agent-memories/codex.js +1 -1
  6. package/dist/agent-memories/gemini.js +1 -1
  7. package/dist/agent-memories/legacy.js +1 -1
  8. package/dist/agent-memories/source.js +1 -1
  9. package/dist/agent-memories/sync.js +8 -3
  10. package/dist/ambient-store.d.ts +14 -0
  11. package/dist/ambient-store.js +90 -0
  12. package/dist/ambient.d.ts +23 -0
  13. package/dist/ambient.js +72 -50
  14. package/dist/api/assemble.d.ts +93 -0
  15. package/dist/api/assemble.js +152 -0
  16. package/dist/api/audit.d.ts +17 -0
  17. package/dist/api/audit.js +23 -0
  18. package/dist/api/auth.d.ts +79 -0
  19. package/dist/api/auth.js +178 -0
  20. package/dist/api/context-types.d.ts +105 -0
  21. package/dist/api/context-types.js +3 -0
  22. package/dist/api/context.d.ts +31 -0
  23. package/dist/api/context.js +705 -0
  24. package/dist/api/dormant.d.ts +30 -0
  25. package/dist/api/dormant.js +140 -0
  26. package/dist/api/drill-down.d.ts +84 -0
  27. package/dist/api/drill-down.js +123 -0
  28. package/dist/api/forget.d.ts +57 -0
  29. package/dist/api/forget.js +87 -0
  30. package/dist/api/goals.d.ts +18 -0
  31. package/dist/api/goals.js +33 -0
  32. package/dist/api/learn.d.ts +31 -0
  33. package/dist/api/learn.js +88 -0
  34. package/dist/api/outcome.d.ts +61 -0
  35. package/dist/api/outcome.js +66 -0
  36. package/dist/api/promote.d.ts +54 -0
  37. package/dist/api/promote.js +203 -0
  38. package/dist/api/quarantine.d.ts +24 -0
  39. package/dist/api/quarantine.js +121 -0
  40. package/dist/api/recall-types.d.ts +390 -0
  41. package/dist/api/recall-types.js +3 -0
  42. package/dist/api/recall.d.ts +36 -0
  43. package/dist/api/recall.js +634 -0
  44. package/dist/api/remember.d.ts +35 -0
  45. package/dist/api/remember.js +43 -0
  46. package/dist/api/sleep.d.ts +136 -0
  47. package/dist/api/sleep.js +271 -0
  48. package/dist/api/tokens.d.ts +26 -0
  49. package/dist/api/tokens.js +60 -0
  50. package/dist/api/types.d.ts +56 -0
  51. package/dist/api/types.js +38 -0
  52. package/dist/api.d.ts +20 -1262
  53. package/dist/api.js +25 -2727
  54. package/dist/audit.d.ts +3 -0
  55. package/dist/audit.js +6 -3
  56. package/dist/auth.d.ts +45 -4
  57. package/dist/auth.js +125 -48
  58. package/dist/autolearn.js +2 -2
  59. package/dist/capture/command.d.ts +33 -0
  60. package/dist/capture/command.js +264 -0
  61. package/dist/capture/compact.d.ts +44 -0
  62. package/dist/capture/compact.js +354 -0
  63. package/dist/capture/extract.d.ts +21 -0
  64. package/dist/capture/extract.js +464 -0
  65. package/dist/capture/transcript.d.ts +40 -0
  66. package/dist/capture/transcript.js +193 -0
  67. package/dist/capture-error.js +2 -1
  68. package/dist/churn-git.js +4 -2
  69. package/dist/cli/audit.d.ts +3 -0
  70. package/dist/cli/audit.js +159 -0
  71. package/dist/cli/auth.d.ts +2 -0
  72. package/dist/cli/auth.js +171 -0
  73. package/dist/cli/briefs.d.ts +4 -0
  74. package/dist/cli/briefs.js +435 -0
  75. package/dist/cli/card.d.ts +3 -0
  76. package/dist/cli/card.js +333 -0
  77. package/dist/cli/context.d.ts +15 -0
  78. package/dist/cli/context.js +366 -0
  79. package/dist/cli/continuity.d.ts +6 -0
  80. package/dist/cli/continuity.js +445 -0
  81. package/dist/cli/curate.d.ts +19 -0
  82. package/dist/cli/curate.js +555 -0
  83. package/dist/cli/dag.d.ts +5 -0
  84. package/dist/cli/dag.js +177 -0
  85. package/dist/cli/decisions.d.ts +4 -0
  86. package/dist/cli/decisions.js +528 -0
  87. package/dist/cli/eval.d.ts +5 -0
  88. package/dist/cli/eval.js +213 -0
  89. package/dist/cli/explain.d.ts +4 -0
  90. package/dist/cli/explain.js +150 -0
  91. package/dist/cli/goals.d.ts +2 -0
  92. package/dist/cli/goals.js +196 -0
  93. package/dist/cli/hook-blocks.d.ts +18 -0
  94. package/dist/cli/hook-blocks.js +233 -0
  95. package/dist/cli/init.d.ts +2 -0
  96. package/dist/cli/init.js +305 -0
  97. package/dist/cli/maintenance.d.ts +4 -0
  98. package/dist/cli/maintenance.js +179 -0
  99. package/dist/cli/playbooks.d.ts +4 -0
  100. package/dist/cli/playbooks.js +556 -0
  101. package/dist/cli/projects.js +1 -1
  102. package/dist/cli/recall.d.ts +7 -0
  103. package/dist/cli/recall.js +597 -0
  104. package/dist/cli/remember.d.ts +5 -0
  105. package/dist/cli/remember.js +442 -0
  106. package/dist/cli/serve.d.ts +5 -0
  107. package/dist/cli/serve.js +40 -0
  108. package/dist/cli/session-hooks.d.ts +29 -0
  109. package/dist/cli/session-hooks.js +637 -0
  110. package/dist/cli/setup.d.ts +4 -0
  111. package/dist/cli/setup.js +376 -0
  112. package/dist/cli/shared.d.ts +15 -19
  113. package/dist/cli/shared.js +50 -341
  114. package/dist/cli/slack.d.ts +2 -0
  115. package/dist/cli/slack.js +171 -0
  116. package/dist/cli/status.d.ts +18 -0
  117. package/dist/cli/status.js +400 -0
  118. package/dist/cli/transfer.d.ts +10 -0
  119. package/dist/cli/transfer.js +438 -0
  120. package/dist/cli/usage.d.ts +85 -0
  121. package/dist/cli/usage.js +741 -0
  122. package/dist/cli.d.ts +71 -120
  123. package/dist/cli.js +170 -8469
  124. package/dist/client.js +15 -8
  125. package/dist/compaction-record.js +6 -4
  126. package/dist/connectors/github/backfill.js +94 -87
  127. package/dist/connectors/github/cli-impl.js +4 -3
  128. package/dist/connectors/github/dlq.js +67 -54
  129. package/dist/connectors/github/ingest.js +34 -35
  130. package/dist/connectors/github/tenant-routing.js +3 -2
  131. package/dist/connectors/github/webhook.js +135 -216
  132. package/dist/connectors/slack/dlq.js +49 -61
  133. package/dist/connectors/slack/ingest.js +55 -48
  134. package/dist/connectors/slack/tenant-routing.js +4 -3
  135. package/dist/connectors/slack/webhook.js +72 -74
  136. package/dist/consolidate/conflicts.d.ts +10 -0
  137. package/dist/consolidate/conflicts.js +178 -0
  138. package/dist/consolidate/decay.d.ts +12 -0
  139. package/dist/consolidate/decay.js +145 -0
  140. package/dist/consolidate/llm-passes.d.ts +3 -0
  141. package/dist/consolidate/llm-passes.js +141 -0
  142. package/dist/consolidate/merge.d.ts +7 -0
  143. package/dist/consolidate/merge.js +251 -0
  144. package/dist/consolidate/physics-pass.d.ts +3 -0
  145. package/dist/consolidate/physics-pass.js +60 -0
  146. package/dist/consolidate/run.d.ts +69 -0
  147. package/dist/consolidate/run.js +76 -0
  148. package/dist/consolidate/sleep.d.ts +18 -0
  149. package/dist/consolidate/sleep.js +209 -0
  150. package/dist/consolidate/traces.d.ts +4 -0
  151. package/dist/consolidate/traces.js +178 -0
  152. package/dist/context-auto.js +7 -11
  153. package/dist/context-render.d.ts +1 -1
  154. package/dist/context-render.js +1 -1
  155. package/dist/customer-notes.d.ts +3 -0
  156. package/dist/customer-notes.js +6 -4
  157. package/dist/dag.js +6 -5
  158. package/dist/dashboard-actions.d.ts +20 -0
  159. package/dist/dashboard-actions.js +88 -0
  160. package/dist/dashboard-params.d.ts +45 -0
  161. package/dist/dashboard-params.js +127 -0
  162. package/dist/dashboard-queries.d.ts +15 -0
  163. package/dist/dashboard-queries.js +355 -0
  164. package/dist/dashboard-snapshot.d.ts +138 -0
  165. package/dist/dashboard-snapshot.js +308 -0
  166. package/dist/dashboard-types.d.ts +163 -0
  167. package/dist/dashboard-types.js +3 -0
  168. package/dist/dashboard.d.ts +6 -7
  169. package/dist/dashboard.js +228 -202
  170. package/dist/db/busy.d.ts +5 -0
  171. package/dist/db/busy.js +24 -0
  172. package/dist/db/continuity.d.ts +5 -0
  173. package/dist/db/continuity.js +145 -0
  174. package/dist/db/meta.d.ts +8 -0
  175. package/dist/db/meta.js +35 -0
  176. package/dist/db/migrate.d.ts +9 -0
  177. package/dist/db/migrate.js +138 -0
  178. package/dist/db/migrations/index.d.ts +5 -0
  179. package/dist/db/migrations/index.js +109 -0
  180. package/dist/db/migrations/types.d.ts +14 -0
  181. package/dist/db/migrations/types.js +2 -0
  182. package/dist/db/migrations/v01.d.ts +3 -0
  183. package/dist/db/migrations/v01.js +38 -0
  184. package/dist/db/migrations/v02.d.ts +3 -0
  185. package/dist/db/migrations/v02.js +21 -0
  186. package/dist/db/migrations/v03.d.ts +3 -0
  187. package/dist/db/migrations/v03.js +22 -0
  188. package/dist/db/migrations/v04.d.ts +3 -0
  189. package/dist/db/migrations/v04.js +28 -0
  190. package/dist/db/migrations/v05.d.ts +3 -0
  191. package/dist/db/migrations/v05.js +21 -0
  192. package/dist/db/migrations/v06.d.ts +3 -0
  193. package/dist/db/migrations/v06.js +25 -0
  194. package/dist/db/migrations/v07.d.ts +3 -0
  195. package/dist/db/migrations/v07.js +13 -0
  196. package/dist/db/migrations/v08.d.ts +3 -0
  197. package/dist/db/migrations/v08.js +8 -0
  198. package/dist/db/migrations/v09.d.ts +3 -0
  199. package/dist/db/migrations/v09.js +13 -0
  200. package/dist/db/migrations/v10.d.ts +3 -0
  201. package/dist/db/migrations/v10.js +17 -0
  202. package/dist/db/migrations/v11.d.ts +3 -0
  203. package/dist/db/migrations/v11.js +15 -0
  204. package/dist/db/migrations/v12.d.ts +3 -0
  205. package/dist/db/migrations/v12.js +11 -0
  206. package/dist/db/migrations/v13.d.ts +3 -0
  207. package/dist/db/migrations/v13.js +15 -0
  208. package/dist/db/migrations/v14.d.ts +3 -0
  209. package/dist/db/migrations/v14.js +66 -0
  210. package/dist/db/migrations/v15.d.ts +3 -0
  211. package/dist/db/migrations/v15.js +42 -0
  212. package/dist/db/migrations/v16.d.ts +3 -0
  213. package/dist/db/migrations/v16.js +61 -0
  214. package/dist/db/migrations/v17.d.ts +3 -0
  215. package/dist/db/migrations/v17.js +46 -0
  216. package/dist/db/migrations/v18.d.ts +3 -0
  217. package/dist/db/migrations/v18.js +60 -0
  218. package/dist/db/migrations/v19.d.ts +3 -0
  219. package/dist/db/migrations/v19.js +28 -0
  220. package/dist/db/migrations/v20.d.ts +3 -0
  221. package/dist/db/migrations/v20.js +42 -0
  222. package/dist/db/migrations/v21.d.ts +3 -0
  223. package/dist/db/migrations/v21.js +16 -0
  224. package/dist/db/migrations/v22.d.ts +3 -0
  225. package/dist/db/migrations/v22.js +82 -0
  226. package/dist/db/migrations/v23.d.ts +3 -0
  227. package/dist/db/migrations/v23.js +48 -0
  228. package/dist/db/migrations/v24.d.ts +3 -0
  229. package/dist/db/migrations/v24.js +72 -0
  230. package/dist/db/migrations/v25.d.ts +3 -0
  231. package/dist/db/migrations/v25.js +46 -0
  232. package/dist/db/migrations/v26.d.ts +3 -0
  233. package/dist/db/migrations/v26.js +23 -0
  234. package/dist/db/migrations/v27.d.ts +3 -0
  235. package/dist/db/migrations/v27.js +57 -0
  236. package/dist/db/migrations/v28.d.ts +3 -0
  237. package/dist/db/migrations/v28.js +38 -0
  238. package/dist/db/migrations/v29.d.ts +3 -0
  239. package/dist/db/migrations/v29.js +78 -0
  240. package/dist/db/migrations/v30.d.ts +3 -0
  241. package/dist/db/migrations/v30.js +92 -0
  242. package/dist/db/migrations/v31.d.ts +3 -0
  243. package/dist/db/migrations/v31.js +74 -0
  244. package/dist/db/migrations/v32.d.ts +3 -0
  245. package/dist/db/migrations/v32.js +102 -0
  246. package/dist/db/migrations/v33.d.ts +3 -0
  247. package/dist/db/migrations/v33.js +104 -0
  248. package/dist/db/migrations/v34.d.ts +3 -0
  249. package/dist/db/migrations/v34.js +93 -0
  250. package/dist/db/migrations/v35.d.ts +3 -0
  251. package/dist/db/migrations/v35.js +98 -0
  252. package/dist/db/migrations/v36.d.ts +3 -0
  253. package/dist/db/migrations/v36.js +98 -0
  254. package/dist/db/migrations/v37.d.ts +3 -0
  255. package/dist/db/migrations/v37.js +219 -0
  256. package/dist/db/migrations/v38.d.ts +3 -0
  257. package/dist/db/migrations/v38.js +277 -0
  258. package/dist/db/migrations/v39.d.ts +3 -0
  259. package/dist/db/migrations/v39.js +59 -0
  260. package/dist/db/migrations/v40.d.ts +3 -0
  261. package/dist/db/migrations/v40.js +74 -0
  262. package/dist/db/migrations/v41.d.ts +3 -0
  263. package/dist/db/migrations/v41.js +43 -0
  264. package/dist/db/migrations/v42.d.ts +3 -0
  265. package/dist/db/migrations/v42.js +41 -0
  266. package/dist/db/migrations/v43.d.ts +3 -0
  267. package/dist/db/migrations/v43.js +67 -0
  268. package/dist/db/migrations/v44.d.ts +3 -0
  269. package/dist/db/migrations/v44.js +28 -0
  270. package/dist/db/migrations/v45.d.ts +3 -0
  271. package/dist/db/migrations/v45.js +30 -0
  272. package/dist/db/migrations/v46.d.ts +3 -0
  273. package/dist/db/migrations/v46.js +25 -0
  274. package/dist/db/migrations/v47.d.ts +3 -0
  275. package/dist/db/migrations/v47.js +17 -0
  276. package/dist/db/migrations/v48.d.ts +3 -0
  277. package/dist/db/migrations/v48.js +10 -0
  278. package/dist/db/migrations/v49.d.ts +3 -0
  279. package/dist/db/migrations/v49.js +31 -0
  280. package/dist/db/migrations/v50.d.ts +3 -0
  281. package/dist/db/migrations/v50.js +67 -0
  282. package/dist/db/migrations/v51.d.ts +3 -0
  283. package/dist/db/migrations/v51.js +14 -0
  284. package/dist/db/migrations/v52.d.ts +3 -0
  285. package/dist/db/migrations/v52.js +7 -0
  286. package/dist/db/open.d.ts +23 -0
  287. package/dist/db/open.js +146 -0
  288. package/dist/db/sqlite.d.ts +21 -0
  289. package/dist/db/sqlite.js +8 -0
  290. package/dist/db/tables.d.ts +7 -0
  291. package/dist/db/tables.js +38 -0
  292. package/dist/db.d.ts +6 -46
  293. package/dist/db.js +5 -3049
  294. package/dist/decisions.d.ts +4 -1
  295. package/dist/decisions.js +9 -7
  296. package/dist/dedupe.js +3 -2
  297. package/dist/delivery-recorder.js +4 -1
  298. package/dist/doctor.js +3 -3
  299. package/dist/embedding-provider.d.ts +1 -1
  300. package/dist/embedding-provider.js +4 -3
  301. package/dist/embeddings.d.ts +9 -52
  302. package/dist/embeddings.js +43 -297
  303. package/dist/env.d.ts +75 -0
  304. package/dist/env.js +119 -0
  305. package/dist/eval-suite.js +1 -1
  306. package/dist/eval.js +2 -2
  307. package/dist/extract.js +1 -1
  308. package/dist/gated-write.js +3 -1
  309. package/dist/goals.d.ts +3 -1
  310. package/dist/goals.js +18 -0
  311. package/dist/graph/read.d.ts +73 -0
  312. package/dist/graph/read.js +325 -0
  313. package/dist/graph/rows.d.ts +45 -0
  314. package/dist/graph/rows.js +51 -0
  315. package/dist/graph/types.d.ts +83 -0
  316. package/dist/graph/types.js +11 -0
  317. package/dist/graph/write.d.ts +93 -0
  318. package/dist/{graph.js → graph/write.js} +6 -384
  319. package/dist/graph-extract.js +2 -1
  320. package/dist/graph-recall.d.ts +1 -1
  321. package/dist/graph-recall.js +2 -2
  322. package/dist/graph-stream.js +1 -1
  323. package/dist/graph-view.d.ts +1 -1
  324. package/dist/graph-view.js +1 -1
  325. package/dist/half-life-migration.d.ts +1 -1
  326. package/dist/half-life-migration.js +2 -1
  327. package/dist/hooks/codex-session.d.ts +8 -0
  328. package/dist/hooks/codex-session.js +76 -0
  329. package/dist/hooks/codex-wrapper.d.ts +55 -0
  330. package/dist/hooks/codex-wrapper.js +288 -0
  331. package/dist/hooks/json-hooks.d.ts +63 -0
  332. package/dist/hooks/json-hooks.js +356 -0
  333. package/dist/hooks/opencode.d.ts +50 -0
  334. package/dist/hooks/opencode.js +202 -0
  335. package/dist/hooks/shared.d.ts +54 -0
  336. package/dist/hooks/shared.js +77 -0
  337. package/dist/http-retry.d.ts +2 -0
  338. package/dist/http-retry.js +4 -3
  339. package/dist/http-util.d.ts +3 -0
  340. package/dist/http-util.js +10 -0
  341. package/dist/{importers.d.ts → importers/core.d.ts} +13 -17
  342. package/dist/importers/core.js +141 -0
  343. package/dist/importers/markdown-parse.d.ts +41 -0
  344. package/dist/importers/markdown-parse.js +132 -0
  345. package/dist/importers/markdown.d.ts +3 -0
  346. package/dist/importers/markdown.js +92 -0
  347. package/dist/importers/sources.d.ts +7 -0
  348. package/dist/importers/sources.js +229 -0
  349. package/dist/importers/vault.d.ts +11 -0
  350. package/dist/importers/vault.js +352 -0
  351. package/dist/incidents.d.ts +3 -0
  352. package/dist/incidents.js +7 -5
  353. package/dist/index.d.ts +25 -6
  354. package/dist/index.js +23 -6
  355. package/dist/invalidation.js +2 -1
  356. package/dist/judgment.js +2 -1
  357. package/dist/keyset.d.ts +13 -0
  358. package/dist/keyset.js +8 -0
  359. package/dist/local-embedding.d.ts +13 -0
  360. package/dist/local-embedding.js +165 -0
  361. package/dist/log.d.ts +7 -0
  362. package/dist/log.js +19 -1
  363. package/dist/mcp/admin-tools.d.ts +8 -0
  364. package/dist/mcp/admin-tools.js +116 -0
  365. package/dist/mcp/format.d.ts +27 -0
  366. package/dist/mcp/format.js +135 -0
  367. package/dist/mcp/memory-tools.d.ts +5 -0
  368. package/dist/mcp/memory-tools.js +83 -0
  369. package/dist/mcp/protocol.d.ts +83 -0
  370. package/dist/mcp/protocol.js +55 -0
  371. package/dist/mcp/recall-tools.d.ts +6 -0
  372. package/dist/mcp/recall-tools.js +320 -0
  373. package/dist/mcp/request.d.ts +10 -0
  374. package/dist/mcp/request.js +163 -0
  375. package/dist/mcp/server.d.ts +4 -75
  376. package/dist/mcp/server.js +7 -1190
  377. package/dist/mcp/session-state.d.ts +11 -0
  378. package/dist/mcp/session-state.js +28 -0
  379. package/dist/mcp/stdio.d.ts +8 -0
  380. package/dist/mcp/stdio.js +79 -0
  381. package/dist/mcp/tools.d.ts +11 -0
  382. package/dist/mcp/tools.js +247 -0
  383. package/dist/memory.d.ts +3 -0
  384. package/dist/memory.js +27 -1
  385. package/dist/multihop.d.ts +1 -1
  386. package/dist/multihop.js +2 -1
  387. package/dist/owner-validation.js +2 -1
  388. package/dist/physics-state.js +10 -7
  389. package/dist/policies.d.ts +3 -0
  390. package/dist/policies.js +8 -6
  391. package/dist/postinstall.js +3 -2
  392. package/dist/predictions/planning-fallacy.d.ts +100 -0
  393. package/dist/predictions/planning-fallacy.js +190 -0
  394. package/dist/{predictions.d.ts → predictions/store.d.ts} +7 -102
  395. package/dist/predictions/store.js +434 -0
  396. package/dist/processes.d.ts +3 -0
  397. package/dist/processes.js +7 -5
  398. package/dist/project-briefs.d.ts +3 -0
  399. package/dist/project-briefs.js +7 -5
  400. package/dist/project-identity.js +3 -2
  401. package/dist/project-merge.js +3 -1
  402. package/dist/quarantine.d.ts +2 -1
  403. package/dist/quarantine.js +8 -5
  404. package/dist/raw-archive.js +1 -1
  405. package/dist/recall-history.js +3 -2
  406. package/dist/recall-pipeline.d.ts +2 -2
  407. package/dist/recall-pipeline.js +16 -8
  408. package/dist/recall-scope.js +1 -1
  409. package/dist/recall-trace.d.ts +1 -1
  410. package/dist/refine-llm.js +2 -1
  411. package/dist/reject-flow.js +6 -1
  412. package/dist/rerankers/clef.js +6 -5
  413. package/dist/rerankers/jev.d.ts +1 -1
  414. package/dist/rerankers/jev.js +4 -4
  415. package/dist/rerankers/llm.d.ts +4 -2
  416. package/dist/rerankers/llm.js +58 -38
  417. package/dist/rerankers/types.d.ts +1 -1
  418. package/dist/salience.js +1 -1
  419. package/dist/scheduler.d.ts +1 -0
  420. package/dist/scheduler.js +26 -4
  421. package/dist/scope.js +4 -3
  422. package/dist/search/as-of.d.ts +10 -0
  423. package/dist/search/as-of.js +22 -0
  424. package/dist/search/bm25-search.d.ts +14 -0
  425. package/dist/search/bm25-search.js +43 -0
  426. package/dist/search/bm25.d.ts +15 -0
  427. package/dist/search/bm25.js +54 -0
  428. package/dist/search/boosts.d.ts +54 -0
  429. package/dist/search/boosts.js +94 -0
  430. package/dist/search/breakdown.d.ts +7 -0
  431. package/dist/search/breakdown.js +20 -0
  432. package/dist/search/explain.d.ts +25 -0
  433. package/dist/search/explain.js +31 -0
  434. package/dist/search/finalize.d.ts +8 -0
  435. package/dist/search/finalize.js +52 -0
  436. package/dist/search/fusion.d.ts +27 -0
  437. package/dist/search/fusion.js +42 -0
  438. package/dist/search/hybrid-score.d.ts +20 -0
  439. package/dist/search/hybrid-score.js +73 -0
  440. package/dist/search/hybrid.d.ts +46 -0
  441. package/dist/search/hybrid.js +64 -0
  442. package/dist/search/physics-search.d.ts +29 -0
  443. package/dist/search/physics-search.js +162 -0
  444. package/dist/search/rerank.d.ts +10 -0
  445. package/dist/search/rerank.js +72 -0
  446. package/dist/search/temporal.d.ts +15 -0
  447. package/dist/search/temporal.js +45 -0
  448. package/dist/search/types.d.ts +91 -0
  449. package/dist/search/types.js +2 -0
  450. package/dist/search/vector.d.ts +30 -0
  451. package/dist/search/vector.js +71 -0
  452. package/dist/secret-detect.d.ts +2 -0
  453. package/dist/secret-detect.js +2 -1
  454. package/dist/server/auth.d.ts +52 -0
  455. package/dist/server/auth.js +221 -0
  456. package/dist/server/client-ip.d.ts +25 -0
  457. package/dist/server/client-ip.js +92 -0
  458. package/dist/server/cursor.d.ts +23 -0
  459. package/dist/server/cursor.js +58 -0
  460. package/dist/server/lifecycle.d.ts +7 -0
  461. package/dist/server/lifecycle.js +29 -0
  462. package/dist/server/mcp-http.d.ts +5 -0
  463. package/dist/server/mcp-http.js +199 -0
  464. package/dist/server/request.d.ts +33 -0
  465. package/dist/server/request.js +103 -0
  466. package/dist/server/routes/admin.d.ts +9 -0
  467. package/dist/server/routes/admin.js +158 -0
  468. package/dist/server/routes/customer-notes.d.ts +7 -0
  469. package/dist/server/routes/customer-notes.js +112 -0
  470. package/dist/server/routes/decisions.d.ts +7 -0
  471. package/dist/server/routes/decisions.js +133 -0
  472. package/dist/server/routes/incidents.d.ts +7 -0
  473. package/dist/server/routes/incidents.js +126 -0
  474. package/dist/server/routes/memories.d.ts +10 -0
  475. package/dist/server/routes/memories.js +179 -0
  476. package/dist/server/routes/policies.d.ts +8 -0
  477. package/dist/server/routes/policies.js +151 -0
  478. package/dist/server/routes/predictions.d.ts +7 -0
  479. package/dist/server/routes/predictions.js +164 -0
  480. package/dist/server/routes/processes.d.ts +7 -0
  481. package/dist/server/routes/processes.js +161 -0
  482. package/dist/server/routes/project-briefs.d.ts +8 -0
  483. package/dist/server/routes/project-briefs.js +136 -0
  484. package/dist/server/routes/recall.d.ts +8 -0
  485. package/dist/server/routes/recall.js +340 -0
  486. package/dist/server/routes/skills.d.ts +8 -0
  487. package/dist/server/routes/skills.js +150 -0
  488. package/dist/server/types.d.ts +56 -0
  489. package/dist/server/types.js +2 -0
  490. package/dist/server/validation.d.ts +14 -0
  491. package/dist/server/validation.js +91 -0
  492. package/dist/server.d.ts +7 -61
  493. package/dist/server.js +69 -2362
  494. package/dist/session-digest.d.ts +1 -1
  495. package/dist/session-digest.js +9 -2
  496. package/dist/shared.d.ts +3 -1
  497. package/dist/shared.js +26 -22
  498. package/dist/skills.d.ts +3 -0
  499. package/dist/skills.js +7 -5
  500. package/dist/stdin.js +2 -2
  501. package/dist/store/audit-event.d.ts +19 -0
  502. package/dist/store/audit-event.js +33 -0
  503. package/dist/store/candidates.d.ts +42 -0
  504. package/dist/store/candidates.js +152 -0
  505. package/dist/store/conflicts.d.ts +44 -0
  506. package/dist/store/conflicts.js +444 -0
  507. package/dist/store/delete-and-batch.d.ts +63 -0
  508. package/dist/store/delete-and-batch.js +310 -0
  509. package/dist/store/entry-reads.d.ts +92 -0
  510. package/dist/store/entry-reads.js +255 -0
  511. package/dist/store/entry-row.d.ts +67 -0
  512. package/dist/store/entry-row.js +208 -0
  513. package/dist/store/entry-writes.d.ts +46 -0
  514. package/dist/store/entry-writes.js +148 -0
  515. package/dist/store/handoffs.d.ts +26 -0
  516. package/dist/store/handoffs.js +184 -0
  517. package/dist/store/index-and-stats.d.ts +52 -0
  518. package/dist/store/index-and-stats.js +214 -0
  519. package/dist/store/markdown.d.ts +10 -0
  520. package/dist/store/markdown.js +108 -0
  521. package/dist/store/mirrors.d.ts +49 -0
  522. package/dist/store/mirrors.js +312 -0
  523. package/dist/store/open.d.ts +15 -0
  524. package/dist/store/open.js +223 -0
  525. package/dist/store/rows.d.ts +178 -0
  526. package/dist/store/rows.js +158 -0
  527. package/dist/store/search-rows.d.ts +86 -0
  528. package/dist/store/search-rows.js +254 -0
  529. package/dist/store/sessions.d.ts +83 -0
  530. package/dist/store/sessions.js +272 -0
  531. package/dist/store/summaries.d.ts +94 -0
  532. package/dist/store/summaries.js +377 -0
  533. package/dist/store/tenant-lookup.d.ts +12 -0
  534. package/dist/store/tenant-lookup.js +16 -0
  535. package/dist/store-cards.js +2 -1
  536. package/dist/summary-dirty.d.ts +4 -0
  537. package/dist/summary-dirty.js +32 -0
  538. package/dist/support-bundle.js +4 -3
  539. package/dist/tenant.js +2 -2
  540. package/dist/tokenize.d.ts +2 -0
  541. package/dist/tokenize.js +16 -0
  542. package/dist/transcript-tail.d.ts +7 -0
  543. package/dist/transcript-tail.js +48 -0
  544. package/dist/vector-store.d.ts +27 -0
  545. package/dist/vector-store.js +210 -0
  546. package/dist/version.d.ts +2 -2
  547. package/dist/version.js +2 -2
  548. package/dist/working-memory.js +1 -1
  549. package/dist/yaml.js +36 -11
  550. package/dist-ui/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  551. package/dist-ui/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  552. package/dist-ui/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  553. package/dist-ui/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  554. package/dist-ui/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  555. package/dist-ui/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  556. package/dist-ui/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  557. package/dist-ui/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  558. package/dist-ui/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  559. package/dist-ui/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  560. package/dist-ui/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  561. package/dist-ui/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  562. package/dist-ui/assets/index-DPN7cP19.js +33 -0
  563. package/dist-ui/assets/index-dFloKRVr.css +1 -0
  564. package/dist-ui/index.html +3 -25
  565. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  566. package/extensions/openclaw-plugin/package.json +1 -1
  567. package/openclaw.plugin.json +1 -1
  568. package/package.json +1 -1
  569. package/dist/capture.d.ts +0 -155
  570. package/dist/capture.js +0 -1295
  571. package/dist/consolidate.d.ts +0 -58
  572. package/dist/consolidate.js +0 -1124
  573. package/dist/graph.d.ts +0 -245
  574. package/dist/hooks.d.ts +0 -208
  575. package/dist/hooks.js +0 -1076
  576. package/dist/importers.js +0 -900
  577. package/dist/predictions.js +0 -620
  578. package/dist/search.d.ts +0 -320
  579. package/dist/search.js +0 -970
  580. package/dist/store.d.ts +0 -776
  581. package/dist/store.js +0 -3473
  582. package/dist-ui/assets/d3-BiWEKnn4.js +0 -1
  583. package/dist-ui/assets/index-BhT8RvO6.js +0 -61
  584. package/dist-ui/assets/index-RoXXJ5dq.css +0 -1
  585. package/dist-ui/assets/three-BDgTxR1l.js +0 -4112
@@ -7,1195 +7,12 @@
7
7
  *
8
8
  * Usage: hippo mcp (or npx hippo-memory mcp)
9
9
  */
10
- import * as fs from 'fs';
11
- import * as path from 'path';
12
- import { randomUUID } from 'node:crypto';
13
- import { INTERNAL_ERROR_MESSAGE, mapApiError } from '../http-util.js';
14
- import { log } from '../log.js';
15
- import { createMemory, Layer, calculateStrength, } from '../memory.js';
16
- import { fitBudget, estimateTokens } from '../search.js';
17
- import { evalNow } from '../ablation.js';
18
- import { loadStrengthRows, loadTextsHoldingWords, writeEntry, readEntry, listMemoryConflicts, resolveConflict, countCreatedSinceLastSleep } from '../store.js';
19
- import { shareMemory, listPeers, getGlobalRoot, initGlobal } from '../shared.js';
20
- import { consolidate } from '../consolidate.js';
21
- import { fetchGitLog, extractLessons, partitionLessons, isGitRepo } from '../autolearn.js';
22
- import { dropHeldCopies, duplicateKey, longestWord, storedTextKeys } from '../same-text.js';
23
- import { loadConfig } from '../config.js';
24
- import { confidenceLabel } from '../memory.js';
25
- import { resolveTenantId } from '../tenant.js';
26
- import { retrieve as apiRetrieve, remember as apiRemember, outcome as apiOutcome, drillDown as apiDrillDown, assemble as apiAssemble, getContext as apiGetContext, buildSuppressionSummary } from '../api.js';
27
- import { autoDetectContext } from '../context-auto.js';
28
- import { resolveProjectIdentity, findHippoStoreDir } from '../project-identity.js';
29
- import { computePredictionBaserate } from '../predictions.js';
30
- import { appendAuditEvent, auditQueryFields } from '../audit.js';
31
- import { RejectedValueError } from '../rejection.js';
32
- import { detectAnchoring, hashQueryText, biasHintEnabled, buildSessionKey, getOrCreateRing, appendRecall, snapshotRing, } from '../recall-history.js';
33
- import { detectAvailabilityBias } from '../availability.js';
34
- // v0.33 / J1 — Module-level per-(tenant, session) recall-history ring map
35
- // for the MCP pipeline. Separate from CLI/HTTP rings per plan v3
36
- // architecture (per-pipeline rings; no IPC).
37
- const sessionRecallHistoryMcp = new Map();
38
- /** Test-only: reset the module-level recall-history Map. Call from beforeEach. */
39
- export function __resetSessionRecallHistoryMcp() {
40
- sessionRecallHistoryMcp.clear();
41
- }
42
- import { openHippoDb, closeHippoDb } from '../db.js';
43
- import { recordTokenUse } from '../token-ledger.js';
44
- import { PACKAGE_VERSION } from '../version.js';
45
- import { validateToolArgs } from './tool-args.js';
46
- // ── Find hippo root ──
47
- /** Same bounded walk as the CLI (ends at home, so HIPPO_HOME wins over ~/.hippo); cwd/opts are the test seam. */
48
- export function findHippoRoot(cwd = process.cwd(), opts) {
49
- const local = findHippoStoreDir(cwd, opts);
50
- if (local !== null)
51
- return local;
52
- // Global fallback (respects $HIPPO_HOME / $XDG_DATA_HOME)
53
- const global = getGlobalRoot();
54
- return fs.existsSync(global) ? global : null;
55
- }
56
- /** JSON-RPC reply for a request that threw: typed API errors keep their text; anything else is logged and answered generically. */
57
- export function mcpErrorResponse(id, err, requestId = randomUUID()) {
58
- const { status, message } = mapApiError(err);
59
- if (status !== 500)
60
- return { jsonrpc: '2.0', id, error: { code: -32603, message } };
61
- log.error(`mcp request failed: ${err instanceof Error ? err.message : String(err)}`, { requestId });
62
- return {
63
- jsonrpc: '2.0',
64
- id,
65
- error: { code: -32603, message: `${INTERNAL_ERROR_MESSAGE} (request id ${requestId})`, data: { requestId } },
66
- };
67
- }
68
- /**
69
- * The api-layer actor for a tool call. Stdio (no ctx) is the local operator
70
- * and runs as admin; over HTTP the transport's authenticated role is used, so
71
- * a member key never acts as admin through MCP.
72
- */
73
- function mcpActor(ctx) {
74
- const actor = { subject: ctx?.actor ?? 'mcp', role: ctx?.role ?? 'admin', scopes: ctx?.scopes };
75
- if (ctx?.viaAuthResolver)
76
- actor.viaAuthResolver = true;
77
- return actor;
78
- }
79
- // MCP stdio transport spec: messages are newline-delimited JSON-RPC, no embedded newlines.
80
- // https://modelcontextprotocol.io/specification/.../basic/transports#stdio
81
- function send(msg) {
82
- process.stdout.write(JSON.stringify(msg) + '\n');
83
- }
84
- function isJsonString(v) {
85
- return typeof v === 'string';
86
- }
87
- function isJsonBoolean(v) {
88
- return typeof v === 'boolean';
89
- }
90
- function isJsonObjectRecord(v) {
91
- return v !== undefined && v !== null && typeof v === 'object' && !Array.isArray(v);
92
- }
93
- import { formatHandoffEvidenceLine } from '../handoff.js';
94
- import { assembleCost, assembleText, drillCost, drillText, printedTokens } from '../context-render.js';
95
- function handoffLines(h) {
96
- const lines = [`- Summary: ${h.summary}`];
97
- if (h.nextAction)
98
- lines.push(`- Next action: ${h.nextAction}`);
99
- if ((h.artifacts ?? []).length > 0)
100
- lines.push(`- Artifacts: ${(h.artifacts ?? []).join(', ')}`);
101
- if (h.outcome)
102
- lines.push(`- Outcome: ${h.outcome}`);
103
- if (h.targetRuntime)
104
- lines.push(`- Target runtime: ${h.targetRuntime}`);
105
- if (h.cardId)
106
- lines.push(`- Card: ${h.cardId}`);
107
- if ((h.constraints ?? []).length > 0)
108
- lines.push(`- Constraints: ${(h.constraints ?? []).join(', ')}`);
109
- if (h.evidence)
110
- lines.push(`- Evidence: ${formatHandoffEvidenceLine(h.evidence)}`);
111
- return lines;
112
- }
113
- function trailLines(events) {
114
- return events.map((e) => {
115
- const preview = e.content.length > 200 ? e.content.slice(0, 200) + '…' : e.content;
116
- return `- [${e.event_type}] ${preview}`;
117
- });
118
- }
119
- function formatContinuityBlock(block) {
120
- const lines = ['## Continuity'];
121
- if (block.activeSnapshot) {
122
- lines.push('');
123
- lines.push('### Active Task Snapshot');
124
- lines.push(`- Task: ${block.activeSnapshot.task}`);
125
- lines.push(`- Summary: ${block.activeSnapshot.summary}`);
126
- lines.push(`- Next: ${block.activeSnapshot.next_step}`);
127
- }
128
- if (block.sessionHandoff) {
129
- lines.push('');
130
- lines.push('### Session Handoff');
131
- lines.push(...handoffLines(block.sessionHandoff));
132
- }
133
- if (block.recentSessionEvents.length > 0) {
134
- lines.push('');
135
- lines.push('### Recent Session Trail');
136
- lines.push(...trailLines(block.recentSessionEvents));
137
- }
138
- if (lines.length === 1) {
139
- lines.push('');
140
- lines.push('(no active task snapshot, handoff, or recent events for this tenant)');
141
- }
142
- return lines.join('\n');
143
- }
144
- const NO_MEMORIES = 'No relevant memories found.';
145
- function memoriesHeading(count) {
146
- return `Found ${count} memories:\n`;
147
- }
148
- function formatMemory(r) {
149
- const conf = confidenceLabel(r.entry).text;
150
- const tags = r.entry.tags.length > 0 ? ` tags: ${r.entry.tags.join(', ')}` : '';
151
- return `[${conf}]${tags} (strength=${r.entry.strength.toFixed(2)})\n${r.entry.content}\n`;
152
- }
153
- function formatMemories(results) {
154
- if (results.length === 0)
155
- return NO_MEMORIES;
156
- return [memoriesHeading(results.length), ...results.map(formatMemory)].join('\n');
157
- }
158
- /** What a memory costs the budget: the text formatMemories prints for it. */
159
- const memoryCost = (r) => printedTokens(formatMemory(r));
160
- // The widest heading or the empty-list line, whichever costs more, so either prints inside the budget.
161
- function memoriesReserve(budget) {
162
- return Math.max(printedTokens(memoriesHeading(budget)), estimateTokens(NO_MEMORIES));
163
- }
164
- function snapshotPiece(s) {
165
- return [
166
- '## Active Task Snapshot',
167
- `- Task: ${s.task}`,
168
- `- Status: ${s.status}`,
169
- `- Updated: ${s.updated_at}`,
170
- '',
171
- '### Summary',
172
- s.summary,
173
- '',
174
- '### Next step',
175
- s.next_step,
176
- '',
177
- '',
178
- ].join('\n');
179
- }
180
- function handoffPiece(h) {
181
- return ['## Session Handoff', ...handoffLines(h), '', ''].join('\n');
182
- }
183
- function trailPiece(events) {
184
- return ['## Recent Session Trail', ...trailLines(events), '', ''].join('\n');
185
- }
186
- // Sections print ahead of the memories in hippo_context, so getContext pays for each as printed before any memory.
187
- const contextCost = {
188
- entry: memoryCost,
189
- fixed: (budget) => memoriesReserve(budget),
190
- snapshot: (s) => estimateTokens(snapshotPiece(s)),
191
- handoff: (h) => estimateTokens(handoffPiece(h)),
192
- trail: (events) => estimateTokens(trailPiece(events)),
193
- };
194
- // Rows the ranked list already shows drop out of this section, so pricing every row bounds what it prints.
195
- function tailSection(rows) {
196
- if (rows.length === 0)
197
- return '';
198
- const lines = ['', '## Fresh tail / substituted summaries'];
199
- for (const r of rows) {
200
- const tag = r.isSummary ? '[summary]' : '[tail]';
201
- const head = r.content.length > 200 ? r.content.slice(0, 200) + '…' : r.content;
202
- if (r.isSummary && r.substitutedFor && r.substitutedFor.length > 0) {
203
- lines.push(`- ${tag} ${r.id} (covers ${r.substitutedFor.length} rows): ${head}`);
204
- }
205
- else {
206
- lines.push(`- ${tag} ${r.id}: ${head}`);
207
- }
208
- }
209
- return '\n' + lines.join('\n');
210
- }
211
- // J3.2: the hint depends on the query alone, so api.retrieve's copy is the one shown; JSON.stringify fences the phrase.
212
- function planningSection(r) {
213
- if (r.planningFallacyHint) {
214
- const h = r.planningFallacyHint;
215
- return `## Planning fallacy hint\nClass: ${h.classTag}\n${h.baserateSummary}\n(detected: ${JSON.stringify(h.detectedPhrase)})\n\n---\n\n`;
216
- }
217
- if (r.planningFallacyWatching) {
218
- const w = r.planningFallacyWatching;
219
- return `## Planning fallacy watch\nReason: ${w.reason}\n${w.suggestion}\n(detected: ${JSON.stringify(w.detectedPhrase)})\n\n---\n\n`;
220
- }
221
- return '';
222
- }
223
- // ── Tool definitions ──
224
- // HTTP sets no budget cap; 25x the 4000 recall default leaves room for large-context clients while bounding one call's work.
225
- const MAX_BUDGET_TOKENS = 100_000;
226
- // Same ceiling as the HTTP list routes' parseListLimit.
227
- const MAX_LIST_LIMIT = 1000;
228
- const TOOLS = [
229
- {
230
- name: 'hippo_recall',
231
- 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.',
232
- inputSchema: {
233
- type: 'object',
234
- properties: {
235
- query: { type: 'string', description: 'What to search for in memory (natural language)' },
236
- budget: {
237
- type: 'number',
238
- minimum: 0,
239
- maximum: MAX_BUDGET_TOKENS,
240
- description: `Max tokens to return (default: config.defaultBudget, 4000; max ${MAX_BUDGET_TOKENS})`,
241
- },
242
- include_continuity: {
243
- type: 'boolean',
244
- description: 'Append continuity context (active snapshot + handoff + last 5 session events) below the memory results. Useful at session boot.',
245
- },
246
- scope: {
247
- type: 'string',
248
- 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.',
249
- },
250
- fresh_tail_count: {
251
- type: 'number',
252
- 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.',
253
- },
254
- fresh_tail_session_id: {
255
- type: 'string',
256
- 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).',
257
- },
258
- summarize_overflow: {
259
- type: 'boolean',
260
- 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.',
261
- },
262
- scorer_window: {
263
- type: 'number',
264
- description: 'How many of the top-ranked memories the fresh-tail and summarize-overflow appendix is worked out against. The main list ranks the whole tenant store, so scorer_window does not narrow it. Default 200. Rejected as RecallContractError code=invalid_scorer_window if 0/negative/non-finite/non-numeric.',
265
- },
266
- session_id: {
267
- type: 'string',
268
- maxLength: 256,
269
- description: 'Optional session id (v1.7.4). When set AND (tenant, session) has active goals, applies the dlPFC goal-stack boost to the ranked memories before formatting. Mirrors fresh_tail_session_id shape (256-char cap).',
270
- },
271
- },
272
- required: ['query'],
273
- },
274
- },
275
- {
276
- name: 'hippo_assemble',
277
- 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.',
278
- inputSchema: {
279
- type: 'object',
280
- properties: {
281
- session_id: {
282
- type: 'string',
283
- description: 'Session identifier. Returns clean empty result if no kind=raw rows match.',
284
- },
285
- budget: {
286
- type: 'number',
287
- minimum: 0,
288
- maximum: MAX_BUDGET_TOKENS,
289
- description: `Token budget for the assembled context (default 4000; max ${MAX_BUDGET_TOKENS}). Eviction kicks in over budget.`,
290
- },
291
- fresh_tail_count: {
292
- type: 'number',
293
- description: 'Recent raw rows always kept verbatim (default 10). These are never evicted.',
294
- },
295
- summarize_older: {
296
- type: 'boolean',
297
- description: 'When true (default), older raws sharing a level-2 parent summary get substituted. Set false to keep every older raw as-is.',
298
- },
299
- scope: {
300
- type: 'string',
301
- 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.',
302
- },
303
- },
304
- required: ['session_id'],
305
- },
306
- },
307
- {
308
- name: 'hippo_drill',
309
- 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.',
310
- inputSchema: {
311
- type: 'object',
312
- properties: {
313
- summary_id: {
314
- type: 'string',
315
- description: 'ID of the level-2 (or higher) summary to drill into. Must be a summary, not a leaf — leaves are not drillable.',
316
- },
317
- limit: {
318
- type: 'number',
319
- minimum: 0,
320
- maximum: MAX_LIST_LIMIT,
321
- description: `Max children to return (default 50; max ${MAX_LIST_LIMIT}).`,
322
- },
323
- budget: {
324
- type: 'number',
325
- minimum: 0,
326
- maximum: MAX_BUDGET_TOKENS,
327
- description: `Max total token cost (~ chars/4) of returned children (max ${MAX_BUDGET_TOKENS}). Truncates chronologically.`,
328
- },
329
- depth: {
330
- type: 'integer',
331
- minimum: 1,
332
- maximum: 10,
333
- 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.',
334
- },
335
- },
336
- required: ['summary_id'],
337
- },
338
- },
339
- {
340
- name: 'hippo_remember',
341
- 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.',
342
- inputSchema: {
343
- type: 'object',
344
- properties: {
345
- text: {
346
- type: 'string',
347
- description: 'The memory to store (1-2 sentences, specific and concrete)',
348
- },
349
- error: { type: 'boolean', description: 'Mark as error memory (doubles half-life)' },
350
- pin: { type: 'boolean', description: 'Pin memory (never decays)' },
351
- tag: { type: 'string', description: 'Optional tag for categorization' },
352
- },
353
- required: ['text'],
354
- },
355
- },
356
- {
357
- name: 'hippo_outcome',
358
- description: 'Report whether recalled memories were useful. Strengthens good memories (+5 days half-life) and weakens bad ones (-3 days). Call after completing work.',
359
- inputSchema: {
360
- type: 'object',
361
- properties: {
362
- good: {
363
- type: 'boolean',
364
- description: 'true = memories helped, false = memories were irrelevant',
365
- },
366
- },
367
- required: ['good'],
368
- },
369
- },
370
- {
371
- name: 'hippo_context',
372
- description: 'Smart context injection: auto-detects current task from git state and returns relevant memories plus the active task snapshot, session handoff and recent session trail (the same bundle as GET /v1/context). Use at the start of any session. Memories and those sections are scope-filtered: a no-scope caller does NOT see ANY <source>:private:* (slack, github, ...) or legacy-quarantine rows.',
373
- inputSchema: {
374
- type: 'object',
375
- properties: {
376
- budget: {
377
- type: 'number',
378
- minimum: 0,
379
- maximum: MAX_BUDGET_TOKENS,
380
- description: `Max tokens (default: config.defaultContextBudget, 3000; max ${MAX_BUDGET_TOKENS})`,
381
- },
382
- scope: {
383
- type: 'string',
384
- description: 'Restrict memories, snapshot, handoff and trail to this scope exactly. When omitted, default-deny applies to ANY <source>:private:* (slack, github, ...) and unknown-legacy rows.',
385
- },
386
- },
387
- },
388
- },
389
- {
390
- name: 'hippo_status',
391
- description: 'Check memory health: counts, strengths, at-risk memories, last consolidation time.',
392
- inputSchema: {
393
- type: 'object',
394
- properties: {},
395
- },
396
- },
397
- {
398
- name: 'hippo_learn',
399
- description: 'Scan recent git commits for lessons from fix/revert/bug/refactor/perf patterns. Run after coding sessions.',
400
- inputSchema: {
401
- type: 'object',
402
- properties: {
403
- days: { type: 'number', description: 'Days to scan back (default: 7)' },
404
- },
405
- },
406
- },
407
- {
408
- name: 'hippo_conflicts',
409
- description: 'List open memory conflicts — contradictory memories that need resolution.',
410
- inputSchema: {
411
- type: 'object',
412
- properties: {},
413
- },
414
- },
415
- {
416
- name: 'hippo_resolve',
417
- description: 'Resolve a memory conflict by keeping one memory and weakening or deleting the other.',
418
- inputSchema: {
419
- type: 'object',
420
- properties: {
421
- conflict_id: { type: 'number', description: 'The conflict ID to resolve' },
422
- keep: { type: 'string', description: 'ID of the memory to keep' },
423
- forget: { type: 'boolean', description: 'Delete the loser instead of weakening (default: false)' },
424
- rejectLoser: { type: 'boolean', description: 'Tombstone the loser\'s value too, so it refuses re-ingestion (implies removal; default: false)' },
425
- reason: { type: 'string', description: 'Reason recorded on the tombstone when rejectLoser is set (default: a conflict-context string)' },
426
- },
427
- required: ['conflict_id', 'keep'],
428
- },
429
- },
430
- {
431
- name: 'hippo_share',
432
- description: 'Share a memory to the global store for cross-project use with transfer scoring.',
433
- inputSchema: {
434
- type: 'object',
435
- properties: {
436
- id: { type: 'string', description: 'Memory ID to share' },
437
- force: { type: 'boolean', description: 'Share even if transfer score is low' },
438
- },
439
- required: ['id'],
440
- },
441
- },
442
- {
443
- name: 'hippo_peers',
444
- description: 'List all projects that have contributed memories to the global shared store.',
445
- inputSchema: {
446
- type: 'object',
447
- properties: {},
448
- },
449
- },
450
- {
451
- name: 'hippo_predict_baserate',
452
- 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.',
453
- inputSchema: {
454
- type: 'object',
455
- properties: {
456
- class_tag: {
457
- type: 'string',
458
- 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 ...`).',
459
- },
460
- },
461
- required: ['class_tag'],
462
- },
463
- },
464
- ];
465
- const TOOLS_BY_NAME = new Map(TOOLS.map((t) => [t.name, t]));
466
- // api.retrieve rejects these itself, so MCP and HTTP callers get the same typed error code for the same bad value.
467
- const ARGS_CHECKED_BY_API = new Map([['hippo_recall', new Set(['scorer_window'])]]);
468
- // ── Track last recalled IDs for outcome feedback ──
469
- //
470
- // Keyed per-client so two HTTP-MCP clients hitting the same tenant cannot
471
- // poison each other's outcome feedback. The key is `ctx.clientKey` when the
472
- // transport supplies one (HTTP-MCP via src/server.ts builds
473
- // hash(bearer+remoteAddr)); stdio and any caller without a clientKey falls
474
- // back to `'stdio-${pid}'` (one process = one client) or
475
- // `${tenantId}:default` if a McpContext is constructed in tests without a
476
- // pid-bound transport.
477
- const lastRecalledIds = new Map();
478
- const autoSleepInFlight = new Set();
479
- function resolveClientKey(ctx) {
480
- if (ctx?.clientKey)
481
- return ctx.clientKey;
482
- if (ctx?.tenantId)
483
- return `stdio-${process.pid}:${ctx.tenantId}`;
484
- return `stdio-${process.pid}:default`;
485
- }
486
- /**
487
- * Zero-install first run (`npx -y hippo-memory mcp` with no store anywhere):
488
- * create the global store instead of failing every tool call, and say so on
489
- * stderr (stdout carries the protocol). `hippo init` in a project later adds
490
- * a project store, which then takes precedence.
491
- */
492
- function createGlobalStoreOnFirstRun() {
493
- initGlobal();
494
- const root = getGlobalRoot();
495
- log.warn(`no memory store found; created the global store at ${root}. Run \`hippo init\` in a project for a project store.`);
496
- return root;
497
- }
498
- // ── Token ledger (ROADMAP TE0) ──
499
- const MCP_TOKEN_SURFACES = new Map([
500
- ['hippo_recall', 'mcp_recall'],
501
- ['hippo_context', 'mcp_context'],
502
- ]);
503
- /**
504
- * Record the memory text a recall or context tool returned. Best-effort: a
505
- * ledger failure never fails the tool call. Other tools are not recorded.
506
- */
507
- function recordMcpTokens(toolName, output, ctx) {
508
- const surface = MCP_TOKEN_SURFACES.get(toolName);
509
- if (!surface || !output)
510
- return;
511
- try {
512
- const hippoRoot = ctx?.hippoRoot ?? findHippoRoot();
513
- if (!hippoRoot)
514
- return;
515
- const db = openHippoDb(hippoRoot);
516
- try {
517
- recordTokenUse(db, {
518
- tenantId: ctx?.tenantId ?? resolveTenantId({}),
519
- surface,
520
- event: 'inject',
521
- items: 0,
522
- tokens: estimateTokens(output),
523
- });
524
- }
525
- finally {
526
- closeHippoDb(db);
527
- }
528
- }
529
- catch {
530
- // Ledger is best-effort.
531
- }
532
- }
533
- // ── Tool execution ──
534
- async function executeTool(name, args, ctx) {
535
- // When a transport hands us a context (HTTP path), trust it: the HTTP
536
- // server already resolved hippoRoot from its bound opts and tenantId
537
- // from the Bearer token (or the loopback fallback). The stdio path
538
- // continues to walk from cwd / fall back to the global root, and to
539
- // resolve tenant from HIPPO_TENANT.
540
- const hippoRoot = ctx?.hippoRoot ?? findHippoRoot() ?? createGlobalStoreOnFirstRun();
541
- const config = loadConfig(hippoRoot);
542
- // A5: every store read in this server returns to the caller and is
543
- // tenant-isolated. Resolved once per tool call: prefer the transport's
544
- // ctx.tenantId so an HTTP Bearer for tenant B doesn't drop to HIPPO_TENANT.
545
- const tenantId = ctx?.tenantId ?? resolveTenantId({});
546
- switch (name) {
547
- case 'hippo_recall': {
548
- const query = String(args.query || '');
549
- const budget = Number(args.budget) || config.defaultBudget;
550
- const includeContinuity = Boolean(args.include_continuity);
551
- const explicitScope = isJsonString(args.scope) && args.scope.length > 0
552
- ? args.scope
553
- : undefined;
554
- const freshTailCountArg = Number(args.fresh_tail_count);
555
- const freshTailCount = Number.isFinite(freshTailCountArg) && freshTailCountArg > 0
556
- ? freshTailCountArg
557
- : undefined;
558
- const freshTailSessionId = isJsonString(args.fresh_tail_session_id) && args.fresh_tail_session_id.length > 0
559
- ? args.fresh_tail_session_id
560
- : undefined;
561
- const summarizeOverflow = isJsonBoolean(args.summarize_overflow)
562
- ? args.summarize_overflow
563
- : undefined;
564
- // Number-coerce, never typeof-check: "abc" must reach api.retrieve and fail as invalid_scorer_window, the same code HTTP returns.
565
- const scorerWindow = args.scorer_window === undefined
566
- ? undefined
567
- : Number(args.scorer_window);
568
- // session_id drives the goal-stack boost inside api.retrieve; same trim and 256-char cap as fresh_tail_session_id.
569
- const sessionIdRaw = isJsonString(args.session_id) ? args.session_id.trim() : '';
570
- const sessionId = sessionIdRaw.length > 0 && sessionIdRaw.length <= 256
571
- ? sessionIdRaw
572
- : undefined;
573
- const apiCtx = {
574
- hippoRoot,
575
- tenantId,
576
- actor: mcpActor(ctx),
577
- };
578
- const recallExtra = {};
579
- if (freshTailCount !== undefined)
580
- recallExtra.freshTailCount = freshTailCount;
581
- if (freshTailSessionId !== undefined)
582
- recallExtra.freshTailSessionId = freshTailSessionId;
583
- if (summarizeOverflow !== undefined)
584
- recallExtra.summarizeOverflow = summarizeOverflow;
585
- if (scorerWindow !== undefined)
586
- recallExtra.scorerWindow = scorerWindow;
587
- if (sessionId !== undefined)
588
- recallExtra.sessionId = sessionId;
589
- const anchorRing = biasHintEnabled('anchoring') && sessionId
590
- ? getOrCreateRing(sessionRecallHistoryMcp, buildSessionKey(tenantId, sessionId))
591
- : null;
592
- const queryHash = hashQueryText(query);
593
- const out = {};
594
- // RecallContractError throws reach the MCP caller raw, as mcp-recall-fresh-tail-policy.test.ts pins.
595
- await apiRetrieve(apiCtx, {
596
- query,
597
- limit: 50,
598
- scope: explicitScope,
599
- includeContinuity,
600
- mode: config.physics?.enabled !== false ? 'physics' : 'hybrid',
601
- // The hint is computed below over the list MCP shows; the window band's copy would emit its audit row twice.
602
- suppressAvailabilityHint: true,
603
- keepHeldCopies: true,
604
- ...recallExtra,
605
- showRanked: ({ ranked, pool, droppedByScope }, apiResult) => {
606
- // Sections are paid in print order, ahead of the memories and after the heading; one that does not fit is dropped whole.
607
- let left = budget - memoriesReserve(budget);
608
- const pays = (piece) => {
609
- const tokens = estimateTokens(piece);
610
- if (tokens > left)
611
- return false;
612
- left -= tokens;
613
- return true;
614
- };
615
- const planPiece = planningSection(apiResult);
616
- const showPlan = planPiece !== '' && pays(planPiece);
617
- const tailRows = apiResult.results.filter((r) => r.isFreshTail || r.isSummary);
618
- const showTail = tailRows.length > 0 && pays(tailSection(tailRows));
619
- const continuityPiece = includeContinuity && apiResult.continuity ? `\n\n${formatContinuityBlock(apiResult.continuity)}` : '';
620
- const showContinuity = continuityPiece !== '' && pays(continuityPiece);
621
- // J1, J2 and C5: the hints and Cutoff block describe the list MCP shows, not the window band in apiResult.
622
- const render = (cut) => {
623
- const list = dropHeldCopies(cut, (r) => r.entry); // after every cut, so a merged row cut here never hides its sources
624
- const anchoring = anchorRing ? detectAnchoring(snapshotRing(anchorRing), queryHash, list[0]?.entry.id ?? null) : null;
625
- const availability = biasHintEnabled('availability')
626
- ? detectAvailabilityBias({
627
- topK: list.map((r) => ({ id: r.entry.id, created: r.entry.created })),
628
- pool: pool.map((e) => ({ id: e.id, created: e.created })),
629
- })
630
- : null;
631
- const shownIds = new Set(list.map((r) => r.entry.id));
632
- const shownKeys = storedTextKeys(list.map((r) => r.entry));
633
- const tail = showTail
634
- ? dropHeldCopies(tailRows.filter((r) => !shownIds.has(r.id) && !shownKeys.has(duplicateKey(r.content))), (r) => r)
635
- : [];
636
- const s = buildSuppressionSummary({
637
- totalCandidates: pool.length + droppedByScope,
638
- droppedPreRank: droppedByScope + cut.length - list.length, // the bucket CLI and API recall put hidden copies in
639
- droppedByBudget: Math.max(0, pool.length - cut.length), // an upper bound: rows that never matched count too
640
- summarySubstitutionsAdded: tail.filter((r) => r.isSummary).length,
641
- freshTailAdded: tail.filter((r) => r.isFreshTail && !r.isSummary).length,
642
- suppressedByInterference: anchoring?.reason === 'memory_dominance' ? 1 : 0,
643
- });
644
- // Anchoring is the stronger pull, so it prints first; the Cutoff block sits above the list, where the agent reads it.
645
- let text = anchoring ? `## Anchoring hint\n${anchoring.summary}\n[anchored_on: ${anchoring.memoryId}]\n\n---\n\n` : '';
646
- if (availability)
647
- text += `## Availability bias\n${availability.summary}\n\n---\n\n`;
648
- if (showPlan)
649
- text += planPiece;
650
- const cutoffClauses = [];
651
- if (s.droppedByBudget > 0)
652
- cutoffClauses.push(`${s.droppedByBudget} dropped to fit limit`);
653
- if (s.droppedPreRank > 0)
654
- cutoffClauses.push(`${s.droppedPreRank} filtered pre-rank`);
655
- if (s.summarySubstitutionsAdded > 0)
656
- cutoffClauses.push(`${s.summarySubstitutionsAdded} summary substitutions added`);
657
- if (s.freshTailAdded > 0)
658
- cutoffClauses.push(`${s.freshTailAdded} fresh-tail added`);
659
- if (s.suppressedByInterference > 0)
660
- cutoffClauses.push(`${s.suppressedByInterference} suppressed by interference`);
661
- if (cutoffClauses.length > 0) {
662
- text += `## Cutoff\nShowing ${list.length} of ${s.totalCandidates} candidates; ${cutoffClauses.join('; ')}.\n\n---\n\n`;
663
- }
664
- // The window band's fresh-tail and summary rows follow the ranked list, or the MCP fields go unanswered.
665
- text += formatMemories(list) + tailSection(tail) + (showContinuity ? continuityPiece : '');
666
- return { anchoring, availability, text, list };
667
- };
668
- let results = fitBudget(ranked, Math.max(0, left), 1, memoryCost);
669
- let rendered = render(results);
670
- // The hints, Cutoff block and heading vary with the list, so the lowest-ranked entry goes until the whole response fits.
671
- while (results.length > 1 && estimateTokens(rendered.text) > budget) {
672
- results = results.slice(0, -1);
673
- rendered = render(results);
674
- }
675
- out.rendered = rendered;
676
- return rendered.list.map((r) => r.entry.id);
677
- },
678
- });
679
- if (!out.rendered)
680
- throw new Error('hippo_recall: api.retrieve returned without calling showRanked');
681
- const { anchoring: mcpAnchoringHint, availability: mcpAvailabilityHint, list: shown, text: recallText } = out.rendered;
682
- lastRecalledIds.set(resolveClientKey(ctx), shown.map((r) => r.entry.id));
683
- if (biasHintEnabled('anchoring')) {
684
- if (anchorRing) {
685
- // Appended after the final detect: anchoredOn feeds the cooldown for the next recall on this session.
686
- appendRecall(anchorRing, queryHash, shown[0]?.entry.id ?? null, mcpAnchoringHint?.memoryId);
687
- if (mcpAnchoringHint?.reason === 'memory_dominance') {
688
- const dbForAudit = openHippoDb(hippoRoot);
689
- try {
690
- appendAuditEvent(dbForAudit, {
691
- tenantId,
692
- actor: ctx?.actor ?? 'mcp',
693
- op: 'recall_anchor_detected_memory_dominance',
694
- targetId: mcpAnchoringHint.memoryId,
695
- metadata: {
696
- memory_id: mcpAnchoringHint.memoryId,
697
- query_count: mcpAnchoringHint.queryCount ?? null,
698
- },
699
- });
700
- }
701
- finally {
702
- closeHippoDb(dbForAudit);
703
- }
704
- }
705
- else if (mcpAnchoringHint?.reason === 'query_repeat') {
706
- const dbForAudit = openHippoDb(hippoRoot);
707
- try {
708
- appendAuditEvent(dbForAudit, {
709
- tenantId,
710
- actor: ctx?.actor ?? 'mcp',
711
- op: 'recall_anchor_detected_query_repeat',
712
- targetId: mcpAnchoringHint.memoryId,
713
- metadata: { memory_id: mcpAnchoringHint.memoryId },
714
- });
715
- }
716
- finally {
717
- closeHippoDb(dbForAudit);
718
- }
719
- }
720
- }
721
- else {
722
- // Telemetry: caller had no sessionId so ring tracking skipped.
723
- // Per the recall-audit convention at api.ts:854, use SHA-256/16
724
- // for prompt hashing (NOT hashQueryText which is FNV-1a 32-bit
725
- // for recall matching; brute-force trivial for low-entropy
726
- // queries). Codex round-2 P2 catch.
727
- const dbForAudit = openHippoDb(hippoRoot);
728
- try {
729
- appendAuditEvent(dbForAudit, {
730
- tenantId,
731
- actor: ctx?.actor ?? 'mcp',
732
- op: 'recall_anchor_skipped_no_session',
733
- targetId: undefined,
734
- metadata: auditQueryFields(query),
735
- });
736
- }
737
- finally {
738
- closeHippoDb(dbForAudit);
739
- }
740
- }
741
- }
742
- if (mcpAvailabilityHint) {
743
- const dbForAudit = openHippoDb(hippoRoot);
744
- try {
745
- appendAuditEvent(dbForAudit, {
746
- tenantId,
747
- actor: ctx?.actor ?? 'mcp',
748
- op: 'recall_availability_detected',
749
- metadata: {
750
- recent_fraction: mcpAvailabilityHint.recentFraction,
751
- older_passed_over: mcpAvailabilityHint.olderCandidatesPassedOver,
752
- returned_count: mcpAvailabilityHint.returnedCount,
753
- },
754
- });
755
- }
756
- finally {
757
- closeHippoDb(dbForAudit);
758
- }
759
- }
760
- return recallText;
761
- }
762
- case 'hippo_assemble': {
763
- const sessionId = String(args.session_id || '');
764
- if (!sessionId)
765
- return 'No session_id provided.';
766
- const budget = Number(args.budget);
767
- const freshTailCount = Number(args.fresh_tail_count);
768
- const summarizeOlder = args.summarize_older !== false;
769
- const apiCtx = {
770
- hippoRoot,
771
- tenantId,
772
- actor: mcpActor(ctx),
773
- };
774
- const explicitScope = isJsonString(args.scope) && args.scope.length > 0
775
- ? args.scope
776
- : undefined;
777
- const assembleExtra = {};
778
- if (Number.isFinite(budget) && budget > 0)
779
- assembleExtra.budget = budget;
780
- if (Number.isFinite(freshTailCount) && freshTailCount >= 0)
781
- assembleExtra.freshTailCount = freshTailCount;
782
- if (explicitScope !== undefined)
783
- assembleExtra.scope = explicitScope;
784
- const r = apiAssemble(apiCtx, sessionId, {
785
- summarizeOlder,
786
- ...assembleExtra,
787
- cost: assembleCost(sessionId),
788
- });
789
- return assembleText(r);
790
- }
791
- case 'hippo_drill': {
792
- const summaryId = String(args.summary_id || '');
793
- if (!summaryId)
794
- return 'No summary_id provided.';
795
- const limit = Number(args.limit);
796
- const budget = Number(args.budget);
797
- // The inputSchema rejects a depth outside 1..10 before this runs, so no silent clamp hides the cap.
798
- const depth = args.depth === undefined ? undefined : Number(args.depth);
799
- const apiCtx = {
800
- hippoRoot,
801
- tenantId,
802
- actor: mcpActor(ctx),
803
- };
804
- const drillExtra = {};
805
- if (Number.isFinite(limit) && limit > 0)
806
- drillExtra.limit = limit;
807
- if (Number.isFinite(budget) && budget > 0)
808
- drillExtra.budget = budget;
809
- if (depth !== undefined)
810
- drillExtra.depth = depth;
811
- const r = apiDrillDown(apiCtx, summaryId, { ...drillExtra, cost: drillCost });
812
- if ('failure' in r) {
813
- // v1.6.4: only not_drillable is caller-actionable. not_found
814
- // intentionally collapses cross-tenant + scope-blocked + missing
815
- // (codex round 3 P1: distinguishing scope_blocked would leak
816
- // private-row existence on this surface).
817
- if (r.failure === 'not_drillable') {
818
- return `Id ${summaryId} is a leaf row, not a level-2+ summary; nothing to drill into.`;
819
- }
820
- return `No drillable summary at id=${summaryId}.`;
821
- }
822
- return drillText(r);
823
- }
824
- case 'hippo_predict_baserate': {
825
- // J3 reference-class / planning-fallacy detector. Reads from the E2
826
- // predictions table; returns text-only response matching the existing
827
- // MCP tool convention (no structured JSON over the wire). Direct call
828
- // to computePredictionBaserate; helper opens its own db + emits audit
829
- // (single source of truth, no caller-site drift).
830
- const classTag = String(args.class_tag || '').trim();
831
- if (!classTag)
832
- return 'No class_tag provided. Usage: pass class_tag matching a class used in past predictions (e.g. "migration-effort").';
833
- const baserate = computePredictionBaserate(hippoRoot, tenantId, classTag, ctx?.actor ?? 'mcp');
834
- if (baserate.nClosed === 0) {
835
- 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.`;
836
- }
837
- const lines = [baserate.summary, ''];
838
- lines.push(`n_closed: ${baserate.nClosed}`);
839
- lines.push(`n_ratio_eligible: ${baserate.nRatioEligible}`);
840
- if (baserate.meanEstimate !== null)
841
- lines.push(`mean_estimate: ${baserate.meanEstimate.toFixed(3)}`);
842
- if (baserate.meanActual !== null)
843
- lines.push(`mean_actual: ${baserate.meanActual.toFixed(3)}`);
844
- if (baserate.meanRatio !== null)
845
- lines.push(`mean_ratio: ${baserate.meanRatio.toFixed(3)}x`);
846
- if (baserate.p50Ratio !== null)
847
- lines.push(`p50_ratio: ${baserate.p50Ratio.toFixed(3)}x`);
848
- if (baserate.mae !== null)
849
- lines.push(`mae: ${baserate.mae.toFixed(3)}`);
850
- return lines.join('\n');
851
- }
852
- case 'hippo_remember': {
853
- const text = String(args.text || '');
854
- if (!text)
855
- return 'No text provided.';
856
- const tags = [];
857
- if (args.error)
858
- tags.push('error');
859
- if (args.tag)
860
- tags.push(String(args.tag));
861
- // Route through api.ts so audit_log captures the caller identity
862
- // uniformly with CLI/REST: the auth-resolved ctx.actor under HTTP-MCP,
863
- // 'mcp' for stdio (no ctx). api.ts.remember writes the memory + audit
864
- // row in one transaction-friendly path; we re-read the entry to surface
865
- // the half-life used in the MCP human-readable response.
866
- const apiCtx = {
867
- hippoRoot,
868
- tenantId,
869
- actor: mcpActor(ctx),
870
- };
871
- const result = apiRemember(apiCtx, {
872
- content: text,
873
- tags,
874
- });
875
- const entry = readEntry(hippoRoot, result.id, tenantId);
876
- // Auto-sleep: one run per store at a time, triggered by what arrived since the last one.
877
- if (config.autoSleep.enabled &&
878
- !autoSleepInFlight.has(hippoRoot) &&
879
- countCreatedSinceLastSleep(hippoRoot, tenantId) >= config.autoSleep.threshold) {
880
- autoSleepInFlight.add(hippoRoot);
881
- // Fire-and-forget (never block the response); an unhandled rejection would kill the server, so log it.
882
- consolidate(hippoRoot)
883
- .catch((err) => {
884
- log.error(`auto-sleep consolidate failed (tenant ${tenantId}): ${err instanceof Error ? err.message : String(err)}`);
885
- })
886
- .finally(() => autoSleepInFlight.delete(hippoRoot));
887
- }
888
- const halfLife = entry?.half_life_days ?? config.defaultHalfLifeDays;
889
- const tagStr = entry?.tags.join(', ') || tags.join(', ') || 'none';
890
- const warnings = (result.warnings ?? []).map((w) => `\nWarning: ${w}`).join('');
891
- return `Remembered [${result.id}] (half-life: ${halfLife}d, tags: ${tagStr})${warnings}`;
892
- }
893
- case 'hippo_outcome': {
894
- const good = Boolean(args.good);
895
- const clientKey = resolveClientKey(ctx);
896
- const ids = lastRecalledIds.get(clientKey) ?? [];
897
- if (ids.length === 0)
898
- return 'No recent recalls to apply outcome to.';
899
- // Route through src/api.ts so audit_log captures the caller identity
900
- // (auth-resolved ctx.actor under HTTP-MCP, 'mcp' for stdio) and tenant
901
- // scoping is enforced uniformly (same surface as recall/remember).
902
- // outcome() also handles cross-tenant id skip silently.
903
- const apiCtx = {
904
- hippoRoot,
905
- tenantId,
906
- actor: mcpActor(ctx),
907
- };
908
- const { applied } = apiOutcome(apiCtx, ids, good);
909
- return `Applied ${good ? 'positive' : 'negative'} outcome to ${applied} memories`;
910
- }
911
- case 'hippo_context': {
912
- const budget = args.budget === undefined
913
- ? config.defaultContextBudget
914
- : Number(args.budget);
915
- if (!Number.isFinite(budget) || budget < 0)
916
- return 'budget must be a non-negative number.';
917
- if (budget === 0)
918
- return '';
919
- if (budget < memoriesReserve(budget))
920
- return ''; // not even the heading fits, so nothing prints, as at budget 0
921
- const exactScope = isJsonString(args.scope) && args.scope.length > 0
922
- ? args.scope
923
- : undefined;
924
- // The served store names the project (an HTTP daemon runs from anywhere); the global root names none, so stdio falls back to its launch cwd.
925
- const storeProject = resolveProjectIdentity(path.dirname(path.resolve(hippoRoot))).name;
926
- const result = await apiGetContext({ hippoRoot, tenantId, actor: mcpActor(ctx) }, {
927
- q: autoDetectContext(),
928
- budget,
929
- exactScope,
930
- currentProject: storeProject !== '' ? storeProject : resolveProjectIdentity(process.cwd()).name,
931
- cost: contextCost,
932
- });
933
- lastRecalledIds.set(resolveClientKey(ctx), result.entries.map((r) => r.entry.id));
934
- return (result.activeSnapshot ? snapshotPiece(result.activeSnapshot) : '')
935
- + (result.sessionHandoff ? handoffPiece(result.sessionHandoff) : '')
936
- + (result.recentEvents ? trailPiece(result.recentEvents) : '')
937
- + formatMemories(result.entries);
938
- }
939
- case 'hippo_status': {
940
- // Every row counts toward the averages, so this scans the store, but without its text.
941
- const entries = loadStrengthRows(hippoRoot, tenantId);
942
- const now = evalNow(); // honors HIPPO_FAKE_NOW (eval-only; see ablation.ts)
943
- let atRisk = 0;
944
- let totalStrength = 0;
945
- for (const e of entries) {
946
- const s = calculateStrength(e, now);
947
- totalStrength += s;
948
- if (s < 0.1 && !e.pinned)
949
- atRisk++;
950
- }
951
- const avgStrength = entries.length > 0 ? (totalStrength / entries.length).toFixed(2) : '0';
952
- const pinned = entries.filter((e) => e.pinned).length;
953
- const errors = entries.filter((e) => e.tags.includes('error')).length;
954
- const conflicts = listMemoryConflicts(hippoRoot, 'open', tenantId).length;
955
- return [
956
- `Memories: ${entries.length} (${pinned} pinned, ${errors} errors)`,
957
- `Avg strength: ${avgStrength}`,
958
- `At risk (<0.1): ${atRisk}`,
959
- `Open conflicts: ${conflicts}`,
960
- `Half-life default: ${config.defaultHalfLifeDays}d`,
961
- ].join('\n');
962
- }
963
- case 'hippo_learn': {
964
- const days = Number(args.days) || 7;
965
- if (!isGitRepo(process.cwd()))
966
- return 'No git history found.';
967
- const gitLog = fetchGitLog(process.cwd(), days);
968
- if (!gitLog.trim())
969
- return 'No fix/revert/bug commits found in the specified period.';
970
- const parsedLessons = extractLessons(gitLog, config.gitLearnPatterns);
971
- // DF4: admission gate lives at the write path, not in extractLessons
972
- // (a published API surface that only parses). Bare subjects like
973
- // "fixed signals" are dropped here, before they ever become a memory.
974
- // Gating the loop INPUT is correct here, unlike the CLI path: this
975
- // loop only writes. The CLI's loop also runs invalidation, so there
976
- // the gate has to sit on the write alone or a migration subject stops
977
- // superseding stale memories. Same predicate, different placement,
978
- // because the loops do different work.
979
- const { kept: lessons, dropped } = partitionLessons(parsedLessons);
980
- const lowInfo = dropped.length;
981
- let added = 0;
982
- let skipped = 0;
983
- let rejected = 0;
984
- const keys = storedTextKeys(loadTextsHoldingWords(hippoRoot, tenantId, lessons.map(longestWord)));
985
- for (const lesson of lessons) {
986
- if (keys.has(duplicateKey(lesson))) {
987
- skipped++;
988
- continue;
989
- }
990
- const entry = createMemory(lesson, {
991
- layer: Layer.Episodic,
992
- tags: ['git-learned'],
993
- source: 'git',
994
- confidence: 'observed',
995
- baseHalfLifeDays: config.defaultHalfLifeDays,
996
- tenantId,
997
- });
998
- // AT1 (plan §3 containment): a refused lesson must not crash the
999
- // MCP learn call or lose the rest of the git log scan.
1000
- try {
1001
- writeEntry(hippoRoot, entry, { actor: ctx?.actor ?? 'mcp' });
1002
- }
1003
- catch (err) {
1004
- if (err instanceof RejectedValueError) {
1005
- rejected++;
1006
- continue;
1007
- }
1008
- throw err;
1009
- }
1010
- keys.add(duplicateKey(lesson));
1011
- added++;
1012
- }
1013
- const rejectedSuffix = rejected > 0 ? `, ${rejected} rejected values skipped` : '';
1014
- const lowInfoSuffix = lowInfo > 0 ? `, ${lowInfo} low-information subjects dropped` : '';
1015
- return `Git learn: ${added} new, ${skipped} duplicates skipped${rejectedSuffix}${lowInfoSuffix} (scanned ${days} days)`;
1016
- }
1017
- case 'hippo_conflicts': {
1018
- const conflicts = listMemoryConflicts(hippoRoot, 'open', tenantId);
1019
- if (conflicts.length === 0)
1020
- return 'No open conflicts.';
1021
- return conflicts.map((c) => `conflict_${c.id}: ${c.memory_a_id} <-> ${c.memory_b_id} (score=${c.score.toFixed(2)}) — ${c.reason}`).join('\n');
1022
- }
1023
- case 'hippo_resolve': {
1024
- const conflictId = Number(args.conflict_id);
1025
- const keepId = String(args.keep || '');
1026
- const forget = Boolean(args.forget);
1027
- // AT1: optional rejectLoser + reason, threaded straight through to
1028
- // resolveConflict's opts (plan §5 — mirrors the CLI's --reject-loser).
1029
- const rejectLoser = Boolean(args.rejectLoser);
1030
- const reason = isJsonString(args.reason) ? args.reason : undefined;
1031
- if (isNaN(conflictId) || !keepId)
1032
- return 'Required: conflict_id and keep.';
1033
- const result = resolveConflict(hippoRoot, conflictId, keepId, forget, tenantId, {
1034
- rejectLoserValue: rejectLoser,
1035
- reason,
1036
- // P2 fix: resolveConflict's opts.rejectedBy defaults to 'cli' when
1037
- // omitted — this call site never passed it, so the tombstone's
1038
- // rejected_by AND the conflict_resolve audit's actor both landed as
1039
- // 'cli' even though the caller was MCP. ctx.actor carries the
1040
- // auth-resolved actor for HTTP-MCP (see McpContext above); stdio
1041
- // callers pass no ctx, so 'mcp' is the honest fallback there.
1042
- rejectedBy: ctx?.actor ?? 'mcp',
1043
- });
1044
- if (!result)
1045
- return 'Could not resolve. Check the conflict ID and --keep value.';
1046
- const action = rejectLoser ? 'rejected (tombstoned) and removed' : forget ? 'deleted' : 'weakened';
1047
- return `Resolved conflict ${conflictId}: kept ${keepId}, ${action} ${result.loserId}`;
1048
- }
1049
- case 'hippo_share': {
1050
- const shareId = String(args.id || '');
1051
- if (!shareId)
1052
- return 'Required: id (memory ID to share).';
1053
- const force = Boolean(args.force);
1054
- // Pass tenantId so shareMemory's readEntry filters by tenant. Without
1055
- // this, a Bearer for tenant A could call hippo_share with tenant B's
1056
- // id and copy the row to the global store. The 'Memory not found'
1057
- // error matches the cross-tenant deny shape elsewhere in the code.
1058
- const shared = shareMemory(hippoRoot, shareId, { force, tenantId });
1059
- if (!shared)
1060
- return 'Transfer score too low. Use force=true to override.';
1061
- return `Shared [${shared.id}] to global store. Source: ${shared.source}`;
1062
- }
1063
- case 'hippo_peers': {
1064
- // D4 v1.12.10: tenant-scope the cross-project peer discovery.
1065
- // tenantId is the caller's tenant (matches hippo_share above);
1066
- // passing undefined would restore the pre-D4 host-wide behaviour.
1067
- const peers = listPeers(undefined, tenantId);
1068
- if (peers.length === 0)
1069
- return 'No peers found.';
1070
- return peers.map((p) => `${p.project}: ${p.count} memories (latest: ${p.latest.slice(0, 10)})`).join('\n');
1071
- }
1072
- default:
1073
- // handleMcpRequest rejects names missing from TOOLS, so reaching here means TOOLS and this switch drifted apart.
1074
- throw new Error(`hippo-mcp: tool ${name} is declared but has no handler`);
1075
- }
1076
- }
1077
- // ── Request handling ──
1078
- /**
1079
- * Transport-agnostic MCP dispatcher. Both the stdio loop (below) and the
1080
- * HTTP/SSE transport in src/server.ts route every incoming JSON-RPC message
1081
- * through this single function. Returns null for notifications (no response
1082
- * expected) and a McpResponse otherwise. Errors thrown by `executeTool` are
1083
- * the caller's problem — wrap with try/catch on the transport side.
1084
- */
1085
- export async function handleMcpRequest(req, ctx) {
1086
- const { id, method, params } = req;
1087
- switch (method) {
1088
- case 'initialize':
1089
- return {
1090
- jsonrpc: '2.0',
1091
- id,
1092
- result: {
1093
- protocolVersion: '2024-11-05',
1094
- capabilities: { tools: {} },
1095
- serverInfo: { name: 'hippo-memory', version: PACKAGE_VERSION },
1096
- },
1097
- };
1098
- case 'notifications/initialized':
1099
- return null;
1100
- case 'tools/list':
1101
- return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
1102
- case 'tools/call': {
1103
- const nameValue = params?.name;
1104
- const toolName = isJsonString(nameValue) ? nameValue : '';
1105
- const tool = TOOLS_BY_NAME.get(toolName);
1106
- if (!tool) {
1107
- return { jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown tool: ${toolName.slice(0, 128)}` } };
1108
- }
1109
- const argumentsValue = params?.arguments;
1110
- if (argumentsValue !== undefined && argumentsValue !== null && !isJsonObjectRecord(argumentsValue)) {
1111
- return { jsonrpc: '2.0', id, error: { code: -32602, message: `${toolName}: arguments must be an object` } };
1112
- }
1113
- const toolArgs = isJsonObjectRecord(argumentsValue) ? argumentsValue : {};
1114
- // The MCP spec reports input validation as a tool result with isError, so the model can read it and retry.
1115
- const problems = validateToolArgs(tool.inputSchema, toolArgs, ARGS_CHECKED_BY_API.get(toolName));
1116
- if (problems.length > 0) {
1117
- return {
1118
- jsonrpc: '2.0',
1119
- id,
1120
- result: { content: [{ type: 'text', text: `Invalid arguments for ${toolName}: ${problems.join('; ')}` }], isError: true },
1121
- };
1122
- }
1123
- const output = await executeTool(toolName, toolArgs, ctx);
1124
- recordMcpTokens(toolName, output, ctx);
1125
- return {
1126
- jsonrpc: '2.0',
1127
- id,
1128
- result: {
1129
- content: [{ type: 'text', text: output || 'Done.' }],
1130
- },
1131
- };
1132
- }
1133
- default:
1134
- // Notifications (no id) must not receive a response
1135
- if (method.startsWith('notifications/'))
1136
- return null;
1137
- return {
1138
- jsonrpc: '2.0',
1139
- id,
1140
- error: { code: -32601, message: `Method not found: ${method}` },
1141
- };
1142
- }
1143
- }
1144
- // ── Stdio transport ──
1145
- import { parseFrame } from './framing.js';
1146
- let buffer = Buffer.alloc(0);
1147
- function dispatch(body) {
1148
- let req;
1149
- try {
1150
- // SAFETY: malformed JSON is caught below and the frame is skipped; the
1151
- // JSON-RPC shape itself is validated field-by-field next (req.method
1152
- // truthiness check), matching the src/server.ts HTTP transport's own
1153
- // `JSON.parse(raw) as McpRequest` boundary cast.
1154
- req = JSON.parse(body);
1155
- }
1156
- catch {
1157
- log.debug('mcp: skipped a frame that is not valid JSON');
1158
- return;
1159
- }
1160
- if (!req.method)
1161
- return;
1162
- if (req.method.startsWith('notifications/')) {
1163
- handleMcpRequest(req).catch((err) => {
1164
- log.error(`mcp notification ${req.method} failed: ${err instanceof Error ? err.message : String(err)}`);
1165
- });
1166
- return;
1167
- }
1168
- handleMcpRequest(req).then((resp) => { if (resp)
1169
- send(resp); }).catch((err) => {
1170
- send(mcpErrorResponse(req.id, err));
1171
- });
1172
- }
1173
- /**
1174
- * Wire stdin/stdout to the dispatcher. Idempotent — only the entrypoint
1175
- * (cli.ts `hippo mcp`, or running this file directly) should call this.
1176
- * src/server.ts imports `handleMcpRequest` without invoking this, so the
1177
- * HTTP daemon does not steal stdin or exit when its parent closes a pipe.
1178
- */
1179
- export function startStdioLoop() {
1180
- process.stdin.on('data', (chunk) => {
1181
- buffer = Buffer.concat([buffer, chunk]);
1182
- while (true) {
1183
- const result = parseFrame(buffer);
1184
- if (result.kind === 'incomplete')
1185
- break;
1186
- buffer = result.rest;
1187
- if (result.kind === 'message')
1188
- dispatch(result.body);
1189
- }
1190
- });
1191
- process.stdin.on('end', () => process.exit(0));
1192
- process.on('uncaughtException', (err) => {
1193
- log.error(`mcp uncaught: ${err instanceof Error ? err.message : String(err)}`);
1194
- });
1195
- process.on('unhandledRejection', (err) => {
1196
- log.error(`mcp unhandled: ${err instanceof Error ? err.message : String(err)}`);
1197
- });
1198
- }
10
+ export { __resetSessionRecallHistoryMcp } from './session-state.js';
11
+ export { findHippoRoot, mcpErrorResponse } from './protocol.js';
12
+ export { handleMcpRequest } from './request.js';
13
+ export { startStdioLoop } from './stdio.js';
14
+ import { envMcpStdio } from '../env.js';
15
+ import { startStdioLoop } from './stdio.js';
1199
16
  // Auto-start when invoked as the main module (node dist/mcp/server.js or via
1200
17
  // the cli's `import('./mcp/server.js')`). Importing this file from another
1201
18
  // module (e.g. src/server.ts wiring up the HTTP/SSE transport) will NOT
@@ -1208,7 +25,7 @@ const isMainModule = (() => {
1208
25
  const argv1 = process.argv[1] ?? '';
1209
26
  if (argv1.endsWith('mcp/server.js') || argv1.endsWith('mcp\\server.js'))
1210
27
  return true;
1211
- if (process.env.HIPPO_MCP_STDIO === '1')
28
+ if (envMcpStdio())
1212
29
  return true;
1213
30
  // ESM main-module check
1214
31
  const mainUrl = `file://${argv1.replace(/\\/g, '/')}`;