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
package/dist/decisions.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * E2 decision first-class object (docs/plans/2026-05-28-e2-decision-object.md).
2
+ * Decision first-class object.
3
3
  *
4
4
  * `hippo decide` used to write only a tagged memory (tags ['decision'], source
5
5
  * 'decision') with a 90-day half-life, so an in-force decision decayed out of
@@ -16,7 +16,7 @@
16
16
  * Tenant scoping: every helper requires tenantId. BEFORE INSERT/UPDATE triggers
17
17
  * enforce decisions.tenant_id == the referenced memory's tenant_id, and a
18
18
  * superseded_by same-tenant trigger makes cross-tenant supersession
19
- * unrepresentable. Mirrors the v0.31 predictions pattern (src/predictions.ts).
19
+ * unrepresentable. Mirrors the predictions pattern (src/predictions.ts).
20
20
  *
21
21
  * Dual-write atomicity: `saveDecision` writes the memory + decisions row (and,
22
22
  * when superseding, the old row's UPDATE) inside writeEntry's SAVEPOINT
@@ -60,6 +60,97 @@ const DECISION_COLS = `
60
60
  // ---------------------------------------------------------------------------
61
61
  // Public API
62
62
  // ---------------------------------------------------------------------------
63
+ /** The legacy `hippo decide` memory mirror for a decision. */
64
+ function buildDecisionMemory(hippoRoot, tenantId, opts) {
65
+ const content = opts.context
66
+ ? `${opts.decisionText}\n\nContext: ${opts.context}`
67
+ : opts.decisionText;
68
+ const tags = ['decision', ...(opts.extraTags ?? [])];
69
+ return createMemory(content, {
70
+ tags,
71
+ layer: Layer.Semantic,
72
+ confidence: 'verified',
73
+ source: 'decision',
74
+ baseHalfLifeDays: objectHalfLifeDays(hippoRoot),
75
+ tenantId,
76
+ });
77
+ }
78
+ // Preflight the supersede target BEFORE inserting the new row. The new
79
+ // row's autoincrement id could otherwise collide with a non-existent
80
+ // supersedesDecisionId (e.g. superseding id 1 on an empty store, where the
81
+ // INSERT below would itself become id 1), making the row supersede itself.
82
+ // Validating first means the new row is never a candidate for its own
83
+ // supersede UPDATE.
84
+ function preflightDecisionSupersede(db, tenantId, supersedesId) {
85
+ // SAFETY: row shape matches the single `status` column named in the SELECT below.
86
+ const pred = db.prepare(`SELECT status FROM decisions WHERE id = ? AND tenant_id = ?`).get(supersedesId, tenantId);
87
+ if (!pred) {
88
+ throw new NotFoundError(`saveDecision: decision ${supersedesId} to supersede not found for tenant ${tenantId}`);
89
+ }
90
+ if (pred.status !== 'active') {
91
+ throw new ConflictError(`saveDecision: decision ${supersedesId} is not active (status='${pred.status}'); only active decisions can be superseded.`);
92
+ }
93
+ }
94
+ function insertDecisionRow(db, memoryId, tenantId, opts, now) {
95
+ const result = db.prepare(`
96
+ INSERT INTO decisions(
97
+ memory_id, tenant_id, decision_text, context,
98
+ status, superseded_by, superseded_at, closed_at, created_at
99
+ ) VALUES (?, ?, ?, ?, 'active', NULL, NULL, NULL, ?)
100
+ `).run(memoryId, tenantId, opts.decisionText, opts.context ?? null, now);
101
+ return Number(result.lastInsertRowid ?? 0);
102
+ }
103
+ // Supersede the (preflight-validated) prior active decision in the SAME
104
+ // SAVEPOINT, atomic with the new row. The `id != decisionId` exclusion is
105
+ // defense-in-depth against the self-match described above; combined with
106
+ // the preflight, a 0-change here is unreachable in the single-writer txn.
107
+ function supersedeDecisionRow(db, tenantId, actor, supersedesId, decisionId, now) {
108
+ const sup = db.prepare(`
109
+ UPDATE decisions
110
+ SET status = 'superseded', superseded_by = ?, superseded_at = ?
111
+ WHERE id = ? AND tenant_id = ? AND status = 'active' AND id != ?
112
+ `).run(decisionId, now, supersedesId, tenantId, decisionId);
113
+ if (sup.changes === 0) {
114
+ throw new ConflictError(`saveDecision: decision ${supersedesId} could not be superseded (no longer active or self-reference).`);
115
+ }
116
+ appendAuditEvent(db, {
117
+ tenantId,
118
+ actor,
119
+ op: 'decision_supersede',
120
+ targetId: String(supersedesId),
121
+ metadata: {
122
+ decision_id: supersedesId,
123
+ superseded_by: decisionId,
124
+ },
125
+ });
126
+ }
127
+ /** The afterWrite body: preflight, INSERT, supersede, reload, create audit, all in one SAVEPOINT. */
128
+ function writeDecisionRow(db, memoryId, tenantId, opts, actor, now) {
129
+ if (opts.supersedesDecisionId !== undefined) {
130
+ preflightDecisionSupersede(db, tenantId, opts.supersedesDecisionId);
131
+ }
132
+ const decisionId = insertDecisionRow(db, memoryId, tenantId, opts, now);
133
+ if (opts.supersedesDecisionId !== undefined) {
134
+ supersedeDecisionRow(db, tenantId, actor, opts.supersedesDecisionId, decisionId, now);
135
+ }
136
+ // SAFETY: row's shape matches the columns named in DECISION_COLS above.
137
+ const row = db.prepare(`SELECT ${DECISION_COLS} FROM decisions WHERE id = ?`)
138
+ .get(decisionId);
139
+ if (!row)
140
+ throw new Error('saveDecision: failed to reload saved decision row');
141
+ // GDPR-light metadata: id + flag only, no decision_text.
142
+ appendAuditEvent(db, {
143
+ tenantId,
144
+ actor,
145
+ op: 'decision_create',
146
+ targetId: String(decisionId),
147
+ metadata: {
148
+ decision_id: decisionId,
149
+ has_context: opts.context !== undefined && opts.context !== null && opts.context !== '',
150
+ },
151
+ });
152
+ return row;
153
+ }
63
154
  /**
64
155
  * Create a decision. Writes the memory mirror + the decisions row atomically
65
156
  * inside writeEntry's SAVEPOINT 'write_entry'. When supersedesDecisionId is
@@ -77,92 +168,18 @@ export function saveDecision(hippoRoot, tenantId, opts, actor = 'cli') {
77
168
  if (!opts.decisionText)
78
169
  throw new BadRequestError('saveDecision: decisionText is required');
79
170
  const now = new Date().toISOString();
80
- const content = opts.context
81
- ? `${opts.decisionText}\n\nContext: ${opts.context}`
82
- : opts.decisionText;
83
- const tags = ['decision', ...(opts.extraTags ?? [])];
84
- const mem = createMemory(content, {
85
- tags,
86
- layer: Layer.Semantic,
87
- confidence: 'verified',
88
- source: 'decision',
89
- baseHalfLifeDays: objectHalfLifeDays(hippoRoot),
90
- tenantId,
91
- });
171
+ const mem = buildDecisionMemory(hippoRoot, tenantId, opts);
92
172
  // Populated inside afterWrite so the INSERT, the supersede UPDATE, and the
93
173
  // memory write all share one SAVEPOINT.
94
174
  let savedRow;
95
175
  writeEntry(hippoRoot, mem, {
96
176
  actor,
97
177
  afterWrite: (db, memoryId) => {
98
- // Preflight the supersede target BEFORE inserting the new row. The new
99
- // row's autoincrement id could otherwise collide with a non-existent
100
- // supersedesDecisionId (e.g. superseding id 1 on an empty store, where the
101
- // INSERT below would itself become id 1), making the row supersede itself.
102
- // Validating first means the new row is never a candidate for its own
103
- // supersede UPDATE. codex review 2026-05-28 (P1).
104
- if (opts.supersedesDecisionId !== undefined) {
105
- // SAFETY: row shape matches the single `status` column named in the SELECT below.
106
- const pred = db.prepare(`SELECT status FROM decisions WHERE id = ? AND tenant_id = ?`).get(opts.supersedesDecisionId, tenantId);
107
- if (!pred) {
108
- throw new NotFoundError(`saveDecision: decision ${opts.supersedesDecisionId} to supersede not found for tenant ${tenantId}`);
109
- }
110
- if (pred.status !== 'active') {
111
- throw new ConflictError(`saveDecision: decision ${opts.supersedesDecisionId} is not active (status='${pred.status}'); only active decisions can be superseded.`);
112
- }
113
- }
114
- const result = db.prepare(`
115
- INSERT INTO decisions(
116
- memory_id, tenant_id, decision_text, context,
117
- status, superseded_by, superseded_at, closed_at, created_at
118
- ) VALUES (?, ?, ?, ?, 'active', NULL, NULL, NULL, ?)
119
- `).run(memoryId, tenantId, opts.decisionText, opts.context ?? null, now);
120
- const decisionId = Number(result.lastInsertRowid ?? 0);
121
- // Supersede the (preflight-validated) prior active decision in the SAME
122
- // SAVEPOINT, atomic with the new row. The `id != decisionId` exclusion is
123
- // defense-in-depth against the self-match described above; combined with
124
- // the preflight, a 0-change here is unreachable in the single-writer txn.
125
- if (opts.supersedesDecisionId !== undefined) {
126
- const sup = db.prepare(`
127
- UPDATE decisions
128
- SET status = 'superseded', superseded_by = ?, superseded_at = ?
129
- WHERE id = ? AND tenant_id = ? AND status = 'active' AND id != ?
130
- `).run(decisionId, now, opts.supersedesDecisionId, tenantId, decisionId);
131
- if (sup.changes === 0) {
132
- throw new BadRequestError(`saveDecision: decision ${opts.supersedesDecisionId} could not be superseded (no longer active or self-reference).`);
133
- }
134
- appendAuditEvent(db, {
135
- tenantId,
136
- actor,
137
- op: 'decision_supersede',
138
- targetId: String(opts.supersedesDecisionId),
139
- metadata: {
140
- decision_id: opts.supersedesDecisionId,
141
- superseded_by: decisionId,
142
- },
143
- });
144
- }
145
- // SAFETY: row's shape matches the columns named in DECISION_COLS above.
146
- const row = db.prepare(`SELECT ${DECISION_COLS} FROM decisions WHERE id = ?`)
147
- .get(decisionId);
148
- if (!row)
149
- throw new Error('saveDecision: failed to reload saved decision row');
150
- savedRow = row;
151
- // GDPR-light metadata: id + flag only, no decision_text.
152
- appendAuditEvent(db, {
153
- tenantId,
154
- actor,
155
- op: 'decision_create',
156
- targetId: String(decisionId),
157
- metadata: {
158
- decision_id: decisionId,
159
- has_context: opts.context !== undefined && opts.context !== null && opts.context !== '',
160
- },
161
- });
178
+ savedRow = writeDecisionRow(db, memoryId, tenantId, opts, actor, now);
162
179
  },
163
180
  // Post-commit hook: mark the tenant's graph dirty AFTER the DB row commits
164
181
  // but BEFORE the markdown mirrors are written, so a mirror-write failure can
165
- // never leave a committed save unflagged (codex). markGraphDirty is fail-soft.
182
+ // never leave a committed save unflagged. markGraphDirty is fail-soft.
166
183
  afterCommit: () => markGraphDirty(hippoRoot, tenantId, mem.id),
167
184
  });
168
185
  if (!savedRow) {
@@ -215,7 +232,7 @@ export function closeDecision(hippoRoot, tenantId, id, actor = 'cli') {
215
232
  // Closing removes the object from the graph. Remove its rows DIRECTLY (deterministic),
216
233
  // not only via an enqueued rebuild whose queue item is lost if the mirror is later
217
234
  // forgotten (the queue row cascade-deletes with the memory), which would leave the closed
218
- // object stale and could block that forget (codex P1). Still enqueue when a mirror exists
235
+ // object stale and could block that forget. Still enqueue when a mirror exists
219
236
  // so a concurrent rebuild re-derives consistently (harmless if it also runs).
220
237
  removeGraphEntitiesForObject(hippoRoot, tenantId, 'decision', closed.id);
221
238
  if (closed.memoryId) {
package/dist/dedupe.js CHANGED
@@ -22,7 +22,9 @@
22
22
  */
23
23
  import { textOverlap } from './tokenize.js';
24
24
  import { loadAllEntries } from './store/entry-reads.js';
25
- import { deleteEntry, memoriesBackingObjects } from './store/delete-and-batch.js';
25
+ import { deleteEntryOn, memoriesBackingObjects } from './store/delete-and-batch.js';
26
+ import { openStore } from './store/open.js';
27
+ import { closeHippoDb } from './db.js';
26
28
  import { compareEntryIdentity } from './compare.js';
27
29
  import { canAutoDelete } from './memory.js';
28
30
  import { derivationPartitionKey } from './recall-scope.js';
@@ -56,6 +58,71 @@ export function strengthBucket(strength) {
56
58
  const s = strength ?? 0;
57
59
  return Number.isFinite(s) ? Math.round(s / STRENGTH_TIE_EPSILON) : 0;
58
60
  }
61
+ // finiteCount mirrors strengthBucket's non-finite hardening on the
62
+ // retrieval leg: a NaN retrieval_count would make the comparator return
63
+ // NaN and break the total order the same way a NaN bucket would.
64
+ // Unreachable via the schema (non-nullable INTEGER column), so this is
65
+ // symmetry, not a live bug.
66
+ const finiteCount = (n) => Number.isFinite(n ?? 0) ? (n ?? 0) : 0;
67
+ /** The v1.26.3 survivor total order: strength bucket desc, then retrieval_count desc, then compareEntryIdentity. */
68
+ function survivorOrder(a, b) {
69
+ const bucketDiff = strengthBucket(b.strength) - strengthBucket(a.strength);
70
+ if (bucketDiff !== 0)
71
+ return bucketDiff;
72
+ const retrievalDiff = finiteCount(b.retrieval_count) - finiteCount(a.retrieval_count);
73
+ if (retrievalDiff !== 0)
74
+ return retrievalDiff;
75
+ return compareEntryIdentity(a, b);
76
+ }
77
+ // Tenant partition (mirrors consolidate.ts mergeCandidatesByTenant and
78
+ // dag.ts unparentedByTenant): group by tenantId BEFORE the sort so a
79
+ // duplicate pair can never form across tenants. Map preserves insertion
80
+ // order, so a single-tenant store (every row 'default') gets exactly one
81
+ // group and the sort plus pair loop below run byte-identical to the
82
+ // pre-fix global pass.
83
+ function entriesByPartition(entries) {
84
+ const entriesByTenant = new Map();
85
+ for (const entry of entries) {
86
+ // EI2: also split by restricted scope, else a private copy can delete the readable one.
87
+ const key = derivationPartitionKey(entry.tenantId, entry.scope, entry.origin_project);
88
+ const bucket = entriesByTenant.get(key);
89
+ if (bucket)
90
+ bucket.push(entry);
91
+ else
92
+ entriesByTenant.set(key, [entry]);
93
+ }
94
+ return entriesByTenant;
95
+ }
96
+ function dedupPair(kept, dropped) {
97
+ return {
98
+ kept: kept.id,
99
+ keptContent: kept.content,
100
+ keptLayer: kept.layer,
101
+ keptStrength: kept.strength ?? 0,
102
+ removed: dropped.id,
103
+ removedContent: dropped.content,
104
+ removedLayer: dropped.layer,
105
+ removedStrength: dropped.strength ?? 0,
106
+ similarity: textOverlap(kept.content, dropped.content),
107
+ };
108
+ }
109
+ /** Pairs within one partition, already in survivor order, grouped by survivor; marks each loser in `removed`. */
110
+ function partitionPairs(tenantEntries, removed, backing) {
111
+ const groups = new Map();
112
+ for (const entry of tenantEntries) {
113
+ const key = duplicateKey(entry.content);
114
+ const group = groups.get(key);
115
+ if (!group) {
116
+ groups.set(key, { survivor: entry, pairs: [] });
117
+ continue;
118
+ }
119
+ if (!canAutoDelete(entry) || backing.has(entry.id))
120
+ continue;
121
+ removed.add(entry.id);
122
+ group.pairs.push(dedupPair(group.survivor, entry));
123
+ }
124
+ return [...groups.values()].flatMap((group) => group.pairs);
125
+ }
59
126
  /**
60
127
  * Scan the store for duplicates and remove the weaker copy: same text apart
61
128
  * from spacing, since a near-duplicate can differ in a value (port, version,
@@ -70,28 +137,7 @@ export function deduplicateStore(hippoRoot, options = {}) {
70
137
  // Only current distilled rows compete: raw rows are append-only (the delete
71
138
  // trigger would abort sleep mid-loop) and superseded rows are history, as in consolidate.ts.
72
139
  const entries = loadAllEntries(hippoRoot).filter((e) => e.kind === 'distilled' && !e.superseded_by);
73
- // Tenant partition (mirrors consolidate.ts mergeCandidatesByTenant and
74
- // dag.ts unparentedByTenant): group by tenantId BEFORE the sort so a
75
- // duplicate pair can never form across tenants. Map preserves insertion
76
- // order, so a single-tenant store (every row 'default') gets exactly one
77
- // group and the sort plus pair loop below run byte-identical to the
78
- // pre-fix global pass.
79
- const entriesByTenant = new Map();
80
- for (const entry of entries) {
81
- // EI2: also split by restricted scope, else a private copy can delete the readable one.
82
- const key = derivationPartitionKey(entry.tenantId, entry.scope, entry.origin_project);
83
- const bucket = entriesByTenant.get(key);
84
- if (bucket)
85
- bucket.push(entry);
86
- else
87
- entriesByTenant.set(key, [entry]);
88
- }
89
- // finiteCount mirrors strengthBucket's non-finite hardening on the
90
- // retrieval leg: a NaN retrieval_count would make the comparator return
91
- // NaN and break the total order the same way a NaN bucket would.
92
- // Unreachable via the schema (non-nullable INTEGER column), so this is
93
- // symmetry, not a live bug.
94
- const finiteCount = (n) => Number.isFinite(n ?? 0) ? (n ?? 0) : 0;
140
+ const entriesByTenant = entriesByPartition(entries);
95
141
  // Shared across tenant groups: safe because memory ids are globally
96
142
  // unique (crypto.randomUUID at creation), so an id in `removed` can never
97
143
  // collide with another tenant's row, and deleteEntry below deletes by
@@ -100,45 +146,24 @@ export function deduplicateStore(hippoRoot, options = {}) {
100
146
  const pairs = [];
101
147
  const backing = memoriesBackingObjects(hippoRoot);
102
148
  for (const tenantEntries of entriesByTenant.values()) {
103
- // The v1.26.3 survivor total order (see the file-level docstring:
104
- // strength bucket desc -> retrieval_count desc -> compareEntryIdentity),
105
- // scoped per tenant group: the total order holds within a tenant only,
106
- // matching the partition above.
107
- tenantEntries.sort((a, b) => {
108
- const bucketDiff = strengthBucket(b.strength) - strengthBucket(a.strength);
109
- if (bucketDiff !== 0)
110
- return bucketDiff;
111
- const retrievalDiff = finiteCount(b.retrieval_count) - finiteCount(a.retrieval_count);
112
- if (retrievalDiff !== 0)
113
- return retrievalDiff;
114
- return compareEntryIdentity(a, b);
115
- });
116
- const texts = tenantEntries.map((e) => duplicateKey(e.content));
117
- for (let i = 0; i < tenantEntries.length; i++) {
118
- if (removed.has(tenantEntries[i].id))
119
- continue;
120
- for (let j = i + 1; j < tenantEntries.length; j++) {
121
- if (removed.has(tenantEntries[j].id) || !canAutoDelete(tenantEntries[j]) || backing.has(tenantEntries[j].id))
122
- continue;
123
- if (texts[j] !== texts[i])
124
- continue;
125
- const similarity = textOverlap(tenantEntries[i].content, tenantEntries[j].content);
126
- removed.add(tenantEntries[j].id);
127
- pairs.push({
128
- kept: tenantEntries[i].id,
129
- keptContent: tenantEntries[i].content,
130
- keptLayer: tenantEntries[i].layer,
131
- keptStrength: tenantEntries[i].strength ?? 0,
132
- removed: tenantEntries[j].id,
133
- removedContent: tenantEntries[j].content,
134
- removedLayer: tenantEntries[j].layer,
135
- removedStrength: tenantEntries[j].strength ?? 0,
136
- similarity,
137
- });
138
- }
139
- }
149
+ // The survivor total order (see the file-level docstring), scoped per
150
+ // tenant group: the total order holds within a tenant only, matching the partition above.
151
+ tenantEntries.sort(survivorOrder);
152
+ pairs.push(...partitionPairs(tenantEntries, removed, backing));
140
153
  }
141
- const done = dryRun ? pairs : pairs.filter((p) => deleteEntry(hippoRoot, p.removed, { actor: options.actor, reason: `dedup: duplicate of ${p.kept}`, automatic: true }));
154
+ const done = dryRun ? pairs : deletePairs(hippoRoot, pairs, options.actor);
142
155
  return { removed: done.length, pairs: done };
143
156
  }
157
+ /** The pairs whose loser was deleted; one store handle, one transaction per delete. */
158
+ function deletePairs(hippoRoot, pairs, actor) {
159
+ if (pairs.length === 0)
160
+ return [];
161
+ const db = openStore(hippoRoot);
162
+ try {
163
+ return pairs.filter((p) => deleteEntryOn(db, hippoRoot, p.removed, { actor, reason: `dedup: duplicate of ${p.kept}`, automatic: true }));
164
+ }
165
+ finally {
166
+ closeHippoDb(db);
167
+ }
168
+ }
144
169
  //# sourceMappingURL=dedupe.js.map