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
@@ -72,9 +72,9 @@ async function dagBuildPass(run, { apiKey, llmError, llmOpts }) {
72
72
  }
73
73
  }
74
74
  // -------------------------------------------------------------------------
75
- // 1.8. DAG summary rebuild — drain dirty queue from E2's child-write hooks
75
+ // 1.8. DAG summary rebuild — drain dirty queue from the child-write hooks
76
76
  // -------------------------------------------------------------------------
77
- // Consumer of E2's summary_dirty flag. Walks dirty L2 summaries, regenerates
77
+ // Consumer of the summary_dirty flag. Walks dirty L2 summaries, regenerates
78
78
  // each via generateDagSummary, atomically refreshes content + 6 metadata
79
79
  // columns + clears summary_dirty (with FTS sync). Same apiKey/dryRun gate
80
80
  // as buildDag above. Cap HIPPO_DAG_REBUILD_CAP (default 20, hard ceiling
@@ -84,8 +84,7 @@ async function dagRebuildPass(run, { llmError, llmOpts }) {
84
84
  try {
85
85
  const { rebuildDirtySummaries } = await import('../dag.js');
86
86
  const rawCap = envDagRebuildCap();
87
- // R1 MED must-fix: hard ceiling so misconfigured env can't burn
88
- // unbounded LLM cost.
87
+ // Hard ceiling so a misconfigured env can't burn unbounded LLM cost.
89
88
  const cap = rawCap !== undefined ? Math.min(rawCap, 1000) : 20;
90
89
  const rebuildResult = await rebuildDirtySummaries(run.hippoRoot, { ...llmOpts, onError: llmError('dag rebuild'), cap });
91
90
  result.summariesRebuilt = rebuildResult.rebuilt;
@@ -115,7 +114,7 @@ async function dagRebuildPass(run, { llmError, llmOpts }) {
115
114
  // -------------------------------------------------------------------------
116
115
  // 1.9. DAG entity profiles — cluster L2 topic summaries into L3 profiles
117
116
  // -------------------------------------------------------------------------
118
- // E5 phase: aggregate per-entity L2 summaries (e.g. all the speaker:Alice
117
+ // Aggregate per-entity L2 summaries (e.g. all the speaker:Alice
119
118
  // topic summaries) into a single L3 entity profile. Runs even when phase
120
119
  // 1.7 buildDag was skipped (re-clusters existing L2s every sleep).
121
120
  //
@@ -54,13 +54,8 @@ export function mergePass(run) {
54
54
  && !e.pinned // a pin merged with a look-alike would read as one of two values
55
55
  && tokenize(e.content).length > 0);
56
56
  const used = new Set();
57
- // T1 fix (2026-08-15 hardening pass): partition by tenantId BEFORE the
58
- // overlap loop so a cluster can never span tenants. Previously textOverlap
59
- // clustered across the whole host-wide `survivors` list with no tenant
60
- // boundary, and mergeContents concatenated cross-tenant content into one
61
- // row. Map preserves insertion order, so single-tenant stores (every row
62
- // 'default') get exactly one partition and iterate in the same order as
63
- // before this fix — byte-identical behavior there.
57
+ // Partition BEFORE the overlap loop so a cluster can never span tenants and merge cross-tenant content.
58
+ // Map keeps insertion order, so a single-tenant store gets one partition in the original order.
64
59
  const mergeCandidatesByTenant = new Map();
65
60
  for (const entry of mergeCandidates) {
66
61
  const key = derivationPartitionKey(entry.tenantId, entry.scope, entry.origin_project);
@@ -70,12 +65,8 @@ export function mergePass(run) {
70
65
  else
71
66
  mergeCandidatesByTenant.set(key, [entry]);
72
67
  }
73
- // AT1 consolidation-loop fix (docs/plans/2026-08-15-at1-rejected-value-tombstone.md):
74
- // reuses the single consolidateDb handle opened lazily in consolidate()
75
- // for the whole non-dry-run consolidate — see that
76
- // declaration's comment. Only needed for real writes — a dry-run preview
77
- // never reaches batchWriteAndDelete's guard bypass, so there is nothing
78
- // here for it to protect against.
68
+ // The rejection guard reuses the one consolidateDb handle opened lazily in consolidate(); a dry-run
69
+ // never reaches batchWriteAndDelete's guard bypass, so it has nothing to protect there.
79
70
  let mergesSkippedRejected = 0;
80
71
  for (const [, tenantCandidates] of mergeCandidatesByTenant) {
81
72
  const partition = {
@@ -115,13 +106,8 @@ function mergeCluster(run, partition, cluster, related, used) {
115
106
  const mergedContent = mergeContents(cluster);
116
107
  const allTags = Array.from(new Set(cluster.flatMap((e) => e.tags))).sort();
117
108
  const maxValence = pickStrongestValence(cluster);
118
- // AT1 P2 fix: build the semantic entry FIRST — createMemory is cheap
119
- // and pure — so the tombstone check below runs under the tenant the
120
- // row will ACTUALLY land in.
121
- // T1 fix: createMemory now receives tenantId: mergeTenant (the
122
- // partition's tenant — every member of `cluster` shares it by
123
- // construction), so the row lands in its source tenant instead of
124
- // always 'default'.
109
+ // Build the semantic entry FIRST (createMemory is cheap and pure) so the tombstone check below runs
110
+ // under the partition's tenant, the one every cluster member shares and the row will land in.
125
111
  let semantic = null;
126
112
  if (!dryRun) {
127
113
  semantic = {
@@ -152,9 +138,8 @@ function mergeCluster(run, partition, cluster, related, used) {
152
138
  result.semanticCreated++;
153
139
  // Demote source episodics (they've been compressed into neocortex):
154
140
  // scale half_life_days so they decay sooner while staying recoverable.
155
- // Immediate ranking is deliberately unchanged: the 2026-06-10 DAG
156
- // slice-1 eval measured that dropping children below a worse-retrieving
157
- // summary regresses budget-bounded QA (docs/evals/). The stored
141
+ // Immediate ranking is deliberately unchanged: dropping children below a
142
+ // worse-retrieving summary regresses budget-bounded QA. The stored
158
143
  // strength is refreshed to the live value so inspect, replay sampling,
159
144
  // and strength-sorted assembly see the truth instead of a fake 0.3.
160
145
  // Mutate in place (not a copy): `cluster` holds the same object
@@ -15,7 +15,7 @@ export interface ConsolidationResult {
15
15
  semanticCreated: number;
16
16
  replayed: number;
17
17
  promotedTraces: number;
18
- /** T7: sessions skipped because their events span two derivation scopes. */
18
+ /** Sessions skipped because their events span two derivation scopes. */
19
19
  tracesSkippedMixedScope: number;
20
20
  extractionCandidates: number;
21
21
  extracted: number;
@@ -35,13 +35,6 @@ export interface ConsolidationResult {
35
35
  }
36
36
  export declare const REPLAY_COUNT_DEFAULT = 5;
37
37
  export declare function keptAsWritten(entry: MemoryEntry): boolean;
38
- /** JSON value shape for a session event's free-form metadata field, cast to
39
- * once at its `Record<string, unknown>` origin so it can be narrowed via
40
- * isJsonString below rather than left as unparsed `unknown`. */
41
- export type JsonValue = string | number | boolean | null | JsonValue[] | {
42
- [key: string]: JsonValue;
43
- };
44
- export declare function isJsonString(value: JsonValue): value is string;
45
38
  /** The sleep's one tombstone-check handle: opened on first use, never under dryRun, closed once. */
46
39
  interface LazyDb {
47
40
  get: () => DatabaseSyncLike | null;
@@ -4,31 +4,9 @@ export const REPLAY_COUNT_DEFAULT = 5;
4
4
  export function keptAsWritten(entry) {
5
5
  return entry.tags.some((tag) => NO_MERGE_TAGS.has(tag));
6
6
  }
7
- export function isJsonString(value) {
8
- return typeof value === 'string';
9
- }
10
- // AT1 rejection-guard db handle (docs/plans/2026-08-15-at1-rejected-value-tombstone.md):
11
- // covers BOTH the auto-promote pass (1.4) and the merge
12
- // pass (3) — both build deterministic content that
13
- // batchWriteAndDelete writes through the guard's bypass, so both need a
14
- // producer-side tombstone check before pushing to pendingWrites.
15
- //
16
- // T3 fix (2026-08-15 hardening pass, perf hygiene): memoized lazy getter,
17
- // not an eager open. The handle only serves these two tombstone checks —
18
- // a sleep with zero promotable sessions and zero merge clusters never
19
- // reaches either use site, so opening it unconditionally on every
20
- // non-dry-run sleep paid a db-open cost for nothing. dryRun still never
21
- // opens (getConsolidateDb short-circuits before touching the handle).
22
- // consolidateDbOpened (not just a truthy handle check) is the
23
- // single source of truth for "was this ever opened", so the finally
24
- // closes it exactly once and never double-opens.
25
- //
26
- // AT1 P2 fix (codex, handle-leak restructure): the getter's lifetime must
27
- // start IMMEDIATELY before the try whose finally closes it, covering every
28
- // phase that can touch it — not just the merge pass. An exception thrown by
29
- // auto-promote (1.4), replay (1.5), batch extraction (1.6), the DAG
30
- // passes (1.7-1.9), or physics (2) would otherwise propagate past an open handle
31
- // with nothing to close it.
7
+ // Auto-promote (1.4) and merge (3) write deterministic content through the guard's bypass, so both
8
+ // need this tombstone check. Lazy so a sleep reaching neither never opens it; call it IMMEDIATELY
9
+ // before the try whose finally closes it, so a throw in any phase cannot leak the handle.
32
10
  export function lazyConsolidateDb(hippoRoot, dryRun) {
33
11
  let consolidateDbHandle = null;
34
12
  let consolidateDbOpened = false;
@@ -33,10 +33,8 @@ export async function consolidate(hippoRoot, options = {}) {
33
33
  const dryRun = options.dryRun ?? false;
34
34
  const result = newConsolidationResult(dryRun);
35
35
  const halfLife = migrateHalfLives(hippoRoot, dryRun, result);
36
- // L9: host-wide by design. Consolidation runs across all tenants in one
37
- // pass — per-tenant filtering would create N consolidation runs per host
38
- // with no cross-tenant dedup. The api.sleep audit row tags this with the
39
- // admin synthetic actor; see api.ts:2050 for the rationale.
36
+ // Host-wide by design: per-tenant filtering would mean N runs per host and no cross-tenant dedup.
37
+ // The api.sleep audit row tags this with the admin synthetic actor.
40
38
  const all = loadAllEntries(hippoRoot);
41
39
  if (dryRun)
42
40
  for (const e of all)
@@ -153,24 +151,14 @@ function logRun(run, decay) {
153
151
  if (decay.rescuedEntries.length > 0)
154
152
  auditRescues(run, decay);
155
153
  }
156
- // One audit row per rescue (attributability, D1). Written here, AFTER
157
- // batchWriteAndDelete has committed this cycle's writes/deletes
158
- // (and after conflict detection + run logging), not inline in the decay
159
- // pass — same durability posture as api.ts's top-level 'consolidate'
160
- // summary audit row (written only once the whole sleep has completed).
161
- // Writing it earlier would assert rescues for a cycle whose effects
162
- // never landed if a later phase threw. Real writes only — dry-run
163
- // previews the decision (details line above) but persists nothing.
154
+ // One audit row per rescue, written only after batchWriteAndDelete commits: writing earlier
155
+ // would assert rescues for a cycle whose effects never landed if a later phase threw.
164
156
  function auditRescues(run, { rescuedEntries, rankById }) {
165
157
  const { result } = run;
166
158
  try {
167
159
  const auditDb = openHippoDb(run.hippoRoot);
168
160
  try {
169
- // Review-round F5: per-row try/catch, not one try/catch around the
170
- // whole loop — a single failed appendAuditEvent must not silently
171
- // drop every remaining row. Mirrors the physics pass's
172
- // skipped-warning precedent: count losses, keep
173
- // the overall fail-soft posture, tell the operator via details.
161
+ // Per-row try/catch: one failed appendAuditEvent must not silently drop every remaining row.
174
162
  let auditFailures = 0;
175
163
  for (const entry of rescuedEntries) {
176
164
  try {
@@ -8,7 +8,8 @@ import { resolveTenantId } from '../tenant.js';
8
8
  import { appendAuditEvent, reportAuditWriteFailure } from '../audit.js';
9
9
  import { commonDerivationScope } from '../recall-scope.js';
10
10
  import { log } from '../log.js';
11
- import { REPLAY_COUNT_DEFAULT, isJsonString } from './run.js';
11
+ import { REPLAY_COUNT_DEFAULT } from './run.js';
12
+ import { isJsonString } from '../json.js';
12
13
  // -------------------------------------------------------------------------
13
14
  // 1.4. Auto-promote complete sessions to traces
14
15
  // -------------------------------------------------------------------------
@@ -24,10 +25,8 @@ export function promoteSessionTraces(run) {
24
25
  let tracesSkippedRejected = 0;
25
26
  const windowDays = run.config.autoTraceWindowDays ?? 7;
26
27
  const sinceMs = run.now.getTime() - windowDays * 24 * 60 * 60 * 1000;
27
- // Auto-trace currently runs in a single-tenant context (the env-resolved
28
- // tenant for this process). Multi-tenant deployments that want
29
- // consolidation across all tenants need a per-tenant loop layered on top
30
- // of this — tracked in docs/plans/2026-05-02-continuity-tables-tenant-scope.md.
28
+ // Auto-trace runs single-tenant (the env-resolved tenant); consolidating
29
+ // every tenant needs a per-tenant loop layered on top of this.
31
30
  const consolidationTenant = resolveTenantId({});
32
31
  const promotable = findPromotableSessions(run.hippoRoot, consolidationTenant, sinceMs);
33
32
  for (const session of promotable) {
@@ -60,7 +59,7 @@ function sessionTrace(run, consolidationTenant, sessionId) {
60
59
  session_id: sessionId,
61
60
  limit: 1000,
62
61
  });
63
- // T7: a mixed-scope session would otherwise leak into one trace.
62
+ // A mixed-scope session would otherwise leak into one trace.
64
63
  const sessionScope = commonDerivationScope(events.map((e) => e.scope));
65
64
  if (!sessionScope.ok) {
66
65
  run.result.tracesSkippedMixedScope++;
@@ -91,26 +90,15 @@ function sessionTrace(run, consolidationTenant, sessionId) {
91
90
  tags: ['auto-promoted'],
92
91
  source: 'auto-promote',
93
92
  scope: sessionScope.scope,
94
- // T1 fix (2026-08-15 hardening pass): stamp the trace into the SAME
95
- // tenant the traceExistsForSession idempotency check (above) runs
96
- // under. Before
97
- // this, createMemory omitted tenantId and the trace always landed
98
- // 'default' (memory.ts:535) while the idempotency check ran under
99
- // consolidationTenant — for any non-default tenant that check never
100
- // hit, and the trace regenerated every sleep.
93
+ // Same tenant as the traceExistsForSession check above, or a
94
+ // non-default tenant's trace regenerates every sleep.
101
95
  tenantId: consolidationTenant,
102
96
  baseHalfLifeDays: run.config.defaultHalfLifeDays,
103
97
  });
104
98
  return { trace, outcome };
105
99
  }
106
- // AT1 (same producer-side pattern as the merge pass below): traceExistsForSession
107
- // only sees rows CURRENTLY in the store — once a rejected trace is
108
- // removed, that idempotency check no longer blocks regeneration, and
109
- // this write would otherwise reach batchWriteAndDelete's guard bypass
110
- // unchecked, resurrecting it every sleep. Check under THE ENTRY'S OWN
111
- // stamped tenantId (read off `trace` after createMemory — never guess
112
- // the tenant) + the built content's digest. A hit skips the push
113
- // entirely: not counted as promoted, not added to survivors.
100
+ // traceExistsForSession only sees live rows, so a removed rejected trace would regenerate every
101
+ // sleep; check the tombstone under the entry's own stamped tenantId, never a guessed one.
114
102
  function traceRejected(run, trace, sessionId) {
115
103
  const consolidateDb = run.getConsolidateDb();
116
104
  if (!consolidateDb)
@@ -1,17 +1,16 @@
1
1
  /**
2
- * E2 customer_note first-class object - the LAST E2 object
3
- * (docs/plans/2026-06-01-e2-customer-note-object.md).
2
+ * customer_note first-class object.
4
3
  *
5
4
  * A `customer_note` is a discrete note recorded against an account/customer entity:
6
5
  * a `note` body scoped to a `customer`, evolving via the supersede delta lifecycle.
7
- * Entity-scoping is a free-form `customer` column (the `entities` table is unbuilt -
8
- * E3.1 planned - so an FK is deferred). Unlike project_brief's one-summary-per-repo,
6
+ * Entity-scoping is a free-form `customer` column (the `entities` table is unbuilt,
7
+ * so an FK is deferred). Unlike project_brief's one-summary-per-repo,
9
8
  * a customer accrues MANY discrete notes over time, each with its own supersede chain
10
9
  * (correct a note -> a new version preserving history; close retires it).
11
10
  *
12
11
  * Reuses the project_brief/skill supersede machinery verbatim (superseded_by self-FK
13
12
  * + CAS + INSERT-preflight + server-derived version + change_summary + supersede
14
- * tenant-match trigger). It has NO assembler/renderer (the simplest E2 object): the
13
+ * tenant-match trigger). It has NO assembler/renderer (the simplest first-class object): the
15
14
  * contribution is purely the entity-scoping dimension.
16
15
  *
17
16
  * The `customer_notes` table is the source of truth (survives memory decay); the
@@ -73,8 +72,7 @@ export interface ListCustomerNotesOpts {
73
72
  * the new version = predecessor.version + 1 (server-derived).
74
73
  *
75
74
  * The memory mirror carries a `customer:<lc>` tag (in addition to ['customer_note']
76
- * + caller extraTags) so scope-aware recall treats the note as entity-local - the
77
- * project_brief codex-P2 recall-locality lesson applied to entity scoping. There is
75
+ * + caller extraTags) so scope-aware recall treats the note as entity-local. There is
78
76
  * no self-recursion path (customer_note has no receipt-query/refresh).
79
77
  */
80
78
  export declare function saveCustomerNote(hippoRoot: string, tenantId: string, opts: SaveCustomerNoteOpts, actor?: string): CustomerNote;
@@ -1,17 +1,16 @@
1
1
  /**
2
- * E2 customer_note first-class object - the LAST E2 object
3
- * (docs/plans/2026-06-01-e2-customer-note-object.md).
2
+ * customer_note first-class object.
4
3
  *
5
4
  * A `customer_note` is a discrete note recorded against an account/customer entity:
6
5
  * a `note` body scoped to a `customer`, evolving via the supersede delta lifecycle.
7
- * Entity-scoping is a free-form `customer` column (the `entities` table is unbuilt -
8
- * E3.1 planned - so an FK is deferred). Unlike project_brief's one-summary-per-repo,
6
+ * Entity-scoping is a free-form `customer` column (the `entities` table is unbuilt,
7
+ * so an FK is deferred). Unlike project_brief's one-summary-per-repo,
9
8
  * a customer accrues MANY discrete notes over time, each with its own supersede chain
10
9
  * (correct a note -> a new version preserving history; close retires it).
11
10
  *
12
11
  * Reuses the project_brief/skill supersede machinery verbatim (superseded_by self-FK
13
12
  * + CAS + INSERT-preflight + server-derived version + change_summary + supersede
14
- * tenant-match trigger). It has NO assembler/renderer (the simplest E2 object): the
13
+ * tenant-match trigger). It has NO assembler/renderer (the simplest first-class object): the
15
14
  * contribution is purely the entity-scoping dimension.
16
15
  *
17
16
  * The `customer_notes` table is the source of truth (survives memory decay); the
@@ -97,7 +96,6 @@ function buildNoteContent(customer, note) {
97
96
  // Preflight the supersede target BEFORE inserting the new row (so the new
98
97
  // autoincrement id can never be its own supersede target); read the
99
98
  // predecessor version in the same SELECT for server-derived versioning.
100
- // Mirrors saveProjectBrief / saveSkill (codex P1 2026-05-28).
101
99
  function preflightNoteSupersede(db, tenantId, supersedesId) {
102
100
  // SAFETY: SELECT projects exactly status, version; .get() returns that
103
101
  // shape for the matching row, or undefined when no note/tenant pair matches.
@@ -175,8 +173,7 @@ function writeNoteRow(db, memoryId, w) {
175
173
  * the new version = predecessor.version + 1 (server-derived).
176
174
  *
177
175
  * The memory mirror carries a `customer:<lc>` tag (in addition to ['customer_note']
178
- * + caller extraTags) so scope-aware recall treats the note as entity-local - the
179
- * project_brief codex-P2 recall-locality lesson applied to entity scoping. There is
176
+ * + caller extraTags) so scope-aware recall treats the note as entity-local. There is
180
177
  * no self-recursion path (customer_note has no receipt-query/refresh).
181
178
  */
182
179
  export function saveCustomerNote(hippoRoot, tenantId, opts, actor = 'cli') {
@@ -255,7 +252,7 @@ export function closeCustomerNote(hippoRoot, tenantId, id, actor = 'cli') {
255
252
  // Closing removes the object from the graph. Remove its rows DIRECTLY (deterministic),
256
253
  // not only via an enqueued rebuild whose queue item is lost if the mirror is later
257
254
  // forgotten (the queue row cascade-deletes with the memory), which would leave the closed
258
- // object stale and could block that forget (codex P1). Still enqueue when a mirror exists
255
+ // object stale and could block that forget. Still enqueue when a mirror exists
259
256
  // so a concurrent rebuild re-derives consistently (harmless if it also runs).
260
257
  removeGraphEntitiesForObject(hippoRoot, tenantId, 'customer', closed.id);
261
258
  if (closed.memoryId) {
package/dist/dag.d.ts CHANGED
@@ -16,11 +16,8 @@ export interface DagBuildResult {
16
16
  candidateClusters: number;
17
17
  summariesCreated: number;
18
18
  factsLinked: number;
19
- /** AT1: clusters skipped because the LLM-synthesized summary landed on a
20
- * rejected value (plan §3 containment — per-cluster catch, not a whole-
21
- * phase abort). Member re-parenting writes are unaffected by construction
22
- * (same id + same content = guard-exempt), so this only ever counts
23
- * summary-creation refusals. */
19
+ /** Clusters skipped because the summary matched a rejected value. Re-parenting writes are
20
+ * guard-exempt (same id + content), so only summary-creation refusals count. */
24
21
  rejected: number;
25
22
  }
26
23
  export declare function buildDag(hippoRoot: string, facts: MemoryEntry[], opts: DagSummaryOptions): Promise<DagBuildResult>;
@@ -33,21 +30,17 @@ export interface DagRebuildResult {
33
30
  capped: boolean;
34
31
  }
35
32
  /**
36
- * v0.30 / E3 — sleep-cycle phase that drains the dirty L2 summary queue.
33
+ * Sleep-cycle phase that drains the dirty L2 summary queue.
37
34
  * Thin orchestrator; the heavy lifting lives in store.ts (load + apply)
38
35
  * and dag.ts:generateDagSummary (LLM call).
39
36
  *
40
- * Per-summary try/catch isolation (plan-eng-r1 MED must-fix) — one
41
- * throwing rebuild does NOT abort the rest of the queue.
37
+ * Per-summary try/catch: one throwing rebuild does NOT abort the rest of the queue.
42
38
  *
43
39
  * Race-loser handling: applyRebuildResult's UPDATE WHERE includes
44
40
  * AND summary_dirty=1, so concurrent sleep's second writer returns
45
41
  * changed=false. Silent skip (neither rebuilt++ nor refused++ nor failed++).
46
42
  *
47
- * T4: applyRebuildResult returns { changed, refused } — a tombstone hit
48
- * (rebuild content matches a previously-rejected value) increments
49
- * `refused`, not `rebuilt`. Dirty still clears either way; only the stat
50
- * split changed (docs/plans/2026-08-15-hardening-at1-followups.md T4).
43
+ * A tombstone hit counts as `refused`, not `rebuilt`; dirty clears either way.
51
44
  */
52
45
  export declare function rebuildDirtySummaries(hippoRoot: string, opts: DagSummaryOptions & {
53
46
  cap?: number;
@@ -57,21 +50,17 @@ export interface EntityProfilesBuildResult {
57
50
  profilesCreated: number;
58
51
  l2sLinked: number;
59
52
  failed: number;
60
- /** AT1: clusters skipped because the profile summary landed on a rejected
61
- * value (plan §3 containment). Kept distinct from `failed` (LLM null /
62
- * rate-limit) — a tombstone hit is a deliberate refusal, not an error. */
53
+ /** Clusters skipped because the profile matched a rejected value. Kept apart from
54
+ * `failed`: a tombstone hit is a deliberate refusal, not an error. */
63
55
  rejected: number;
64
56
  }
65
57
  /**
66
- * v0.30 / E5 — build L3 entity profiles by clustering L2 summaries with
58
+ * Build L3 entity profiles by clustering L2 summaries with
67
59
  * shared entity tags. Threshold 2+ L2s per entity. Mirrors buildDag L1->L2
68
60
  * pattern, one level up.
69
61
  *
70
- * Born-dirty cancellation (E3 lesson): after linking L2 children to the new
71
- * L3 (each link write fires E2 hook on L3 via widened markSummaryDirtyInTx),
72
- * call clearSummaryDirtyAfterBuild with source='buildEntityProfiles-clean'
73
- * so E3 sleep-cycle rebuild doesn't re-rebuild the freshly-built L3 this
74
- * same cycle.
62
+ * Born-dirty cancellation: each L2 link write marks the new L3 dirty, so clear it
63
+ * or the same sleep cycle's rebuild re-rebuilds the freshly built L3.
75
64
  */
76
65
  export declare function buildEntityProfiles(hippoRoot: string, l2Summaries: MemoryEntry[], opts: DagSummaryOptions): Promise<EntityProfilesBuildResult>;
77
66
  //# sourceMappingURL=dag.d.ts.map
package/dist/dag.js CHANGED
@@ -87,13 +87,9 @@ export async function generateDagSummary(label, factContents, opts) {
87
87
  return null;
88
88
  }
89
89
  }
90
- // Hardening follow-up (mirrors consolidate.ts's mergeCandidatesByTenant,
91
- // T1): partition unparented facts by tenantId BEFORE clustering so a
92
- // cluster can never mix facts from different tenants into one
93
- // LLM-synthesized summary. Map preserves insertion order, so
94
- // single-tenant stores (every row 'default') get exactly one partition
95
- // and iterate in the same order as before this fix — byte-identical
96
- // behavior there.
90
+ // Partition BEFORE clustering so one LLM summary never mixes facts from different tenants.
91
+ // Map keeps insertion order, so a single-tenant store iterates in its original order.
92
+ // buildEntityProfiles partitions its L2s with this same key.
97
93
  function partitionFactsByTenant(unparented) {
98
94
  const unparentedByTenant = new Map();
99
95
  for (const fact of unparented) {
@@ -123,9 +119,8 @@ function createClusterSummaryEntry(summary, cluster, home) {
123
119
  baseHalfLifeDays: home.baseHalfLifeDays,
124
120
  });
125
121
  summaryEntry.origin_project = home.originProject;
126
- // Schema v25: cache descendant_count + earliest/latest_at on the summary
127
- // row so DAG-aware recall (docs/plans/2026-05-05-dag-recall.md Task 2)
128
- // can reason about scope without walking the children.
122
+ // Cache descendant_count + earliest/latest_at on the summary row so
123
+ // DAG-aware recall can reason about scope without walking the children.
129
124
  summaryEntry.descendant_count = cluster.members.length;
130
125
  summaryEntry.earliest_at = memberCreatedAts[0];
131
126
  summaryEntry.latest_at = memberCreatedAts[memberCreatedAts.length - 1];
@@ -137,21 +132,8 @@ async function summarizeCluster(hippoRoot, cluster, home, opts, result) {
137
132
  if (!summary)
138
133
  return;
139
134
  const summaryEntry = createClusterSummaryEntry(summary, cluster, home);
140
- // AT1 (plan §3 containment): a refused LLM-synthesized summary skips
141
- // ONLY this cluster — the sleep cycle continues to the next one. The
142
- // member re-parenting writes below never run for a skipped cluster
143
- // (there is no summary id to parent them under).
144
- //
145
- // The tombstone check itself is tenant-scoped for free: writeEntry ->
146
- // writeEntryDbOnly -> upsertEntryRow calls
147
- // checkRejectionGuard(db, entry.tenantId ?? 'default', ...)
148
- // (store/entry-writes.ts), reading tenantId off the entry being written. Now
149
- // that summaryEntry carries home.tenantId instead of the implicit
150
- // 'default', the guard consults that tenant's tombstones — no
151
- // separate check needed here (unlike consolidate.ts's merge pass,
152
- // which pre-checks via findRejectedValue because it writes through
153
- // batchWriteAndDelete's bypassRejectionGuard path instead of
154
- // writeEntry).
135
+ // A refused summary skips only this cluster. writeEntry's guard reads tenantId off the entry
136
+ // (home.tenantId), so it already checks this tenant's tombstones: no separate check here.
155
137
  try {
156
138
  writeEntry(hippoRoot, summaryEntry);
157
139
  }
@@ -169,11 +151,8 @@ async function summarizeCluster(hippoRoot, cluster, home, opts, result) {
169
151
  writeEntry(hippoRoot, updated);
170
152
  result.factsLinked++;
171
153
  }
172
- // v0.30 / E3 — cancel the cascade of dirty-marks fired by member
173
- // writeEntry calls (E2 hook on writeEntryDbOnly in store/entry-writes.ts).
174
- // The summary we just built IS fresh, no rebuild needed. Without this,
175
- // E3 in the SAME sleep cycle would re-rebuild every new summary
176
- // (2x LLM cost). plan-eng-r1 HIGH must-fix.
154
+ // Member writes just marked this fresh summary dirty; clear it, or the same sleep cycle's
155
+ // rebuild pass would re-rebuild every new summary at twice the LLM cost.
177
156
  clearSummaryDirtyAfterBuild(hippoRoot, summaryEntry.id, summaryEntry.tenantId, 'buildDag');
178
157
  }
179
158
  export async function buildDag(hippoRoot, facts, opts) {
@@ -248,21 +227,17 @@ async function rebuildOneSummary(hippoRoot, summary, opts, result) {
248
227
  // success, silently skip
249
228
  }
250
229
  /**
251
- * v0.30 / E3 — sleep-cycle phase that drains the dirty L2 summary queue.
230
+ * Sleep-cycle phase that drains the dirty L2 summary queue.
252
231
  * Thin orchestrator; the heavy lifting lives in store.ts (load + apply)
253
232
  * and dag.ts:generateDagSummary (LLM call).
254
233
  *
255
- * Per-summary try/catch isolation (plan-eng-r1 MED must-fix) — one
256
- * throwing rebuild does NOT abort the rest of the queue.
234
+ * Per-summary try/catch: one throwing rebuild does NOT abort the rest of the queue.
257
235
  *
258
236
  * Race-loser handling: applyRebuildResult's UPDATE WHERE includes
259
237
  * AND summary_dirty=1, so concurrent sleep's second writer returns
260
238
  * changed=false. Silent skip (neither rebuilt++ nor refused++ nor failed++).
261
239
  *
262
- * T4: applyRebuildResult returns { changed, refused } — a tombstone hit
263
- * (rebuild content matches a previously-rejected value) increments
264
- * `refused`, not `rebuilt`. Dirty still clears either way; only the stat
265
- * split changed (docs/plans/2026-08-15-hardening-at1-followups.md T4).
240
+ * A tombstone hit counts as `refused`, not `rebuilt`; dirty clears either way.
266
241
  */
267
242
  export async function rebuildDirtySummaries(hippoRoot, opts) {
268
243
  const cap = opts.cap ?? 20;
@@ -291,33 +266,14 @@ export async function rebuildDirtySummaries(hippoRoot, opts) {
291
266
  await rebuildOneSummary(hippoRoot, summary, opts, result);
292
267
  }
293
268
  catch (err) {
294
- // Per-summary failure isolation — one throw doesn't abort the queue.
295
- // independent-review MED #2 fold: log enough to triage in production
296
- // (audit() wraps its own writes try/catch per store/audit-event.ts, so a
297
- // throw here is exotic: SQLite I/O error, prepare failure, etc).
269
+ // One throw doesn't abort the queue; log enough to triage an exotic
270
+ // failure (SQLite I/O, prepare), since audit() already catches its own.
298
271
  result.failed++;
299
272
  log.error(`rebuildDirtySummaries: summary ${summary.id} (tenant ${summary.tenantId}) failed: ${err instanceof Error ? err.message : String(err)}`);
300
273
  }
301
274
  }
302
275
  return result;
303
276
  }
304
- // independent-review HIGH #1 fold: cluster ONLY within-tenant.
305
- // clusterFacts has no tenant awareness; without this partition step a
306
- // multi-tenant host could form a cluster spanning tenants and produce
307
- // a single L3 with tenantId='default' that doesn't belong to either
308
- // child tenant. Fix: bucket by tenantId, run clusterFacts per-tenant,
309
- // pass tenantId to createMemory.
310
- function partitionL2sByTenant(unparented) {
311
- const byTenant = new Map();
312
- for (const l2 of unparented) {
313
- const tid = l2.tenantId ?? 'default';
314
- const key = derivationPartitionKey(tid, l2.scope, l2.origin_project);
315
- const list = byTenant.get(key) ?? [];
316
- list.push(l2);
317
- byTenant.set(key, list);
318
- }
319
- return byTenant;
320
- }
321
277
  /** The L3 profile entry for one cluster of L2 summaries. */
322
278
  function createProfileEntry(summary, cluster, home) {
323
279
  const memberCreatedAts = cluster.members.map((m) => m.created).sort();
@@ -327,7 +283,7 @@ function createProfileEntry(summary, cluster, home) {
327
283
  tags: [...cluster.entityTags, ...neverAutoShareTags(cluster.members), 'dag-entity-profile'],
328
284
  confidence: 'inferred',
329
285
  dag_level: 3,
330
- tenantId: home.tenantId, // HIGH #1 fold: thread tenant explicitly
286
+ tenantId: home.tenantId,
331
287
  scope: home.scope,
332
288
  baseHalfLifeDays: home.baseHalfLifeDays,
333
289
  });
@@ -346,9 +302,7 @@ async function profileCluster(hippoRoot, cluster, home, opts, result) {
346
302
  return;
347
303
  }
348
304
  const profileEntry = createProfileEntry(summary, cluster, home);
349
- // AT1 (plan §3 containment): per-cluster catch — skip this cluster,
350
- // count, log once. The member re-linking writes below never run for a
351
- // skipped cluster (mirrors buildDag above).
305
+ // A refused profile skips only this cluster; the re-linking below never runs for it.
352
306
  try {
353
307
  writeEntry(hippoRoot, profileEntry);
354
308
  }
@@ -366,21 +320,17 @@ async function profileCluster(hippoRoot, cluster, home, opts, result) {
366
320
  writeEntry(hippoRoot, updated);
367
321
  result.l2sLinked++;
368
322
  }
369
- // E3 born-dirty cancellation, same dance as buildDag L161-168 but for
370
- // L3. Pass source='buildEntityProfiles-clean' to distinguish in audit.
371
- // Args: (root, id, tenantId, actor, source).
323
+ // Re-linking just marked the fresh L3 dirty; clear it so this cycle's rebuild skips it.
324
+ // The distinct source tags the audit row.
372
325
  clearSummaryDirtyAfterBuild(hippoRoot, profileEntry.id, home.tenantId, 'buildEntityProfiles', 'buildEntityProfiles-clean');
373
326
  }
374
327
  /**
375
- * v0.30 / E5 — build L3 entity profiles by clustering L2 summaries with
328
+ * Build L3 entity profiles by clustering L2 summaries with
376
329
  * shared entity tags. Threshold 2+ L2s per entity. Mirrors buildDag L1->L2
377
330
  * pattern, one level up.
378
331
  *
379
- * Born-dirty cancellation (E3 lesson): after linking L2 children to the new
380
- * L3 (each link write fires E2 hook on L3 via widened markSummaryDirtyInTx),
381
- * call clearSummaryDirtyAfterBuild with source='buildEntityProfiles-clean'
382
- * so E3 sleep-cycle rebuild doesn't re-rebuild the freshly-built L3 this
383
- * same cycle.
332
+ * Born-dirty cancellation: each L2 link write marks the new L3 dirty, so clear it
333
+ * or the same sleep cycle's rebuild re-rebuilds the freshly built L3.
384
334
  */
385
335
  export async function buildEntityProfiles(hippoRoot, l2Summaries, opts) {
386
336
  const result = {
@@ -393,9 +343,9 @@ export async function buildEntityProfiles(hippoRoot, l2Summaries, opts) {
393
343
  const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
394
344
  // Only L2 with no L3 parent yet (avoid re-clustering already-profiled L2s).
395
345
  const unparented = l2Summaries.filter((s) => s.dag_level === 2 && !s.dag_parent_id);
396
- for (const [, tenantL2s] of partitionL2sByTenant(unparented)) {
346
+ for (const [, tenantL2s] of partitionFactsByTenant(unparented)) {
397
347
  const home = {
398
- tenantId: tenantL2s[0].tenantId ?? 'default',
348
+ tenantId: tenantL2s[0].tenantId,
399
349
  scope: derivationScope(tenantL2s[0].scope),
400
350
  originProject: tenantL2s[0].origin_project,
401
351
  baseHalfLifeDays,
@@ -112,8 +112,8 @@ const CARD_COMMENTS_DDL = `
112
112
  tenant_id TEXT NOT NULL DEFAULT 'default'
113
113
  )
114
114
  `;
115
- // Before the loop on stamped stores: a table lost after its migration stamped (2026-08-15
116
- // incident) is never re-migrated, and v4/v16/v22 ALTER or read it. Fresh stores use the chain.
115
+ // Before the loop on stamped stores: a table lost after its migration stamped
116
+ // is never re-migrated, and v4/v16/v22 ALTER or read it. Fresh stores use the chain.
117
117
  export function ensureContinuityTables(db) {
118
118
  db.exec(TASK_SNAPSHOTS_DDL);
119
119
  db.exec(SESSION_EVENTS_DDL);
@@ -2,7 +2,7 @@ import { tableHasColumn } from '../tables.js';
2
2
  export const v14 = {
3
3
  version: 14,
4
4
  up: (db) => {
5
- // A3 provenance envelope: kind, scope, owner, artifact_ref.
5
+ // Provenance envelope: kind, scope, owner, artifact_ref.
6
6
  // SQLite ALTER TABLE ADD COLUMN cannot add CHECK; CHECK enforcement lives
7
7
  // in INSERT/UPDATE triggers added later in this migration.
8
8
  if (!tableHasColumn(db, 'memories', 'kind')) {
@@ -1,8 +1,7 @@
1
1
  export const v15 = {
2
2
  version: 15,
3
3
  up: (db) => {
4
- // A3 hardening (post-review): close the NULL-kind bypass and add raw_archive
5
- // dedup safety. Both findings landed in /review on commits 41b1f4d..6456e7d.
4
+ // Close the NULL-kind bypass and add raw_archive dedup safety.
6
5
  //
7
6
  // (1) Original v14 triggers used `WHEN NEW.kind IS NOT NULL AND NEW.kind NOT IN (...)`.
8
7
  // A direct INSERT/UPDATE setting kind=NULL bypassed the CHECK substitute. Replace