hippo-memory 1.62.0 → 1.63.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (379) hide show
  1. package/README.md +11 -2
  2. package/dist/agent-memories/apply.d.ts +2 -0
  3. package/dist/agent-memories/apply.js +23 -1
  4. package/dist/agent-memories/claude-code.d.ts +2 -1
  5. package/dist/agent-memories/claude-code.js +4 -4
  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.js +37 -23
  9. package/dist/ambient-store.d.ts +4 -4
  10. package/dist/ambient-store.js +4 -3
  11. package/dist/api/assemble.d.ts +7 -10
  12. package/dist/api/assemble.js +7 -12
  13. package/dist/api/audit.d.ts +2 -2
  14. package/dist/api/audit.js +2 -2
  15. package/dist/api/auth.d.ts +6 -6
  16. package/dist/api/auth.js +5 -7
  17. package/dist/api/context-select.d.ts +4 -3
  18. package/dist/api/context-select.js +9 -11
  19. package/dist/api/context-types.d.ts +12 -17
  20. package/dist/api/context.d.ts +3 -2
  21. package/dist/api/context.js +20 -20
  22. package/dist/api/dormant.js +1 -1
  23. package/dist/api/drill-down.d.ts +4 -4
  24. package/dist/api/drill-down.js +22 -10
  25. package/dist/api/outcome.d.ts +8 -13
  26. package/dist/api/outcome.js +13 -17
  27. package/dist/api/promote.d.ts +5 -8
  28. package/dist/api/quarantine.js +3 -2
  29. package/dist/api/recall-types.d.ts +43 -55
  30. package/dist/api/recall.d.ts +3 -3
  31. package/dist/api/recall.js +7 -18
  32. package/dist/api/remember.d.ts +2 -2
  33. package/dist/api/sleep.d.ts +7 -35
  34. package/dist/api/sleep.js +3 -2
  35. package/dist/api/tokens.d.ts +2 -2
  36. package/dist/api/tokens.js +2 -2
  37. package/dist/api/types.d.ts +6 -10
  38. package/dist/api/types.js +2 -4
  39. package/dist/audit-prune.d.ts +4 -6
  40. package/dist/audit-prune.js +3 -5
  41. package/dist/audit.js +11 -33
  42. package/dist/auth.d.ts +8 -9
  43. package/dist/auth.js +5 -7
  44. package/dist/autolearn.d.ts +1 -1
  45. package/dist/autolearn.js +1 -1
  46. package/dist/availability.js +3 -5
  47. package/dist/capture/command.d.ts +5 -4
  48. package/dist/capture/command.js +8 -22
  49. package/dist/capture/compact.d.ts +1 -1
  50. package/dist/capture/compact.js +9 -13
  51. package/dist/capture/extract.js +37 -115
  52. package/dist/capture-error.d.ts +1 -1
  53. package/dist/capture-error.js +1 -1
  54. package/dist/churn-git.d.ts +1 -1
  55. package/dist/churn-git.js +1 -1
  56. package/dist/cli/audit.js +3 -4
  57. package/dist/cli/auth.js +3 -6
  58. package/dist/cli/curate.d.ts +1 -1
  59. package/dist/cli/curate.js +8 -24
  60. package/dist/cli/dag.js +5 -9
  61. package/dist/cli/decisions.js +10 -21
  62. package/dist/cli/explain.js +2 -1
  63. package/dist/cli/goals.js +1 -1
  64. package/dist/cli/init.js +1 -1
  65. package/dist/cli/playbooks.js +4 -9
  66. package/dist/cli/projects.js +3 -1
  67. package/dist/cli/recall.js +2 -1
  68. package/dist/cli/remember.js +6 -18
  69. package/dist/cli/session-hooks.js +21 -40
  70. package/dist/cli/setup.d.ts +1 -1
  71. package/dist/cli/setup.js +2 -6
  72. package/dist/cli/shared.js +3 -3
  73. package/dist/cli/slack.js +1 -1
  74. package/dist/cli/sleep.js +27 -1
  75. package/dist/cli/status.d.ts +4 -4
  76. package/dist/cli/status.js +7 -12
  77. package/dist/cli/transfer.js +8 -13
  78. package/dist/cli/usage.d.ts +1 -1
  79. package/dist/cli/usage.js +10 -7
  80. package/dist/cli.d.ts +1 -1
  81. package/dist/cli.js +4 -9
  82. package/dist/compaction-record.d.ts +2 -0
  83. package/dist/compaction-record.js +89 -68
  84. package/dist/compare.d.ts +11 -16
  85. package/dist/compare.js +11 -16
  86. package/dist/config.d.ts +19 -18
  87. package/dist/config.js +90 -67
  88. package/dist/connectors/github/backfill.d.ts +2 -2
  89. package/dist/connectors/github/backfill.js +8 -15
  90. package/dist/connectors/github/cli-impl.js +3 -8
  91. package/dist/connectors/github/deletion.d.ts +5 -12
  92. package/dist/connectors/github/deletion.js +5 -12
  93. package/dist/connectors/github/dlq.d.ts +6 -9
  94. package/dist/connectors/github/dlq.js +2 -3
  95. package/dist/connectors/github/ingest.d.ts +5 -7
  96. package/dist/connectors/github/ingest.js +8 -12
  97. package/dist/connectors/github/octokit-client.d.ts +3 -5
  98. package/dist/connectors/github/octokit-client.js +5 -6
  99. package/dist/connectors/github/signature.d.ts +9 -39
  100. package/dist/connectors/github/signature.js +9 -39
  101. package/dist/connectors/github/tenant-routing.d.ts +1 -1
  102. package/dist/connectors/github/tenant-routing.js +1 -1
  103. package/dist/connectors/github/transform.js +2 -2
  104. package/dist/connectors/github/types.d.ts +2 -10
  105. package/dist/connectors/github/types.js +1 -3
  106. package/dist/connectors/slack/deletion.d.ts +3 -8
  107. package/dist/connectors/slack/deletion.js +3 -8
  108. package/dist/connectors/slack/dlq.d.ts +1 -1
  109. package/dist/connectors/slack/ingest.d.ts +1 -1
  110. package/dist/connectors/slack/ingest.js +7 -16
  111. package/dist/connectors/slack/signature.d.ts +1 -1
  112. package/dist/connectors/slack/tenant-routing.d.ts +3 -5
  113. package/dist/connectors/slack/tenant-routing.js +3 -5
  114. package/dist/connectors/slack/transform.d.ts +5 -6
  115. package/dist/connectors/slack/transform.js +5 -6
  116. package/dist/connectors/slack/types.d.ts +2 -6
  117. package/dist/connectors/slack/types.js +1 -3
  118. package/dist/connectors/slack/web-client.js +10 -3
  119. package/dist/connectors/slack/workspaces.d.ts +3 -5
  120. package/dist/connectors/slack/workspaces.js +3 -5
  121. package/dist/consolidate/conflicts.js +3 -14
  122. package/dist/consolidate/decay.js +9 -29
  123. package/dist/consolidate/llm-passes.js +4 -5
  124. package/dist/consolidate/merge.js +8 -23
  125. package/dist/consolidate/run.d.ts +3 -8
  126. package/dist/consolidate/run.js +21 -26
  127. package/dist/consolidate/sleep.js +7 -18
  128. package/dist/consolidate/traces.js +9 -21
  129. package/dist/customer-notes.d.ts +5 -7
  130. package/dist/customer-notes.js +6 -9
  131. package/dist/dag.d.ts +10 -21
  132. package/dist/dag.js +23 -73
  133. package/dist/db/continuity.d.ts +5 -0
  134. package/dist/db/continuity.js +43 -25
  135. package/dist/db/meta.d.ts +1 -0
  136. package/dist/db/meta.js +4 -3
  137. package/dist/db/migrate.d.ts +9 -0
  138. package/dist/db/migrate.js +110 -52
  139. package/dist/db/migrations/v14.js +1 -1
  140. package/dist/db/migrations/v15.js +1 -2
  141. package/dist/db/migrations/v16.js +3 -4
  142. package/dist/db/migrations/v17.js +2 -3
  143. package/dist/db/migrations/v19.js +1 -1
  144. package/dist/db/migrations/v20.js +1 -1
  145. package/dist/db/migrations/v21.js +2 -6
  146. package/dist/db/migrations/v22.js +2 -4
  147. package/dist/db/migrations/v23.js +1 -1
  148. package/dist/db/migrations/v24.js +4 -6
  149. package/dist/db/migrations/v25.js +2 -3
  150. package/dist/db/migrations/v26.js +3 -3
  151. package/dist/db/migrations/v27.js +2 -10
  152. package/dist/db/migrations/v28.js +5 -8
  153. package/dist/db/migrations/v29.js +3 -4
  154. package/dist/db/migrations/v30.js +2 -2
  155. package/dist/db/migrations/v31.js +1 -1
  156. package/dist/db/migrations/v32.js +1 -1
  157. package/dist/db/migrations/v33.js +3 -3
  158. package/dist/db/migrations/v34.js +1 -1
  159. package/dist/db/migrations/v35.js +3 -4
  160. package/dist/db/migrations/v36.js +3 -4
  161. package/dist/db/migrations/v37.js +5 -5
  162. package/dist/db/migrations/v38.js +7 -8
  163. package/dist/db/migrations/v39.js +1 -1
  164. package/dist/db/migrations/v40.js +4 -16
  165. package/dist/db/migrations/v41.js +3 -4
  166. package/dist/db/migrations/v42.js +3 -4
  167. package/dist/db/migrations/v45.js +1 -1
  168. package/dist/db/migrations/v46.js +1 -1
  169. package/dist/db/migrations/v47.js +1 -1
  170. package/dist/db/migrations/v48.js +1 -1
  171. package/dist/db.d.ts +1 -1
  172. package/dist/db.js +1 -1
  173. package/dist/decisions.d.ts +2 -2
  174. package/dist/decisions.js +6 -6
  175. package/dist/dedupe.js +86 -61
  176. package/dist/delivery-recorder.js +154 -135
  177. package/dist/doctor.js +140 -105
  178. package/dist/dormant.js +1 -4
  179. package/dist/embedding-provider.d.ts +4 -8
  180. package/dist/embedding-provider.js +4 -8
  181. package/dist/embeddings.js +55 -47
  182. package/dist/env.d.ts +1 -1
  183. package/dist/env.js +12 -12
  184. package/dist/escape.d.ts +5 -0
  185. package/dist/escape.js +10 -0
  186. package/dist/eval-stats.d.ts +1 -2
  187. package/dist/eval-stats.js +1 -2
  188. package/dist/eval-suite.js +27 -21
  189. package/dist/extract.js +4 -9
  190. package/dist/failure-log.d.ts +3 -3
  191. package/dist/failure-log.js +1 -1
  192. package/dist/forward-claim-detector.d.ts +2 -4
  193. package/dist/forward-claim-detector.js +6 -11
  194. package/dist/goals.d.ts +3 -3
  195. package/dist/goals.js +5 -6
  196. package/dist/graph/read.d.ts +2 -2
  197. package/dist/graph/read.js +5 -6
  198. package/dist/graph/types.d.ts +8 -8
  199. package/dist/graph/write.d.ts +7 -14
  200. package/dist/graph/write.js +16 -23
  201. package/dist/graph-extract.d.ts +7 -8
  202. package/dist/graph-extract.js +62 -72
  203. package/dist/graph-recall.d.ts +2 -2
  204. package/dist/graph-recall.js +55 -49
  205. package/dist/graph-stream.d.ts +5 -6
  206. package/dist/graph-stream.js +66 -57
  207. package/dist/graph-view.d.ts +2 -2
  208. package/dist/graph-view.js +7 -7
  209. package/dist/half-life-migration.d.ts +1 -2
  210. package/dist/half-life-migration.js +2 -3
  211. package/dist/hooks/codex-session.js +1 -1
  212. package/dist/hooks/codex-wrapper.d.ts +1 -1
  213. package/dist/hooks/codex-wrapper.js +3 -2
  214. package/dist/hooks/json-hooks.d.ts +2 -2
  215. package/dist/hooks/json-hooks.js +5 -4
  216. package/dist/hooks/opencode.d.ts +1 -1
  217. package/dist/hooks/opencode.js +5 -4
  218. package/dist/hooks/shared.d.ts +3 -7
  219. package/dist/hooks/shared.js +1 -8
  220. package/dist/http-util.d.ts +2 -3
  221. package/dist/http-util.js +3 -0
  222. package/dist/importers/core.d.ts +2 -9
  223. package/dist/importers/core.js +15 -30
  224. package/dist/importers/sources.js +2 -1
  225. package/dist/importers/vault.js +2 -20
  226. package/dist/incidents.d.ts +1 -1
  227. package/dist/incidents.js +1 -1
  228. package/dist/instruction-detect.d.ts +1 -1
  229. package/dist/instruction-detect.js +1 -1
  230. package/dist/invalidation.d.ts +3 -0
  231. package/dist/invalidation.js +160 -114
  232. package/dist/json.d.ts +5 -0
  233. package/dist/json.js +4 -0
  234. package/dist/judgment.js +1 -2
  235. package/dist/local-embedding.js +1 -1
  236. package/dist/mcp/admin-tools.js +7 -17
  237. package/dist/mcp/format.js +1 -1
  238. package/dist/mcp/framing.js +3 -6
  239. package/dist/mcp/protocol.d.ts +2 -5
  240. package/dist/mcp/protocol.js +1 -3
  241. package/dist/mcp/recall-tools.js +12 -15
  242. package/dist/mcp/request.js +4 -3
  243. package/dist/mcp/session-state.js +2 -3
  244. package/dist/mcp/stdio.js +2 -1
  245. package/dist/mcp/tools.js +9 -6
  246. package/dist/memory-value-weights.d.ts +5 -8
  247. package/dist/memory-value-weights.js +5 -8
  248. package/dist/memory-value.d.ts +13 -13
  249. package/dist/memory-value.js +26 -37
  250. package/dist/memory.d.ts +20 -22
  251. package/dist/memory.js +24 -48
  252. package/dist/multihop.d.ts +1 -1
  253. package/dist/multihop.js +3 -2
  254. package/dist/owner-validation.d.ts +4 -5
  255. package/dist/owner-validation.js +4 -5
  256. package/dist/physics.d.ts +4 -4
  257. package/dist/physics.js +7 -9
  258. package/dist/policies.d.ts +9 -10
  259. package/dist/policies.js +12 -14
  260. package/dist/postinstall.js +3 -6
  261. package/dist/predictions/planning-fallacy.d.ts +9 -14
  262. package/dist/predictions/planning-fallacy.js +10 -16
  263. package/dist/predictions/store.d.ts +15 -23
  264. package/dist/predictions/store.js +36 -33
  265. package/dist/processes.d.ts +2 -7
  266. package/dist/processes.js +3 -3
  267. package/dist/project-briefs.d.ts +2 -3
  268. package/dist/project-briefs.js +9 -13
  269. package/dist/project-identity.d.ts +22 -9
  270. package/dist/project-identity.js +47 -12
  271. package/dist/project-merge.d.ts +17 -2
  272. package/dist/project-merge.js +109 -27
  273. package/dist/project-remote.d.ts +12 -0
  274. package/dist/project-remote.js +138 -0
  275. package/dist/prompt-recall.js +1 -2
  276. package/dist/rate-limit.d.ts +1 -1
  277. package/dist/rate-limit.js +1 -1
  278. package/dist/raw-archive.d.ts +9 -0
  279. package/dist/raw-archive.js +60 -53
  280. package/dist/recall-history.d.ts +19 -20
  281. package/dist/recall-history.js +24 -42
  282. package/dist/recall-pipeline.js +4 -28
  283. package/dist/recall-scope.d.ts +7 -8
  284. package/dist/recall-scope.js +7 -8
  285. package/dist/recall-trace.d.ts +5 -9
  286. package/dist/recall-trace.js +6 -10
  287. package/dist/refine-llm.d.ts +1 -1
  288. package/dist/refine-llm.js +2 -2
  289. package/dist/reject-flow.d.ts +3 -4
  290. package/dist/reject-flow.js +122 -117
  291. package/dist/rejection.d.ts +5 -6
  292. package/dist/rejection.js +7 -15
  293. package/dist/rerankers/clef.d.ts +1 -1
  294. package/dist/rerankers/jev.d.ts +1 -2
  295. package/dist/rerankers/jev.js +4 -5
  296. package/dist/rerankers/llm.d.ts +1 -2
  297. package/dist/rerankers/llm.js +1 -2
  298. package/dist/rerankers/types.d.ts +1 -2
  299. package/dist/rrf.d.ts +2 -2
  300. package/dist/rrf.js +2 -2
  301. package/dist/search/bm25-search.d.ts +1 -1
  302. package/dist/search/bm25-search.js +2 -1
  303. package/dist/search/boosts.js +2 -1
  304. package/dist/search/hybrid.d.ts +1 -1
  305. package/dist/search/hybrid.js +2 -1
  306. package/dist/search/physics-search.d.ts +1 -1
  307. package/dist/search/physics-search.js +2 -1
  308. package/dist/search/types.d.ts +2 -0
  309. package/dist/search/types.js +3 -1
  310. package/dist/secret-detect.d.ts +4 -5
  311. package/dist/secret-detect.js +6 -10
  312. package/dist/server/auth.js +5 -5
  313. package/dist/server/client-ip.js +1 -1
  314. package/dist/server/cursor.js +2 -1
  315. package/dist/server/mcp-http.js +4 -4
  316. package/dist/server/request.d.ts +3 -6
  317. package/dist/server/request.js +6 -7
  318. package/dist/server/routes/admin.js +5 -4
  319. package/dist/server/routes/customer-notes.js +6 -5
  320. package/dist/server/routes/decisions.js +4 -3
  321. package/dist/server/routes/incidents.js +7 -5
  322. package/dist/server/routes/memories.js +7 -7
  323. package/dist/server/routes/policies.js +3 -2
  324. package/dist/server/routes/predictions.js +12 -15
  325. package/dist/server/routes/processes.js +3 -2
  326. package/dist/server/routes/project-briefs.js +8 -7
  327. package/dist/server/routes/recall.js +95 -93
  328. package/dist/server/routes/skills.js +6 -5
  329. package/dist/server/types.d.ts +1 -1
  330. package/dist/server/validation.d.ts +1 -2
  331. package/dist/server/validation.js +7 -14
  332. package/dist/server-detect.js +72 -58
  333. package/dist/server.d.ts +2 -2
  334. package/dist/server.js +131 -117
  335. package/dist/shared.d.ts +17 -17
  336. package/dist/shared.js +93 -97
  337. package/dist/skills.d.ts +3 -3
  338. package/dist/skills.js +8 -8
  339. package/dist/store/audit-event.d.ts +2 -2
  340. package/dist/store/audit-event.js +1 -1
  341. package/dist/store/candidates.d.ts +2 -2
  342. package/dist/store/candidates.js +4 -3
  343. package/dist/store/conflicts.js +30 -22
  344. package/dist/store/delete-and-batch.d.ts +11 -14
  345. package/dist/store/delete-and-batch.js +40 -91
  346. package/dist/store/entry-reads.d.ts +17 -25
  347. package/dist/store/entry-reads.js +59 -40
  348. package/dist/store/entry-row.d.ts +6 -24
  349. package/dist/store/entry-row.js +6 -24
  350. package/dist/store/entry-writes.d.ts +6 -7
  351. package/dist/store/entry-writes.js +13 -11
  352. package/dist/store/handoffs.d.ts +1 -1
  353. package/dist/store/handoffs.js +7 -10
  354. package/dist/store/index-and-stats.d.ts +2 -6
  355. package/dist/store/index-and-stats.js +4 -10
  356. package/dist/store/mirrors.d.ts +6 -19
  357. package/dist/store/mirrors.js +14 -39
  358. package/dist/store/open.js +11 -36
  359. package/dist/store/rows.d.ts +5 -11
  360. package/dist/store/rows.js +6 -11
  361. package/dist/store/search-rows.d.ts +17 -34
  362. package/dist/store/search-rows.js +31 -59
  363. package/dist/store/sessions.d.ts +4 -5
  364. package/dist/store/sessions.js +5 -6
  365. package/dist/store/summaries.d.ts +13 -17
  366. package/dist/store/summaries.js +26 -70
  367. package/dist/support-bundle.js +4 -8
  368. package/dist/tenant.d.ts +1 -5
  369. package/dist/token-ledger.d.ts +1 -1
  370. package/dist/token-ledger.js +3 -5
  371. package/dist/trace.js +1 -3
  372. package/dist/version.d.ts +1 -1
  373. package/dist/version.js +1 -1
  374. package/dist/working-memory.d.ts +1 -1
  375. package/extensions/openclaw-plugin/README.md +3 -1
  376. package/extensions/openclaw-plugin/openclaw.plugin.json +2 -2
  377. package/extensions/openclaw-plugin/package.json +3 -3
  378. package/openclaw.plugin.json +2 -2
  379. package/package.json +2 -2
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
@@ -24,12 +24,7 @@ function byDepthThenScore(a, b) {
24
24
  return score;
25
25
  return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
26
26
  }
27
- /** A recorder for one call; every observer method is guarded, and a throw marks it broken instead of escaping. */
28
- export function createDeliveryRecorder(init) {
29
- const startedMs = Date.now();
30
- const ts = evalNow().toISOString();
31
- // Test-only fault injection, as HIPPO_FAKE_NOW is for time.
32
- const fault = envTestDeliveryFault();
27
+ function readPayload(init) {
33
28
  const payloadSession = hookPayloadSessionId(init.stdinText);
34
29
  const subagent = isSubagentPayload(init.stdinText);
35
30
  const envSession = init.envSessionId !== undefined && init.envSessionId !== '' ? init.envSessionId : null;
@@ -40,148 +35,127 @@ export function createDeliveryRecorder(init) {
40
35
  const sessionState = subagent
41
36
  ? 'subagent'
42
37
  : payloadSession !== null ? 'payload' : envSession !== null ? 'env' : 'missing';
43
- const candidates = new Map();
44
- const picked = new Map();
45
- const filtered = new Set();
46
- let facts = null;
47
- let shown = 0;
48
- let dropped = 0;
49
- let disabledSeen = false;
50
- let outcome = { state: 'empty' };
51
- let broken = null;
52
- let flushed = false;
53
- const guard = (fn) => {
54
- if (broken !== null)
55
- return;
56
- try {
57
- if (fault === 'observe')
58
- throw new Error('injected observe fault');
59
- fn();
60
- }
61
- catch (error) {
62
- broken = error instanceof Error ? error.message : String(error);
63
- }
64
- };
65
- const rejectId = (id, stage, reason, score, tokens) => {
66
- const held = candidates.get(id);
67
- // First rejection wins; an id never offered is not a candidate of this call.
68
- if (!held || held.reason !== null)
69
- return;
70
- candidates.set(id, { ...held, stage, reason, score, tokens });
71
- };
72
- const build = () => {
73
- if (fault === 'build')
74
- throw new Error('injected build fault');
75
- const staticReused = outcome.staticReused === true;
76
- const rows = [];
77
- for (const p of picked.values()) {
78
- const held = candidates.get(p.entry.id);
79
- rows.push({
80
- memoryId: p.entry.id,
81
- sourceStore: p.sourceStore,
82
- pool: p.promptRecall ? 'prompt-recall' : held?.pool === 'pin' || p.entry.pinned ? 'pin' : 'recent',
83
- stage: 'final',
84
- outcome: staticReused && !p.promptRecall ? 'reused' : 'emitted',
85
- reason: null,
86
- rank: p.rank,
87
- score: p.score,
88
- tokens: p.tokens,
89
- });
90
- }
91
- const rejected = [];
92
- let undecided = 0;
93
- for (const c of candidates.values()) {
94
- if (picked.has(c.id))
95
- continue;
96
- if (c.reason === null)
97
- undecided += 1;
98
- else
99
- rejected.push(c);
100
- }
101
- rejected.sort(byDepthThenScore);
102
- for (const c of rejected.slice(0, DELIVERY_REJECTED_ROW_CAP)) {
103
- rows.push({
104
- memoryId: c.id, sourceStore: c.sourceStore, pool: c.pool, stage: c.stage ?? 'load', outcome: 'rejected',
105
- reason: c.reason, rank: null, score: c.score, tokens: c.tokens,
106
- });
107
- }
108
- const overflow = Math.max(0, rejected.length - DELIVERY_REJECTED_ROW_CAP);
109
- const emitted = outcome.emittedText ?? null;
110
- return {
111
- ts,
112
- tenantId: init.tenantId,
113
- runtime: hostTurnId !== null ? 'codex' : hookEvent !== null ? 'claude-code' : 'unknown',
114
- eventType: hookEvent === 'UserPromptSubmit' ? 'prompt-submit' : 'pinned-manual',
115
- surface: 'hook',
116
- storeHash: init.storeHash,
117
- writeStore: init.writeStore,
118
- projectHash: facts !== null && facts.projectName !== '' ? blockHash(facts.projectName) : null,
119
- sessionId: payloadSession ?? envSession,
120
- sessionState,
121
- hostTurnId,
122
- promptHash: prompt !== null ? blockHash(prompt) : null,
123
- promptLength: prompt?.length ?? 0,
124
- queryHash: null,
125
- recallTraceId: null,
126
- blockState: disabledSeen ? 'disabled' : outcome.state,
127
- promptRecall: facts?.promptRecall === true,
128
- consideredCount: new Set([...candidates.keys(), ...picked.keys()]).size,
129
- filteredCount: filtered.size,
130
- selectedCount: picked.size,
131
- emittedCount: rows.filter((r) => r.outcome === 'emitted').length,
132
- rejectedCount: rejected.length + undecided,
133
- rejectedUnlisted: overflow + undecided,
134
- sectionsShown: shown,
135
- sectionsDropped: dropped,
136
- budgetTokens: facts?.budgetTokens ?? 0,
137
- selectedTokens: [...picked.values()].reduce((sum, p) => sum + p.tokens, 0),
138
- injectedTokens: emitted !== null ? estimateTokens(emitted) : 0,
139
- staticHash: outcome.staticHash ?? null,
140
- recallHash: outcome.recallHash ?? null,
141
- emittedHash: emitted !== null ? blockHash(emitted) : null,
142
- elapsedMs: Math.max(0, Date.now() - startedMs),
143
- candidates: rows,
144
- };
38
+ return { payloadSession, envSession, prompt, hostTurnId, hookEvent, sessionState };
39
+ }
40
+ function rejectId(state, id, stage, reason, score, tokens) {
41
+ const held = state.candidates.get(id);
42
+ // First rejection wins; an id never offered is not a candidate of this call.
43
+ if (!held || held.reason !== null)
44
+ return;
45
+ state.candidates.set(id, { ...held, stage, reason, score, tokens });
46
+ }
47
+ function pickedRows(state) {
48
+ const staticReused = state.outcome.staticReused === true;
49
+ const rows = [];
50
+ for (const p of state.picked.values()) {
51
+ const held = state.candidates.get(p.entry.id);
52
+ rows.push({
53
+ memoryId: p.entry.id,
54
+ sourceStore: p.sourceStore,
55
+ pool: p.promptRecall ? 'prompt-recall' : held?.pool === 'pin' || p.entry.pinned ? 'pin' : 'recent',
56
+ stage: 'final',
57
+ outcome: staticReused && !p.promptRecall ? 'reused' : 'emitted',
58
+ reason: null,
59
+ rank: p.rank,
60
+ score: p.score,
61
+ tokens: p.tokens,
62
+ });
63
+ }
64
+ return rows;
65
+ }
66
+ /** Rejected candidates deepest first, capped at DELIVERY_REJECTED_ROW_CAP rows. */
67
+ function rejectedRows(state) {
68
+ const rejected = [];
69
+ let undecided = 0;
70
+ for (const c of state.candidates.values()) {
71
+ if (state.picked.has(c.id))
72
+ continue;
73
+ if (c.reason === null)
74
+ undecided += 1;
75
+ else
76
+ rejected.push(c);
77
+ }
78
+ rejected.sort(byDepthThenScore);
79
+ const rows = rejected.slice(0, DELIVERY_REJECTED_ROW_CAP).map((c) => ({
80
+ memoryId: c.id, sourceStore: c.sourceStore, pool: c.pool, stage: c.stage ?? 'load', outcome: 'rejected',
81
+ reason: c.reason, rank: null, score: c.score, tokens: c.tokens,
82
+ }));
83
+ const overflow = Math.max(0, rejected.length - DELIVERY_REJECTED_ROW_CAP);
84
+ return { rows, rejected: rejected.length, undecided, overflow };
85
+ }
86
+ function buildEvent(init, payload, state, ts, startedMs) {
87
+ const { facts, outcome } = state;
88
+ const picked = pickedRows(state);
89
+ const rejected = rejectedRows(state);
90
+ const rows = [...picked, ...rejected.rows];
91
+ const emitted = outcome.emittedText ?? null;
92
+ return {
93
+ ts,
94
+ tenantId: init.tenantId,
95
+ runtime: payload.hostTurnId !== null ? 'codex' : payload.hookEvent !== null ? 'claude-code' : 'unknown',
96
+ eventType: payload.hookEvent === 'UserPromptSubmit' ? 'prompt-submit' : 'pinned-manual',
97
+ surface: 'hook',
98
+ storeHash: init.storeHash,
99
+ writeStore: init.writeStore,
100
+ projectHash: facts !== null && facts.projectName !== '' ? blockHash(facts.projectName) : null,
101
+ sessionId: payload.payloadSession ?? payload.envSession,
102
+ sessionState: payload.sessionState,
103
+ hostTurnId: payload.hostTurnId,
104
+ promptHash: payload.prompt !== null ? blockHash(payload.prompt) : null,
105
+ promptLength: payload.prompt?.length ?? 0,
106
+ queryHash: null,
107
+ recallTraceId: null,
108
+ blockState: state.disabledSeen ? 'disabled' : outcome.state,
109
+ promptRecall: facts?.promptRecall === true,
110
+ consideredCount: new Set([...state.candidates.keys(), ...state.picked.keys()]).size,
111
+ filteredCount: state.filtered.size,
112
+ selectedCount: state.picked.size,
113
+ emittedCount: rows.filter((r) => r.outcome === 'emitted').length,
114
+ rejectedCount: rejected.rejected + rejected.undecided,
115
+ rejectedUnlisted: rejected.overflow + rejected.undecided,
116
+ sectionsShown: state.shown,
117
+ sectionsDropped: state.dropped,
118
+ budgetTokens: facts?.budgetTokens ?? 0,
119
+ selectedTokens: [...state.picked.values()].reduce((sum, p) => sum + p.tokens, 0),
120
+ injectedTokens: emitted !== null ? estimateTokens(emitted) : 0,
121
+ staticHash: outcome.staticHash ?? null,
122
+ recallHash: outcome.recallHash ?? null,
123
+ emittedHash: emitted !== null ? blockHash(emitted) : null,
124
+ elapsedMs: Math.max(0, Date.now() - startedMs),
125
+ candidates: rows,
145
126
  };
127
+ }
128
+ /** The candidate-tracking half of the observer, each method run through `guard`. */
129
+ function candidateMethods(state, guard) {
146
130
  return {
147
- root: init.root,
148
- facts: (f) => guard(() => { facts = { ...f }; }),
149
- sections: (s, d) => guard(() => { shown = s; dropped = d; }),
150
- watchAdmit: (admit) => (e) => {
151
- const ok = admit(e);
152
- if (!ok)
153
- guard(() => { filtered.add(e.id); });
154
- return ok;
155
- },
156
131
  qualityDropped: (e, isGlobal) => guard(() => {
157
- filtered.add(e.id);
158
- if (!candidates.has(e.id)) {
159
- candidates.set(e.id, {
132
+ state.filtered.add(e.id);
133
+ if (!state.candidates.has(e.id)) {
134
+ state.candidates.set(e.id, {
160
135
  id: e.id, sourceStore: storeOf(isGlobal), pool: 'recent', stage: null, reason: null, score: null, tokens: null,
161
136
  });
162
137
  }
163
- rejectId(e.id, 'load', 'quality', null, null);
138
+ rejectId(state, e.id, 'load', 'quality', null, null);
164
139
  }),
165
- disabled: () => guard(() => { disabledSeen = true; }),
166
140
  offer: (entries, isGlobal, pool) => guard(() => {
167
141
  for (const e of entries) {
168
- const held = candidates.get(e.id);
142
+ const held = state.candidates.get(e.id);
169
143
  const wanted = pool ?? (e.pinned ? 'pin' : 'recent');
170
144
  // A loaded recent row the prompt-recall gate then judges belongs to that pool.
171
145
  const relabel = held !== undefined && held.reason === null && held.pool === 'recent' && wanted === 'prompt-recall';
172
146
  if (held !== undefined && !relabel)
173
147
  continue;
174
- candidates.set(e.id, {
148
+ state.candidates.set(e.id, {
175
149
  id: e.id, sourceStore: storeOf(isGlobal), pool: wanted, stage: null, reason: null, score: null, tokens: null,
176
150
  });
177
151
  }
178
152
  }),
179
- reject: (e, stage, reason, score, tokens) => guard(() => rejectId(e.id, stage, reason, score ?? null, tokens ?? null)),
153
+ reject: (e, stage, reason, score, tokens) => guard(() => rejectId(state, e.id, stage, reason, score ?? null, tokens ?? null)),
180
154
  dropMissing: (before, after, stage, reason) => guard(() => {
181
155
  const kept = new Set(after.map((e) => e.id));
182
156
  for (const e of before)
183
157
  if (!kept.has(e.id))
184
- rejectId(e.id, stage, reason, null, null);
158
+ rejectId(state, e.id, stage, reason, null, null);
185
159
  }),
186
160
  gated: (prompt, items, gate, kept) => guard(() => {
187
161
  const keptIds = new Set(kept.map((g) => g.item.id));
@@ -190,29 +164,74 @@ export function createDeliveryRecorder(init) {
190
164
  continue;
191
165
  const { score, shared } = scoreOverlap(prompt, c.tokens, gate.metric);
192
166
  const cleared = score >= gate.threshold && shared >= gate.minShared;
193
- rejectId(c.id, 'gate', cleared ? 'gate-max-items' : 'gate-below-threshold', score, null);
167
+ rejectId(state, c.id, 'gate', cleared ? 'gate-max-items' : 'gate-below-threshold', score, null);
194
168
  }
195
169
  }),
196
170
  selected: (items) => guard(() => {
197
- picked.clear();
171
+ state.picked.clear();
198
172
  items.forEach((r, i) => {
199
- picked.set(r.entry.id, {
173
+ state.picked.set(r.entry.id, {
200
174
  entry: r.entry, rank: i + 1, score: r.score, tokens: r.tokens,
201
175
  sourceStore: storeOf(r.isGlobal), promptRecall: r.promptRecall === true,
202
176
  });
203
177
  });
204
178
  }),
205
- delivered: (o) => guard(() => { outcome = { ...o }; }),
179
+ };
180
+ }
181
+ /** A recorder for one call; every observer method is guarded, and a throw marks it broken instead of escaping. */
182
+ export function createDeliveryRecorder(init) {
183
+ const startedMs = Date.now();
184
+ const ts = evalNow().toISOString();
185
+ // Test-only fault injection, as HIPPO_FAKE_NOW is for time.
186
+ const fault = envTestDeliveryFault();
187
+ const payload = readPayload(init);
188
+ const state = {
189
+ candidates: new Map(), picked: new Map(), filtered: new Set(), facts: null, shown: 0, dropped: 0,
190
+ disabledSeen: false, outcome: { state: 'empty' }, broken: null, flushed: false,
191
+ };
192
+ const guard = (fn) => {
193
+ if (state.broken !== null)
194
+ return;
195
+ try {
196
+ if (fault === 'observe')
197
+ throw new Error('injected observe fault');
198
+ fn();
199
+ }
200
+ catch (error) {
201
+ state.broken = error instanceof Error ? error.message : String(error);
202
+ }
203
+ };
204
+ const { qualityDropped, offer, reject, dropMissing, gated, selected } = candidateMethods(state, guard);
205
+ return {
206
+ root: init.root,
207
+ facts: (f) => guard(() => { state.facts = { ...f }; }),
208
+ sections: (s, d) => guard(() => { state.shown = s; state.dropped = d; }),
209
+ watchAdmit: (admit) => (e) => {
210
+ const ok = admit(e);
211
+ if (!ok)
212
+ guard(() => { state.filtered.add(e.id); });
213
+ return ok;
214
+ },
215
+ qualityDropped,
216
+ disabled: () => guard(() => { state.disabledSeen = true; }),
217
+ offer,
218
+ reject,
219
+ dropMissing,
220
+ gated,
221
+ selected,
222
+ delivered: (o) => guard(() => { state.outcome = { ...o }; }),
206
223
  flush: (write) => {
207
- if (flushed)
224
+ if (state.flushed)
208
225
  return;
209
- flushed = true;
210
- if (broken !== null) {
226
+ state.flushed = true;
227
+ if (state.broken !== null) {
211
228
  // Same pinned `[hippo] delivery ledger` hook stderr line as recall-trace.ts's write failure.
212
- console.error(`[hippo] delivery ledger skipped: recorder failed: ${broken}`);
229
+ console.error(`[hippo] delivery ledger skipped: recorder failed: ${state.broken}`);
213
230
  return;
214
231
  }
215
- const input = build();
232
+ if (fault === 'build')
233
+ throw new Error('injected build fault');
234
+ const input = buildEvent(init, payload, state, ts, startedMs);
216
235
  if (fault === 'flush')
217
236
  throw new Error('injected flush fault');
218
237
  write(input);