hippo-memory 1.61.0 → 1.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (377) hide show
  1. package/README.md +37 -53
  2. package/dist/agent-memories/apply.d.ts +3 -1
  3. package/dist/agent-memories/apply.js +23 -1
  4. package/dist/agent-memories/claude-code.d.ts +4 -1
  5. package/dist/agent-memories/claude-code.js +53 -9
  6. package/dist/agent-memories/report.d.ts +1 -0
  7. package/dist/agent-memories/report.js +2 -2
  8. package/dist/agent-memories/sync.d.ts +3 -3
  9. package/dist/agent-memories/sync.js +46 -24
  10. package/dist/agent-memories/types.d.ts +0 -2
  11. package/dist/ambient-store.d.ts +4 -4
  12. package/dist/ambient-store.js +4 -3
  13. package/dist/api/assemble.d.ts +7 -10
  14. package/dist/api/assemble.js +62 -66
  15. package/dist/api/audit.d.ts +2 -2
  16. package/dist/api/audit.js +2 -2
  17. package/dist/api/auth.d.ts +6 -6
  18. package/dist/api/auth.js +5 -7
  19. package/dist/api/context-select.d.ts +50 -0
  20. package/dist/api/context-select.js +342 -0
  21. package/dist/api/context-types.d.ts +12 -17
  22. package/dist/api/context.d.ts +5 -4
  23. package/dist/api/context.js +208 -535
  24. package/dist/api/dormant.js +1 -1
  25. package/dist/api/drill-down.d.ts +4 -4
  26. package/dist/api/drill-down.js +56 -41
  27. package/dist/api/outcome.d.ts +8 -13
  28. package/dist/api/outcome.js +13 -17
  29. package/dist/api/promote.d.ts +5 -8
  30. package/dist/api/promote.js +55 -66
  31. package/dist/api/quarantine.js +3 -2
  32. package/dist/api/recall-types.d.ts +43 -55
  33. package/dist/api/recall.d.ts +3 -3
  34. package/dist/api/recall.js +310 -456
  35. package/dist/api/remember.d.ts +2 -2
  36. package/dist/api/sleep.d.ts +7 -35
  37. package/dist/api/sleep.js +206 -220
  38. package/dist/api/tokens.d.ts +2 -2
  39. package/dist/api/tokens.js +2 -2
  40. package/dist/api/types.d.ts +6 -10
  41. package/dist/api/types.js +2 -4
  42. package/dist/audit-prune.d.ts +4 -6
  43. package/dist/audit-prune.js +3 -5
  44. package/dist/audit.js +11 -33
  45. package/dist/auth.d.ts +8 -9
  46. package/dist/auth.js +5 -7
  47. package/dist/autolearn.d.ts +1 -1
  48. package/dist/autolearn.js +1 -1
  49. package/dist/availability.js +3 -5
  50. package/dist/capture/command.d.ts +5 -4
  51. package/dist/capture/command.js +8 -22
  52. package/dist/capture/compact.d.ts +2 -2
  53. package/dist/capture/compact.js +11 -15
  54. package/dist/capture/extract.js +37 -115
  55. package/dist/capture-error.d.ts +1 -1
  56. package/dist/capture-error.js +1 -1
  57. package/dist/churn-git.d.ts +1 -1
  58. package/dist/churn-git.js +1 -1
  59. package/dist/cli/audit.js +3 -4
  60. package/dist/cli/auth.js +3 -6
  61. package/dist/cli/briefs.js +324 -306
  62. package/dist/cli/context.js +44 -34
  63. package/dist/cli/continuity.js +283 -271
  64. package/dist/cli/curate.d.ts +1 -1
  65. package/dist/cli/curate.js +43 -58
  66. package/dist/cli/dag.js +5 -9
  67. package/dist/cli/decisions.js +334 -345
  68. package/dist/cli/explain.js +68 -61
  69. package/dist/cli/goals.js +1 -1
  70. package/dist/cli/init.js +1 -1
  71. package/dist/cli/maintenance.js +62 -51
  72. package/dist/cli/playbooks.js +391 -379
  73. package/dist/cli/projects.js +11 -6
  74. package/dist/cli/recall.js +30 -44
  75. package/dist/cli/remember.js +118 -87
  76. package/dist/cli/session-hooks.js +106 -115
  77. package/dist/cli/setup.d.ts +1 -1
  78. package/dist/cli/setup.js +267 -250
  79. package/dist/cli/shared.js +3 -3
  80. package/dist/cli/slack.js +1 -1
  81. package/dist/cli/sleep.js +27 -1
  82. package/dist/cli/status.d.ts +4 -4
  83. package/dist/cli/status.js +80 -76
  84. package/dist/cli/transfer.js +88 -107
  85. package/dist/cli/usage.js +9 -6
  86. package/dist/cli.d.ts +1 -1
  87. package/dist/cli.js +4 -9
  88. package/dist/compaction-record.d.ts +2 -2
  89. package/dist/compaction-record.js +89 -68
  90. package/dist/compare.d.ts +11 -16
  91. package/dist/compare.js +11 -16
  92. package/dist/config.d.ts +19 -18
  93. package/dist/config.js +90 -67
  94. package/dist/connectors/github/backfill.d.ts +2 -2
  95. package/dist/connectors/github/backfill.js +8 -15
  96. package/dist/connectors/github/cli-impl.js +3 -8
  97. package/dist/connectors/github/deletion.d.ts +5 -12
  98. package/dist/connectors/github/deletion.js +5 -12
  99. package/dist/connectors/github/dlq.d.ts +6 -9
  100. package/dist/connectors/github/dlq.js +2 -3
  101. package/dist/connectors/github/ingest.d.ts +5 -7
  102. package/dist/connectors/github/ingest.js +8 -12
  103. package/dist/connectors/github/octokit-client.d.ts +3 -5
  104. package/dist/connectors/github/octokit-client.js +5 -6
  105. package/dist/connectors/github/signature.d.ts +9 -39
  106. package/dist/connectors/github/signature.js +9 -39
  107. package/dist/connectors/github/tenant-routing.d.ts +1 -1
  108. package/dist/connectors/github/tenant-routing.js +1 -1
  109. package/dist/connectors/github/transform.js +2 -2
  110. package/dist/connectors/github/types.d.ts +2 -10
  111. package/dist/connectors/github/types.js +1 -3
  112. package/dist/connectors/slack/deletion.d.ts +3 -8
  113. package/dist/connectors/slack/deletion.js +3 -8
  114. package/dist/connectors/slack/dlq.d.ts +1 -1
  115. package/dist/connectors/slack/ingest.d.ts +1 -1
  116. package/dist/connectors/slack/ingest.js +7 -16
  117. package/dist/connectors/slack/signature.d.ts +1 -1
  118. package/dist/connectors/slack/tenant-routing.d.ts +3 -5
  119. package/dist/connectors/slack/tenant-routing.js +3 -5
  120. package/dist/connectors/slack/transform.d.ts +5 -6
  121. package/dist/connectors/slack/transform.js +5 -6
  122. package/dist/connectors/slack/types.d.ts +2 -6
  123. package/dist/connectors/slack/types.js +1 -3
  124. package/dist/connectors/slack/web-client.js +10 -3
  125. package/dist/connectors/slack/workspaces.d.ts +3 -5
  126. package/dist/connectors/slack/workspaces.js +3 -5
  127. package/dist/consolidate/conflicts.js +3 -14
  128. package/dist/consolidate/decay.js +9 -29
  129. package/dist/consolidate/llm-passes.js +4 -5
  130. package/dist/consolidate/merge.js +8 -23
  131. package/dist/consolidate/run.d.ts +1 -8
  132. package/dist/consolidate/run.js +3 -25
  133. package/dist/consolidate/sleep.js +5 -17
  134. package/dist/consolidate/traces.js +9 -21
  135. package/dist/customer-notes.d.ts +5 -7
  136. package/dist/customer-notes.js +82 -76
  137. package/dist/dag.d.ts +10 -21
  138. package/dist/dag.js +189 -203
  139. package/dist/db/continuity.js +2 -2
  140. package/dist/db/migrations/v14.js +1 -1
  141. package/dist/db/migrations/v15.js +1 -2
  142. package/dist/db/migrations/v16.js +3 -4
  143. package/dist/db/migrations/v17.js +2 -3
  144. package/dist/db/migrations/v19.js +1 -1
  145. package/dist/db/migrations/v20.js +1 -1
  146. package/dist/db/migrations/v21.js +2 -6
  147. package/dist/db/migrations/v22.js +2 -4
  148. package/dist/db/migrations/v23.js +1 -1
  149. package/dist/db/migrations/v24.js +4 -6
  150. package/dist/db/migrations/v25.js +2 -3
  151. package/dist/db/migrations/v26.js +3 -3
  152. package/dist/db/migrations/v27.js +2 -10
  153. package/dist/db/migrations/v28.js +5 -8
  154. package/dist/db/migrations/v29.js +3 -4
  155. package/dist/db/migrations/v30.js +2 -2
  156. package/dist/db/migrations/v31.js +1 -1
  157. package/dist/db/migrations/v32.js +1 -1
  158. package/dist/db/migrations/v33.js +3 -3
  159. package/dist/db/migrations/v34.js +1 -1
  160. package/dist/db/migrations/v35.js +3 -4
  161. package/dist/db/migrations/v36.js +3 -4
  162. package/dist/db/migrations/v37.js +5 -5
  163. package/dist/db/migrations/v38.js +7 -8
  164. package/dist/db/migrations/v39.js +1 -1
  165. package/dist/db/migrations/v40.js +4 -16
  166. package/dist/db/migrations/v41.js +3 -4
  167. package/dist/db/migrations/v42.js +3 -4
  168. package/dist/db/migrations/v45.js +1 -1
  169. package/dist/db/migrations/v46.js +1 -1
  170. package/dist/db/migrations/v47.js +1 -1
  171. package/dist/db/migrations/v48.js +1 -1
  172. package/dist/decisions.d.ts +2 -2
  173. package/dist/decisions.js +97 -80
  174. package/dist/dedupe.js +86 -61
  175. package/dist/delivery-recorder.js +154 -135
  176. package/dist/doctor.js +129 -110
  177. package/dist/dormant.js +1 -4
  178. package/dist/embedding-provider.d.ts +4 -8
  179. package/dist/embedding-provider.js +4 -8
  180. package/dist/embeddings.js +55 -47
  181. package/dist/env.d.ts +1 -1
  182. package/dist/env.js +12 -12
  183. package/dist/escape.d.ts +5 -0
  184. package/dist/escape.js +10 -0
  185. package/dist/eval-stats.d.ts +1 -2
  186. package/dist/eval-stats.js +1 -2
  187. package/dist/eval-suite.js +27 -21
  188. package/dist/extract.js +4 -9
  189. package/dist/failure-log.d.ts +3 -3
  190. package/dist/failure-log.js +1 -1
  191. package/dist/forward-claim-detector.d.ts +2 -4
  192. package/dist/forward-claim-detector.js +6 -11
  193. package/dist/goals.d.ts +3 -3
  194. package/dist/goals.js +103 -91
  195. package/dist/graph/read.d.ts +2 -2
  196. package/dist/graph/read.js +5 -6
  197. package/dist/graph/types.d.ts +8 -8
  198. package/dist/graph/write.d.ts +7 -14
  199. package/dist/graph/write.js +16 -23
  200. package/dist/graph-extract.d.ts +7 -8
  201. package/dist/graph-extract.js +62 -72
  202. package/dist/graph-recall.d.ts +2 -2
  203. package/dist/graph-recall.js +55 -49
  204. package/dist/graph-stream.d.ts +5 -6
  205. package/dist/graph-stream.js +66 -57
  206. package/dist/graph-view.d.ts +2 -2
  207. package/dist/graph-view.js +7 -7
  208. package/dist/half-life-migration.d.ts +1 -2
  209. package/dist/half-life-migration.js +2 -3
  210. package/dist/hooks/codex-session.js +1 -1
  211. package/dist/hooks/codex-wrapper.d.ts +1 -1
  212. package/dist/hooks/codex-wrapper.js +3 -2
  213. package/dist/hooks/json-hooks.d.ts +2 -2
  214. package/dist/hooks/json-hooks.js +5 -4
  215. package/dist/hooks/opencode.d.ts +1 -1
  216. package/dist/hooks/opencode.js +5 -4
  217. package/dist/hooks/shared.d.ts +3 -7
  218. package/dist/hooks/shared.js +1 -8
  219. package/dist/http-util.d.ts +2 -3
  220. package/dist/http-util.js +3 -0
  221. package/dist/importers/core.d.ts +2 -9
  222. package/dist/importers/core.js +15 -30
  223. package/dist/importers/sources.js +2 -1
  224. package/dist/importers/vault.js +2 -20
  225. package/dist/incidents.d.ts +1 -1
  226. package/dist/incidents.js +46 -39
  227. package/dist/instruction-detect.d.ts +1 -1
  228. package/dist/instruction-detect.js +1 -1
  229. package/dist/invalidation.d.ts +3 -0
  230. package/dist/invalidation.js +160 -114
  231. package/dist/json.d.ts +5 -0
  232. package/dist/json.js +4 -0
  233. package/dist/judgment.js +1 -2
  234. package/dist/local-embedding.js +1 -1
  235. package/dist/mcp/admin-tools.js +7 -17
  236. package/dist/mcp/format.js +1 -1
  237. package/dist/mcp/framing.js +3 -6
  238. package/dist/mcp/protocol.d.ts +2 -5
  239. package/dist/mcp/protocol.js +1 -3
  240. package/dist/mcp/recall-tools.js +12 -15
  241. package/dist/mcp/request.js +4 -3
  242. package/dist/mcp/session-state.js +2 -3
  243. package/dist/mcp/stdio.js +2 -1
  244. package/dist/mcp/tools.js +9 -6
  245. package/dist/memory-value-weights.d.ts +5 -8
  246. package/dist/memory-value-weights.js +5 -8
  247. package/dist/memory-value.d.ts +13 -13
  248. package/dist/memory-value.js +26 -37
  249. package/dist/memory.d.ts +20 -22
  250. package/dist/memory.js +24 -48
  251. package/dist/multihop.d.ts +1 -1
  252. package/dist/multihop.js +3 -2
  253. package/dist/owner-validation.d.ts +4 -5
  254. package/dist/owner-validation.js +4 -5
  255. package/dist/physics.d.ts +4 -4
  256. package/dist/physics.js +7 -9
  257. package/dist/policies.d.ts +9 -10
  258. package/dist/policies.js +96 -81
  259. package/dist/postinstall.js +3 -6
  260. package/dist/predictions/planning-fallacy.d.ts +9 -14
  261. package/dist/predictions/planning-fallacy.js +10 -16
  262. package/dist/predictions/store.d.ts +15 -23
  263. package/dist/predictions/store.js +36 -33
  264. package/dist/processes.d.ts +2 -7
  265. package/dist/processes.js +88 -72
  266. package/dist/project-briefs.d.ts +2 -3
  267. package/dist/project-briefs.js +141 -118
  268. package/dist/project-identity.d.ts +22 -9
  269. package/dist/project-identity.js +47 -12
  270. package/dist/project-merge.d.ts +28 -5
  271. package/dist/project-merge.js +213 -46
  272. package/dist/project-remote.d.ts +12 -0
  273. package/dist/project-remote.js +138 -0
  274. package/dist/prompt-recall.js +1 -2
  275. package/dist/rate-limit.d.ts +1 -1
  276. package/dist/rate-limit.js +1 -1
  277. package/dist/raw-archive.d.ts +9 -0
  278. package/dist/raw-archive.js +70 -53
  279. package/dist/recall-history.d.ts +19 -20
  280. package/dist/recall-history.js +24 -42
  281. package/dist/recall-pipeline.js +4 -28
  282. package/dist/recall-scope.d.ts +7 -8
  283. package/dist/recall-scope.js +7 -8
  284. package/dist/recall-trace.d.ts +5 -9
  285. package/dist/recall-trace.js +6 -10
  286. package/dist/refine-llm.d.ts +1 -1
  287. package/dist/refine-llm.js +2 -2
  288. package/dist/reject-flow.d.ts +3 -4
  289. package/dist/reject-flow.js +122 -117
  290. package/dist/rejection.d.ts +5 -6
  291. package/dist/rejection.js +7 -15
  292. package/dist/rerankers/clef.d.ts +1 -1
  293. package/dist/rerankers/jev.d.ts +1 -2
  294. package/dist/rerankers/jev.js +4 -5
  295. package/dist/rerankers/llm.d.ts +1 -2
  296. package/dist/rerankers/llm.js +1 -2
  297. package/dist/rerankers/types.d.ts +1 -2
  298. package/dist/rrf.d.ts +2 -2
  299. package/dist/rrf.js +2 -2
  300. package/dist/search/bm25-search.d.ts +1 -1
  301. package/dist/search/bm25-search.js +2 -1
  302. package/dist/search/boosts.js +2 -1
  303. package/dist/search/hybrid.d.ts +1 -1
  304. package/dist/search/hybrid.js +2 -1
  305. package/dist/search/physics-search.d.ts +1 -1
  306. package/dist/search/physics-search.js +2 -1
  307. package/dist/search/types.d.ts +2 -0
  308. package/dist/search/types.js +3 -1
  309. package/dist/secret-detect.d.ts +4 -5
  310. package/dist/secret-detect.js +6 -10
  311. package/dist/server/auth.js +5 -5
  312. package/dist/server/client-ip.js +1 -1
  313. package/dist/server/cursor.js +2 -1
  314. package/dist/server/mcp-http.js +4 -4
  315. package/dist/server/request.d.ts +3 -6
  316. package/dist/server/request.js +6 -7
  317. package/dist/server/routes/admin.js +5 -4
  318. package/dist/server/routes/customer-notes.js +6 -5
  319. package/dist/server/routes/decisions.js +4 -3
  320. package/dist/server/routes/incidents.js +7 -5
  321. package/dist/server/routes/memories.js +7 -7
  322. package/dist/server/routes/policies.js +3 -2
  323. package/dist/server/routes/predictions.js +12 -15
  324. package/dist/server/routes/processes.js +3 -2
  325. package/dist/server/routes/project-briefs.js +8 -7
  326. package/dist/server/routes/recall.js +95 -93
  327. package/dist/server/routes/skills.js +6 -5
  328. package/dist/server/types.d.ts +1 -1
  329. package/dist/server/validation.d.ts +1 -2
  330. package/dist/server/validation.js +7 -14
  331. package/dist/server-detect.js +72 -58
  332. package/dist/server.d.ts +2 -2
  333. package/dist/server.js +131 -117
  334. package/dist/shared.d.ts +26 -17
  335. package/dist/shared.js +102 -104
  336. package/dist/skills.d.ts +3 -3
  337. package/dist/skills.js +88 -72
  338. package/dist/store/audit-event.d.ts +2 -2
  339. package/dist/store/audit-event.js +1 -1
  340. package/dist/store/candidates.d.ts +2 -2
  341. package/dist/store/candidates.js +4 -3
  342. package/dist/store/conflicts.js +30 -22
  343. package/dist/store/delete-and-batch.d.ts +11 -14
  344. package/dist/store/delete-and-batch.js +40 -91
  345. package/dist/store/entry-reads.d.ts +17 -25
  346. package/dist/store/entry-reads.js +59 -40
  347. package/dist/store/entry-row.d.ts +6 -24
  348. package/dist/store/entry-row.js +6 -24
  349. package/dist/store/entry-writes.d.ts +6 -7
  350. package/dist/store/entry-writes.js +13 -11
  351. package/dist/store/handoffs.d.ts +1 -1
  352. package/dist/store/handoffs.js +7 -10
  353. package/dist/store/index-and-stats.d.ts +2 -6
  354. package/dist/store/index-and-stats.js +4 -10
  355. package/dist/store/mirrors.d.ts +6 -19
  356. package/dist/store/mirrors.js +14 -39
  357. package/dist/store/open.js +9 -31
  358. package/dist/store/rows.d.ts +5 -11
  359. package/dist/store/rows.js +6 -11
  360. package/dist/store/search-rows.d.ts +17 -34
  361. package/dist/store/search-rows.js +34 -56
  362. package/dist/store/sessions.d.ts +4 -5
  363. package/dist/store/sessions.js +5 -6
  364. package/dist/store/summaries.d.ts +13 -17
  365. package/dist/store/summaries.js +26 -70
  366. package/dist/support-bundle.js +4 -8
  367. package/dist/tenant.d.ts +1 -5
  368. package/dist/token-ledger.d.ts +1 -1
  369. package/dist/token-ledger.js +3 -5
  370. package/dist/trace.js +1 -3
  371. package/dist/version.d.ts +1 -1
  372. package/dist/version.js +1 -1
  373. package/dist/working-memory.d.ts +1 -1
  374. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  375. package/extensions/openclaw-plugin/package.json +1 -1
  376. package/openclaw.plugin.json +1 -1
  377. package/package.json +1 -1
package/dist/dag.js CHANGED
@@ -87,17 +87,10 @@ export async function generateDagSummary(label, factContents, opts) {
87
87
  return null;
88
88
  }
89
89
  }
90
- export async function buildDag(hippoRoot, facts, opts) {
91
- const result = { candidateClusters: 0, summariesCreated: 0, factsLinked: 0, rejected: 0 };
92
- const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
93
- const unparented = facts.filter((f) => f.dag_level === 1 && !f.dag_parent_id && f.tags.includes('extracted'));
94
- // Hardening follow-up (mirrors consolidate.ts's mergeCandidatesByTenant,
95
- // T1): partition unparented facts by tenantId BEFORE clustering so a
96
- // cluster can never mix facts from different tenants into one
97
- // LLM-synthesized summary. Map preserves insertion order, so
98
- // single-tenant stores (every row 'default') get exactly one partition
99
- // and iterate in the same order as before this fix — byte-identical
100
- // 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.
93
+ function partitionFactsByTenant(unparented) {
101
94
  const unparentedByTenant = new Map();
102
95
  for (const fact of unparented) {
103
96
  const key = derivationPartitionKey(fact.tenantId, fact.scope, fact.origin_project);
@@ -107,95 +100,144 @@ export async function buildDag(hippoRoot, facts, opts) {
107
100
  else
108
101
  unparentedByTenant.set(key, [fact]);
109
102
  }
110
- for (const [, tenantFacts] of unparentedByTenant) {
111
- const factTenant = tenantFacts[0].tenantId;
112
- const factScope = derivationScope(tenantFacts[0].scope);
103
+ return unparentedByTenant;
104
+ }
105
+ /** The L2 summary entry for one cluster, landing in the facts' own tenant and scope. */
106
+ function createClusterSummaryEntry(summary, cluster, home) {
107
+ const memberCreatedAts = cluster.members.map((m) => m.created).sort();
108
+ // Every member of `cluster` shares home.tenantId by construction (the
109
+ // tenant partition above), so the summary lands in the same tenant
110
+ // as the facts it summarizes instead of always 'default'
111
+ // (memory.ts:535 defaults tenantId when the option is omitted).
112
+ const summaryEntry = createMemory(summary, {
113
+ layer: Layer.Semantic,
114
+ tags: [...cluster.entityTags, ...neverAutoShareTags(cluster.members), 'dag-summary'],
115
+ confidence: 'inferred',
116
+ dag_level: 2,
117
+ tenantId: home.tenantId,
118
+ scope: home.scope,
119
+ baseHalfLifeDays: home.baseHalfLifeDays,
120
+ });
121
+ summaryEntry.origin_project = home.originProject;
122
+ // Cache descendant_count + earliest/latest_at on the summary row so
123
+ // DAG-aware recall can reason about scope without walking the children.
124
+ summaryEntry.descendant_count = cluster.members.length;
125
+ summaryEntry.earliest_at = memberCreatedAts[0];
126
+ summaryEntry.latest_at = memberCreatedAts[memberCreatedAts.length - 1];
127
+ return summaryEntry;
128
+ }
129
+ /** Summarize one eligible cluster, write the summary, then re-parent its members under it. */
130
+ async function summarizeCluster(hippoRoot, cluster, home, opts, result) {
131
+ const summary = await generateDagSummary(cluster.label, cluster.members.map((m) => m.content), opts);
132
+ if (!summary)
133
+ return;
134
+ const summaryEntry = createClusterSummaryEntry(summary, cluster, home);
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.
137
+ try {
138
+ writeEntry(hippoRoot, summaryEntry);
139
+ }
140
+ catch (err) {
141
+ if (err instanceof RejectedValueError) {
142
+ result.rejected++;
143
+ log.warn(`buildDag: cluster "${cluster.label}" skipped: summary matches a rejected value`);
144
+ return;
145
+ }
146
+ throw err;
147
+ }
148
+ result.summariesCreated++;
149
+ for (const member of cluster.members) {
150
+ const updated = { ...member, dag_parent_id: summaryEntry.id };
151
+ writeEntry(hippoRoot, updated);
152
+ result.factsLinked++;
153
+ }
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.
156
+ clearSummaryDirtyAfterBuild(hippoRoot, summaryEntry.id, summaryEntry.tenantId, 'buildDag');
157
+ }
158
+ export async function buildDag(hippoRoot, facts, opts) {
159
+ const result = { candidateClusters: 0, summariesCreated: 0, factsLinked: 0, rejected: 0 };
160
+ const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
161
+ const unparented = facts.filter((f) => f.dag_level === 1 && !f.dag_parent_id && f.tags.includes('extracted'));
162
+ for (const [, tenantFacts] of partitionFactsByTenant(unparented)) {
163
+ const home = {
164
+ tenantId: tenantFacts[0].tenantId,
165
+ scope: derivationScope(tenantFacts[0].scope),
166
+ originProject: tenantFacts[0].origin_project,
167
+ baseHalfLifeDays,
168
+ };
113
169
  const clusters = clusterFacts(tenantFacts);
114
170
  const eligibleClusters = clusters.filter((c) => c.members.length >= 3);
115
171
  result.candidateClusters += eligibleClusters.length;
116
172
  for (const cluster of eligibleClusters) {
117
- const summary = await generateDagSummary(cluster.label, cluster.members.map((m) => m.content), opts);
118
- if (!summary)
119
- continue;
120
- const memberCreatedAts = cluster.members.map((m) => m.created).sort();
121
- // Every member of `cluster` shares factTenant by construction (the
122
- // tenant partition above), so the summary lands in the same tenant
123
- // as the facts it summarizes instead of always 'default'
124
- // (memory.ts:535 defaults tenantId when the option is omitted).
125
- const summaryEntry = createMemory(summary, {
126
- layer: Layer.Semantic,
127
- tags: [...cluster.entityTags, ...neverAutoShareTags(cluster.members), 'dag-summary'],
128
- confidence: 'inferred',
129
- dag_level: 2,
130
- tenantId: factTenant,
131
- scope: factScope,
132
- baseHalfLifeDays,
133
- });
134
- summaryEntry.origin_project = tenantFacts[0].origin_project;
135
- // Schema v25: cache descendant_count + earliest/latest_at on the summary
136
- // row so DAG-aware recall (docs/plans/2026-05-05-dag-recall.md Task 2)
137
- // can reason about scope without walking the children.
138
- summaryEntry.descendant_count = cluster.members.length;
139
- summaryEntry.earliest_at = memberCreatedAts[0];
140
- summaryEntry.latest_at = memberCreatedAts[memberCreatedAts.length - 1];
141
- // AT1 (plan §3 containment): a refused LLM-synthesized summary skips
142
- // ONLY this cluster — the sleep cycle continues to the next one. The
143
- // member re-parenting writes below never run for a skipped cluster
144
- // (there is no summary id to parent them under).
145
- //
146
- // The tombstone check itself is tenant-scoped for free: writeEntry ->
147
- // writeEntryDbOnly -> upsertEntryRow calls
148
- // checkRejectionGuard(db, entry.tenantId ?? 'default', ...)
149
- // (store/entry-writes.ts), reading tenantId off the entry being written. Now
150
- // that summaryEntry carries factTenant instead of the implicit
151
- // 'default', the guard consults that tenant's tombstones — no
152
- // separate check needed here (unlike consolidate.ts's merge pass,
153
- // which pre-checks via findRejectedValue because it writes through
154
- // batchWriteAndDelete's bypassRejectionGuard path instead of
155
- // writeEntry).
156
- try {
157
- writeEntry(hippoRoot, summaryEntry);
158
- }
159
- catch (err) {
160
- if (err instanceof RejectedValueError) {
161
- result.rejected++;
162
- log.warn(`buildDag: cluster "${cluster.label}" skipped: summary matches a rejected value`);
163
- continue;
164
- }
165
- throw err;
166
- }
167
- result.summariesCreated++;
168
- for (const member of cluster.members) {
169
- const updated = { ...member, dag_parent_id: summaryEntry.id };
170
- writeEntry(hippoRoot, updated);
171
- result.factsLinked++;
172
- }
173
- // v0.30 / E3 — cancel the cascade of dirty-marks fired by member
174
- // writeEntry calls (E2 hook on writeEntryDbOnly in store/entry-writes.ts).
175
- // The summary we just built IS fresh, no rebuild needed. Without this,
176
- // E3 in the SAME sleep cycle would re-rebuild every new summary
177
- // (2x LLM cost). plan-eng-r1 HIGH must-fix.
178
- clearSummaryDirtyAfterBuild(hippoRoot, summaryEntry.id, summaryEntry.tenantId, 'buildDag');
173
+ await summarizeCluster(hippoRoot, cluster, home, opts, result);
179
174
  }
180
175
  }
181
176
  return result;
182
177
  }
178
+ /** Regenerate one dirty summary from its children and tally the outcome; throws reach the caller's isolation. */
179
+ async function rebuildOneSummary(hippoRoot, summary, opts, result) {
180
+ const children = loadChildrenOfSummary(hippoRoot, summary.id, summary.tenantId);
181
+ if (children.length === 0) {
182
+ // Zero-child case: clear dirty + zero counts, no LLM call, no rebuild_count bump.
183
+ const { changed } = applyRebuildResult(hippoRoot, summary, {
184
+ content: summary.content,
185
+ descendant_count: 0,
186
+ earliest_at: null,
187
+ latest_at: null,
188
+ bumpRebuildCount: false,
189
+ zeroChildren: true,
190
+ actor: 'sleep',
191
+ });
192
+ if (changed)
193
+ result.zeroChildSkipped++;
194
+ // changed=false → race lost / row vanished; silently skip. `refused`
195
+ // is always false here — applyRebuildResult only checks the
196
+ // tombstone when bumpRebuildCount is true (store/summaries.ts).
197
+ return;
198
+ }
199
+ // Derive label from summary's existing entity tags (mirrors clusterFacts)
200
+ const entityTags = summary.tags.filter((t) => t.startsWith('speaker:') || t.startsWith('topic:'));
201
+ const label = entityTags.length > 0
202
+ ? entityTags.map((t) => t.split(':')[1]).join(': ')
203
+ : summary.content.slice(0, 40);
204
+ const newContent = await generateDagSummary(label, children.map((c) => c.content), opts);
205
+ if (!newContent) {
206
+ // LLM null / fetch error → leave dirty for next cycle
207
+ result.failed++;
208
+ return;
209
+ }
210
+ const childCreatedAts = children.map((c) => c.created).sort();
211
+ const { changed, refused } = applyRebuildResult(hippoRoot, summary, {
212
+ content: newContent,
213
+ descendant_count: children.length,
214
+ earliest_at: childCreatedAts[0],
215
+ latest_at: childCreatedAts[childCreatedAts.length - 1],
216
+ bumpRebuildCount: true,
217
+ zeroChildren: false,
218
+ actor: 'sleep',
219
+ });
220
+ if (refused) {
221
+ result.refused++;
222
+ }
223
+ else if (changed) {
224
+ result.rebuilt++;
225
+ }
226
+ // changed=false (refused also false) → race lost; not failure, not
227
+ // success, silently skip
228
+ }
183
229
  /**
184
- * v0.30 / E3 — sleep-cycle phase that drains the dirty L2 summary queue.
230
+ * Sleep-cycle phase that drains the dirty L2 summary queue.
185
231
  * Thin orchestrator; the heavy lifting lives in store.ts (load + apply)
186
232
  * and dag.ts:generateDagSummary (LLM call).
187
233
  *
188
- * Per-summary try/catch isolation (plan-eng-r1 MED must-fix) — one
189
- * 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.
190
235
  *
191
236
  * Race-loser handling: applyRebuildResult's UPDATE WHERE includes
192
237
  * AND summary_dirty=1, so concurrent sleep's second writer returns
193
238
  * changed=false. Silent skip (neither rebuilt++ nor refused++ nor failed++).
194
239
  *
195
- * T4: applyRebuildResult returns { changed, refused } — a tombstone hit
196
- * (rebuild content matches a previously-rejected value) increments
197
- * `refused`, not `rebuilt`. Dirty still clears either way; only the stat
198
- * 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.
199
241
  */
200
242
  export async function rebuildDirtySummaries(hippoRoot, opts) {
201
243
  const cap = opts.cap ?? 20;
@@ -221,76 +263,74 @@ export async function rebuildDirtySummaries(hippoRoot, opts) {
221
263
  await new Promise((resolve) => setImmediate(resolve));
222
264
  }
223
265
  try {
224
- const children = loadChildrenOfSummary(hippoRoot, summary.id, summary.tenantId);
225
- if (children.length === 0) {
226
- // Zero-child case: clear dirty + zero counts, no LLM call, no rebuild_count bump.
227
- const { changed } = applyRebuildResult(hippoRoot, summary, {
228
- content: summary.content,
229
- descendant_count: 0,
230
- earliest_at: null,
231
- latest_at: null,
232
- bumpRebuildCount: false,
233
- zeroChildren: true,
234
- actor: 'sleep',
235
- });
236
- if (changed)
237
- result.zeroChildSkipped++;
238
- // changed=false → race lost / row vanished; silently skip. `refused`
239
- // is always false here — applyRebuildResult only checks the
240
- // tombstone when bumpRebuildCount is true (store/summaries.ts).
241
- continue;
242
- }
243
- // Derive label from summary's existing entity tags (mirrors clusterFacts)
244
- const entityTags = summary.tags.filter((t) => t.startsWith('speaker:') || t.startsWith('topic:'));
245
- const label = entityTags.length > 0
246
- ? entityTags.map((t) => t.split(':')[1]).join(': ')
247
- : summary.content.slice(0, 40);
248
- const newContent = await generateDagSummary(label, children.map((c) => c.content), opts);
249
- if (!newContent) {
250
- // LLM null / fetch error → leave dirty for next cycle
251
- result.failed++;
252
- continue;
253
- }
254
- const childCreatedAts = children.map((c) => c.created).sort();
255
- const { changed, refused } = applyRebuildResult(hippoRoot, summary, {
256
- content: newContent,
257
- descendant_count: children.length,
258
- earliest_at: childCreatedAts[0],
259
- latest_at: childCreatedAts[childCreatedAts.length - 1],
260
- bumpRebuildCount: true,
261
- zeroChildren: false,
262
- actor: 'sleep',
263
- });
264
- if (refused) {
265
- result.refused++;
266
- }
267
- else if (changed) {
268
- result.rebuilt++;
269
- }
270
- // changed=false (refused also false) → race lost; not failure, not
271
- // success, silently skip
266
+ await rebuildOneSummary(hippoRoot, summary, opts, result);
272
267
  }
273
268
  catch (err) {
274
- // Per-summary failure isolation — one throw doesn't abort the queue.
275
- // independent-review MED #2 fold: log enough to triage in production
276
- // (audit() wraps its own writes try/catch per store/audit-event.ts, so a
277
- // 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.
278
271
  result.failed++;
279
272
  log.error(`rebuildDirtySummaries: summary ${summary.id} (tenant ${summary.tenantId}) failed: ${err instanceof Error ? err.message : String(err)}`);
280
273
  }
281
274
  }
282
275
  return result;
283
276
  }
277
+ /** The L3 profile entry for one cluster of L2 summaries. */
278
+ function createProfileEntry(summary, cluster, home) {
279
+ const memberCreatedAts = cluster.members.map((m) => m.created).sort();
280
+ const nowIso = new Date().toISOString();
281
+ const profileEntry = createMemory(summary, {
282
+ layer: Layer.Semantic,
283
+ tags: [...cluster.entityTags, ...neverAutoShareTags(cluster.members), 'dag-entity-profile'],
284
+ confidence: 'inferred',
285
+ dag_level: 3,
286
+ tenantId: home.tenantId,
287
+ scope: home.scope,
288
+ baseHalfLifeDays: home.baseHalfLifeDays,
289
+ });
290
+ profileEntry.origin_project = home.originProject;
291
+ profileEntry.descendant_count = cluster.members.length;
292
+ profileEntry.earliest_at = memberCreatedAts[0];
293
+ profileEntry.latest_at = memberCreatedAts[memberCreatedAts.length - 1];
294
+ profileEntry.dag_level_3_built_at = nowIso;
295
+ return profileEntry;
296
+ }
297
+ /** Profile one eligible cluster of L2s, write it, then re-link the L2s under it. */
298
+ async function profileCluster(hippoRoot, cluster, home, opts, result) {
299
+ const summary = await generateDagSummary(cluster.label, cluster.members.map((m) => m.content), opts);
300
+ if (!summary) {
301
+ result.failed++;
302
+ return;
303
+ }
304
+ const profileEntry = createProfileEntry(summary, cluster, home);
305
+ // A refused profile skips only this cluster; the re-linking below never runs for it.
306
+ try {
307
+ writeEntry(hippoRoot, profileEntry);
308
+ }
309
+ catch (err) {
310
+ if (err instanceof RejectedValueError) {
311
+ result.rejected++;
312
+ log.warn(`buildEntityProfiles: cluster "${cluster.label}" skipped: profile matches a rejected value`);
313
+ return;
314
+ }
315
+ throw err;
316
+ }
317
+ result.profilesCreated++;
318
+ for (const member of cluster.members) {
319
+ const updated = { ...member, dag_parent_id: profileEntry.id };
320
+ writeEntry(hippoRoot, updated);
321
+ result.l2sLinked++;
322
+ }
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.
325
+ clearSummaryDirtyAfterBuild(hippoRoot, profileEntry.id, home.tenantId, 'buildEntityProfiles', 'buildEntityProfiles-clean');
326
+ }
284
327
  /**
285
- * v0.30 / E5 — build L3 entity profiles by clustering L2 summaries with
328
+ * Build L3 entity profiles by clustering L2 summaries with
286
329
  * shared entity tags. Threshold 2+ L2s per entity. Mirrors buildDag L1->L2
287
330
  * pattern, one level up.
288
331
  *
289
- * Born-dirty cancellation (E3 lesson): after linking L2 children to the new
290
- * L3 (each link write fires E2 hook on L3 via widened markSummaryDirtyInTx),
291
- * call clearSummaryDirtyAfterBuild with source='buildEntityProfiles-clean'
292
- * so E3 sleep-cycle rebuild doesn't re-rebuild the freshly-built L3 this
293
- * 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.
294
334
  */
295
335
  export async function buildEntityProfiles(hippoRoot, l2Summaries, opts) {
296
336
  const result = {
@@ -303,72 +343,18 @@ export async function buildEntityProfiles(hippoRoot, l2Summaries, opts) {
303
343
  const baseHalfLifeDays = loadConfig(hippoRoot).defaultHalfLifeDays;
304
344
  // Only L2 with no L3 parent yet (avoid re-clustering already-profiled L2s).
305
345
  const unparented = l2Summaries.filter((s) => s.dag_level === 2 && !s.dag_parent_id);
306
- // independent-review HIGH #1 fold: cluster ONLY within-tenant.
307
- // clusterFacts has no tenant awareness; without this partition step a
308
- // multi-tenant host could form a cluster spanning tenants and produce
309
- // a single L3 with tenantId='default' that doesn't belong to either
310
- // child tenant. Fix: bucket by tenantId, run clusterFacts per-tenant,
311
- // pass tenantId to createMemory.
312
- const byTenant = new Map();
313
- for (const l2 of unparented) {
314
- const tid = l2.tenantId ?? 'default';
315
- const key = derivationPartitionKey(tid, l2.scope, l2.origin_project);
316
- const list = byTenant.get(key) ?? [];
317
- list.push(l2);
318
- byTenant.set(key, list);
319
- }
320
- for (const [, tenantL2s] of byTenant) {
321
- const tenantId = tenantL2s[0].tenantId ?? 'default';
322
- const scope = derivationScope(tenantL2s[0].scope);
346
+ for (const [, tenantL2s] of partitionFactsByTenant(unparented)) {
347
+ const home = {
348
+ tenantId: tenantL2s[0].tenantId,
349
+ scope: derivationScope(tenantL2s[0].scope),
350
+ originProject: tenantL2s[0].origin_project,
351
+ baseHalfLifeDays,
352
+ };
323
353
  const clusters = clusterFacts(tenantL2s);
324
354
  const eligible = clusters.filter((c) => c.members.length >= 2);
325
355
  result.candidateClusters += eligible.length;
326
356
  for (const cluster of eligible) {
327
- const summary = await generateDagSummary(cluster.label, cluster.members.map((m) => m.content), opts);
328
- if (!summary) {
329
- result.failed++;
330
- continue;
331
- }
332
- const memberCreatedAts = cluster.members.map((m) => m.created).sort();
333
- const nowIso = new Date().toISOString();
334
- const profileEntry = createMemory(summary, {
335
- layer: Layer.Semantic,
336
- tags: [...cluster.entityTags, ...neverAutoShareTags(cluster.members), 'dag-entity-profile'],
337
- confidence: 'inferred',
338
- dag_level: 3,
339
- tenantId, // HIGH #1 fold: thread tenant explicitly
340
- scope,
341
- baseHalfLifeDays,
342
- });
343
- profileEntry.origin_project = tenantL2s[0].origin_project;
344
- profileEntry.descendant_count = cluster.members.length;
345
- profileEntry.earliest_at = memberCreatedAts[0];
346
- profileEntry.latest_at = memberCreatedAts[memberCreatedAts.length - 1];
347
- profileEntry.dag_level_3_built_at = nowIso;
348
- // AT1 (plan §3 containment): per-cluster catch — skip this cluster,
349
- // count, log once. The member re-linking writes below never run for a
350
- // skipped cluster (mirrors buildDag above).
351
- try {
352
- writeEntry(hippoRoot, profileEntry);
353
- }
354
- catch (err) {
355
- if (err instanceof RejectedValueError) {
356
- result.rejected++;
357
- log.warn(`buildEntityProfiles: cluster "${cluster.label}" skipped: profile matches a rejected value`);
358
- continue;
359
- }
360
- throw err;
361
- }
362
- result.profilesCreated++;
363
- for (const member of cluster.members) {
364
- const updated = { ...member, dag_parent_id: profileEntry.id };
365
- writeEntry(hippoRoot, updated);
366
- result.l2sLinked++;
367
- }
368
- // E3 born-dirty cancellation, same dance as buildDag L161-168 but for
369
- // L3. Pass source='buildEntityProfiles-clean' to distinguish in audit.
370
- // Args: (root, id, tenantId, actor, source).
371
- clearSummaryDirtyAfterBuild(hippoRoot, profileEntry.id, tenantId, 'buildEntityProfiles', 'buildEntityProfiles-clean');
357
+ await profileCluster(hippoRoot, cluster, home, opts, result);
372
358
  }
373
359
  }
374
360
  return result;
@@ -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
@@ -2,9 +2,8 @@ import { tableHasColumn } from '../tables.js';
2
2
  export const v16 = {
3
3
  version: 16,
4
4
  up: (db) => {
5
- // A5 stub auth: add tenant_id to all data tables. Single-tenant per deployment;
6
- // multi-tenant enforcement deferred to v2 (full A5). The columns are needed now
7
- // so future B-track tables don't have to backfill.
5
+ // Stub auth: add tenant_id to all data tables. Single-tenant per deployment for now;
6
+ // the columns land early so later tables don't have to backfill.
8
7
  if (!tableHasColumn(db, 'memories', 'tenant_id')) {
9
8
  db.exec(`ALTER TABLE memories ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
10
9
  }
@@ -26,7 +25,7 @@ export const v16 = {
26
25
  db.exec(`CREATE INDEX IF NOT EXISTS idx_working_memory_tenant ON working_memory(tenant_id, importance DESC, created_at DESC)`);
27
26
  db.exec(`CREATE INDEX IF NOT EXISTS idx_consolidation_runs_tenant_ts ON consolidation_runs(tenant_id, timestamp DESC)`);
28
27
  db.exec(`CREATE INDEX IF NOT EXISTS idx_task_snapshots_tenant_status ON task_snapshots(tenant_id, status, updated_at DESC)`);
29
- // A5 stub auth: api_keys (scrypt-hashed; plaintext returned to caller exactly once)
28
+ // Stub auth: api_keys (scrypt-hashed; plaintext returned to caller exactly once)
30
29
  // and audit_log (append-only mutation trail). Both carry tenant_id from day 1 so
31
30
  // future multi-tenant enforcement is a config flip, not a re-migration.
32
31
  db.exec(`
@@ -1,8 +1,7 @@
1
1
  export const v17 = {
2
2
  version: 17,
3
3
  up: (db) => {
4
- // E1.3 Slack ingestion: idempotency log, per-channel backfill cursors, DLQ.
5
- // See docs/plans/2026-04-29-e1.3-slack-ingestion.md.
4
+ // Slack ingestion: idempotency log, per-channel backfill cursors, DLQ.
6
5
  db.exec(`
7
6
  CREATE TABLE IF NOT EXISTS slack_event_log (
8
7
  event_id TEXT PRIMARY KEY,
@@ -31,7 +30,7 @@ export const v17 = {
31
30
  )
32
31
  `);
33
32
  db.exec(`CREATE INDEX IF NOT EXISTS idx_slack_dlq_tenant_received ON slack_dlq(tenant_id, received_at)`);
34
- // Multi-tenant routing seam (review patch #6). Empty by default — single-
33
+ // Multi-tenant routing seam. Empty by default: single-
35
34
  // tenant deployments resolve via HIPPO_TENANT fallback. Multi-workspace
36
35
  // deployments populate this table to map team_id → tenant_id.
37
36
  db.exec(`
@@ -2,7 +2,7 @@ import { tableHasColumn } from '../tables.js';
2
2
  export const v19 = {
3
3
  version: 19,
4
4
  up: (db) => {
5
- // v0.39 commit 3 (Slack hardening): widen slack_dlq with bucketing,
5
+ // Slack hardening: widen slack_dlq with bucketing,
6
6
  // retry tracking, and the signature/timestamp pair that lets `hippo
7
7
  // slack dlq replay` re-verify before re-running ingest. ALTER ADD
8
8
  // COLUMN with DEFAULT is non-destructive — legacy rows take the
@@ -1,7 +1,7 @@
1
1
  export const v20 = {
2
2
  version: 20,
3
3
  up: (db) => {
4
- // v0.39 commit 4 (GDPR Path A backfill): redact every existing
4
+ // GDPR Path A backfill: redact every existing
5
5
  // raw_archive.payload_json so historical archives match the new
6
6
  // metadata-only contract from src/raw-archive.ts. Read each row, parse
7
7
  // the existing JSON to extract tenant_id and kind (best effort), then
@@ -2,12 +2,8 @@ import { tableHasColumn } from '../tables.js';
2
2
  export const v21 = {
3
3
  version: 21,
4
4
  up: (db) => {
5
- // v0.39 codex round 3: per-row mirror cleanup tracking. Replaces the
6
- // global gdpr_v20_mirror_cleanup meta gate (which made the reaper
7
- // one-shot and silently swallowed failed unlinks). With this column the
8
- // reaper processes only rows WHERE mirror_cleaned_at IS NULL, sets the
9
- // timestamp on success, and leaves it NULL on any unlink failure so the
10
- // next openHippoDb retries automatically.
5
+ // Per-row mirror cleanup tracking: a failed unlink leaves mirror_cleaned_at NULL so the
6
+ // next openHippoDb retries it, where a one-shot global gate swallowed the failure.
11
7
  if (!tableHasColumn(db, 'raw_archive', 'mirror_cleaned_at')) {
12
8
  db.exec(`ALTER TABLE raw_archive ADD COLUMN mirror_cleaned_at TEXT`);
13
9
  }
@@ -2,10 +2,8 @@ import { tableHasColumn } from '../tables.js';
2
2
  export const v22 = {
3
3
  version: 22,
4
4
  up: (db) => {
5
- // Tenant-isolation gap on continuity tables (codex review 2026-05-02).
6
- // session_events and session_handoffs predate the v16 tenant migration
7
- // and were never added to it, so the v0.40.0 provenance gate work
8
- // exposed a real cross-tenant leak when continuity primitives are used.
5
+ // session_events and session_handoffs predate the v16 tenant migration and were
6
+ // never added to it: a cross-tenant leak whenever continuity primitives are used.
9
7
  // Adds tenant_id (NOT NULL DEFAULT 'default') with smart backfill from
10
8
  // task_snapshots.session_id when unambiguous, plus an optional scope
11
9
  // column so a private-channel-derived handoff can default-deny via the
@@ -22,7 +22,7 @@ export const v23 = {
22
22
  if (!tableHasColumn(db, 'task_snapshots', 'scope')) {
23
23
  db.exec(`ALTER TABLE task_snapshots ADD COLUMN scope TEXT`);
24
24
  }
25
- // Quarantine policy (codex round 1 P1): pre-existing continuity rows
25
+ // Quarantine policy: pre-existing continuity rows
26
26
  // with NULL scope cannot be safely classified as public after the fact.
27
27
  // Mark them 'unknown:legacy' so the api.recall + cmdRecall default-deny
28
28
  // filter excludes them for no-scope callers. Fresh rows from new
@@ -1,7 +1,7 @@
1
1
  export const v24 = {
2
2
  version: 24,
3
3
  up: (db) => {
4
- // v1.3.0 GitHub connector schema (codex round 1, 2026-05-04).
4
+ // GitHub connector schema.
5
5
  // Six tables + a min_compatible_binary meta row for rollback safety.
6
6
  db.exec(`
7
7
  CREATE TABLE IF NOT EXISTS github_event_log (
@@ -50,7 +50,7 @@ export const v24 = {
50
50
  added_at TEXT NOT NULL
51
51
  )
52
52
  `);
53
- // PAT-mode multi-tenant routing (codex P0 #4). Maps repo_full_name to
53
+ // PAT-mode multi-tenant routing. Maps repo_full_name to
54
54
  // tenant when the webhook envelope has no `installation` field. Composite
55
55
  // PK so the same repo can intentionally be visible to multiple tenants
56
56
  // (e.g., shared tooling accounts) — collision is on (repo, tenant) pair.
@@ -62,10 +62,8 @@ export const v24 = {
62
62
  PRIMARY KEY (repo_full_name, tenant_id)
63
63
  )
64
64
  `);
65
- // Rollback-safety guard (codex P0 #2). Any binary < 1.2.1 lacks the
66
- // generic *:private:* default-deny and would leak github:private:* rows
67
- // if it opened this DB. The startup guard in v1.2.1+ refuses to open a
68
- // DB whose min_compatible_binary is newer than its own version.
65
+ // Rollback-safety guard: an older binary lacks the *:private:* default-deny and would leak
66
+ // github:private:* rows; the startup guard refuses a DB whose min_compatible_binary is newer.
69
67
  db.prepare(`INSERT INTO meta(key, value) VALUES('min_compatible_binary', ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run('1.2.1');
70
68
  },
71
69
  };
@@ -2,12 +2,11 @@ import { tableHasColumn } from '../tables.js';
2
2
  export const v25 = {
3
3
  version: 25,
4
4
  up: (db) => {
5
- // v1.5.0 DAG-aware recall — cache summary metadata so the assembler can
5
+ // DAG-aware recall: cache summary metadata so the assembler can
6
6
  // reason about scope without re-walking the DAG. Three additive,
7
7
  // optional columns. No min_compatible_binary bump: these columns are
8
8
  // pure metadata; older binaries opening this DB will see them as NULL
9
- // / 0 and behave as before. See docs/plans/2026-05-05-dag-recall.md
10
- // Task 1.
9
+ // / 0 and behave as before.
11
10
  if (!tableHasColumn(db, 'memories', 'descendant_count')) {
12
11
  db.exec(`ALTER TABLE memories ADD COLUMN descendant_count INTEGER NOT NULL DEFAULT 0`);
13
12
  }