hippo-memory 1.62.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 (370) hide show
  1. package/README.md +9 -0
  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.js +9 -6
  79. package/dist/cli.d.ts +1 -1
  80. package/dist/cli.js +4 -9
  81. package/dist/compaction-record.d.ts +2 -0
  82. package/dist/compaction-record.js +89 -68
  83. package/dist/compare.d.ts +11 -16
  84. package/dist/compare.js +11 -16
  85. package/dist/config.d.ts +19 -18
  86. package/dist/config.js +90 -67
  87. package/dist/connectors/github/backfill.d.ts +2 -2
  88. package/dist/connectors/github/backfill.js +8 -15
  89. package/dist/connectors/github/cli-impl.js +3 -8
  90. package/dist/connectors/github/deletion.d.ts +5 -12
  91. package/dist/connectors/github/deletion.js +5 -12
  92. package/dist/connectors/github/dlq.d.ts +6 -9
  93. package/dist/connectors/github/dlq.js +2 -3
  94. package/dist/connectors/github/ingest.d.ts +5 -7
  95. package/dist/connectors/github/ingest.js +8 -12
  96. package/dist/connectors/github/octokit-client.d.ts +3 -5
  97. package/dist/connectors/github/octokit-client.js +5 -6
  98. package/dist/connectors/github/signature.d.ts +9 -39
  99. package/dist/connectors/github/signature.js +9 -39
  100. package/dist/connectors/github/tenant-routing.d.ts +1 -1
  101. package/dist/connectors/github/tenant-routing.js +1 -1
  102. package/dist/connectors/github/transform.js +2 -2
  103. package/dist/connectors/github/types.d.ts +2 -10
  104. package/dist/connectors/github/types.js +1 -3
  105. package/dist/connectors/slack/deletion.d.ts +3 -8
  106. package/dist/connectors/slack/deletion.js +3 -8
  107. package/dist/connectors/slack/dlq.d.ts +1 -1
  108. package/dist/connectors/slack/ingest.d.ts +1 -1
  109. package/dist/connectors/slack/ingest.js +7 -16
  110. package/dist/connectors/slack/signature.d.ts +1 -1
  111. package/dist/connectors/slack/tenant-routing.d.ts +3 -5
  112. package/dist/connectors/slack/tenant-routing.js +3 -5
  113. package/dist/connectors/slack/transform.d.ts +5 -6
  114. package/dist/connectors/slack/transform.js +5 -6
  115. package/dist/connectors/slack/types.d.ts +2 -6
  116. package/dist/connectors/slack/types.js +1 -3
  117. package/dist/connectors/slack/web-client.js +10 -3
  118. package/dist/connectors/slack/workspaces.d.ts +3 -5
  119. package/dist/connectors/slack/workspaces.js +3 -5
  120. package/dist/consolidate/conflicts.js +3 -14
  121. package/dist/consolidate/decay.js +9 -29
  122. package/dist/consolidate/llm-passes.js +4 -5
  123. package/dist/consolidate/merge.js +8 -23
  124. package/dist/consolidate/run.d.ts +1 -8
  125. package/dist/consolidate/run.js +3 -25
  126. package/dist/consolidate/sleep.js +5 -17
  127. package/dist/consolidate/traces.js +9 -21
  128. package/dist/customer-notes.d.ts +5 -7
  129. package/dist/customer-notes.js +6 -9
  130. package/dist/dag.d.ts +10 -21
  131. package/dist/dag.js +23 -73
  132. package/dist/db/continuity.js +2 -2
  133. package/dist/db/migrations/v14.js +1 -1
  134. package/dist/db/migrations/v15.js +1 -2
  135. package/dist/db/migrations/v16.js +3 -4
  136. package/dist/db/migrations/v17.js +2 -3
  137. package/dist/db/migrations/v19.js +1 -1
  138. package/dist/db/migrations/v20.js +1 -1
  139. package/dist/db/migrations/v21.js +2 -6
  140. package/dist/db/migrations/v22.js +2 -4
  141. package/dist/db/migrations/v23.js +1 -1
  142. package/dist/db/migrations/v24.js +4 -6
  143. package/dist/db/migrations/v25.js +2 -3
  144. package/dist/db/migrations/v26.js +3 -3
  145. package/dist/db/migrations/v27.js +2 -10
  146. package/dist/db/migrations/v28.js +5 -8
  147. package/dist/db/migrations/v29.js +3 -4
  148. package/dist/db/migrations/v30.js +2 -2
  149. package/dist/db/migrations/v31.js +1 -1
  150. package/dist/db/migrations/v32.js +1 -1
  151. package/dist/db/migrations/v33.js +3 -3
  152. package/dist/db/migrations/v34.js +1 -1
  153. package/dist/db/migrations/v35.js +3 -4
  154. package/dist/db/migrations/v36.js +3 -4
  155. package/dist/db/migrations/v37.js +5 -5
  156. package/dist/db/migrations/v38.js +7 -8
  157. package/dist/db/migrations/v39.js +1 -1
  158. package/dist/db/migrations/v40.js +4 -16
  159. package/dist/db/migrations/v41.js +3 -4
  160. package/dist/db/migrations/v42.js +3 -4
  161. package/dist/db/migrations/v45.js +1 -1
  162. package/dist/db/migrations/v46.js +1 -1
  163. package/dist/db/migrations/v47.js +1 -1
  164. package/dist/db/migrations/v48.js +1 -1
  165. package/dist/decisions.d.ts +2 -2
  166. package/dist/decisions.js +6 -6
  167. package/dist/dedupe.js +86 -61
  168. package/dist/delivery-recorder.js +154 -135
  169. package/dist/doctor.js +119 -104
  170. package/dist/dormant.js +1 -4
  171. package/dist/embedding-provider.d.ts +4 -8
  172. package/dist/embedding-provider.js +4 -8
  173. package/dist/embeddings.js +55 -47
  174. package/dist/env.d.ts +1 -1
  175. package/dist/env.js +12 -12
  176. package/dist/escape.d.ts +5 -0
  177. package/dist/escape.js +10 -0
  178. package/dist/eval-stats.d.ts +1 -2
  179. package/dist/eval-stats.js +1 -2
  180. package/dist/eval-suite.js +27 -21
  181. package/dist/extract.js +4 -9
  182. package/dist/failure-log.d.ts +3 -3
  183. package/dist/failure-log.js +1 -1
  184. package/dist/forward-claim-detector.d.ts +2 -4
  185. package/dist/forward-claim-detector.js +6 -11
  186. package/dist/goals.d.ts +3 -3
  187. package/dist/goals.js +5 -6
  188. package/dist/graph/read.d.ts +2 -2
  189. package/dist/graph/read.js +5 -6
  190. package/dist/graph/types.d.ts +8 -8
  191. package/dist/graph/write.d.ts +7 -14
  192. package/dist/graph/write.js +16 -23
  193. package/dist/graph-extract.d.ts +7 -8
  194. package/dist/graph-extract.js +62 -72
  195. package/dist/graph-recall.d.ts +2 -2
  196. package/dist/graph-recall.js +55 -49
  197. package/dist/graph-stream.d.ts +5 -6
  198. package/dist/graph-stream.js +66 -57
  199. package/dist/graph-view.d.ts +2 -2
  200. package/dist/graph-view.js +7 -7
  201. package/dist/half-life-migration.d.ts +1 -2
  202. package/dist/half-life-migration.js +2 -3
  203. package/dist/hooks/codex-session.js +1 -1
  204. package/dist/hooks/codex-wrapper.d.ts +1 -1
  205. package/dist/hooks/codex-wrapper.js +3 -2
  206. package/dist/hooks/json-hooks.d.ts +2 -2
  207. package/dist/hooks/json-hooks.js +5 -4
  208. package/dist/hooks/opencode.d.ts +1 -1
  209. package/dist/hooks/opencode.js +5 -4
  210. package/dist/hooks/shared.d.ts +3 -7
  211. package/dist/hooks/shared.js +1 -8
  212. package/dist/http-util.d.ts +2 -3
  213. package/dist/http-util.js +3 -0
  214. package/dist/importers/core.d.ts +2 -9
  215. package/dist/importers/core.js +15 -30
  216. package/dist/importers/sources.js +2 -1
  217. package/dist/importers/vault.js +2 -20
  218. package/dist/incidents.d.ts +1 -1
  219. package/dist/incidents.js +1 -1
  220. package/dist/instruction-detect.d.ts +1 -1
  221. package/dist/instruction-detect.js +1 -1
  222. package/dist/invalidation.d.ts +3 -0
  223. package/dist/invalidation.js +160 -114
  224. package/dist/json.d.ts +5 -0
  225. package/dist/json.js +4 -0
  226. package/dist/judgment.js +1 -2
  227. package/dist/local-embedding.js +1 -1
  228. package/dist/mcp/admin-tools.js +7 -17
  229. package/dist/mcp/format.js +1 -1
  230. package/dist/mcp/framing.js +3 -6
  231. package/dist/mcp/protocol.d.ts +2 -5
  232. package/dist/mcp/protocol.js +1 -3
  233. package/dist/mcp/recall-tools.js +12 -15
  234. package/dist/mcp/request.js +4 -3
  235. package/dist/mcp/session-state.js +2 -3
  236. package/dist/mcp/stdio.js +2 -1
  237. package/dist/mcp/tools.js +9 -6
  238. package/dist/memory-value-weights.d.ts +5 -8
  239. package/dist/memory-value-weights.js +5 -8
  240. package/dist/memory-value.d.ts +13 -13
  241. package/dist/memory-value.js +26 -37
  242. package/dist/memory.d.ts +20 -22
  243. package/dist/memory.js +24 -48
  244. package/dist/multihop.d.ts +1 -1
  245. package/dist/multihop.js +3 -2
  246. package/dist/owner-validation.d.ts +4 -5
  247. package/dist/owner-validation.js +4 -5
  248. package/dist/physics.d.ts +4 -4
  249. package/dist/physics.js +7 -9
  250. package/dist/policies.d.ts +9 -10
  251. package/dist/policies.js +12 -14
  252. package/dist/postinstall.js +3 -6
  253. package/dist/predictions/planning-fallacy.d.ts +9 -14
  254. package/dist/predictions/planning-fallacy.js +10 -16
  255. package/dist/predictions/store.d.ts +15 -23
  256. package/dist/predictions/store.js +36 -33
  257. package/dist/processes.d.ts +2 -7
  258. package/dist/processes.js +3 -3
  259. package/dist/project-briefs.d.ts +2 -3
  260. package/dist/project-briefs.js +9 -13
  261. package/dist/project-identity.d.ts +22 -9
  262. package/dist/project-identity.js +47 -12
  263. package/dist/project-merge.d.ts +17 -2
  264. package/dist/project-merge.js +109 -27
  265. package/dist/project-remote.d.ts +12 -0
  266. package/dist/project-remote.js +138 -0
  267. package/dist/prompt-recall.js +1 -2
  268. package/dist/rate-limit.d.ts +1 -1
  269. package/dist/rate-limit.js +1 -1
  270. package/dist/raw-archive.d.ts +9 -0
  271. package/dist/raw-archive.js +70 -53
  272. package/dist/recall-history.d.ts +19 -20
  273. package/dist/recall-history.js +24 -42
  274. package/dist/recall-pipeline.js +4 -28
  275. package/dist/recall-scope.d.ts +7 -8
  276. package/dist/recall-scope.js +7 -8
  277. package/dist/recall-trace.d.ts +5 -9
  278. package/dist/recall-trace.js +6 -10
  279. package/dist/refine-llm.d.ts +1 -1
  280. package/dist/refine-llm.js +2 -2
  281. package/dist/reject-flow.d.ts +3 -4
  282. package/dist/reject-flow.js +122 -117
  283. package/dist/rejection.d.ts +5 -6
  284. package/dist/rejection.js +7 -15
  285. package/dist/rerankers/clef.d.ts +1 -1
  286. package/dist/rerankers/jev.d.ts +1 -2
  287. package/dist/rerankers/jev.js +4 -5
  288. package/dist/rerankers/llm.d.ts +1 -2
  289. package/dist/rerankers/llm.js +1 -2
  290. package/dist/rerankers/types.d.ts +1 -2
  291. package/dist/rrf.d.ts +2 -2
  292. package/dist/rrf.js +2 -2
  293. package/dist/search/bm25-search.d.ts +1 -1
  294. package/dist/search/bm25-search.js +2 -1
  295. package/dist/search/boosts.js +2 -1
  296. package/dist/search/hybrid.d.ts +1 -1
  297. package/dist/search/hybrid.js +2 -1
  298. package/dist/search/physics-search.d.ts +1 -1
  299. package/dist/search/physics-search.js +2 -1
  300. package/dist/search/types.d.ts +2 -0
  301. package/dist/search/types.js +3 -1
  302. package/dist/secret-detect.d.ts +4 -5
  303. package/dist/secret-detect.js +6 -10
  304. package/dist/server/auth.js +5 -5
  305. package/dist/server/client-ip.js +1 -1
  306. package/dist/server/cursor.js +2 -1
  307. package/dist/server/mcp-http.js +4 -4
  308. package/dist/server/request.d.ts +3 -6
  309. package/dist/server/request.js +6 -7
  310. package/dist/server/routes/admin.js +5 -4
  311. package/dist/server/routes/customer-notes.js +6 -5
  312. package/dist/server/routes/decisions.js +4 -3
  313. package/dist/server/routes/incidents.js +7 -5
  314. package/dist/server/routes/memories.js +7 -7
  315. package/dist/server/routes/policies.js +3 -2
  316. package/dist/server/routes/predictions.js +12 -15
  317. package/dist/server/routes/processes.js +3 -2
  318. package/dist/server/routes/project-briefs.js +8 -7
  319. package/dist/server/routes/recall.js +95 -93
  320. package/dist/server/routes/skills.js +6 -5
  321. package/dist/server/types.d.ts +1 -1
  322. package/dist/server/validation.d.ts +1 -2
  323. package/dist/server/validation.js +7 -14
  324. package/dist/server-detect.js +72 -58
  325. package/dist/server.d.ts +2 -2
  326. package/dist/server.js +131 -117
  327. package/dist/shared.d.ts +17 -17
  328. package/dist/shared.js +93 -97
  329. package/dist/skills.d.ts +3 -3
  330. package/dist/skills.js +8 -8
  331. package/dist/store/audit-event.d.ts +2 -2
  332. package/dist/store/audit-event.js +1 -1
  333. package/dist/store/candidates.d.ts +2 -2
  334. package/dist/store/candidates.js +4 -3
  335. package/dist/store/conflicts.js +30 -22
  336. package/dist/store/delete-and-batch.d.ts +11 -14
  337. package/dist/store/delete-and-batch.js +40 -91
  338. package/dist/store/entry-reads.d.ts +17 -25
  339. package/dist/store/entry-reads.js +59 -40
  340. package/dist/store/entry-row.d.ts +6 -24
  341. package/dist/store/entry-row.js +6 -24
  342. package/dist/store/entry-writes.d.ts +6 -7
  343. package/dist/store/entry-writes.js +13 -11
  344. package/dist/store/handoffs.d.ts +1 -1
  345. package/dist/store/handoffs.js +7 -10
  346. package/dist/store/index-and-stats.d.ts +2 -6
  347. package/dist/store/index-and-stats.js +4 -10
  348. package/dist/store/mirrors.d.ts +6 -19
  349. package/dist/store/mirrors.js +14 -39
  350. package/dist/store/open.js +9 -31
  351. package/dist/store/rows.d.ts +5 -11
  352. package/dist/store/rows.js +6 -11
  353. package/dist/store/search-rows.d.ts +17 -34
  354. package/dist/store/search-rows.js +31 -59
  355. package/dist/store/sessions.d.ts +4 -5
  356. package/dist/store/sessions.js +5 -6
  357. package/dist/store/summaries.d.ts +13 -17
  358. package/dist/store/summaries.js +26 -70
  359. package/dist/support-bundle.js +4 -8
  360. package/dist/tenant.d.ts +1 -5
  361. package/dist/token-ledger.d.ts +1 -1
  362. package/dist/token-ledger.js +3 -5
  363. package/dist/trace.js +1 -3
  364. package/dist/version.d.ts +1 -1
  365. package/dist/version.js +1 -1
  366. package/dist/working-memory.d.ts +1 -1
  367. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  368. package/extensions/openclaw-plugin/package.json +1 -1
  369. package/openclaw.plugin.json +1 -1
  370. 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
@@ -80,7 +80,7 @@ function buildDecisionMemory(hippoRoot, tenantId, opts) {
80
80
  // supersedesDecisionId (e.g. superseding id 1 on an empty store, where the
81
81
  // INSERT below would itself become id 1), making the row supersede itself.
82
82
  // Validating first means the new row is never a candidate for its own
83
- // supersede UPDATE. codex review 2026-05-28 (P1).
83
+ // supersede UPDATE.
84
84
  function preflightDecisionSupersede(db, tenantId, supersedesId) {
85
85
  // SAFETY: row shape matches the single `status` column named in the SELECT below.
86
86
  const pred = db.prepare(`SELECT status FROM decisions WHERE id = ? AND tenant_id = ?`).get(supersedesId, tenantId);
@@ -111,7 +111,7 @@ function supersedeDecisionRow(db, tenantId, actor, supersedesId, decisionId, now
111
111
  WHERE id = ? AND tenant_id = ? AND status = 'active' AND id != ?
112
112
  `).run(decisionId, now, supersedesId, tenantId, decisionId);
113
113
  if (sup.changes === 0) {
114
- throw new BadRequestError(`saveDecision: decision ${supersedesId} could not be superseded (no longer active or self-reference).`);
114
+ throw new ConflictError(`saveDecision: decision ${supersedesId} could not be superseded (no longer active or self-reference).`);
115
115
  }
116
116
  appendAuditEvent(db, {
117
117
  tenantId,
@@ -179,7 +179,7 @@ export function saveDecision(hippoRoot, tenantId, opts, actor = 'cli') {
179
179
  },
180
180
  // Post-commit hook: mark the tenant's graph dirty AFTER the DB row commits
181
181
  // but BEFORE the markdown mirrors are written, so a mirror-write failure can
182
- // never leave a committed save unflagged (codex). markGraphDirty is fail-soft.
182
+ // never leave a committed save unflagged. markGraphDirty is fail-soft.
183
183
  afterCommit: () => markGraphDirty(hippoRoot, tenantId, mem.id),
184
184
  });
185
185
  if (!savedRow) {
@@ -232,7 +232,7 @@ export function closeDecision(hippoRoot, tenantId, id, actor = 'cli') {
232
232
  // Closing removes the object from the graph. Remove its rows DIRECTLY (deterministic),
233
233
  // not only via an enqueued rebuild whose queue item is lost if the mirror is later
234
234
  // forgotten (the queue row cascade-deletes with the memory), which would leave the closed
235
- // 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
236
236
  // so a concurrent rebuild re-derives consistently (harmless if it also runs).
237
237
  removeGraphEntitiesForObject(hippoRoot, tenantId, 'decision', closed.id);
238
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
@@ -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);