hippo-memory 1.61.0 → 1.63.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 (377) hide show
  1. package/README.md +37 -53
  2. package/dist/agent-memories/apply.d.ts +3 -1
  3. package/dist/agent-memories/apply.js +23 -1
  4. package/dist/agent-memories/claude-code.d.ts +4 -1
  5. package/dist/agent-memories/claude-code.js +53 -9
  6. package/dist/agent-memories/report.d.ts +1 -0
  7. package/dist/agent-memories/report.js +2 -2
  8. package/dist/agent-memories/sync.d.ts +3 -3
  9. package/dist/agent-memories/sync.js +46 -24
  10. package/dist/agent-memories/types.d.ts +0 -2
  11. package/dist/ambient-store.d.ts +4 -4
  12. package/dist/ambient-store.js +4 -3
  13. package/dist/api/assemble.d.ts +7 -10
  14. package/dist/api/assemble.js +62 -66
  15. package/dist/api/audit.d.ts +2 -2
  16. package/dist/api/audit.js +2 -2
  17. package/dist/api/auth.d.ts +6 -6
  18. package/dist/api/auth.js +5 -7
  19. package/dist/api/context-select.d.ts +50 -0
  20. package/dist/api/context-select.js +342 -0
  21. package/dist/api/context-types.d.ts +12 -17
  22. package/dist/api/context.d.ts +5 -4
  23. package/dist/api/context.js +208 -535
  24. package/dist/api/dormant.js +1 -1
  25. package/dist/api/drill-down.d.ts +4 -4
  26. package/dist/api/drill-down.js +56 -41
  27. package/dist/api/outcome.d.ts +8 -13
  28. package/dist/api/outcome.js +13 -17
  29. package/dist/api/promote.d.ts +5 -8
  30. package/dist/api/promote.js +55 -66
  31. package/dist/api/quarantine.js +3 -2
  32. package/dist/api/recall-types.d.ts +43 -55
  33. package/dist/api/recall.d.ts +3 -3
  34. package/dist/api/recall.js +310 -456
  35. package/dist/api/remember.d.ts +2 -2
  36. package/dist/api/sleep.d.ts +7 -35
  37. package/dist/api/sleep.js +206 -220
  38. package/dist/api/tokens.d.ts +2 -2
  39. package/dist/api/tokens.js +2 -2
  40. package/dist/api/types.d.ts +6 -10
  41. package/dist/api/types.js +2 -4
  42. package/dist/audit-prune.d.ts +4 -6
  43. package/dist/audit-prune.js +3 -5
  44. package/dist/audit.js +11 -33
  45. package/dist/auth.d.ts +8 -9
  46. package/dist/auth.js +5 -7
  47. package/dist/autolearn.d.ts +1 -1
  48. package/dist/autolearn.js +1 -1
  49. package/dist/availability.js +3 -5
  50. package/dist/capture/command.d.ts +5 -4
  51. package/dist/capture/command.js +8 -22
  52. package/dist/capture/compact.d.ts +2 -2
  53. package/dist/capture/compact.js +11 -15
  54. package/dist/capture/extract.js +37 -115
  55. package/dist/capture-error.d.ts +1 -1
  56. package/dist/capture-error.js +1 -1
  57. package/dist/churn-git.d.ts +1 -1
  58. package/dist/churn-git.js +1 -1
  59. package/dist/cli/audit.js +3 -4
  60. package/dist/cli/auth.js +3 -6
  61. package/dist/cli/briefs.js +324 -306
  62. package/dist/cli/context.js +44 -34
  63. package/dist/cli/continuity.js +283 -271
  64. package/dist/cli/curate.d.ts +1 -1
  65. package/dist/cli/curate.js +43 -58
  66. package/dist/cli/dag.js +5 -9
  67. package/dist/cli/decisions.js +334 -345
  68. package/dist/cli/explain.js +68 -61
  69. package/dist/cli/goals.js +1 -1
  70. package/dist/cli/init.js +1 -1
  71. package/dist/cli/maintenance.js +62 -51
  72. package/dist/cli/playbooks.js +391 -379
  73. package/dist/cli/projects.js +11 -6
  74. package/dist/cli/recall.js +30 -44
  75. package/dist/cli/remember.js +118 -87
  76. package/dist/cli/session-hooks.js +106 -115
  77. package/dist/cli/setup.d.ts +1 -1
  78. package/dist/cli/setup.js +267 -250
  79. package/dist/cli/shared.js +3 -3
  80. package/dist/cli/slack.js +1 -1
  81. package/dist/cli/sleep.js +27 -1
  82. package/dist/cli/status.d.ts +4 -4
  83. package/dist/cli/status.js +80 -76
  84. package/dist/cli/transfer.js +88 -107
  85. package/dist/cli/usage.js +9 -6
  86. package/dist/cli.d.ts +1 -1
  87. package/dist/cli.js +4 -9
  88. package/dist/compaction-record.d.ts +2 -2
  89. package/dist/compaction-record.js +89 -68
  90. package/dist/compare.d.ts +11 -16
  91. package/dist/compare.js +11 -16
  92. package/dist/config.d.ts +19 -18
  93. package/dist/config.js +90 -67
  94. package/dist/connectors/github/backfill.d.ts +2 -2
  95. package/dist/connectors/github/backfill.js +8 -15
  96. package/dist/connectors/github/cli-impl.js +3 -8
  97. package/dist/connectors/github/deletion.d.ts +5 -12
  98. package/dist/connectors/github/deletion.js +5 -12
  99. package/dist/connectors/github/dlq.d.ts +6 -9
  100. package/dist/connectors/github/dlq.js +2 -3
  101. package/dist/connectors/github/ingest.d.ts +5 -7
  102. package/dist/connectors/github/ingest.js +8 -12
  103. package/dist/connectors/github/octokit-client.d.ts +3 -5
  104. package/dist/connectors/github/octokit-client.js +5 -6
  105. package/dist/connectors/github/signature.d.ts +9 -39
  106. package/dist/connectors/github/signature.js +9 -39
  107. package/dist/connectors/github/tenant-routing.d.ts +1 -1
  108. package/dist/connectors/github/tenant-routing.js +1 -1
  109. package/dist/connectors/github/transform.js +2 -2
  110. package/dist/connectors/github/types.d.ts +2 -10
  111. package/dist/connectors/github/types.js +1 -3
  112. package/dist/connectors/slack/deletion.d.ts +3 -8
  113. package/dist/connectors/slack/deletion.js +3 -8
  114. package/dist/connectors/slack/dlq.d.ts +1 -1
  115. package/dist/connectors/slack/ingest.d.ts +1 -1
  116. package/dist/connectors/slack/ingest.js +7 -16
  117. package/dist/connectors/slack/signature.d.ts +1 -1
  118. package/dist/connectors/slack/tenant-routing.d.ts +3 -5
  119. package/dist/connectors/slack/tenant-routing.js +3 -5
  120. package/dist/connectors/slack/transform.d.ts +5 -6
  121. package/dist/connectors/slack/transform.js +5 -6
  122. package/dist/connectors/slack/types.d.ts +2 -6
  123. package/dist/connectors/slack/types.js +1 -3
  124. package/dist/connectors/slack/web-client.js +10 -3
  125. package/dist/connectors/slack/workspaces.d.ts +3 -5
  126. package/dist/connectors/slack/workspaces.js +3 -5
  127. package/dist/consolidate/conflicts.js +3 -14
  128. package/dist/consolidate/decay.js +9 -29
  129. package/dist/consolidate/llm-passes.js +4 -5
  130. package/dist/consolidate/merge.js +8 -23
  131. package/dist/consolidate/run.d.ts +1 -8
  132. package/dist/consolidate/run.js +3 -25
  133. package/dist/consolidate/sleep.js +5 -17
  134. package/dist/consolidate/traces.js +9 -21
  135. package/dist/customer-notes.d.ts +5 -7
  136. package/dist/customer-notes.js +82 -76
  137. package/dist/dag.d.ts +10 -21
  138. package/dist/dag.js +189 -203
  139. package/dist/db/continuity.js +2 -2
  140. package/dist/db/migrations/v14.js +1 -1
  141. package/dist/db/migrations/v15.js +1 -2
  142. package/dist/db/migrations/v16.js +3 -4
  143. package/dist/db/migrations/v17.js +2 -3
  144. package/dist/db/migrations/v19.js +1 -1
  145. package/dist/db/migrations/v20.js +1 -1
  146. package/dist/db/migrations/v21.js +2 -6
  147. package/dist/db/migrations/v22.js +2 -4
  148. package/dist/db/migrations/v23.js +1 -1
  149. package/dist/db/migrations/v24.js +4 -6
  150. package/dist/db/migrations/v25.js +2 -3
  151. package/dist/db/migrations/v26.js +3 -3
  152. package/dist/db/migrations/v27.js +2 -10
  153. package/dist/db/migrations/v28.js +5 -8
  154. package/dist/db/migrations/v29.js +3 -4
  155. package/dist/db/migrations/v30.js +2 -2
  156. package/dist/db/migrations/v31.js +1 -1
  157. package/dist/db/migrations/v32.js +1 -1
  158. package/dist/db/migrations/v33.js +3 -3
  159. package/dist/db/migrations/v34.js +1 -1
  160. package/dist/db/migrations/v35.js +3 -4
  161. package/dist/db/migrations/v36.js +3 -4
  162. package/dist/db/migrations/v37.js +5 -5
  163. package/dist/db/migrations/v38.js +7 -8
  164. package/dist/db/migrations/v39.js +1 -1
  165. package/dist/db/migrations/v40.js +4 -16
  166. package/dist/db/migrations/v41.js +3 -4
  167. package/dist/db/migrations/v42.js +3 -4
  168. package/dist/db/migrations/v45.js +1 -1
  169. package/dist/db/migrations/v46.js +1 -1
  170. package/dist/db/migrations/v47.js +1 -1
  171. package/dist/db/migrations/v48.js +1 -1
  172. package/dist/decisions.d.ts +2 -2
  173. package/dist/decisions.js +97 -80
  174. package/dist/dedupe.js +86 -61
  175. package/dist/delivery-recorder.js +154 -135
  176. package/dist/doctor.js +129 -110
  177. package/dist/dormant.js +1 -4
  178. package/dist/embedding-provider.d.ts +4 -8
  179. package/dist/embedding-provider.js +4 -8
  180. package/dist/embeddings.js +55 -47
  181. package/dist/env.d.ts +1 -1
  182. package/dist/env.js +12 -12
  183. package/dist/escape.d.ts +5 -0
  184. package/dist/escape.js +10 -0
  185. package/dist/eval-stats.d.ts +1 -2
  186. package/dist/eval-stats.js +1 -2
  187. package/dist/eval-suite.js +27 -21
  188. package/dist/extract.js +4 -9
  189. package/dist/failure-log.d.ts +3 -3
  190. package/dist/failure-log.js +1 -1
  191. package/dist/forward-claim-detector.d.ts +2 -4
  192. package/dist/forward-claim-detector.js +6 -11
  193. package/dist/goals.d.ts +3 -3
  194. package/dist/goals.js +103 -91
  195. package/dist/graph/read.d.ts +2 -2
  196. package/dist/graph/read.js +5 -6
  197. package/dist/graph/types.d.ts +8 -8
  198. package/dist/graph/write.d.ts +7 -14
  199. package/dist/graph/write.js +16 -23
  200. package/dist/graph-extract.d.ts +7 -8
  201. package/dist/graph-extract.js +62 -72
  202. package/dist/graph-recall.d.ts +2 -2
  203. package/dist/graph-recall.js +55 -49
  204. package/dist/graph-stream.d.ts +5 -6
  205. package/dist/graph-stream.js +66 -57
  206. package/dist/graph-view.d.ts +2 -2
  207. package/dist/graph-view.js +7 -7
  208. package/dist/half-life-migration.d.ts +1 -2
  209. package/dist/half-life-migration.js +2 -3
  210. package/dist/hooks/codex-session.js +1 -1
  211. package/dist/hooks/codex-wrapper.d.ts +1 -1
  212. package/dist/hooks/codex-wrapper.js +3 -2
  213. package/dist/hooks/json-hooks.d.ts +2 -2
  214. package/dist/hooks/json-hooks.js +5 -4
  215. package/dist/hooks/opencode.d.ts +1 -1
  216. package/dist/hooks/opencode.js +5 -4
  217. package/dist/hooks/shared.d.ts +3 -7
  218. package/dist/hooks/shared.js +1 -8
  219. package/dist/http-util.d.ts +2 -3
  220. package/dist/http-util.js +3 -0
  221. package/dist/importers/core.d.ts +2 -9
  222. package/dist/importers/core.js +15 -30
  223. package/dist/importers/sources.js +2 -1
  224. package/dist/importers/vault.js +2 -20
  225. package/dist/incidents.d.ts +1 -1
  226. package/dist/incidents.js +46 -39
  227. package/dist/instruction-detect.d.ts +1 -1
  228. package/dist/instruction-detect.js +1 -1
  229. package/dist/invalidation.d.ts +3 -0
  230. package/dist/invalidation.js +160 -114
  231. package/dist/json.d.ts +5 -0
  232. package/dist/json.js +4 -0
  233. package/dist/judgment.js +1 -2
  234. package/dist/local-embedding.js +1 -1
  235. package/dist/mcp/admin-tools.js +7 -17
  236. package/dist/mcp/format.js +1 -1
  237. package/dist/mcp/framing.js +3 -6
  238. package/dist/mcp/protocol.d.ts +2 -5
  239. package/dist/mcp/protocol.js +1 -3
  240. package/dist/mcp/recall-tools.js +12 -15
  241. package/dist/mcp/request.js +4 -3
  242. package/dist/mcp/session-state.js +2 -3
  243. package/dist/mcp/stdio.js +2 -1
  244. package/dist/mcp/tools.js +9 -6
  245. package/dist/memory-value-weights.d.ts +5 -8
  246. package/dist/memory-value-weights.js +5 -8
  247. package/dist/memory-value.d.ts +13 -13
  248. package/dist/memory-value.js +26 -37
  249. package/dist/memory.d.ts +20 -22
  250. package/dist/memory.js +24 -48
  251. package/dist/multihop.d.ts +1 -1
  252. package/dist/multihop.js +3 -2
  253. package/dist/owner-validation.d.ts +4 -5
  254. package/dist/owner-validation.js +4 -5
  255. package/dist/physics.d.ts +4 -4
  256. package/dist/physics.js +7 -9
  257. package/dist/policies.d.ts +9 -10
  258. package/dist/policies.js +96 -81
  259. package/dist/postinstall.js +3 -6
  260. package/dist/predictions/planning-fallacy.d.ts +9 -14
  261. package/dist/predictions/planning-fallacy.js +10 -16
  262. package/dist/predictions/store.d.ts +15 -23
  263. package/dist/predictions/store.js +36 -33
  264. package/dist/processes.d.ts +2 -7
  265. package/dist/processes.js +88 -72
  266. package/dist/project-briefs.d.ts +2 -3
  267. package/dist/project-briefs.js +141 -118
  268. package/dist/project-identity.d.ts +22 -9
  269. package/dist/project-identity.js +47 -12
  270. package/dist/project-merge.d.ts +28 -5
  271. package/dist/project-merge.js +213 -46
  272. package/dist/project-remote.d.ts +12 -0
  273. package/dist/project-remote.js +138 -0
  274. package/dist/prompt-recall.js +1 -2
  275. package/dist/rate-limit.d.ts +1 -1
  276. package/dist/rate-limit.js +1 -1
  277. package/dist/raw-archive.d.ts +9 -0
  278. package/dist/raw-archive.js +70 -53
  279. package/dist/recall-history.d.ts +19 -20
  280. package/dist/recall-history.js +24 -42
  281. package/dist/recall-pipeline.js +4 -28
  282. package/dist/recall-scope.d.ts +7 -8
  283. package/dist/recall-scope.js +7 -8
  284. package/dist/recall-trace.d.ts +5 -9
  285. package/dist/recall-trace.js +6 -10
  286. package/dist/refine-llm.d.ts +1 -1
  287. package/dist/refine-llm.js +2 -2
  288. package/dist/reject-flow.d.ts +3 -4
  289. package/dist/reject-flow.js +122 -117
  290. package/dist/rejection.d.ts +5 -6
  291. package/dist/rejection.js +7 -15
  292. package/dist/rerankers/clef.d.ts +1 -1
  293. package/dist/rerankers/jev.d.ts +1 -2
  294. package/dist/rerankers/jev.js +4 -5
  295. package/dist/rerankers/llm.d.ts +1 -2
  296. package/dist/rerankers/llm.js +1 -2
  297. package/dist/rerankers/types.d.ts +1 -2
  298. package/dist/rrf.d.ts +2 -2
  299. package/dist/rrf.js +2 -2
  300. package/dist/search/bm25-search.d.ts +1 -1
  301. package/dist/search/bm25-search.js +2 -1
  302. package/dist/search/boosts.js +2 -1
  303. package/dist/search/hybrid.d.ts +1 -1
  304. package/dist/search/hybrid.js +2 -1
  305. package/dist/search/physics-search.d.ts +1 -1
  306. package/dist/search/physics-search.js +2 -1
  307. package/dist/search/types.d.ts +2 -0
  308. package/dist/search/types.js +3 -1
  309. package/dist/secret-detect.d.ts +4 -5
  310. package/dist/secret-detect.js +6 -10
  311. package/dist/server/auth.js +5 -5
  312. package/dist/server/client-ip.js +1 -1
  313. package/dist/server/cursor.js +2 -1
  314. package/dist/server/mcp-http.js +4 -4
  315. package/dist/server/request.d.ts +3 -6
  316. package/dist/server/request.js +6 -7
  317. package/dist/server/routes/admin.js +5 -4
  318. package/dist/server/routes/customer-notes.js +6 -5
  319. package/dist/server/routes/decisions.js +4 -3
  320. package/dist/server/routes/incidents.js +7 -5
  321. package/dist/server/routes/memories.js +7 -7
  322. package/dist/server/routes/policies.js +3 -2
  323. package/dist/server/routes/predictions.js +12 -15
  324. package/dist/server/routes/processes.js +3 -2
  325. package/dist/server/routes/project-briefs.js +8 -7
  326. package/dist/server/routes/recall.js +95 -93
  327. package/dist/server/routes/skills.js +6 -5
  328. package/dist/server/types.d.ts +1 -1
  329. package/dist/server/validation.d.ts +1 -2
  330. package/dist/server/validation.js +7 -14
  331. package/dist/server-detect.js +72 -58
  332. package/dist/server.d.ts +2 -2
  333. package/dist/server.js +131 -117
  334. package/dist/shared.d.ts +26 -17
  335. package/dist/shared.js +102 -104
  336. package/dist/skills.d.ts +3 -3
  337. package/dist/skills.js +88 -72
  338. package/dist/store/audit-event.d.ts +2 -2
  339. package/dist/store/audit-event.js +1 -1
  340. package/dist/store/candidates.d.ts +2 -2
  341. package/dist/store/candidates.js +4 -3
  342. package/dist/store/conflicts.js +30 -22
  343. package/dist/store/delete-and-batch.d.ts +11 -14
  344. package/dist/store/delete-and-batch.js +40 -91
  345. package/dist/store/entry-reads.d.ts +17 -25
  346. package/dist/store/entry-reads.js +59 -40
  347. package/dist/store/entry-row.d.ts +6 -24
  348. package/dist/store/entry-row.js +6 -24
  349. package/dist/store/entry-writes.d.ts +6 -7
  350. package/dist/store/entry-writes.js +13 -11
  351. package/dist/store/handoffs.d.ts +1 -1
  352. package/dist/store/handoffs.js +7 -10
  353. package/dist/store/index-and-stats.d.ts +2 -6
  354. package/dist/store/index-and-stats.js +4 -10
  355. package/dist/store/mirrors.d.ts +6 -19
  356. package/dist/store/mirrors.js +14 -39
  357. package/dist/store/open.js +9 -31
  358. package/dist/store/rows.d.ts +5 -11
  359. package/dist/store/rows.js +6 -11
  360. package/dist/store/search-rows.d.ts +17 -34
  361. package/dist/store/search-rows.js +34 -56
  362. package/dist/store/sessions.d.ts +4 -5
  363. package/dist/store/sessions.js +5 -6
  364. package/dist/store/summaries.d.ts +13 -17
  365. package/dist/store/summaries.js +26 -70
  366. package/dist/support-bundle.js +4 -8
  367. package/dist/tenant.d.ts +1 -5
  368. package/dist/token-ledger.d.ts +1 -1
  369. package/dist/token-ledger.js +3 -5
  370. package/dist/trace.js +1 -3
  371. package/dist/version.d.ts +1 -1
  372. package/dist/version.js +1 -1
  373. package/dist/working-memory.d.ts +1 -1
  374. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  375. package/extensions/openclaw-plugin/package.json +1 -1
  376. package/openclaw.plugin.json +1 -1
  377. package/package.json +1 -1
@@ -63,7 +63,7 @@ export function restoreDormant(ctx, id) {
63
63
  writeEntryDbOnly(db, restored, { actor: ctx.actor.subject });
64
64
  deleteDormantRow(db, ctx.tenantId, id);
65
65
  // A restore is a labelled "forgot it, then needed it" event: the
66
- // signal a learned lifecycle (ROADMAP LC3) trains on. Same transaction
66
+ // signal a learned lifecycle trains on. Same transaction
67
67
  // as the restore, so the label exists exactly when the restore does.
68
68
  appendAuditEvent(db, {
69
69
  tenantId: ctx.tenantId,
@@ -73,11 +73,11 @@ export type DrillDownOutcome = DrillDownResult | DrillDownFailure;
73
73
  * if the underlying DAG accidentally linked across scopes.
74
74
  *
75
75
  * Returns a discriminated `DrillDownOutcome`: `DrillDownResult` on success,
76
- * or `{failure: '...'}` for `not_found` (covers genuinely-missing AND wrong-
77
- * tenant, intentionally indistinguishable), `not_drillable` (id is a leaf
78
- * row), or `scope_blocked` (caller has no scope grant for the row's scope).
76
+ * or `{failure: '...'}` for `not_found` (covers genuinely-missing, wrong-
77
+ * tenant and scope-blocked, intentionally indistinguishable) or
78
+ * `not_drillable` (id is a leaf row).
79
79
  *
80
- * Pre-v1.6.4 returned null for all four cases. JS callers migrate via
80
+ * Pre-v1.6.4 returned null for all three cases. JS callers migrate via
81
81
  * `'failure' in result` checks; HTTP route maps `not_drillable` to 422.
82
82
  */
83
83
  export declare function drillDown(ctx: Context, summaryId: string, opts?: DrillDownOpts): DrillDownOutcome;
@@ -1,5 +1,7 @@
1
1
  // DAG drill-down from a summary to its children.
2
- import { readEntry, loadChildrenOf } from '../store/entry-reads.js';
2
+ import { closeHippoDb } from '../db.js';
3
+ import { openStore } from '../store/open.js';
4
+ import { selectEntriesByIds, selectChildrenByParent } from '../store/entry-reads.js';
3
5
  import { estimateTokens } from '../token-ledger.js';
4
6
  import { passesScopeFilterForRecall } from '../recall-scope.js';
5
7
  /**
@@ -14,11 +16,11 @@ import { passesScopeFilterForRecall } from '../recall-scope.js';
14
16
  * if the underlying DAG accidentally linked across scopes.
15
17
  *
16
18
  * Returns a discriminated `DrillDownOutcome`: `DrillDownResult` on success,
17
- * or `{failure: '...'}` for `not_found` (covers genuinely-missing AND wrong-
18
- * tenant, intentionally indistinguishable), `not_drillable` (id is a leaf
19
- * row), or `scope_blocked` (caller has no scope grant for the row's scope).
19
+ * or `{failure: '...'}` for `not_found` (covers genuinely-missing, wrong-
20
+ * tenant and scope-blocked, intentionally indistinguishable) or
21
+ * `not_drillable` (id is a leaf row).
20
22
  *
21
- * Pre-v1.6.4 returned null for all four cases. JS callers migrate via
23
+ * Pre-v1.6.4 returned null for all three cases. JS callers migrate via
22
24
  * `'failure' in result` checks; HTTP route maps `not_drillable` to 422.
23
25
  */
24
26
  export function drillDown(ctx, summaryId, opts = {}) {
@@ -26,8 +28,17 @@ export function drillDown(ctx, summaryId, opts = {}) {
26
28
  // v0.30 / E5: depth defaults 1 (backward compat); hard cap 10 levels
27
29
  // prevents pathological deep trees. CLI/HTTP/MCP reject invalid values.
28
30
  const depth = Math.max(1, Math.min(Math.trunc(opts.depth ?? 1), 10));
29
- const summary = readEntry(ctx.hippoRoot, summaryId, ctx.tenantId);
30
- // No unscoped cross-tenant probe here — readEntry's null return covers
31
+ const db = openStore(ctx.hippoRoot);
32
+ try {
33
+ return drillDownOn(db, ctx, summaryId, depth, opts, limit);
34
+ }
35
+ finally {
36
+ closeHippoDb(db);
37
+ }
38
+ }
39
+ function drillDownOn(db, ctx, summaryId, depth, opts, limit) {
40
+ const summary = selectEntriesByIds(db, [summaryId], ctx.tenantId).get(summaryId) ?? null;
41
+ // No unscoped cross-tenant probe here: the tenant-scoped read's miss covers
31
42
  // both "doesn't exist" and "exists in another tenant" by design.
32
43
  // Distinguishing them via an unscoped lookup would leak existence to
33
44
  // unauthorised tenants. The two cases collapse into not_found.
@@ -42,21 +53,46 @@ export function drillDown(ctx, summaryId, opts = {}) {
42
53
  // already preventing. Match the HTTP behaviour at the API level.
43
54
  return { failure: 'not_found' };
44
55
  }
45
- // v0.30 / E5: BFS walk levels 1..depth with visited-Set dedup. Defensive
46
- // against shared-child data anomalies (dag_parent_id has no uniqueness
47
- // constraint, so a misconfigured tree could double-emit at depth > 1).
48
- // Each level uses loadChildrenOf which is tenant-scoped via ctx.tenantId.
56
+ const { collected, level0DirectCount } = collectDescendants(db, ctx.tenantId, summaryId, depth);
57
+ const summaryOut = {
58
+ id: summary.id,
59
+ content: summary.content,
60
+ // v0.30 / E5: the STORED direct-child count; the legacy fallback counts
61
+ // level-0 children, never the BFS-depth-N total (independent-review MED #4).
62
+ descendantCount: summary.descendant_count ?? level0DirectCount,
63
+ earliestAt: summary.earliest_at ?? null,
64
+ latestAt: summary.latest_at ?? null,
65
+ };
66
+ const all = collected.map((c) => ({
67
+ id: c.id,
68
+ content: c.content,
69
+ layer: c.layer,
70
+ dagLevel: c.dag_level ?? 0,
71
+ created: c.created,
72
+ }));
73
+ const { children, truncated } = capChildren(all, summaryOut, opts, limit);
74
+ return {
75
+ summary: summaryOut,
76
+ children,
77
+ // v0.30 / E5: totalChildren = BFS-collected count (depth-aware). For
78
+ // depth=1 this equals the eligible direct-children count (backward
79
+ // compat). For depth>1 it is the cumulative count across levels.
80
+ totalChildren: collected.length,
81
+ truncated,
82
+ };
83
+ }
84
+ // BFS with a visited set: dag_parent_id is not unique, so a misconfigured tree could emit a child twice past depth 1.
85
+ // The level-0 count is kept apart so a legacy summary's descendantCount fallback counts direct children only.
86
+ function collectDescendants(db, tenantId, summaryId, depth) {
49
87
  const collected = [];
50
88
  const visited = new Set([summaryId]);
51
89
  let frontier = [summaryId];
52
- // independent-review MED #4 fold: track level-0 direct-children count
53
- // separately so the descendantCount fallback (for legacy summaries with
54
- // null descendant_count) reflects DIRECT children, not BFS-collected total.
55
90
  let level0DirectCount = 0;
56
91
  for (let level = 0; level < depth; level++) {
57
92
  const nextFrontier = [];
93
+ const kidsByParent = selectChildrenByParent(db, frontier, tenantId);
58
94
  for (const parentId of frontier) {
59
- const kids = loadChildrenOf(ctx.hippoRoot, parentId, ctx.tenantId);
95
+ const kids = kidsByParent.get(parentId) ?? [];
60
96
  const eligibleKids = kids.filter((c) => passesScopeFilterForRecall(c.scope ?? null, undefined));
61
97
  for (const k of eligibleKids) {
62
98
  if (visited.has(k.id))
@@ -72,23 +108,10 @@ export function drillDown(ctx, summaryId, opts = {}) {
72
108
  break;
73
109
  frontier = nextFrontier;
74
110
  }
75
- const summaryOut = {
76
- id: summary.id,
77
- content: summary.content,
78
- // v0.30 / E5: the STORED direct-child count; the legacy fallback counts
79
- // level-0 children, never the BFS-depth-N total (independent-review MED #4).
80
- descendantCount: summary.descendant_count ?? level0DirectCount,
81
- earliestAt: summary.earliest_at ?? null,
82
- latestAt: summary.latest_at ?? null,
83
- };
84
- const all = collected.map((c) => ({
85
- id: c.id,
86
- content: c.content,
87
- layer: c.layer,
88
- dagLevel: c.dag_level ?? 0,
89
- created: c.created,
90
- }));
91
- // Apply global cumulative token budget + limit cap on collected.
111
+ return { collected, level0DirectCount };
112
+ }
113
+ /** Global cumulative token budget first, then the `limit` cap. */
114
+ function capChildren(all, summaryOut, opts, limit) {
92
115
  let children = all;
93
116
  let truncated = false;
94
117
  if (opts.budget !== undefined) {
@@ -110,14 +133,6 @@ export function drillDown(ctx, summaryId, opts = {}) {
110
133
  children = children.slice(0, limit);
111
134
  truncated = true;
112
135
  }
113
- return {
114
- summary: summaryOut,
115
- children,
116
- // v0.30 / E5: totalChildren = BFS-collected count (depth-aware). For
117
- // depth=1 this equals the eligible direct-children count (backward
118
- // compat). For depth>1 it is the cumulative count across levels.
119
- totalChildren: collected.length,
120
- truncated,
121
- };
136
+ return { children, truncated };
122
137
  }
123
138
  //# sourceMappingURL=drill-down.js.map
@@ -8,14 +8,13 @@ import type { Context } from './types.js';
8
8
  * op='outcome' tagged with ctx.actor.subject.
9
9
  *
10
10
  * Returns `{applied, appliedIds}`. `appliedIds` is the tenant-filtered subset
11
- * of input ids that actually had `applyOutcome` run on them (i.e. ids whose
12
- * `readEntry(..., ctx.tenantId)` resolved). Callers that surface the id list
11
+ * of input ids that actually had `applyOutcome` run on them (i.e. ids found
12
+ * in ctx.tenantId). Callers that surface the id list
13
13
  * over a multi-tenant boundary (HTTP /v1/outcome last-recall path, Python SDK)
14
14
  * MUST return `appliedIds` instead of the raw input list — otherwise the
15
- * non-applied (cross-tenant) ids leak to the caller. Added in v1.11.4 to
16
- * close that disclosure path on POST /v1/outcome.
15
+ * non-applied (cross-tenant) ids leak to the caller.
17
16
  *
18
- * `opts.traceId` (LC1, docs/plans/2026-08-02-lc1-recall-trace-persistence.md):
17
+ * `opts.traceId`:
19
18
  * OPTIONAL additive opt so a programmatic caller can link this outcome to
20
19
  * the recall_traces row it judges. NOT applied unconditionally — an SDK
21
20
  * caller passing explicit ids with no preceding CLI/context recall would
@@ -36,18 +35,14 @@ export declare function outcome(ctx: Context, ids: ReadonlyArray<string>, good:
36
35
  *
37
36
  * Reads `loadIndex(ctx.hippoRoot).last_retrieval_ids` (per-hippoRoot local
38
37
  * state; not tenant-scoped at the index layer) and forwards to `outcome()`,
39
- * which DOES tenant-filter via `readEntry(..., ctx.tenantId)`. Cross-tenant
38
+ * which DOES tenant-filter its read by `ctx.tenantId`. Cross-tenant
40
39
  * ids in `last_retrieval_ids` are silently skipped, matching the MCP
41
40
  * `hippo_outcome` semantics.
42
41
  *
43
- * **Tenant-safe response shape (v1.11.4 security fix):** the returned `ids`
42
+ * **Tenant-safe response shape:** the returned `ids`
44
43
  * field contains ONLY the tenant-filtered subset that actually had outcomes
45
- * applied (i.e. `appliedIds` from the inner `outcome()` call). Earlier
46
- * versions returned the raw `last_retrieval_ids` regardless of tenant, which
47
- * leaked cross-tenant memory IDs to the caller via POST /v1/outcome's
48
- * no-body last-recall response. The fix is at this helper so all callers
49
- * (CLI cmdOutcome, HTTP /v1/outcome, MCP `hippo_outcome` if added later)
50
- * inherit the tenant-safe contract.
44
+ * applied (i.e. `appliedIds` from the inner `outcome()` call). It lives in this
45
+ * helper so every caller (CLI, HTTP /v1/outcome, MCP) inherits the contract.
51
46
  *
52
47
  * Do NOT tighten `loadIndex` with `tenantId` inside this helper — doing so
53
48
  * would break the (correct) cross-tenant-silent-skip behavior covered by
@@ -1,24 +1,27 @@
1
1
  // Outcome feedback on recalled memories.
2
- import { openHippoDb, closeHippoDb } from '../db.js';
3
- import { writeEntry } from '../store/entry-writes.js';
4
- import { readEntry } from '../store/entry-reads.js';
2
+ import { closeHippoDb } from '../db.js';
3
+ import { openStore } from '../store/open.js';
4
+ import { writeEntryOn } from '../store/entry-writes.js';
5
+ import { selectEntriesByIds } from '../store/entry-reads.js';
5
6
  import { loadIndex } from '../store/index-and-stats.js';
6
7
  import { applyOutcome, CHURN_STALE_TAG } from '../memory.js';
7
8
  import { appendAuditEvent } from '../audit.js';
8
9
  import { recordTraceOutcome } from '../recall-trace.js';
9
10
  export function outcome(ctx, ids, good, opts) {
10
11
  const appliedIds = [];
11
- const db = openHippoDb(ctx.hippoRoot);
12
+ const db = openStore(ctx.hippoRoot);
12
13
  try {
14
+ const live = selectEntriesByIds(db, ids, ctx.tenantId);
13
15
  for (const id of ids) {
14
- const entry = readEntry(ctx.hippoRoot, id, ctx.tenantId);
16
+ const entry = live.get(id);
15
17
  if (!entry)
16
18
  continue;
17
19
  let updated = applyOutcome(entry, good);
18
- if (good && updated.tags.includes(CHURN_STALE_TAG)) { // FE2: a good outcome reconfirms the entry
20
+ if (good && updated.tags.includes(CHURN_STALE_TAG)) { // a good outcome reconfirms the entry
19
21
  updated = { ...updated, tags: updated.tags.filter((t) => t !== CHURN_STALE_TAG) };
20
22
  }
21
- writeEntry(ctx.hippoRoot, updated, { actor: ctx.actor.subject });
23
+ writeEntryOn(db, ctx.hippoRoot, updated, { actor: ctx.actor.subject });
24
+ live.set(id, updated); // a repeated id builds on its first outcome, as a fresh read would
22
25
  appendAuditEvent(db, {
23
26
  tenantId: ctx.tenantId,
24
27
  actor: ctx.actor.subject,
@@ -28,7 +31,7 @@ export function outcome(ctx, ids, good, opts) {
28
31
  });
29
32
  appliedIds.push(id);
30
33
  }
31
- // LC1: link the outcome to its trace, recording only the ids actually
34
+ // Link the outcome to its trace, recording only the ids actually
32
35
  // credited (post tenant-filtering, matches appliedIds). Lives in its own
33
36
  // append-only table so audit_log pruning can never erase training data.
34
37
  if (opts?.traceId !== undefined && appliedIds.length > 0) {
@@ -50,15 +53,8 @@ export function outcomeForLastRecall(ctx, good) {
50
53
  const ids = idx.last_retrieval_ids;
51
54
  if (ids.length === 0)
52
55
  return { applied: 0, ids: [] };
53
- // LC1 F1(d) structural fix (docs/plans/2026-08-02-lc1-recall-trace-persistence.md):
54
- // read the trace id from the SAME `loadIndex` snapshot already in hand
55
- // (idx.last_trace_id) — a single-snapshot read, not a second DB round
56
- // trip via a now-deleted readLastTraceId helper. The value is already
57
- // strict-parsed by buildIndexFromDb's parseLastTraceId (store.ts): every
58
- // consumer gets a clean positive-integer string or null, never a garbage
59
- // value that could reach outcome() and INSERT trace_id=0/NaN. null on a
60
- // fresh store / pre-v40 flow / api.recall-only usage — outcome() skips
61
- // linkage silently when traceId is undefined.
56
+ // Same `loadIndex` snapshot as the ids; buildIndexFromDb already strict-parses it to a
57
+ // positive-integer string or null, so no trace_id=0/NaN reaches outcome().
62
58
  const traceId = idx.last_trace_id !== null ? Number(idx.last_trace_id) : null;
63
59
  const { applied, appliedIds } = outcome(ctx, ids, good, traceId !== null ? { traceId } : undefined);
64
60
  return { applied, ids: appliedIds };
@@ -8,8 +8,7 @@ import type { Context } from './types.js';
8
8
  *
9
9
  * Note: `promoteToGlobal` does not currently take a tenantId override — it
10
10
  * reads the entry from the local root via `readEntry` (no tenant filter) and
11
- * preserves the entry's existing tenantId on the global side. Task 4 may
12
- * tighten this once writeEntry/readEntry thread tenant context.
11
+ * preserves the entry's existing tenantId on the global side.
13
12
  */
14
13
  export interface PromoteResult {
15
14
  ok: true;
@@ -19,9 +18,8 @@ export interface PromoteResult {
19
18
  export declare function promote(ctx: Context, id: string): PromoteResult;
20
19
  /**
21
20
  * Replace an old memory with new content, chaining old.superseded_by = new.id.
22
- * Mirrors `cmdSupersede` in cli.ts (without flag-driven layer/tag/pin overrides
23
- * — A1 keeps the API minimal; the CLI handler will continue to handle those
24
- * flags and pass the resolved values once Task 4 lands).
21
+ * Mirrors `cmdSupersede` in cli.ts minus the flag-driven layer/tag/pin
22
+ * overrides: the CLI handler resolves those so the API stays minimal.
25
23
  */
26
24
  export interface SupersedeResult {
27
25
  ok: true;
@@ -34,13 +32,12 @@ export declare function supersede(ctx: Context, oldId: string, newContent: strin
34
32
  *
35
33
  * `archiveRawMemory` audits the operation internally (op='archive_raw') using the
36
34
  * row's own tenant_id. We DO NOT emit a second audit event here to avoid double-
37
- * emitting the archive_raw op (unlike Task 1 remember/forget where the underlying
38
- * helpers hardcode actor='cli'). Instead we pass `ctx.actor.subject` through as `who`,
35
+ * emitting the archive_raw op. Instead we pass `ctx.actor.subject` through as `who`,
39
36
  * and raw-archive.ts uses that for the audit row.
40
37
  */
41
38
  export interface ArchiveRawOpts {
42
39
  /**
43
- * Connector idempotency hook (v0.39 commit 3). Runs inside the same
40
+ * Connector idempotency hook. Runs inside the same
44
41
  * SAVEPOINT as the archive — throwing rolls the archive back. Used by the
45
42
  * Slack deletion connector to mark the deletion event seen atomically.
46
43
  */
@@ -47,19 +47,7 @@ export function promote(ctx, id) {
47
47
  return { ok: true, sourceId: id, globalId: globalEntry.id };
48
48
  }
49
49
  export function supersede(ctx, oldId, newContent) {
50
- // Read old (tenant-scoped). readEntry filters by tenantId, so a Bearer for
51
- // tenant A on tenant B's id throws "Memory not found" here without any
52
- // info leak.
53
- const old = readEntry(ctx.hippoRoot, oldId, ctx.tenantId);
54
- if (!old) {
55
- throw new NotFoundError(`Memory not found: ${oldId}`);
56
- }
57
- // Guard: not already superseded. The CAS UPDATE below race-safely closes
58
- // the window between this read and the write; this check just produces a
59
- // clearer error in the common single-writer case.
60
- if (old.superseded_by) {
61
- throw new ConflictError(`Memory ${oldId} is already superseded by ${old.superseded_by}. Supersede that one instead.`);
62
- }
50
+ const old = readSupersedable(ctx, oldId);
63
51
  const newEntry = createSuccessor(old, newContent, {
64
52
  tenantId: ctx.tenantId,
65
53
  baseHalfLifeDays: loadConfig(ctx.hippoRoot).defaultHalfLifeDays,
@@ -72,59 +60,7 @@ export function supersede(ctx, oldId, newContent) {
72
60
  // the old.superseded_by pointer.
73
61
  const db = openHippoDb(ctx.hippoRoot);
74
62
  try {
75
- db.exec('BEGIN IMMEDIATE');
76
- try {
77
- // 1. CAS update: only succeed if old.superseded_by IS NULL AND the
78
- // row still belongs to ctx.tenantId. Tenant filter is belt-and-
79
- // braces with the readEntry above — it costs nothing and closes
80
- // a hypothetical window where ownership changes between read and
81
- // update.
82
- const result = db.prepare(`
83
- UPDATE memories
84
- SET superseded_by = ?
85
- WHERE id = ? AND tenant_id = ? AND superseded_by IS NULL
86
- `).run(newEntry.id, oldId, ctx.tenantId);
87
- if ((result.changes ?? 0) === 0) {
88
- db.exec('ROLLBACK');
89
- throw new ConflictError(`Memory ${oldId} already superseded by another writer`);
90
- }
91
- // v0.30 / E2 — DAG live-coupling: OLD entry just transitioned to
92
- // superseded. Its parent (if any) needs rebuild. Lands strictly
93
- // between the rollback guard above and the writeEntryDbOnly(NEW)
94
- // below so a failed CAS hits throw before this hook. The NEW
95
- // entry's parent (typically same parent) is auto-marked by the
96
- // writeEntryDbOnly hook (same parent → idempotent, audits once).
97
- if (old.dag_parent_id) {
98
- markSummaryDirtyInTx(db, old.dag_parent_id, ctx.tenantId, ctx.actor.subject);
99
- }
100
- // 2. Write new memory inside same tx via writeEntryDbOnly (DB-only
101
- // path). This emits its OWN 'remember' audit row for the new
102
- // memory inside the SAVEPOINT — atomic with the row INSERT.
103
- writeEntryDbOnly(db, stampOriginProject(ctx.hippoRoot, newEntry), { actor: ctx.actor.subject });
104
- // 3. User-facing 'supersede' audit row inside the same tx so the
105
- // chain pointer + audit trail commit atomically.
106
- appendAuditEvent(db, {
107
- tenantId: ctx.tenantId,
108
- actor: ctx.actor.subject,
109
- op: 'supersede',
110
- targetId: oldId,
111
- metadata: { newId: newEntry.id },
112
- });
113
- db.exec('COMMIT');
114
- }
115
- catch (err) {
116
- try {
117
- db.exec('ROLLBACK');
118
- }
119
- catch { /* already rolled back */ }
120
- // AT1 (plan §3): refusal audit lands post-ROLLBACK, in a fresh
121
- // implicit transaction the aborted outer one cannot claw back — then
122
- // rethrow so the caller sees the refusal.
123
- if (err instanceof RejectedValueError) {
124
- auditRejectionRefusal(db, err, ctx.actor.subject);
125
- }
126
- throw err;
127
- }
63
+ commitSupersede(db, ctx, oldId, old, newEntry);
128
64
  // Mirrors after COMMIT, while the db handle is still open. Same
129
65
  // invariant as the original writeEntry: a mirror failure leaves disk
130
66
  // MISSING the markdown for the new memory (rebuildIndex rewrites every
@@ -142,6 +78,59 @@ export function supersede(ctx, oldId, newContent) {
142
78
  }
143
79
  return { ok: true, oldId, newId: newEntry.id };
144
80
  }
81
+ /** The tenant-scoped row to supersede; readEntry's tenant filter makes another tenant's id read as not found. */
82
+ function readSupersedable(ctx, oldId) {
83
+ const old = readEntry(ctx.hippoRoot, oldId, ctx.tenantId);
84
+ if (!old) {
85
+ throw new NotFoundError(`Memory not found: ${oldId}`);
86
+ }
87
+ // The CAS UPDATE closes the race; this check only gives a clearer error in the common single-writer case.
88
+ if (old.superseded_by) {
89
+ throw new ConflictError(`Memory ${oldId} is already superseded by ${old.superseded_by}. Supersede that one instead.`);
90
+ }
91
+ return old;
92
+ }
93
+ /** CAS on the old row, the successor's insert and the 'supersede' audit row, in one BEGIN IMMEDIATE transaction. */
94
+ function commitSupersede(db, ctx, oldId, old, newEntry) {
95
+ db.exec('BEGIN IMMEDIATE');
96
+ try {
97
+ // The tenant filter repeats readEntry's check at no cost, closing an ownership change between read and update.
98
+ const result = db.prepare(`
99
+ UPDATE memories
100
+ SET superseded_by = ?
101
+ WHERE id = ? AND tenant_id = ? AND superseded_by IS NULL
102
+ `).run(newEntry.id, oldId, ctx.tenantId);
103
+ if ((result.changes ?? 0) === 0) {
104
+ db.exec('ROLLBACK');
105
+ throw new ConflictError(`Memory ${oldId} already superseded by another writer`);
106
+ }
107
+ // After the CAS guard, so a lost race throws before the old parent is marked for rebuild.
108
+ if (old.dag_parent_id) {
109
+ markSummaryDirtyInTx(db, old.dag_parent_id, ctx.tenantId, ctx.actor.subject);
110
+ }
111
+ // Emits its own 'remember' audit row inside the same transaction.
112
+ writeEntryDbOnly(db, stampOriginProject(ctx.hippoRoot, newEntry), { actor: ctx.actor.subject });
113
+ appendAuditEvent(db, {
114
+ tenantId: ctx.tenantId,
115
+ actor: ctx.actor.subject,
116
+ op: 'supersede',
117
+ targetId: oldId,
118
+ metadata: { newId: newEntry.id },
119
+ });
120
+ db.exec('COMMIT');
121
+ }
122
+ catch (err) {
123
+ try {
124
+ db.exec('ROLLBACK');
125
+ }
126
+ catch { /* already rolled back */ }
127
+ // The refusal audit lands after ROLLBACK, in a fresh implicit transaction the aborted one cannot undo.
128
+ if (err instanceof RejectedValueError) {
129
+ auditRejectionRefusal(db, err, ctx.actor.subject);
130
+ }
131
+ throw err;
132
+ }
133
+ }
145
134
  export function archiveRaw(ctx, id, reason, opts = {}) {
146
135
  const db = openHippoDb(ctx.hippoRoot);
147
136
  let mirrorOk = false;
@@ -2,7 +2,7 @@
2
2
  import { openHippoDb, closeHippoDb } from '../db.js';
3
3
  import { ConflictError, ForbiddenError, NotFoundError } from '../api-errors.js';
4
4
  import { writeEntryMirrors } from '../store/entry-writes.js';
5
- import { readEntry } from '../store/entry-reads.js';
5
+ import { readEntry, selectEntriesByIds } from '../store/entry-reads.js';
6
6
  import { quarantineScopeFor, getQuarantineRow, listQuarantineRows, approveQuarantineRow, rejectQuarantineRow, } from '../quarantine.js';
7
7
  import { log } from '../log.js';
8
8
  import { appendAuditEvent } from '../audit.js';
@@ -12,8 +12,9 @@ export function quarantineList(ctx, opts = {}) {
12
12
  const db = openHippoDb(ctx.hippoRoot);
13
13
  try {
14
14
  const rows = listQuarantineRows(db, ctx.tenantId, opts.status ?? 'pending', opts.limit, opts.after);
15
+ const entries = selectEntriesByIds(db, rows.map((row) => row.memoryId), ctx.tenantId);
15
16
  return rows.map((row) => {
16
- const entry = readEntry(ctx.hippoRoot, row.memoryId, ctx.tenantId);
17
+ const entry = entries.get(row.memoryId);
17
18
  return {
18
19
  id: row.memoryId,
19
20
  originalScope: row.originalScope,