hippo-memory 1.60.0 → 1.62.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 (593) hide show
  1. package/README.md +29 -54
  2. package/dist/ablation.js +9 -27
  3. package/dist/agent-memories/apply.d.ts +1 -1
  4. package/dist/agent-memories/apply.js +4 -1
  5. package/dist/agent-memories/claude-code.d.ts +3 -1
  6. package/dist/agent-memories/claude-code.js +54 -10
  7. package/dist/agent-memories/codex.js +1 -1
  8. package/dist/agent-memories/gemini.js +1 -1
  9. package/dist/agent-memories/legacy.js +1 -1
  10. package/dist/agent-memories/source.js +1 -1
  11. package/dist/agent-memories/sync.d.ts +3 -3
  12. package/dist/agent-memories/sync.js +22 -9
  13. package/dist/agent-memories/types.d.ts +0 -2
  14. package/dist/ambient-store.d.ts +14 -0
  15. package/dist/ambient-store.js +90 -0
  16. package/dist/ambient.d.ts +23 -0
  17. package/dist/ambient.js +72 -50
  18. package/dist/api/assemble.d.ts +93 -0
  19. package/dist/api/assemble.js +153 -0
  20. package/dist/api/audit.d.ts +17 -0
  21. package/dist/api/audit.js +23 -0
  22. package/dist/api/auth.d.ts +79 -0
  23. package/dist/api/auth.js +178 -0
  24. package/dist/api/context-select.d.ts +49 -0
  25. package/dist/api/context-select.js +344 -0
  26. package/dist/api/context-types.d.ts +105 -0
  27. package/dist/api/context-types.js +3 -0
  28. package/dist/api/context.d.ts +31 -0
  29. package/dist/api/context.js +378 -0
  30. package/dist/api/dormant.d.ts +30 -0
  31. package/dist/api/dormant.js +140 -0
  32. package/dist/api/drill-down.d.ts +84 -0
  33. package/dist/api/drill-down.js +126 -0
  34. package/dist/api/forget.d.ts +57 -0
  35. package/dist/api/forget.js +87 -0
  36. package/dist/api/goals.d.ts +18 -0
  37. package/dist/api/goals.js +33 -0
  38. package/dist/api/learn.d.ts +31 -0
  39. package/dist/api/learn.js +88 -0
  40. package/dist/api/outcome.d.ts +61 -0
  41. package/dist/api/outcome.js +66 -0
  42. package/dist/api/promote.d.ts +54 -0
  43. package/dist/api/promote.js +192 -0
  44. package/dist/api/quarantine.d.ts +24 -0
  45. package/dist/api/quarantine.js +121 -0
  46. package/dist/api/recall-types.d.ts +390 -0
  47. package/dist/api/recall-types.js +3 -0
  48. package/dist/api/recall.d.ts +36 -0
  49. package/dist/api/recall.js +499 -0
  50. package/dist/api/remember.d.ts +35 -0
  51. package/dist/api/remember.js +43 -0
  52. package/dist/api/sleep.d.ts +136 -0
  53. package/dist/api/sleep.js +256 -0
  54. package/dist/api/tokens.d.ts +26 -0
  55. package/dist/api/tokens.js +60 -0
  56. package/dist/api/types.d.ts +56 -0
  57. package/dist/api/types.js +38 -0
  58. package/dist/api.d.ts +20 -1262
  59. package/dist/api.js +25 -2727
  60. package/dist/audit.d.ts +3 -0
  61. package/dist/audit.js +6 -3
  62. package/dist/auth.d.ts +45 -4
  63. package/dist/auth.js +125 -48
  64. package/dist/autolearn.js +2 -2
  65. package/dist/capture/command.d.ts +33 -0
  66. package/dist/capture/command.js +264 -0
  67. package/dist/capture/compact.d.ts +44 -0
  68. package/dist/capture/compact.js +354 -0
  69. package/dist/capture/extract.d.ts +21 -0
  70. package/dist/capture/extract.js +464 -0
  71. package/dist/capture/transcript.d.ts +40 -0
  72. package/dist/capture/transcript.js +193 -0
  73. package/dist/capture-error.js +2 -1
  74. package/dist/churn-git.js +4 -2
  75. package/dist/cli/audit.d.ts +3 -0
  76. package/dist/cli/audit.js +159 -0
  77. package/dist/cli/auth.d.ts +2 -0
  78. package/dist/cli/auth.js +171 -0
  79. package/dist/cli/briefs.d.ts +4 -0
  80. package/dist/cli/briefs.js +453 -0
  81. package/dist/cli/card.d.ts +3 -0
  82. package/dist/cli/card.js +333 -0
  83. package/dist/cli/context.d.ts +15 -0
  84. package/dist/cli/context.js +376 -0
  85. package/dist/cli/continuity.d.ts +6 -0
  86. package/dist/cli/continuity.js +457 -0
  87. package/dist/cli/curate.d.ts +19 -0
  88. package/dist/cli/curate.js +556 -0
  89. package/dist/cli/dag.d.ts +5 -0
  90. package/dist/cli/dag.js +177 -0
  91. package/dist/cli/decisions.d.ts +4 -0
  92. package/dist/cli/decisions.js +528 -0
  93. package/dist/cli/eval.d.ts +5 -0
  94. package/dist/cli/eval.js +213 -0
  95. package/dist/cli/explain.d.ts +4 -0
  96. package/dist/cli/explain.js +156 -0
  97. package/dist/cli/goals.d.ts +2 -0
  98. package/dist/cli/goals.js +196 -0
  99. package/dist/cli/hook-blocks.d.ts +18 -0
  100. package/dist/cli/hook-blocks.js +233 -0
  101. package/dist/cli/init.d.ts +2 -0
  102. package/dist/cli/init.js +305 -0
  103. package/dist/cli/maintenance.d.ts +4 -0
  104. package/dist/cli/maintenance.js +190 -0
  105. package/dist/cli/playbooks.d.ts +4 -0
  106. package/dist/cli/playbooks.js +573 -0
  107. package/dist/cli/projects.js +9 -6
  108. package/dist/cli/recall.d.ts +7 -0
  109. package/dist/cli/recall.js +582 -0
  110. package/dist/cli/remember.d.ts +5 -0
  111. package/dist/cli/remember.js +485 -0
  112. package/dist/cli/serve.d.ts +5 -0
  113. package/dist/cli/serve.js +40 -0
  114. package/dist/cli/session-hooks.d.ts +29 -0
  115. package/dist/cli/session-hooks.js +647 -0
  116. package/dist/cli/setup.d.ts +4 -0
  117. package/dist/cli/setup.js +397 -0
  118. package/dist/cli/shared.d.ts +15 -19
  119. package/dist/cli/shared.js +50 -341
  120. package/dist/cli/slack.d.ts +2 -0
  121. package/dist/cli/slack.js +171 -0
  122. package/dist/cli/status.d.ts +18 -0
  123. package/dist/cli/status.js +409 -0
  124. package/dist/cli/transfer.d.ts +10 -0
  125. package/dist/cli/transfer.js +424 -0
  126. package/dist/cli/usage.d.ts +85 -0
  127. package/dist/cli/usage.js +741 -0
  128. package/dist/cli.d.ts +71 -120
  129. package/dist/cli.js +170 -8469
  130. package/dist/client.js +15 -8
  131. package/dist/compaction-record.d.ts +0 -2
  132. package/dist/compaction-record.js +7 -5
  133. package/dist/connectors/github/backfill.js +94 -87
  134. package/dist/connectors/github/cli-impl.js +4 -3
  135. package/dist/connectors/github/dlq.js +67 -54
  136. package/dist/connectors/github/ingest.js +34 -35
  137. package/dist/connectors/github/tenant-routing.js +3 -2
  138. package/dist/connectors/github/webhook.js +135 -216
  139. package/dist/connectors/slack/dlq.js +49 -61
  140. package/dist/connectors/slack/ingest.js +55 -48
  141. package/dist/connectors/slack/tenant-routing.js +4 -3
  142. package/dist/connectors/slack/webhook.js +72 -74
  143. package/dist/consolidate/conflicts.d.ts +10 -0
  144. package/dist/consolidate/conflicts.js +178 -0
  145. package/dist/consolidate/decay.d.ts +12 -0
  146. package/dist/consolidate/decay.js +145 -0
  147. package/dist/consolidate/llm-passes.d.ts +3 -0
  148. package/dist/consolidate/llm-passes.js +141 -0
  149. package/dist/consolidate/merge.d.ts +7 -0
  150. package/dist/consolidate/merge.js +251 -0
  151. package/dist/consolidate/physics-pass.d.ts +3 -0
  152. package/dist/consolidate/physics-pass.js +60 -0
  153. package/dist/consolidate/run.d.ts +69 -0
  154. package/dist/consolidate/run.js +76 -0
  155. package/dist/consolidate/sleep.d.ts +18 -0
  156. package/dist/consolidate/sleep.js +209 -0
  157. package/dist/consolidate/traces.d.ts +4 -0
  158. package/dist/consolidate/traces.js +178 -0
  159. package/dist/context-auto.js +7 -11
  160. package/dist/context-render.d.ts +1 -1
  161. package/dist/context-render.js +1 -1
  162. package/dist/customer-notes.d.ts +3 -0
  163. package/dist/customer-notes.js +83 -72
  164. package/dist/dag.js +225 -188
  165. package/dist/dashboard-actions.d.ts +20 -0
  166. package/dist/dashboard-actions.js +88 -0
  167. package/dist/dashboard-params.d.ts +45 -0
  168. package/dist/dashboard-params.js +127 -0
  169. package/dist/dashboard-queries.d.ts +15 -0
  170. package/dist/dashboard-queries.js +355 -0
  171. package/dist/dashboard-snapshot.d.ts +138 -0
  172. package/dist/dashboard-snapshot.js +308 -0
  173. package/dist/dashboard-types.d.ts +163 -0
  174. package/dist/dashboard-types.js +3 -0
  175. package/dist/dashboard.d.ts +6 -7
  176. package/dist/dashboard.js +228 -202
  177. package/dist/db/busy.d.ts +5 -0
  178. package/dist/db/busy.js +24 -0
  179. package/dist/db/continuity.d.ts +5 -0
  180. package/dist/db/continuity.js +145 -0
  181. package/dist/db/meta.d.ts +8 -0
  182. package/dist/db/meta.js +35 -0
  183. package/dist/db/migrate.d.ts +9 -0
  184. package/dist/db/migrate.js +138 -0
  185. package/dist/db/migrations/index.d.ts +5 -0
  186. package/dist/db/migrations/index.js +109 -0
  187. package/dist/db/migrations/types.d.ts +14 -0
  188. package/dist/db/migrations/types.js +2 -0
  189. package/dist/db/migrations/v01.d.ts +3 -0
  190. package/dist/db/migrations/v01.js +38 -0
  191. package/dist/db/migrations/v02.d.ts +3 -0
  192. package/dist/db/migrations/v02.js +21 -0
  193. package/dist/db/migrations/v03.d.ts +3 -0
  194. package/dist/db/migrations/v03.js +22 -0
  195. package/dist/db/migrations/v04.d.ts +3 -0
  196. package/dist/db/migrations/v04.js +28 -0
  197. package/dist/db/migrations/v05.d.ts +3 -0
  198. package/dist/db/migrations/v05.js +21 -0
  199. package/dist/db/migrations/v06.d.ts +3 -0
  200. package/dist/db/migrations/v06.js +25 -0
  201. package/dist/db/migrations/v07.d.ts +3 -0
  202. package/dist/db/migrations/v07.js +13 -0
  203. package/dist/db/migrations/v08.d.ts +3 -0
  204. package/dist/db/migrations/v08.js +8 -0
  205. package/dist/db/migrations/v09.d.ts +3 -0
  206. package/dist/db/migrations/v09.js +13 -0
  207. package/dist/db/migrations/v10.d.ts +3 -0
  208. package/dist/db/migrations/v10.js +17 -0
  209. package/dist/db/migrations/v11.d.ts +3 -0
  210. package/dist/db/migrations/v11.js +15 -0
  211. package/dist/db/migrations/v12.d.ts +3 -0
  212. package/dist/db/migrations/v12.js +11 -0
  213. package/dist/db/migrations/v13.d.ts +3 -0
  214. package/dist/db/migrations/v13.js +15 -0
  215. package/dist/db/migrations/v14.d.ts +3 -0
  216. package/dist/db/migrations/v14.js +66 -0
  217. package/dist/db/migrations/v15.d.ts +3 -0
  218. package/dist/db/migrations/v15.js +42 -0
  219. package/dist/db/migrations/v16.d.ts +3 -0
  220. package/dist/db/migrations/v16.js +61 -0
  221. package/dist/db/migrations/v17.d.ts +3 -0
  222. package/dist/db/migrations/v17.js +46 -0
  223. package/dist/db/migrations/v18.d.ts +3 -0
  224. package/dist/db/migrations/v18.js +60 -0
  225. package/dist/db/migrations/v19.d.ts +3 -0
  226. package/dist/db/migrations/v19.js +28 -0
  227. package/dist/db/migrations/v20.d.ts +3 -0
  228. package/dist/db/migrations/v20.js +42 -0
  229. package/dist/db/migrations/v21.d.ts +3 -0
  230. package/dist/db/migrations/v21.js +16 -0
  231. package/dist/db/migrations/v22.d.ts +3 -0
  232. package/dist/db/migrations/v22.js +82 -0
  233. package/dist/db/migrations/v23.d.ts +3 -0
  234. package/dist/db/migrations/v23.js +48 -0
  235. package/dist/db/migrations/v24.d.ts +3 -0
  236. package/dist/db/migrations/v24.js +72 -0
  237. package/dist/db/migrations/v25.d.ts +3 -0
  238. package/dist/db/migrations/v25.js +46 -0
  239. package/dist/db/migrations/v26.d.ts +3 -0
  240. package/dist/db/migrations/v26.js +23 -0
  241. package/dist/db/migrations/v27.d.ts +3 -0
  242. package/dist/db/migrations/v27.js +57 -0
  243. package/dist/db/migrations/v28.d.ts +3 -0
  244. package/dist/db/migrations/v28.js +38 -0
  245. package/dist/db/migrations/v29.d.ts +3 -0
  246. package/dist/db/migrations/v29.js +78 -0
  247. package/dist/db/migrations/v30.d.ts +3 -0
  248. package/dist/db/migrations/v30.js +92 -0
  249. package/dist/db/migrations/v31.d.ts +3 -0
  250. package/dist/db/migrations/v31.js +74 -0
  251. package/dist/db/migrations/v32.d.ts +3 -0
  252. package/dist/db/migrations/v32.js +102 -0
  253. package/dist/db/migrations/v33.d.ts +3 -0
  254. package/dist/db/migrations/v33.js +104 -0
  255. package/dist/db/migrations/v34.d.ts +3 -0
  256. package/dist/db/migrations/v34.js +93 -0
  257. package/dist/db/migrations/v35.d.ts +3 -0
  258. package/dist/db/migrations/v35.js +98 -0
  259. package/dist/db/migrations/v36.d.ts +3 -0
  260. package/dist/db/migrations/v36.js +98 -0
  261. package/dist/db/migrations/v37.d.ts +3 -0
  262. package/dist/db/migrations/v37.js +219 -0
  263. package/dist/db/migrations/v38.d.ts +3 -0
  264. package/dist/db/migrations/v38.js +277 -0
  265. package/dist/db/migrations/v39.d.ts +3 -0
  266. package/dist/db/migrations/v39.js +59 -0
  267. package/dist/db/migrations/v40.d.ts +3 -0
  268. package/dist/db/migrations/v40.js +74 -0
  269. package/dist/db/migrations/v41.d.ts +3 -0
  270. package/dist/db/migrations/v41.js +43 -0
  271. package/dist/db/migrations/v42.d.ts +3 -0
  272. package/dist/db/migrations/v42.js +41 -0
  273. package/dist/db/migrations/v43.d.ts +3 -0
  274. package/dist/db/migrations/v43.js +67 -0
  275. package/dist/db/migrations/v44.d.ts +3 -0
  276. package/dist/db/migrations/v44.js +28 -0
  277. package/dist/db/migrations/v45.d.ts +3 -0
  278. package/dist/db/migrations/v45.js +30 -0
  279. package/dist/db/migrations/v46.d.ts +3 -0
  280. package/dist/db/migrations/v46.js +25 -0
  281. package/dist/db/migrations/v47.d.ts +3 -0
  282. package/dist/db/migrations/v47.js +17 -0
  283. package/dist/db/migrations/v48.d.ts +3 -0
  284. package/dist/db/migrations/v48.js +10 -0
  285. package/dist/db/migrations/v49.d.ts +3 -0
  286. package/dist/db/migrations/v49.js +31 -0
  287. package/dist/db/migrations/v50.d.ts +3 -0
  288. package/dist/db/migrations/v50.js +67 -0
  289. package/dist/db/migrations/v51.d.ts +3 -0
  290. package/dist/db/migrations/v51.js +14 -0
  291. package/dist/db/migrations/v52.d.ts +3 -0
  292. package/dist/db/migrations/v52.js +7 -0
  293. package/dist/db/open.d.ts +23 -0
  294. package/dist/db/open.js +146 -0
  295. package/dist/db/sqlite.d.ts +21 -0
  296. package/dist/db/sqlite.js +8 -0
  297. package/dist/db/tables.d.ts +7 -0
  298. package/dist/db/tables.js +38 -0
  299. package/dist/db.d.ts +6 -46
  300. package/dist/db.js +5 -3049
  301. package/dist/decisions.d.ts +4 -1
  302. package/dist/decisions.js +102 -83
  303. package/dist/dedupe.js +3 -2
  304. package/dist/delivery-recorder.js +4 -1
  305. package/dist/doctor.js +14 -10
  306. package/dist/embedding-provider.d.ts +1 -1
  307. package/dist/embedding-provider.js +4 -3
  308. package/dist/embeddings.d.ts +9 -52
  309. package/dist/embeddings.js +43 -297
  310. package/dist/env.d.ts +75 -0
  311. package/dist/env.js +119 -0
  312. package/dist/eval-suite.js +1 -1
  313. package/dist/eval.js +2 -2
  314. package/dist/extract.js +1 -1
  315. package/dist/gated-write.js +3 -1
  316. package/dist/goals.d.ts +3 -1
  317. package/dist/goals.js +117 -86
  318. package/dist/graph/read.d.ts +73 -0
  319. package/dist/graph/read.js +325 -0
  320. package/dist/graph/rows.d.ts +45 -0
  321. package/dist/graph/rows.js +51 -0
  322. package/dist/graph/types.d.ts +83 -0
  323. package/dist/graph/types.js +11 -0
  324. package/dist/graph/write.d.ts +93 -0
  325. package/dist/{graph.js → graph/write.js} +6 -384
  326. package/dist/graph-extract.js +2 -1
  327. package/dist/graph-recall.d.ts +1 -1
  328. package/dist/graph-recall.js +2 -2
  329. package/dist/graph-stream.js +1 -1
  330. package/dist/graph-view.d.ts +1 -1
  331. package/dist/graph-view.js +1 -1
  332. package/dist/half-life-migration.d.ts +1 -1
  333. package/dist/half-life-migration.js +2 -1
  334. package/dist/hooks/codex-session.d.ts +8 -0
  335. package/dist/hooks/codex-session.js +76 -0
  336. package/dist/hooks/codex-wrapper.d.ts +55 -0
  337. package/dist/hooks/codex-wrapper.js +288 -0
  338. package/dist/hooks/json-hooks.d.ts +63 -0
  339. package/dist/hooks/json-hooks.js +356 -0
  340. package/dist/hooks/opencode.d.ts +50 -0
  341. package/dist/hooks/opencode.js +202 -0
  342. package/dist/hooks/shared.d.ts +54 -0
  343. package/dist/hooks/shared.js +77 -0
  344. package/dist/http-retry.d.ts +2 -0
  345. package/dist/http-retry.js +4 -3
  346. package/dist/http-util.d.ts +3 -0
  347. package/dist/http-util.js +10 -0
  348. package/dist/{importers.d.ts → importers/core.d.ts} +13 -17
  349. package/dist/importers/core.js +141 -0
  350. package/dist/importers/markdown-parse.d.ts +41 -0
  351. package/dist/importers/markdown-parse.js +132 -0
  352. package/dist/importers/markdown.d.ts +3 -0
  353. package/dist/importers/markdown.js +92 -0
  354. package/dist/importers/sources.d.ts +7 -0
  355. package/dist/importers/sources.js +229 -0
  356. package/dist/importers/vault.d.ts +11 -0
  357. package/dist/importers/vault.js +352 -0
  358. package/dist/incidents.d.ts +3 -0
  359. package/dist/incidents.js +52 -43
  360. package/dist/index.d.ts +25 -6
  361. package/dist/index.js +23 -6
  362. package/dist/invalidation.js +2 -1
  363. package/dist/judgment.js +2 -1
  364. package/dist/keyset.d.ts +13 -0
  365. package/dist/keyset.js +8 -0
  366. package/dist/local-embedding.d.ts +13 -0
  367. package/dist/local-embedding.js +165 -0
  368. package/dist/log.d.ts +7 -0
  369. package/dist/log.js +19 -1
  370. package/dist/mcp/admin-tools.d.ts +8 -0
  371. package/dist/mcp/admin-tools.js +116 -0
  372. package/dist/mcp/format.d.ts +27 -0
  373. package/dist/mcp/format.js +135 -0
  374. package/dist/mcp/memory-tools.d.ts +5 -0
  375. package/dist/mcp/memory-tools.js +83 -0
  376. package/dist/mcp/protocol.d.ts +83 -0
  377. package/dist/mcp/protocol.js +55 -0
  378. package/dist/mcp/recall-tools.d.ts +6 -0
  379. package/dist/mcp/recall-tools.js +320 -0
  380. package/dist/mcp/request.d.ts +10 -0
  381. package/dist/mcp/request.js +163 -0
  382. package/dist/mcp/server.d.ts +4 -75
  383. package/dist/mcp/server.js +7 -1190
  384. package/dist/mcp/session-state.d.ts +11 -0
  385. package/dist/mcp/session-state.js +28 -0
  386. package/dist/mcp/stdio.d.ts +8 -0
  387. package/dist/mcp/stdio.js +79 -0
  388. package/dist/mcp/tools.d.ts +11 -0
  389. package/dist/mcp/tools.js +247 -0
  390. package/dist/memory.d.ts +3 -0
  391. package/dist/memory.js +27 -1
  392. package/dist/multihop.d.ts +1 -1
  393. package/dist/multihop.js +2 -1
  394. package/dist/owner-validation.js +2 -1
  395. package/dist/physics-state.js +10 -7
  396. package/dist/policies.d.ts +3 -0
  397. package/dist/policies.js +93 -74
  398. package/dist/postinstall.js +3 -2
  399. package/dist/predictions/planning-fallacy.d.ts +100 -0
  400. package/dist/predictions/planning-fallacy.js +190 -0
  401. package/dist/{predictions.d.ts → predictions/store.d.ts} +7 -102
  402. package/dist/predictions/store.js +434 -0
  403. package/dist/processes.d.ts +3 -0
  404. package/dist/processes.js +94 -76
  405. package/dist/project-briefs.d.ts +3 -0
  406. package/dist/project-briefs.js +142 -113
  407. package/dist/project-identity.js +3 -2
  408. package/dist/project-merge.d.ts +12 -4
  409. package/dist/project-merge.js +132 -45
  410. package/dist/quarantine.d.ts +2 -1
  411. package/dist/quarantine.js +8 -5
  412. package/dist/raw-archive.js +1 -1
  413. package/dist/recall-history.js +3 -2
  414. package/dist/recall-pipeline.d.ts +2 -2
  415. package/dist/recall-pipeline.js +16 -8
  416. package/dist/recall-scope.js +1 -1
  417. package/dist/recall-trace.d.ts +1 -1
  418. package/dist/refine-llm.js +2 -1
  419. package/dist/reject-flow.js +6 -1
  420. package/dist/rerankers/clef.js +6 -5
  421. package/dist/rerankers/jev.d.ts +1 -1
  422. package/dist/rerankers/jev.js +4 -4
  423. package/dist/rerankers/llm.d.ts +4 -2
  424. package/dist/rerankers/llm.js +58 -38
  425. package/dist/rerankers/types.d.ts +1 -1
  426. package/dist/salience.js +1 -1
  427. package/dist/scheduler.d.ts +1 -0
  428. package/dist/scheduler.js +26 -4
  429. package/dist/scope.js +4 -3
  430. package/dist/search/as-of.d.ts +10 -0
  431. package/dist/search/as-of.js +22 -0
  432. package/dist/search/bm25-search.d.ts +14 -0
  433. package/dist/search/bm25-search.js +43 -0
  434. package/dist/search/bm25.d.ts +15 -0
  435. package/dist/search/bm25.js +54 -0
  436. package/dist/search/boosts.d.ts +54 -0
  437. package/dist/search/boosts.js +94 -0
  438. package/dist/search/breakdown.d.ts +7 -0
  439. package/dist/search/breakdown.js +20 -0
  440. package/dist/search/explain.d.ts +25 -0
  441. package/dist/search/explain.js +31 -0
  442. package/dist/search/finalize.d.ts +8 -0
  443. package/dist/search/finalize.js +52 -0
  444. package/dist/search/fusion.d.ts +27 -0
  445. package/dist/search/fusion.js +42 -0
  446. package/dist/search/hybrid-score.d.ts +20 -0
  447. package/dist/search/hybrid-score.js +73 -0
  448. package/dist/search/hybrid.d.ts +46 -0
  449. package/dist/search/hybrid.js +64 -0
  450. package/dist/search/physics-search.d.ts +29 -0
  451. package/dist/search/physics-search.js +162 -0
  452. package/dist/search/rerank.d.ts +10 -0
  453. package/dist/search/rerank.js +72 -0
  454. package/dist/search/temporal.d.ts +15 -0
  455. package/dist/search/temporal.js +45 -0
  456. package/dist/search/types.d.ts +91 -0
  457. package/dist/search/types.js +2 -0
  458. package/dist/search/vector.d.ts +30 -0
  459. package/dist/search/vector.js +71 -0
  460. package/dist/secret-detect.d.ts +2 -0
  461. package/dist/secret-detect.js +2 -1
  462. package/dist/server/auth.d.ts +52 -0
  463. package/dist/server/auth.js +221 -0
  464. package/dist/server/client-ip.d.ts +25 -0
  465. package/dist/server/client-ip.js +92 -0
  466. package/dist/server/cursor.d.ts +23 -0
  467. package/dist/server/cursor.js +58 -0
  468. package/dist/server/lifecycle.d.ts +7 -0
  469. package/dist/server/lifecycle.js +29 -0
  470. package/dist/server/mcp-http.d.ts +5 -0
  471. package/dist/server/mcp-http.js +199 -0
  472. package/dist/server/request.d.ts +33 -0
  473. package/dist/server/request.js +103 -0
  474. package/dist/server/routes/admin.d.ts +9 -0
  475. package/dist/server/routes/admin.js +158 -0
  476. package/dist/server/routes/customer-notes.d.ts +7 -0
  477. package/dist/server/routes/customer-notes.js +112 -0
  478. package/dist/server/routes/decisions.d.ts +7 -0
  479. package/dist/server/routes/decisions.js +133 -0
  480. package/dist/server/routes/incidents.d.ts +7 -0
  481. package/dist/server/routes/incidents.js +126 -0
  482. package/dist/server/routes/memories.d.ts +10 -0
  483. package/dist/server/routes/memories.js +179 -0
  484. package/dist/server/routes/policies.d.ts +8 -0
  485. package/dist/server/routes/policies.js +151 -0
  486. package/dist/server/routes/predictions.d.ts +7 -0
  487. package/dist/server/routes/predictions.js +164 -0
  488. package/dist/server/routes/processes.d.ts +7 -0
  489. package/dist/server/routes/processes.js +161 -0
  490. package/dist/server/routes/project-briefs.d.ts +8 -0
  491. package/dist/server/routes/project-briefs.js +136 -0
  492. package/dist/server/routes/recall.d.ts +8 -0
  493. package/dist/server/routes/recall.js +340 -0
  494. package/dist/server/routes/skills.d.ts +8 -0
  495. package/dist/server/routes/skills.js +150 -0
  496. package/dist/server/types.d.ts +56 -0
  497. package/dist/server/types.js +2 -0
  498. package/dist/server/validation.d.ts +14 -0
  499. package/dist/server/validation.js +91 -0
  500. package/dist/server.d.ts +7 -61
  501. package/dist/server.js +69 -2362
  502. package/dist/session-digest.d.ts +1 -1
  503. package/dist/session-digest.js +9 -2
  504. package/dist/shared.d.ts +12 -1
  505. package/dist/shared.js +33 -27
  506. package/dist/skills.d.ts +3 -0
  507. package/dist/skills.js +88 -70
  508. package/dist/stdin.js +2 -2
  509. package/dist/store/audit-event.d.ts +19 -0
  510. package/dist/store/audit-event.js +33 -0
  511. package/dist/store/candidates.d.ts +42 -0
  512. package/dist/store/candidates.js +152 -0
  513. package/dist/store/conflicts.d.ts +44 -0
  514. package/dist/store/conflicts.js +444 -0
  515. package/dist/store/delete-and-batch.d.ts +63 -0
  516. package/dist/store/delete-and-batch.js +310 -0
  517. package/dist/store/entry-reads.d.ts +92 -0
  518. package/dist/store/entry-reads.js +255 -0
  519. package/dist/store/entry-row.d.ts +67 -0
  520. package/dist/store/entry-row.js +208 -0
  521. package/dist/store/entry-writes.d.ts +46 -0
  522. package/dist/store/entry-writes.js +148 -0
  523. package/dist/store/handoffs.d.ts +26 -0
  524. package/dist/store/handoffs.js +184 -0
  525. package/dist/store/index-and-stats.d.ts +52 -0
  526. package/dist/store/index-and-stats.js +214 -0
  527. package/dist/store/markdown.d.ts +10 -0
  528. package/dist/store/markdown.js +108 -0
  529. package/dist/store/mirrors.d.ts +49 -0
  530. package/dist/store/mirrors.js +312 -0
  531. package/dist/store/open.d.ts +15 -0
  532. package/dist/store/open.js +223 -0
  533. package/dist/store/rows.d.ts +178 -0
  534. package/dist/store/rows.js +158 -0
  535. package/dist/store/search-rows.d.ts +86 -0
  536. package/dist/store/search-rows.js +260 -0
  537. package/dist/store/sessions.d.ts +83 -0
  538. package/dist/store/sessions.js +272 -0
  539. package/dist/store/summaries.d.ts +94 -0
  540. package/dist/store/summaries.js +377 -0
  541. package/dist/store/tenant-lookup.d.ts +12 -0
  542. package/dist/store/tenant-lookup.js +16 -0
  543. package/dist/store-cards.js +2 -1
  544. package/dist/summary-dirty.d.ts +4 -0
  545. package/dist/summary-dirty.js +32 -0
  546. package/dist/support-bundle.js +4 -3
  547. package/dist/tenant.js +2 -2
  548. package/dist/tokenize.d.ts +2 -0
  549. package/dist/tokenize.js +16 -0
  550. package/dist/transcript-tail.d.ts +7 -0
  551. package/dist/transcript-tail.js +48 -0
  552. package/dist/vector-store.d.ts +27 -0
  553. package/dist/vector-store.js +210 -0
  554. package/dist/version.d.ts +2 -2
  555. package/dist/version.js +2 -2
  556. package/dist/working-memory.js +1 -1
  557. package/dist/yaml.js +36 -11
  558. package/dist-ui/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  559. package/dist-ui/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  560. package/dist-ui/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  561. package/dist-ui/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  562. package/dist-ui/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  563. package/dist-ui/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  564. package/dist-ui/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  565. package/dist-ui/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  566. package/dist-ui/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  567. package/dist-ui/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  568. package/dist-ui/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  569. package/dist-ui/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  570. package/dist-ui/assets/index-DPN7cP19.js +33 -0
  571. package/dist-ui/assets/index-dFloKRVr.css +1 -0
  572. package/dist-ui/index.html +3 -25
  573. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  574. package/extensions/openclaw-plugin/package.json +1 -1
  575. package/openclaw.plugin.json +1 -1
  576. package/package.json +1 -1
  577. package/dist/capture.d.ts +0 -155
  578. package/dist/capture.js +0 -1295
  579. package/dist/consolidate.d.ts +0 -58
  580. package/dist/consolidate.js +0 -1124
  581. package/dist/graph.d.ts +0 -245
  582. package/dist/hooks.d.ts +0 -208
  583. package/dist/hooks.js +0 -1076
  584. package/dist/importers.js +0 -900
  585. package/dist/predictions.js +0 -620
  586. package/dist/search.d.ts +0 -320
  587. package/dist/search.js +0 -970
  588. package/dist/store.d.ts +0 -776
  589. package/dist/store.js +0 -3473
  590. package/dist-ui/assets/d3-BiWEKnn4.js +0 -1
  591. package/dist-ui/assets/index-BhT8RvO6.js +0 -61
  592. package/dist-ui/assets/index-RoXXJ5dq.css +0 -1
  593. package/dist-ui/assets/three-BDgTxR1l.js +0 -4112
package/dist/db.js CHANGED
@@ -1,3050 +1,6 @@
1
- import * as fs from 'fs';
2
- import * as os from 'os';
3
- import * as path from 'path';
4
- import { createRequire } from 'module';
5
- import { createPhysicsTable } from './physics-state.js';
6
- import { cleanupArchivedMirrors } from './raw-archive-mirror-cleanup.js';
7
- import { PACKAGE_VERSION, compareSemver } from './version.js';
8
- import { deriveOriginProject, originFromSource, isGlobalStoreRoot } from './project-identity.js';
9
- import { log } from './log.js';
10
- const require = createRequire(import.meta.url);
11
- // SAFETY: node:sqlite's DatabaseSync constructor genuinely has this shape at
12
- // runtime (Node's built-in synchronous SQLite module); there are no bundled
13
- // types for it here, so this require + cast is the module's documented boundary.
14
- const { DatabaseSync } = require('node:sqlite');
15
- const CURRENT_SCHEMA_VERSION = 51;
16
- const MIGRATIONS = [
17
- {
18
- version: 1,
19
- up: (db) => {
20
- db.exec(`
21
- CREATE TABLE IF NOT EXISTS memories (
22
- id TEXT PRIMARY KEY,
23
- created TEXT NOT NULL,
24
- last_retrieved TEXT NOT NULL,
25
- retrieval_count INTEGER NOT NULL,
26
- strength REAL NOT NULL,
27
- half_life_days REAL NOT NULL,
28
- layer TEXT NOT NULL,
29
- tags_json TEXT NOT NULL,
30
- emotional_valence TEXT NOT NULL,
31
- schema_fit REAL NOT NULL,
32
- source TEXT NOT NULL,
33
- outcome_score REAL,
34
- conflicts_with_json TEXT NOT NULL,
35
- pinned INTEGER NOT NULL,
36
- confidence TEXT NOT NULL,
37
- content TEXT NOT NULL,
38
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
39
- );
40
-
41
- CREATE INDEX IF NOT EXISTS idx_memories_layer_created ON memories(layer, created);
42
- CREATE INDEX IF NOT EXISTS idx_memories_last_retrieved ON memories(last_retrieved);
43
-
44
- CREATE TABLE IF NOT EXISTS consolidation_runs (
45
- id INTEGER PRIMARY KEY AUTOINCREMENT,
46
- timestamp TEXT NOT NULL,
47
- decayed INTEGER NOT NULL,
48
- merged INTEGER NOT NULL,
49
- removed INTEGER NOT NULL
50
- );
51
- `);
52
- },
53
- },
54
- {
55
- version: 2,
56
- up: (db) => {
57
- db.exec(`
58
- CREATE TABLE IF NOT EXISTS task_snapshots (
59
- id INTEGER PRIMARY KEY AUTOINCREMENT,
60
- task TEXT NOT NULL,
61
- summary TEXT NOT NULL,
62
- next_step TEXT NOT NULL,
63
- status TEXT NOT NULL,
64
- source TEXT NOT NULL,
65
- created_at TEXT NOT NULL,
66
- updated_at TEXT NOT NULL
67
- );
68
-
69
- CREATE INDEX IF NOT EXISTS idx_task_snapshots_status_updated
70
- ON task_snapshots(status, updated_at DESC, id DESC);
71
- `);
72
- },
73
- },
74
- {
75
- version: 3,
76
- up: (db) => {
77
- db.exec(`
78
- CREATE TABLE IF NOT EXISTS memory_conflicts (
79
- id INTEGER PRIMARY KEY AUTOINCREMENT,
80
- memory_a_id TEXT NOT NULL,
81
- memory_b_id TEXT NOT NULL,
82
- reason TEXT NOT NULL,
83
- score REAL NOT NULL DEFAULT 0,
84
- status TEXT NOT NULL,
85
- detected_at TEXT NOT NULL,
86
- updated_at TEXT NOT NULL,
87
- UNIQUE(memory_a_id, memory_b_id)
88
- );
89
-
90
- CREATE INDEX IF NOT EXISTS idx_memory_conflicts_status_updated
91
- ON memory_conflicts(status, updated_at DESC, id DESC);
92
- `);
93
- },
94
- },
95
- {
96
- version: 4,
97
- up: (db) => {
98
- if (!tableHasColumn(db, 'task_snapshots', 'session_id')) {
99
- db.exec(`ALTER TABLE task_snapshots ADD COLUMN session_id TEXT`);
100
- }
101
- db.exec(`
102
- CREATE TABLE IF NOT EXISTS session_events (
103
- id INTEGER PRIMARY KEY AUTOINCREMENT,
104
- session_id TEXT NOT NULL,
105
- task TEXT,
106
- event_type TEXT NOT NULL,
107
- content TEXT NOT NULL,
108
- source TEXT NOT NULL,
109
- metadata_json TEXT NOT NULL,
110
- created_at TEXT NOT NULL
111
- );
112
-
113
- CREATE INDEX IF NOT EXISTS idx_session_events_session_created
114
- ON session_events(session_id, created_at DESC, id DESC);
115
-
116
- CREATE INDEX IF NOT EXISTS idx_session_events_task_created
117
- ON session_events(task, created_at DESC, id DESC);
118
- `);
119
- },
120
- },
121
- {
122
- version: 5,
123
- up: (db) => {
124
- db.exec(`
125
- CREATE TABLE IF NOT EXISTS session_handoffs (
126
- id INTEGER PRIMARY KEY AUTOINCREMENT,
127
- session_id TEXT NOT NULL,
128
- repo_root TEXT,
129
- task_id TEXT,
130
- summary TEXT NOT NULL,
131
- next_action TEXT,
132
- artifacts_json TEXT NOT NULL DEFAULT '[]',
133
- created_at TEXT NOT NULL
134
- );
135
-
136
- CREATE INDEX IF NOT EXISTS idx_session_handoffs_session
137
- ON session_handoffs(session_id, created_at DESC);
138
- `);
139
- },
140
- },
141
- {
142
- version: 6,
143
- up: (db) => {
144
- db.exec(`
145
- CREATE TABLE IF NOT EXISTS working_memory (
146
- id INTEGER PRIMARY KEY AUTOINCREMENT,
147
- scope TEXT NOT NULL,
148
- session_id TEXT,
149
- task_id TEXT,
150
- importance REAL NOT NULL DEFAULT 0,
151
- content TEXT NOT NULL,
152
- metadata_json TEXT NOT NULL DEFAULT '{}',
153
- created_at TEXT NOT NULL,
154
- updated_at TEXT NOT NULL
155
- );
156
-
157
- CREATE INDEX IF NOT EXISTS idx_working_memory_scope
158
- ON working_memory(scope, importance DESC, created_at DESC);
159
-
160
- CREATE INDEX IF NOT EXISTS idx_working_memory_session
161
- ON working_memory(session_id, created_at DESC);
162
- `);
163
- },
164
- },
165
- {
166
- version: 7,
167
- up: (db) => {
168
- if (!tableHasColumn(db, 'memories', 'outcome_positive')) {
169
- db.exec(`ALTER TABLE memories ADD COLUMN outcome_positive INTEGER NOT NULL DEFAULT 0`);
170
- }
171
- if (!tableHasColumn(db, 'memories', 'outcome_negative')) {
172
- db.exec(`ALTER TABLE memories ADD COLUMN outcome_negative INTEGER NOT NULL DEFAULT 0`);
173
- }
174
- },
175
- },
176
- {
177
- version: 8,
178
- up: (db) => {
179
- createPhysicsTable(db);
180
- },
181
- },
182
- {
183
- version: 9,
184
- up: (db) => {
185
- if (!tableHasColumn(db, 'memories', 'parents_json')) {
186
- db.exec(`ALTER TABLE memories ADD COLUMN parents_json TEXT NOT NULL DEFAULT '[]'`);
187
- }
188
- if (!tableHasColumn(db, 'memories', 'starred')) {
189
- db.exec(`ALTER TABLE memories ADD COLUMN starred INTEGER NOT NULL DEFAULT 0`);
190
- }
191
- },
192
- },
193
- {
194
- version: 10,
195
- up: (db) => {
196
- if (!tableHasColumn(db, 'memories', 'trace_outcome')) {
197
- db.exec(`ALTER TABLE memories ADD COLUMN trace_outcome TEXT`);
198
- }
199
- if (!tableHasColumn(db, 'memories', 'source_session_id')) {
200
- db.exec(`ALTER TABLE memories ADD COLUMN source_session_id TEXT`);
201
- }
202
- db.exec(`
203
- CREATE INDEX IF NOT EXISTS idx_memories_source_session_id
204
- ON memories(source_session_id) WHERE source_session_id IS NOT NULL
205
- `);
206
- },
207
- },
208
- {
209
- version: 11,
210
- up: (db) => {
211
- if (!tableHasColumn(db, 'memories', 'valid_from')) {
212
- db.exec(`ALTER TABLE memories ADD COLUMN valid_from TEXT`);
213
- db.exec(`UPDATE memories SET valid_from = created WHERE valid_from IS NULL`);
214
- }
215
- if (!tableHasColumn(db, 'memories', 'superseded_by')) {
216
- db.exec(`ALTER TABLE memories ADD COLUMN superseded_by TEXT`);
217
- }
218
- db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_current ON memories(layer, created) WHERE superseded_by IS NULL`);
219
- },
220
- },
221
- {
222
- version: 12,
223
- up: (db) => {
224
- if (!tableHasColumn(db, 'memories', 'extracted_from')) {
225
- db.exec(`ALTER TABLE memories ADD COLUMN extracted_from TEXT`);
226
- }
227
- db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_extracted_from ON memories(extracted_from) WHERE extracted_from IS NOT NULL`);
228
- },
229
- },
230
- {
231
- version: 13,
232
- up: (db) => {
233
- if (!tableHasColumn(db, 'memories', 'dag_level')) {
234
- db.exec(`ALTER TABLE memories ADD COLUMN dag_level INTEGER NOT NULL DEFAULT 0`);
235
- }
236
- if (!tableHasColumn(db, 'memories', 'dag_parent_id')) {
237
- db.exec(`ALTER TABLE memories ADD COLUMN dag_parent_id TEXT`);
238
- }
239
- db.exec(`UPDATE memories SET dag_level = 1 WHERE extracted_from IS NOT NULL AND dag_level = 0`);
240
- db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_dag_parent ON memories(dag_parent_id) WHERE dag_parent_id IS NOT NULL`);
241
- },
242
- },
243
- {
244
- version: 14,
245
- up: (db) => {
246
- // A3 provenance envelope: kind, scope, owner, artifact_ref.
247
- // SQLite ALTER TABLE ADD COLUMN cannot add CHECK; CHECK enforcement lives
248
- // in INSERT/UPDATE triggers added later in this migration.
249
- if (!tableHasColumn(db, 'memories', 'kind')) {
250
- db.exec(`ALTER TABLE memories ADD COLUMN kind TEXT DEFAULT 'distilled'`);
251
- }
252
- if (!tableHasColumn(db, 'memories', 'scope')) {
253
- db.exec(`ALTER TABLE memories ADD COLUMN scope TEXT`);
254
- db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope) WHERE scope IS NOT NULL`);
255
- }
256
- if (!tableHasColumn(db, 'memories', 'owner')) {
257
- db.exec(`ALTER TABLE memories ADD COLUMN owner TEXT`);
258
- }
259
- if (!tableHasColumn(db, 'memories', 'artifact_ref')) {
260
- db.exec(`ALTER TABLE memories ADD COLUMN artifact_ref TEXT`);
261
- }
262
- // Backfill kind for any rows where it's NULL (pre-migration data).
263
- db.exec(`UPDATE memories SET kind = 'superseded' WHERE kind IS NULL AND superseded_by IS NOT NULL`);
264
- db.exec(`UPDATE memories SET kind = 'distilled' WHERE kind IS NULL`);
265
- // raw_archive: legitimate path for kind='raw' removal (used by archiveRawMemory).
266
- db.exec(`
267
- CREATE TABLE IF NOT EXISTS raw_archive (
268
- id INTEGER PRIMARY KEY AUTOINCREMENT,
269
- memory_id TEXT NOT NULL,
270
- archived_at TEXT NOT NULL,
271
- reason TEXT NOT NULL,
272
- archived_by TEXT,
273
- payload_json TEXT NOT NULL
274
- )
275
- `);
276
- db.exec(`CREATE INDEX IF NOT EXISTS idx_raw_archive_memory_id ON raw_archive(memory_id)`);
277
- // Append-only invariant: kind='raw' rows cannot be deleted directly.
278
- // Use raw_archive flow: archive-then-update-then-delete (see src/raw-archive.ts).
279
- db.exec(`
280
- CREATE TRIGGER IF NOT EXISTS trg_memories_raw_append_only
281
- BEFORE DELETE ON memories
282
- WHEN OLD.kind = 'raw'
283
- BEGIN
284
- SELECT RAISE(ABORT, 'raw is append-only');
285
- END
286
- `);
287
- // CHECK substitute: ALTER TABLE cannot add CHECK, so enforce kind allowed-set
288
- // via INSERT/UPDATE triggers.
289
- db.exec(`
290
- CREATE TRIGGER IF NOT EXISTS trg_memories_kind_check_insert
291
- BEFORE INSERT ON memories
292
- WHEN NEW.kind IS NOT NULL AND NEW.kind NOT IN ('raw','distilled','superseded','archived')
293
- BEGIN
294
- SELECT RAISE(ABORT, 'invalid kind: must be raw|distilled|superseded|archived');
295
- END
296
- `);
297
- db.exec(`
298
- CREATE TRIGGER IF NOT EXISTS trg_memories_kind_check_update
299
- BEFORE UPDATE ON memories
300
- WHEN NEW.kind IS NOT NULL AND NEW.kind NOT IN ('raw','distilled','superseded','archived')
301
- BEGIN
302
- SELECT RAISE(ABORT, 'invalid kind: must be raw|distilled|superseded|archived');
303
- END
304
- `);
305
- },
306
- },
307
- {
308
- version: 15,
309
- up: (db) => {
310
- // A3 hardening (post-review): close the NULL-kind bypass and add raw_archive
311
- // dedup safety. Both findings landed in /review on commits 41b1f4d..6456e7d.
312
- //
313
- // (1) Original v14 triggers used `WHEN NEW.kind IS NOT NULL AND NEW.kind NOT IN (...)`.
314
- // A direct INSERT/UPDATE setting kind=NULL bypassed the CHECK substitute. Replace
315
- // with `WHEN NEW.kind IS NULL OR NEW.kind NOT IN (...)` so NULL is rejected too.
316
- // (2) Add UNIQUE(memory_id, archived_at) to raw_archive so re-archiving the same id
317
- // in the same instant cannot produce ambiguous audit rows. Per-id history is still
318
- // allowed (different timestamps).
319
- db.exec(`DROP TRIGGER IF EXISTS trg_memories_kind_check_insert`);
320
- db.exec(`DROP TRIGGER IF EXISTS trg_memories_kind_check_update`);
321
- db.exec(`
322
- CREATE TRIGGER trg_memories_kind_check_insert
323
- BEFORE INSERT ON memories
324
- WHEN NEW.kind IS NULL OR NEW.kind NOT IN ('raw','distilled','superseded','archived')
325
- BEGIN
326
- SELECT RAISE(ABORT, 'invalid kind: must be raw|distilled|superseded|archived (not null)');
327
- END
328
- `);
329
- db.exec(`
330
- CREATE TRIGGER trg_memories_kind_check_update
331
- BEFORE UPDATE ON memories
332
- WHEN NEW.kind IS NULL OR NEW.kind NOT IN ('raw','distilled','superseded','archived')
333
- BEGIN
334
- SELECT RAISE(ABORT, 'invalid kind: must be raw|distilled|superseded|archived (not null)');
335
- END
336
- `);
337
- // Defensive: any rows that somehow have NULL kind get fixed (shouldn't exist post-v14
338
- // backfill, but cheap insurance).
339
- db.exec(`UPDATE memories SET kind = 'distilled' WHERE kind IS NULL`);
340
- // raw_archive uniqueness. SQLite cannot ADD CONSTRAINT, but a partial unique index
341
- // on (memory_id, archived_at) is equivalent for INSERT-time enforcement.
342
- db.exec(`
343
- CREATE UNIQUE INDEX IF NOT EXISTS idx_raw_archive_id_at
344
- ON raw_archive(memory_id, archived_at)
345
- `);
346
- },
347
- },
348
- {
349
- version: 16,
350
- up: (db) => {
351
- // A5 stub auth: add tenant_id to all data tables. Single-tenant per deployment;
352
- // multi-tenant enforcement deferred to v2 (full A5). The columns are needed now
353
- // so future B-track tables don't have to backfill.
354
- if (!tableHasColumn(db, 'memories', 'tenant_id')) {
355
- db.exec(`ALTER TABLE memories ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
356
- }
357
- if (!tableHasColumn(db, 'working_memory', 'tenant_id')) {
358
- db.exec(`ALTER TABLE working_memory ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
359
- }
360
- if (!tableHasColumn(db, 'consolidation_runs', 'tenant_id')) {
361
- db.exec(`ALTER TABLE consolidation_runs ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
362
- }
363
- if (!tableHasColumn(db, 'task_snapshots', 'tenant_id')) {
364
- db.exec(`ALTER TABLE task_snapshots ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
365
- }
366
- if (!tableHasColumn(db, 'memory_conflicts', 'tenant_id')) {
367
- db.exec(`ALTER TABLE memory_conflicts ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
368
- }
369
- // Composite indexes for recall hot paths. Leading column is tenant_id so
370
- // single-tenant lookups are O(log n).
371
- db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_tenant_created ON memories(tenant_id, created)`);
372
- db.exec(`CREATE INDEX IF NOT EXISTS idx_working_memory_tenant ON working_memory(tenant_id, importance DESC, created_at DESC)`);
373
- db.exec(`CREATE INDEX IF NOT EXISTS idx_consolidation_runs_tenant_ts ON consolidation_runs(tenant_id, timestamp DESC)`);
374
- db.exec(`CREATE INDEX IF NOT EXISTS idx_task_snapshots_tenant_status ON task_snapshots(tenant_id, status, updated_at DESC)`);
375
- // A5 stub auth: api_keys (scrypt-hashed; plaintext returned to caller exactly once)
376
- // and audit_log (append-only mutation trail). Both carry tenant_id from day 1 so
377
- // future multi-tenant enforcement is a config flip, not a re-migration.
378
- db.exec(`
379
- CREATE TABLE IF NOT EXISTS api_keys (
380
- id INTEGER PRIMARY KEY AUTOINCREMENT,
381
- key_id TEXT UNIQUE NOT NULL,
382
- key_hash TEXT NOT NULL,
383
- tenant_id TEXT NOT NULL DEFAULT 'default',
384
- label TEXT,
385
- created_at TEXT NOT NULL,
386
- revoked_at TEXT
387
- )
388
- `);
389
- db.exec(`
390
- CREATE INDEX IF NOT EXISTS idx_api_keys_tenant_active
391
- ON api_keys(tenant_id) WHERE revoked_at IS NULL
392
- `);
393
- db.exec(`
394
- CREATE TABLE IF NOT EXISTS audit_log (
395
- id INTEGER PRIMARY KEY AUTOINCREMENT,
396
- ts TEXT NOT NULL,
397
- tenant_id TEXT NOT NULL DEFAULT 'default',
398
- actor TEXT NOT NULL,
399
- op TEXT NOT NULL,
400
- target_id TEXT,
401
- metadata_json TEXT NOT NULL DEFAULT '{}'
402
- )
403
- `);
404
- db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_tenant_ts ON audit_log(tenant_id, ts DESC)`);
405
- },
406
- },
407
- {
408
- version: 17,
409
- up: (db) => {
410
- // E1.3 Slack ingestion: idempotency log, per-channel backfill cursors, DLQ.
411
- // See docs/plans/2026-04-29-e1.3-slack-ingestion.md.
412
- db.exec(`
413
- CREATE TABLE IF NOT EXISTS slack_event_log (
414
- event_id TEXT PRIMARY KEY,
415
- ingested_at TEXT NOT NULL,
416
- memory_id TEXT
417
- )
418
- `);
419
- db.exec(`CREATE INDEX IF NOT EXISTS idx_slack_event_log_memory ON slack_event_log(memory_id) WHERE memory_id IS NOT NULL`);
420
- db.exec(`
421
- CREATE TABLE IF NOT EXISTS slack_cursors (
422
- tenant_id TEXT NOT NULL,
423
- channel_id TEXT NOT NULL,
424
- latest_ts TEXT NOT NULL,
425
- updated_at TEXT NOT NULL,
426
- PRIMARY KEY (tenant_id, channel_id)
427
- )
428
- `);
429
- db.exec(`
430
- CREATE TABLE IF NOT EXISTS slack_dlq (
431
- id INTEGER PRIMARY KEY AUTOINCREMENT,
432
- tenant_id TEXT NOT NULL,
433
- raw_payload TEXT NOT NULL,
434
- error TEXT NOT NULL,
435
- received_at TEXT NOT NULL,
436
- retried_at TEXT
437
- )
438
- `);
439
- db.exec(`CREATE INDEX IF NOT EXISTS idx_slack_dlq_tenant_received ON slack_dlq(tenant_id, received_at)`);
440
- // Multi-tenant routing seam (review patch #6). Empty by default — single-
441
- // tenant deployments resolve via HIPPO_TENANT fallback. Multi-workspace
442
- // deployments populate this table to map team_id → tenant_id.
443
- db.exec(`
444
- CREATE TABLE IF NOT EXISTS slack_workspaces (
445
- team_id TEXT PRIMARY KEY,
446
- tenant_id TEXT NOT NULL,
447
- added_at TEXT NOT NULL
448
- )
449
- `);
450
- },
451
- },
452
- {
453
- version: 18,
454
- up: (db) => {
455
- db.exec(`
456
- CREATE TABLE IF NOT EXISTS goal_stack (
457
- id TEXT PRIMARY KEY,
458
- session_id TEXT NOT NULL,
459
- tenant_id TEXT NOT NULL DEFAULT 'default',
460
- goal_name TEXT NOT NULL,
461
- level INTEGER NOT NULL DEFAULT 0
462
- CHECK (level BETWEEN 0 AND 2),
463
- parent_goal_id TEXT REFERENCES goal_stack(id) ON DELETE SET NULL,
464
- status TEXT NOT NULL CHECK (status IN ('active','suspended','completed')),
465
- success_condition TEXT,
466
- retrieval_policy_id TEXT,
467
- created_at TEXT NOT NULL,
468
- completed_at TEXT,
469
- outcome_score REAL
470
- CHECK (outcome_score IS NULL OR (outcome_score >= 0 AND outcome_score <= 1))
471
- );
472
-
473
- CREATE INDEX IF NOT EXISTS idx_goal_stack_tenant_session_status
474
- ON goal_stack(tenant_id, session_id, status, created_at);
475
-
476
- CREATE TABLE IF NOT EXISTS retrieval_policy (
477
- id TEXT PRIMARY KEY,
478
- goal_id TEXT NOT NULL REFERENCES goal_stack(id) ON DELETE CASCADE,
479
- policy_type TEXT NOT NULL CHECK (policy_type IN
480
- ('schema-fit-biased','error-prioritized','recency-first','hybrid')),
481
- weight_schema_fit REAL NOT NULL DEFAULT 1.0
482
- CHECK (weight_schema_fit >= 0 AND weight_schema_fit <= 100),
483
- weight_recency REAL NOT NULL DEFAULT 1.0
484
- CHECK (weight_recency >= 0 AND weight_recency <= 100),
485
- weight_outcome REAL NOT NULL DEFAULT 1.0
486
- CHECK (weight_outcome >= 0 AND weight_outcome <= 100),
487
- error_priority REAL NOT NULL DEFAULT 1.0
488
- CHECK (error_priority >= 0 AND error_priority <= 100)
489
- );
490
-
491
- CREATE INDEX IF NOT EXISTS idx_retrieval_policy_goal
492
- ON retrieval_policy(goal_id);
493
-
494
- CREATE TABLE IF NOT EXISTS goal_recall_log (
495
- id INTEGER PRIMARY KEY AUTOINCREMENT,
496
- goal_id TEXT NOT NULL REFERENCES goal_stack(id) ON DELETE CASCADE,
497
- memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
498
- tenant_id TEXT NOT NULL DEFAULT 'default',
499
- session_id TEXT NOT NULL,
500
- recalled_at TEXT NOT NULL,
501
- score REAL NOT NULL
502
- );
503
-
504
- CREATE INDEX IF NOT EXISTS idx_goal_recall_log_goal
505
- ON goal_recall_log(goal_id);
506
- CREATE UNIQUE INDEX IF NOT EXISTS uniq_goal_recall_log_memory_goal
507
- ON goal_recall_log(memory_id, goal_id);
508
- `);
509
- },
510
- },
511
- {
512
- version: 19,
513
- up: (db) => {
514
- // v0.39 commit 3 (Slack hardening): widen slack_dlq with bucketing,
515
- // retry tracking, and the signature/timestamp pair that lets `hippo
516
- // slack dlq replay` re-verify before re-running ingest. ALTER ADD
517
- // COLUMN with DEFAULT is non-destructive — legacy rows take the
518
- // default values. Idempotent via tableHasColumn().
519
- if (!tableHasColumn(db, 'slack_dlq', 'team_id')) {
520
- db.exec(`ALTER TABLE slack_dlq ADD COLUMN team_id TEXT`);
521
- }
522
- if (!tableHasColumn(db, 'slack_dlq', 'bucket')) {
523
- db.exec(`ALTER TABLE slack_dlq ADD COLUMN bucket TEXT NOT NULL DEFAULT 'parse_error'`);
524
- // SQLite ALTER TABLE cannot add CHECK; bucket value enforcement is app-level.
525
- }
526
- if (!tableHasColumn(db, 'slack_dlq', 'retry_count')) {
527
- db.exec(`ALTER TABLE slack_dlq ADD COLUMN retry_count INTEGER NOT NULL DEFAULT 0`);
528
- }
529
- if (!tableHasColumn(db, 'slack_dlq', 'signature')) {
530
- db.exec(`ALTER TABLE slack_dlq ADD COLUMN signature TEXT`);
531
- }
532
- if (!tableHasColumn(db, 'slack_dlq', 'slack_timestamp')) {
533
- db.exec(`ALTER TABLE slack_dlq ADD COLUMN slack_timestamp TEXT`);
534
- }
535
- },
536
- },
537
- {
538
- version: 20,
539
- up: (db) => {
540
- // v0.39 commit 4 (GDPR Path A backfill): redact every existing
541
- // raw_archive.payload_json so historical archives match the new
542
- // metadata-only contract from src/raw-archive.ts. Read each row, parse
543
- // the existing JSON to extract tenant_id and kind (best effort), then
544
- // UPDATE with the redacted shape. Rows with unparseable legacy JSON get
545
- // redacted with tenant_id='unknown', kind='unknown'. The audit_log
546
- // remains the compliance record.
547
- // SAFETY: rows' shape matches the four columns named in the SELECT above.
548
- const rows = db
549
- .prepare(`SELECT id, archived_at, reason, payload_json FROM raw_archive`)
550
- .all();
551
- const update = db.prepare(`UPDATE raw_archive SET payload_json = ? WHERE id = ?`);
552
- for (const row of rows) {
553
- let tenant = 'unknown';
554
- let kind = 'unknown';
555
- try {
556
- // SAFETY: parsed is a best-effort JSON.parse of legacy payload_json;
557
- // an unexpected shape only yields undefined fields (falling back to
558
- // 'unknown' below), and a parse failure is caught, so this never crashes.
559
- const parsed = JSON.parse(row.payload_json);
560
- tenant = parsed.tenant_id ?? 'unknown';
561
- kind = parsed.kind ?? 'unknown';
562
- }
563
- catch {
564
- // Unparseable legacy payload — redact with unknowns.
565
- }
566
- const redacted = JSON.stringify({
567
- redacted: true,
568
- archived_at: row.archived_at,
569
- tenant_id: tenant,
570
- kind,
571
- reason: row.reason,
572
- migration: 'v20_redact',
573
- });
574
- update.run(redacted, row.id);
575
- }
576
- },
577
- },
578
- {
579
- version: 21,
580
- up: (db) => {
581
- // v0.39 codex round 3: per-row mirror cleanup tracking. Replaces the
582
- // global gdpr_v20_mirror_cleanup meta gate (which made the reaper
583
- // one-shot and silently swallowed failed unlinks). With this column the
584
- // reaper processes only rows WHERE mirror_cleaned_at IS NULL, sets the
585
- // timestamp on success, and leaves it NULL on any unlink failure so the
586
- // next openHippoDb retries automatically.
587
- if (!tableHasColumn(db, 'raw_archive', 'mirror_cleaned_at')) {
588
- db.exec(`ALTER TABLE raw_archive ADD COLUMN mirror_cleaned_at TEXT`);
589
- }
590
- },
591
- },
592
- {
593
- version: 22,
594
- up: (db) => {
595
- // Tenant-isolation gap on continuity tables (codex review 2026-05-02).
596
- // session_events and session_handoffs predate the v16 tenant migration
597
- // and were never added to it, so the v0.40.0 provenance gate work
598
- // exposed a real cross-tenant leak when continuity primitives are used.
599
- // Adds tenant_id (NOT NULL DEFAULT 'default') with smart backfill from
600
- // task_snapshots.session_id when unambiguous, plus an optional scope
601
- // column so a private-channel-derived handoff can default-deny via the
602
- // same rule recall already enforces.
603
- //
604
- // Self-heal partial-init stores: re-run the v4/v5 CREATE TABLE IF NOT
605
- // EXISTS bodies before ALTERing. A silent skip would otherwise stamp
606
- // schema_version=22 on a DB missing the underlying tables, leaving
607
- // them permanently absent.
608
- db.exec(`
609
- CREATE TABLE IF NOT EXISTS session_events (
610
- id INTEGER PRIMARY KEY AUTOINCREMENT,
611
- session_id TEXT NOT NULL,
612
- task TEXT,
613
- event_type TEXT NOT NULL,
614
- content TEXT NOT NULL,
615
- source TEXT NOT NULL,
616
- metadata_json TEXT NOT NULL,
617
- created_at TEXT NOT NULL
618
- );
619
- CREATE TABLE IF NOT EXISTS session_handoffs (
620
- id INTEGER PRIMARY KEY AUTOINCREMENT,
621
- session_id TEXT NOT NULL,
622
- repo_root TEXT,
623
- task_id TEXT,
624
- summary TEXT NOT NULL,
625
- next_action TEXT,
626
- artifacts_json TEXT NOT NULL DEFAULT '[]',
627
- created_at TEXT NOT NULL
628
- );
629
- `);
630
- if (!tableHasColumn(db, 'session_events', 'tenant_id')) {
631
- db.exec(`ALTER TABLE session_events ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
632
- }
633
- if (!tableHasColumn(db, 'session_events', 'scope')) {
634
- db.exec(`ALTER TABLE session_events ADD COLUMN scope TEXT`);
635
- }
636
- if (!tableHasColumn(db, 'session_handoffs', 'tenant_id')) {
637
- db.exec(`ALTER TABLE session_handoffs ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
638
- }
639
- if (!tableHasColumn(db, 'session_handoffs', 'scope')) {
640
- db.exec(`ALTER TABLE session_handoffs ADD COLUMN scope TEXT`);
641
- }
642
- // Smart backfill: rows whose session_id maps to exactly one tenant in
643
- // task_snapshots inherit that tenant. Ambiguous or unmapped rows stay
644
- // at the column default ('default'). Conservative: never crosses
645
- // tenant boundaries on guesses. The COUNT(DISTINCT) gate is the load-
646
- // bearing check; without it, rows with multiple tenants under the same
647
- // session_id would silently pick whichever group came first.
648
- db.exec(`
649
- UPDATE session_events
650
- SET tenant_id = (
651
- SELECT MAX(t.tenant_id) FROM task_snapshots t
652
- WHERE t.session_id = session_events.session_id
653
- )
654
- WHERE tenant_id = 'default'
655
- AND (SELECT COUNT(DISTINCT t.tenant_id) FROM task_snapshots t
656
- WHERE t.session_id = session_events.session_id) = 1
657
- `);
658
- db.exec(`
659
- UPDATE session_handoffs
660
- SET tenant_id = (
661
- SELECT MAX(t.tenant_id) FROM task_snapshots t
662
- WHERE t.session_id = session_handoffs.session_id
663
- )
664
- WHERE tenant_id = 'default'
665
- AND (SELECT COUNT(DISTINCT t.tenant_id) FROM task_snapshots t
666
- WHERE t.session_id = session_handoffs.session_id) = 1
667
- `);
668
- db.exec(`CREATE INDEX IF NOT EXISTS idx_session_events_tenant_session ON session_events(tenant_id, session_id, created_at DESC, id DESC)`);
669
- db.exec(`CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_session ON session_handoffs(tenant_id, session_id, created_at DESC)`);
670
- },
671
- },
672
- {
673
- version: 23,
674
- up: (db) => {
675
- // task_snapshots: add scope so all three continuity tables carry it.
676
- // Self-heal partial-init stores via CREATE TABLE IF NOT EXISTS (the v22
677
- // session_events / session_handoffs healing is upstream).
678
- db.exec(`
679
- CREATE TABLE IF NOT EXISTS task_snapshots (
680
- id INTEGER PRIMARY KEY AUTOINCREMENT,
681
- task TEXT NOT NULL,
682
- summary TEXT NOT NULL,
683
- next_step TEXT NOT NULL,
684
- status TEXT NOT NULL,
685
- source TEXT NOT NULL,
686
- session_id TEXT,
687
- tenant_id TEXT NOT NULL DEFAULT 'default',
688
- created_at TEXT NOT NULL,
689
- updated_at TEXT NOT NULL
690
- )
691
- `);
692
- if (!tableHasColumn(db, 'task_snapshots', 'scope')) {
693
- db.exec(`ALTER TABLE task_snapshots ADD COLUMN scope TEXT`);
694
- }
695
- // Quarantine policy (codex round 1 P1): pre-existing continuity rows
696
- // with NULL scope cannot be safely classified as public after the fact.
697
- // Mark them 'unknown:legacy' so the api.recall + cmdRecall default-deny
698
- // filter excludes them for no-scope callers. Fresh rows from new
699
- // writers carry NULL when scope is unspecified (legitimate non-Slack
700
- // writes); the 'unknown:legacy' marker is a v23-only one-shot for
701
- // pre-upgrade rows.
702
- //
703
- // Run UPDATEs only on tables that exist (some test paths and edge stores
704
- // skip v22's table healing). The UPDATEs are themselves idempotent via
705
- // the WHERE scope IS NULL clause, so re-running is a no-op.
706
- if (tableExists(db, 'task_snapshots')) {
707
- db.exec(`UPDATE task_snapshots SET scope = 'unknown:legacy' WHERE scope IS NULL`);
708
- }
709
- if (tableExists(db, 'session_events')) {
710
- db.exec(`UPDATE session_events SET scope = 'unknown:legacy' WHERE scope IS NULL`);
711
- }
712
- if (tableExists(db, 'session_handoffs')) {
713
- db.exec(`UPDATE session_handoffs SET scope = 'unknown:legacy' WHERE scope IS NULL`);
714
- }
715
- db.exec(`CREATE INDEX IF NOT EXISTS idx_task_snapshots_tenant_scope ON task_snapshots(tenant_id, scope, status)`);
716
- },
717
- },
718
- {
719
- version: 24,
720
- up: (db) => {
721
- // v1.3.0 GitHub connector schema (codex round 1, 2026-05-04).
722
- // Six tables + a min_compatible_binary meta row for rollback safety.
723
- db.exec(`
724
- CREATE TABLE IF NOT EXISTS github_event_log (
725
- idempotency_key TEXT PRIMARY KEY,
726
- delivery_id TEXT NOT NULL,
727
- event_name TEXT NOT NULL,
728
- ingested_at TEXT NOT NULL,
729
- memory_id TEXT
730
- )
731
- `);
732
- db.exec(`CREATE INDEX IF NOT EXISTS idx_github_event_log_memory ON github_event_log(memory_id) WHERE memory_id IS NOT NULL`);
733
- db.exec(`CREATE INDEX IF NOT EXISTS idx_github_event_log_delivery ON github_event_log(delivery_id)`);
734
- db.exec(`
735
- CREATE TABLE IF NOT EXISTS github_cursors (
736
- tenant_id TEXT NOT NULL,
737
- repo_full_name TEXT NOT NULL,
738
- issues_hwm TEXT,
739
- issue_comments_hwm TEXT,
740
- pr_review_comments_hwm TEXT,
741
- updated_at TEXT NOT NULL,
742
- PRIMARY KEY (tenant_id, repo_full_name)
743
- )
744
- `);
745
- db.exec(`
746
- CREATE TABLE IF NOT EXISTS github_dlq (
747
- id INTEGER PRIMARY KEY AUTOINCREMENT,
748
- tenant_id TEXT NOT NULL,
749
- raw_payload TEXT NOT NULL,
750
- error TEXT NOT NULL,
751
- event_name TEXT,
752
- delivery_id TEXT,
753
- signature TEXT,
754
- installation_id TEXT,
755
- repo_full_name TEXT,
756
- retry_count INTEGER NOT NULL DEFAULT 0,
757
- received_at TEXT NOT NULL,
758
- retried_at TEXT,
759
- bucket TEXT NOT NULL DEFAULT 'parse_error'
760
- )
761
- `);
762
- db.exec(`CREATE INDEX IF NOT EXISTS idx_github_dlq_tenant_received ON github_dlq(tenant_id, received_at)`);
763
- db.exec(`
764
- CREATE TABLE IF NOT EXISTS github_installations (
765
- installation_id TEXT PRIMARY KEY,
766
- tenant_id TEXT NOT NULL,
767
- added_at TEXT NOT NULL
768
- )
769
- `);
770
- // PAT-mode multi-tenant routing (codex P0 #4). Maps repo_full_name to
771
- // tenant when the webhook envelope has no `installation` field. Composite
772
- // PK so the same repo can intentionally be visible to multiple tenants
773
- // (e.g., shared tooling accounts) — collision is on (repo, tenant) pair.
774
- db.exec(`
775
- CREATE TABLE IF NOT EXISTS github_repositories (
776
- repo_full_name TEXT NOT NULL,
777
- tenant_id TEXT NOT NULL,
778
- added_at TEXT NOT NULL,
779
- PRIMARY KEY (repo_full_name, tenant_id)
780
- )
781
- `);
782
- // Rollback-safety guard (codex P0 #2). Any binary < 1.2.1 lacks the
783
- // generic *:private:* default-deny and would leak github:private:* rows
784
- // if it opened this DB. The startup guard in v1.2.1+ refuses to open a
785
- // DB whose min_compatible_binary is newer than its own version.
786
- db.prepare(`INSERT INTO meta(key, value) VALUES('min_compatible_binary', ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run('1.2.1');
787
- },
788
- },
789
- {
790
- version: 25,
791
- up: (db) => {
792
- // v1.5.0 DAG-aware recall — cache summary metadata so the assembler can
793
- // reason about scope without re-walking the DAG. Three additive,
794
- // optional columns. No min_compatible_binary bump: these columns are
795
- // pure metadata; older binaries opening this DB will see them as NULL
796
- // / 0 and behave as before. See docs/plans/2026-05-05-dag-recall.md
797
- // Task 1.
798
- if (!tableHasColumn(db, 'memories', 'descendant_count')) {
799
- db.exec(`ALTER TABLE memories ADD COLUMN descendant_count INTEGER NOT NULL DEFAULT 0`);
800
- }
801
- if (!tableHasColumn(db, 'memories', 'earliest_at')) {
802
- db.exec(`ALTER TABLE memories ADD COLUMN earliest_at TEXT`);
803
- }
804
- if (!tableHasColumn(db, 'memories', 'latest_at')) {
805
- db.exec(`ALTER TABLE memories ADD COLUMN latest_at TEXT`);
806
- }
807
- // Backfill descendant_count for existing level-2 summary rows. Use
808
- // dag_parent_id pointing at the summary id. Level-3 (entity profiles)
809
- // not built today; their descendant_count stays at default 0.
810
- db.exec(`
811
- UPDATE memories
812
- SET descendant_count = (
813
- SELECT COUNT(*) FROM memories AS c WHERE c.dag_parent_id = memories.id
814
- )
815
- WHERE dag_level >= 2
816
- AND descendant_count = 0
817
- `);
818
- // Backfill earliest_at / latest_at from child created timestamps for
819
- // existing summaries. Children of a level-2 summary are level-1 facts.
820
- db.exec(`
821
- UPDATE memories
822
- SET earliest_at = (
823
- SELECT MIN(c.created) FROM memories AS c WHERE c.dag_parent_id = memories.id
824
- ),
825
- latest_at = (
826
- SELECT MAX(c.created) FROM memories AS c WHERE c.dag_parent_id = memories.id
827
- )
828
- WHERE dag_level >= 2
829
- AND earliest_at IS NULL
830
- `);
831
- },
832
- },
833
- {
834
- version: 26,
835
- up: (db) => {
836
- // v1.12.0 A5 v2 sub-1: add role column to api_keys for the admin/member
837
- // distinction that gates /v1/sleep. Additive — existing keys backfill to
838
- // 'admin' via DEFAULT (single-tenant operator = admin by definition).
839
- // No min_compatible_binary bump: old binaries (v1.11.x) ignore the
840
- // column on SELECTs that don't name it; new binaries on old data run
841
- // this migration at openHippoDb time before any createApiKey call.
842
- //
843
- // v1.12.7 defensive: also guard on tableExists. If a DB landed in the
844
- // partial-v16-state (api_keys table missing despite schema_version >= 16),
845
- // running ALTER TABLE here would crash and block all later migrations.
846
- // The v27 heal below recreates api_keys with the role column already
847
- // present, so this ALTER becomes a no-op anyway for that path. Defense
848
- // in depth: guard so v26 never crashes on the partial-apply state.
849
- if (tableExists(db, 'api_keys') && !tableHasColumn(db, 'api_keys', 'role')) {
850
- db.exec(`ALTER TABLE api_keys ADD COLUMN role TEXT NOT NULL DEFAULT 'admin'`);
851
- }
852
- },
853
- },
854
- {
855
- version: 27,
856
- up: (db) => {
857
- // v1.12.7 self-heal — re-assert the v16 schema (api_keys + audit_log).
858
- //
859
- // Surfaced 2026-05-24 on Keith's ~/.hippo/hippo.db: schema_version
860
- // recorded as 25 but api_keys and audit_log tables were missing from
861
- // migration v16. Root cause unknown — the migration runner has wrapped
862
- // each migration in BEGIN/COMMIT since the first SQLite commit, so
863
- // atomicity isn't the bug. Possible causes: DROP TABLE post-migration,
864
- // SQL import / restore from a pre-v16 backup over a v16+ schema_version,
865
- // or some edge case the wrapping doesn't catch. Cause may be operator
866
- // action; either way the practical fix is the same.
867
- //
868
- // All CREATE IF NOT EXISTS — zero-cost no-op for users without the
869
- // bug, fixes anyone who has it. Includes the role column from the
870
- // start so it matches v26's intent without needing v26 to ALTER on
871
- // this heal path.
872
- db.exec(`
873
- CREATE TABLE IF NOT EXISTS api_keys (
874
- id INTEGER PRIMARY KEY AUTOINCREMENT,
875
- key_id TEXT UNIQUE NOT NULL,
876
- key_hash TEXT NOT NULL,
877
- tenant_id TEXT NOT NULL DEFAULT 'default',
878
- label TEXT,
879
- created_at TEXT NOT NULL,
880
- revoked_at TEXT,
881
- role TEXT NOT NULL DEFAULT 'admin'
882
- )
883
- `);
884
- db.exec(`
885
- CREATE INDEX IF NOT EXISTS idx_api_keys_tenant_active
886
- ON api_keys(tenant_id) WHERE revoked_at IS NULL
887
- `);
888
- db.exec(`
889
- CREATE TABLE IF NOT EXISTS audit_log (
890
- id INTEGER PRIMARY KEY AUTOINCREMENT,
891
- ts TEXT NOT NULL,
892
- tenant_id TEXT NOT NULL DEFAULT 'default',
893
- actor TEXT NOT NULL,
894
- op TEXT NOT NULL,
895
- target_id TEXT,
896
- metadata_json TEXT NOT NULL DEFAULT '{}'
897
- )
898
- `);
899
- db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_tenant_ts ON audit_log(tenant_id, ts DESC)`);
900
- // Belt-and-braces: if api_keys existed before this migration WITHOUT
901
- // the role column (i.e. v16-shape table that v26's ALTER skipped due
902
- // to tableExists=false on an earlier broken run, then someone manually
903
- // CREATEd it without role), backfill role.
904
- if (tableExists(db, 'api_keys') && !tableHasColumn(db, 'api_keys', 'role')) {
905
- db.exec(`ALTER TABLE api_keys ADD COLUMN role TEXT NOT NULL DEFAULT 'admin'`);
906
- }
907
- },
908
- },
909
- {
910
- version: 28,
911
- up: (db) => {
912
- // E1 of 5-episode DAG live-coupling arc (docs/plans/2026-05-25-dag-e1-schema-v28.md).
913
- // Adds dirty-flag persistence for the existing DAG layer's level-2
914
- // summaries so E2 (child-write propagation) and E3 (sleep-cycle
915
- // rebuild) have somewhere to write + read the staleness signal. Adds
916
- // dag_level_3_built_at as a column (Keith Q5 pick: lets E5 entity-
917
- // profile build path land without a second migration).
918
- //
919
- // All columns are additive + DEFAULTed/nullable, so backfill is
920
- // automatic for existing rows. No min_compatible_binary bump: old
921
- // binaries (v1.12.x) ignore the columns on SELECTs that don't name
922
- // them; new binaries on old data hit this migration at openHippoDb
923
- // time before any DAG path touches summary_dirty.
924
- //
925
- // Precedent for column-only guards on memories: v25 (db.ts:827),
926
- // which added the DAG cache columns the same way. memories table
927
- // itself comes from v1 and is always present, so the tableExists
928
- // half of the v26/v27 guard pattern isn't load-bearing here.
929
- if (!tableHasColumn(db, 'memories', 'summary_dirty')) {
930
- db.exec(`ALTER TABLE memories ADD COLUMN summary_dirty INTEGER NOT NULL DEFAULT 0`);
931
- }
932
- if (!tableHasColumn(db, 'memories', 'last_rebuilt_at')) {
933
- db.exec(`ALTER TABLE memories ADD COLUMN last_rebuilt_at TEXT`);
934
- }
935
- if (!tableHasColumn(db, 'memories', 'rebuild_count')) {
936
- db.exec(`ALTER TABLE memories ADD COLUMN rebuild_count INTEGER NOT NULL DEFAULT 0`);
937
- }
938
- if (!tableHasColumn(db, 'memories', 'dag_level_3_built_at')) {
939
- // Reserved for E5: buildEntityProfiles will set this on level-3
940
- // rows when they're created. Always NULL for level 0/1/2 rows.
941
- db.exec(`ALTER TABLE memories ADD COLUMN dag_level_3_built_at TEXT`);
942
- }
943
- },
944
- },
945
- {
946
- version: 29,
947
- up: (db) => {
948
- // E2 prediction first-class object (docs/plans/2026-05-26-e2-prediction-object.md).
949
- // Adds a canonical predictions table for J3 reference-class /
950
- // planning-fallacy detector (a follow-up episode). Predictions
951
- // duplicate claim_text in the table itself so memory deletion
952
- // (forget/consolidate/archive) does not lose prediction data; FK
953
- // memory_id is NULLABLE with ON DELETE SET NULL.
954
- //
955
- // Cross-tenant safety: BEFORE INSERT + BEFORE UPDATE triggers
956
- // enforce tenant_id match against the referenced memory. SQLite's
957
- // ON DELETE SET NULL is incompatible with composite FK where one
958
- // side is NOT NULL, so the trigger pattern replaces a composite
959
- // FK target. Precedent: v14 memories.kind trigger pair at db.ts:298-322.
960
- //
961
- // CHECK constraint pins closure_state to (open|closed|closed-unknown).
962
- // J3 computes accuracy (clean vs regressed) from (estimate_value,
963
- // actual_value) at query time.
964
- if (!tableExists(db, 'predictions')) {
965
- db.exec(`
966
- CREATE TABLE predictions (
967
- id INTEGER PRIMARY KEY AUTOINCREMENT,
968
- memory_id TEXT,
969
- tenant_id TEXT NOT NULL,
970
- class_tag TEXT NOT NULL,
971
- claim_text TEXT NOT NULL,
972
- estimate_value REAL,
973
- estimate_unit TEXT,
974
- target_date TEXT,
975
- actual_value REAL,
976
- closure_state TEXT NOT NULL DEFAULT 'open'
977
- CHECK (closure_state IN ('open', 'closed', 'closed-unknown')),
978
- closed_at TEXT,
979
- closure_note TEXT,
980
- created_at TEXT NOT NULL,
981
- FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL
982
- )
983
- `);
984
- db.exec(`
985
- CREATE INDEX IF NOT EXISTS idx_predictions_tenant_class
986
- ON predictions(tenant_id, class_tag, closure_state)
987
- `);
988
- db.exec(`
989
- CREATE INDEX IF NOT EXISTS idx_predictions_memory
990
- ON predictions(memory_id) WHERE memory_id IS NOT NULL
991
- `);
992
- // Cross-tenant safety: tenant_id must match the referenced memory's
993
- // tenant_id when memory_id IS NOT NULL. INSERT + UPDATE pair, mirroring
994
- // v14 memories.kind enforcement (db.ts:298-322).
995
- db.exec(`
996
- CREATE TRIGGER IF NOT EXISTS trg_predictions_tenant_match_insert
997
- BEFORE INSERT ON predictions
998
- WHEN NEW.memory_id IS NOT NULL
999
- BEGIN
1000
- SELECT CASE
1001
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1002
- THEN RAISE(ABORT, 'predictions.tenant_id must match memories.tenant_id for the referenced memory')
1003
- END;
1004
- END
1005
- `);
1006
- db.exec(`
1007
- CREATE TRIGGER IF NOT EXISTS trg_predictions_tenant_match_update
1008
- BEFORE UPDATE ON predictions
1009
- WHEN NEW.memory_id IS NOT NULL
1010
- AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
1011
- BEGIN
1012
- SELECT CASE
1013
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1014
- THEN RAISE(ABORT, 'predictions.tenant_id must match memories.tenant_id for the referenced memory')
1015
- END;
1016
- END
1017
- `);
1018
- }
1019
- },
1020
- },
1021
- {
1022
- version: 30,
1023
- up: (db) => {
1024
- // E2 decision first-class object (docs/plans/2026-05-28-e2-decision-object.md).
1025
- // Promotes `hippo decide` from a tagged memory (which decayed on a 90-day
1026
- // half-life even while the decision was still in force) to a canonical
1027
- // decisions table that is the source of truth. The memory mirror is kept
1028
- // for recall but is no longer authoritative; memory_id is NULLABLE with
1029
- // ON DELETE SET NULL so forget/consolidate/archive does not lose a
1030
- // decision. Mirrors the v29 predictions tenant-match trigger pattern.
1031
- //
1032
- // status (active|superseded|closed): superseded carries a self-FK
1033
- // superseded_by to the successor decision; closed is a terminal
1034
- // retire-without-successor. A superseded_by same-tenant trigger makes
1035
- // cross-tenant supersession unrepresentable at the schema level.
1036
- if (!tableExists(db, 'decisions')) {
1037
- db.exec(`
1038
- CREATE TABLE decisions (
1039
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1040
- memory_id TEXT,
1041
- tenant_id TEXT NOT NULL,
1042
- decision_text TEXT NOT NULL,
1043
- context TEXT,
1044
- status TEXT NOT NULL DEFAULT 'active'
1045
- CHECK (status IN ('active', 'superseded', 'closed')),
1046
- superseded_by INTEGER,
1047
- superseded_at TEXT,
1048
- closed_at TEXT,
1049
- created_at TEXT NOT NULL,
1050
- FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
1051
- FOREIGN KEY (superseded_by) REFERENCES decisions(id) ON DELETE SET NULL
1052
- )
1053
- `);
1054
- db.exec(`
1055
- CREATE INDEX IF NOT EXISTS idx_decisions_tenant_status
1056
- ON decisions(tenant_id, status)
1057
- `);
1058
- db.exec(`
1059
- CREATE INDEX IF NOT EXISTS idx_decisions_memory
1060
- ON decisions(memory_id) WHERE memory_id IS NOT NULL
1061
- `);
1062
- // Cross-tenant safety vs the referenced memory (mirrors predictions v29).
1063
- db.exec(`
1064
- CREATE TRIGGER IF NOT EXISTS trg_decisions_tenant_match_insert
1065
- BEFORE INSERT ON decisions
1066
- WHEN NEW.memory_id IS NOT NULL
1067
- BEGIN
1068
- SELECT CASE
1069
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1070
- THEN RAISE(ABORT, 'decisions.tenant_id must match memories.tenant_id for the referenced memory')
1071
- END;
1072
- END
1073
- `);
1074
- db.exec(`
1075
- CREATE TRIGGER IF NOT EXISTS trg_decisions_tenant_match_update
1076
- BEFORE UPDATE ON decisions
1077
- WHEN NEW.memory_id IS NOT NULL
1078
- AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
1079
- BEGIN
1080
- SELECT CASE
1081
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1082
- THEN RAISE(ABORT, 'decisions.tenant_id must match memories.tenant_id for the referenced memory')
1083
- END;
1084
- END
1085
- `);
1086
- // Cross-tenant safety vs the successor decision (self-FK). superseded_by
1087
- // is set only via UPDATE (the supersede path); the successor must share
1088
- // the tenant. The successor row already exists in the same transaction
1089
- // when this fires, so the subquery resolves.
1090
- db.exec(`
1091
- CREATE TRIGGER IF NOT EXISTS trg_decisions_supersede_tenant_match_update
1092
- BEFORE UPDATE ON decisions
1093
- WHEN NEW.superseded_by IS NOT NULL
1094
- AND NEW.superseded_by IS NOT OLD.superseded_by
1095
- BEGIN
1096
- SELECT CASE
1097
- WHEN NEW.tenant_id != (SELECT tenant_id FROM decisions WHERE id = NEW.superseded_by)
1098
- THEN RAISE(ABORT, 'decisions.superseded_by must reference a decision in the same tenant')
1099
- END;
1100
- END
1101
- `);
1102
- }
1103
- },
1104
- },
1105
- {
1106
- version: 31,
1107
- up: (db) => {
1108
- // E2 incident first-class object (docs/plans/2026-05-29-e2-incident-object.md).
1109
- // Mirrors the v30 decisions block but for an open -> resolved -> closed
1110
- // lifecycle (NOT supersede): there is no superseded_by self-FK and no
1111
- // supersede trigger. An incident is a postmortem capsule: a recorded
1112
- // operational event with a lifecycle and optional linked receipts (the
1113
- // memories that are its evidence, stored as a JSON array of ids in
1114
- // linked_memory_ids). The memory mirror is kept for recall but is not
1115
- // authoritative; memory_id is NULLABLE with ON DELETE SET NULL so
1116
- // forget/consolidate/archive does not lose an incident.
1117
- //
1118
- // status (open|resolved|closed): resolved records a resolution_text +
1119
- // resolved_at and stays on record; closed is a terminal retire reachable
1120
- // from open or resolved. Cross-tenant safety: BEFORE INSERT + BEFORE
1121
- // UPDATE triggers enforce incidents.tenant_id == the referenced memory's
1122
- // tenant_id (verbatim mirror of the v30 decisions tenant-match triggers).
1123
- if (!tableExists(db, 'incidents')) {
1124
- db.exec(`
1125
- CREATE TABLE incidents (
1126
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1127
- memory_id TEXT,
1128
- tenant_id TEXT NOT NULL,
1129
- incident_text TEXT NOT NULL,
1130
- context TEXT,
1131
- status TEXT NOT NULL DEFAULT 'open'
1132
- CHECK (status IN ('open', 'resolved', 'closed')),
1133
- resolution_text TEXT,
1134
- resolved_at TEXT,
1135
- closed_at TEXT,
1136
- linked_memory_ids TEXT NOT NULL DEFAULT '[]',
1137
- created_at TEXT NOT NULL,
1138
- FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL
1139
- )
1140
- `);
1141
- db.exec(`
1142
- CREATE INDEX IF NOT EXISTS idx_incidents_tenant_status
1143
- ON incidents(tenant_id, status)
1144
- `);
1145
- db.exec(`
1146
- CREATE INDEX IF NOT EXISTS idx_incidents_memory
1147
- ON incidents(memory_id) WHERE memory_id IS NOT NULL
1148
- `);
1149
- // Cross-tenant safety vs the referenced memory (verbatim mirror of the
1150
- // v30 decisions tenant-match triggers; no supersede trigger).
1151
- db.exec(`
1152
- CREATE TRIGGER IF NOT EXISTS trg_incidents_tenant_match_insert
1153
- BEFORE INSERT ON incidents
1154
- WHEN NEW.memory_id IS NOT NULL
1155
- BEGIN
1156
- SELECT CASE
1157
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1158
- THEN RAISE(ABORT, 'incidents.tenant_id must match memories.tenant_id for the referenced memory')
1159
- END;
1160
- END
1161
- `);
1162
- db.exec(`
1163
- CREATE TRIGGER IF NOT EXISTS trg_incidents_tenant_match_update
1164
- BEFORE UPDATE ON incidents
1165
- WHEN NEW.memory_id IS NOT NULL
1166
- AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
1167
- BEGIN
1168
- SELECT CASE
1169
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1170
- THEN RAISE(ABORT, 'incidents.tenant_id must match memories.tenant_id for the referenced memory')
1171
- END;
1172
- END
1173
- `);
1174
- }
1175
- },
1176
- },
1177
- {
1178
- version: 32,
1179
- up: (db) => {
1180
- // E2 process first-class object (docs/plans/2026-05-29-e2-process-object.md).
1181
- // A process is a "living process map": a named, ordered list of steps that
1182
- // evolves. Unlike incident (open->resolved->closed, no supersede), process
1183
- // REUSES the v30 decisions supersede path as its delta mechanism: a process
1184
- // evolves by being superseded by a NEW VERSION that records what changed
1185
- // (change_summary) and the full new state (steps), carrying a derived
1186
- // version counter. So this table combines the v31 incidents tenant-match
1187
- // trigger pair (vs the referenced memory) WITH the v30 decisions
1188
- // superseded_by self-FK + supersede tenant-match trigger.
1189
- //
1190
- // status (active|superseded|closed): superseded carries a self-FK
1191
- // superseded_by to the successor version; closed is a terminal
1192
- // retire-without-successor (only an active head closes). The memory mirror
1193
- // is kept for recall but is not authoritative; memory_id is NULLABLE with
1194
- // ON DELETE SET NULL so forget/consolidate/archive does not lose a process.
1195
- // steps is a JSON-encoded array of step strings (scoped v1; a normalized
1196
- // process_steps table is deferred). version is server-derived
1197
- // (predecessor.version + 1); change_summary is set on a successor row only.
1198
- if (!tableExists(db, 'processes')) {
1199
- db.exec(`
1200
- CREATE TABLE processes (
1201
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1202
- memory_id TEXT,
1203
- tenant_id TEXT NOT NULL,
1204
- process_name TEXT NOT NULL,
1205
- description TEXT,
1206
- steps TEXT NOT NULL DEFAULT '[]',
1207
- version INTEGER NOT NULL DEFAULT 1,
1208
- status TEXT NOT NULL DEFAULT 'active'
1209
- CHECK (status IN ('active', 'superseded', 'closed')),
1210
- superseded_by INTEGER,
1211
- superseded_at TEXT,
1212
- change_summary TEXT,
1213
- closed_at TEXT,
1214
- created_at TEXT NOT NULL,
1215
- FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
1216
- FOREIGN KEY (superseded_by) REFERENCES processes(id) ON DELETE SET NULL
1217
- )
1218
- `);
1219
- db.exec(`
1220
- CREATE INDEX IF NOT EXISTS idx_processes_tenant_status
1221
- ON processes(tenant_id, status)
1222
- `);
1223
- db.exec(`
1224
- CREATE INDEX IF NOT EXISTS idx_processes_memory
1225
- ON processes(memory_id) WHERE memory_id IS NOT NULL
1226
- `);
1227
- // Cross-tenant safety vs the referenced memory (verbatim mirror of the
1228
- // v31 incidents / v30 decisions tenant-match triggers).
1229
- db.exec(`
1230
- CREATE TRIGGER IF NOT EXISTS trg_processes_tenant_match_insert
1231
- BEFORE INSERT ON processes
1232
- WHEN NEW.memory_id IS NOT NULL
1233
- BEGIN
1234
- SELECT CASE
1235
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1236
- THEN RAISE(ABORT, 'processes.tenant_id must match memories.tenant_id for the referenced memory')
1237
- END;
1238
- END
1239
- `);
1240
- db.exec(`
1241
- CREATE TRIGGER IF NOT EXISTS trg_processes_tenant_match_update
1242
- BEFORE UPDATE ON processes
1243
- WHEN NEW.memory_id IS NOT NULL
1244
- AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
1245
- BEGIN
1246
- SELECT CASE
1247
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1248
- THEN RAISE(ABORT, 'processes.tenant_id must match memories.tenant_id for the referenced memory')
1249
- END;
1250
- END
1251
- `);
1252
- // Cross-tenant safety vs the successor process (self-FK; verbatim mirror
1253
- // of the v30 decisions supersede trigger). superseded_by is set only via
1254
- // the supersede UPDATE; the successor must share the tenant. The successor
1255
- // row already exists in the same transaction when this fires.
1256
- db.exec(`
1257
- CREATE TRIGGER IF NOT EXISTS trg_processes_supersede_tenant_match_update
1258
- BEFORE UPDATE ON processes
1259
- WHEN NEW.superseded_by IS NOT NULL
1260
- AND NEW.superseded_by IS NOT OLD.superseded_by
1261
- BEGIN
1262
- SELECT CASE
1263
- WHEN NEW.tenant_id != (SELECT tenant_id FROM processes WHERE id = NEW.superseded_by)
1264
- THEN RAISE(ABORT, 'processes.superseded_by must reference a process in the same tenant')
1265
- END;
1266
- END
1267
- `);
1268
- }
1269
- },
1270
- },
1271
- {
1272
- version: 33,
1273
- up: (db) => {
1274
- // E2 policy first-class object (docs/plans/2026-05-30-e2-policy-object.md).
1275
- // The "bi-temporal-first" object type: a named rule/statement that is in
1276
- // force over an EFFECTIVE-TIME range (valid_from required, valid_to nullable
1277
- // = open-ended) and evolves via the v32 processes supersede machinery
1278
- // (superseded_by self-FK + supersede tenant-match trigger + version +
1279
- // change_summary). This table = the v32 processes table MINUS `steps`
1280
- // (a policy has policy_text, not an ordered step list) PLUS the first-class
1281
- // effective-time columns valid_from/valid_to. Valid-time is the queryable
1282
- // axis (the as-of query loadPoliciesAsOf); transaction-time is present via
1283
- // created_at + the supersede chain's superseded_at (time-travel deferred).
1284
- //
1285
- // All date inputs are normalized to canonical ISO-8601 datetime
1286
- // (toISOString) at the store boundary before persist/compare, so the
1287
- // fixed-width values sort lexically and the half-open [valid_from, valid_to)
1288
- // as-of comparison is correct (plan-eng-critic round-1 CRIT fix).
1289
- if (!tableExists(db, 'policies')) {
1290
- db.exec(`
1291
- CREATE TABLE policies (
1292
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1293
- memory_id TEXT,
1294
- tenant_id TEXT NOT NULL,
1295
- policy_name TEXT NOT NULL,
1296
- policy_text TEXT NOT NULL,
1297
- valid_from TEXT NOT NULL,
1298
- valid_to TEXT,
1299
- version INTEGER NOT NULL DEFAULT 1,
1300
- status TEXT NOT NULL DEFAULT 'active'
1301
- CHECK (status IN ('active', 'superseded', 'closed')),
1302
- superseded_by INTEGER,
1303
- superseded_at TEXT,
1304
- change_summary TEXT,
1305
- closed_at TEXT,
1306
- created_at TEXT NOT NULL,
1307
- FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
1308
- FOREIGN KEY (superseded_by) REFERENCES policies(id) ON DELETE SET NULL
1309
- )
1310
- `);
1311
- db.exec(`
1312
- CREATE INDEX IF NOT EXISTS idx_policies_tenant_status
1313
- ON policies(tenant_id, status)
1314
- `);
1315
- db.exec(`
1316
- CREATE INDEX IF NOT EXISTS idx_policies_memory
1317
- ON policies(memory_id) WHERE memory_id IS NOT NULL
1318
- `);
1319
- // Supports the as-of query (active policies in force at a valid-time).
1320
- db.exec(`
1321
- CREATE INDEX IF NOT EXISTS idx_policies_asof
1322
- ON policies(tenant_id, valid_from)
1323
- `);
1324
- // Cross-tenant safety vs the referenced memory (verbatim mirror of the
1325
- // v32 processes tenant-match triggers).
1326
- db.exec(`
1327
- CREATE TRIGGER IF NOT EXISTS trg_policies_tenant_match_insert
1328
- BEFORE INSERT ON policies
1329
- WHEN NEW.memory_id IS NOT NULL
1330
- BEGIN
1331
- SELECT CASE
1332
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1333
- THEN RAISE(ABORT, 'policies.tenant_id must match memories.tenant_id for the referenced memory')
1334
- END;
1335
- END
1336
- `);
1337
- db.exec(`
1338
- CREATE TRIGGER IF NOT EXISTS trg_policies_tenant_match_update
1339
- BEFORE UPDATE ON policies
1340
- WHEN NEW.memory_id IS NOT NULL
1341
- AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
1342
- BEGIN
1343
- SELECT CASE
1344
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1345
- THEN RAISE(ABORT, 'policies.tenant_id must match memories.tenant_id for the referenced memory')
1346
- END;
1347
- END
1348
- `);
1349
- // Cross-tenant safety vs the successor policy (self-FK; verbatim mirror of
1350
- // the v32 processes supersede trigger).
1351
- db.exec(`
1352
- CREATE TRIGGER IF NOT EXISTS trg_policies_supersede_tenant_match_update
1353
- BEFORE UPDATE ON policies
1354
- WHEN NEW.superseded_by IS NOT NULL
1355
- AND NEW.superseded_by IS NOT OLD.superseded_by
1356
- BEGIN
1357
- SELECT CASE
1358
- WHEN NEW.tenant_id != (SELECT tenant_id FROM policies WHERE id = NEW.superseded_by)
1359
- THEN RAISE(ABORT, 'policies.superseded_by must reference a policy in the same tenant')
1360
- END;
1361
- END
1362
- `);
1363
- }
1364
- },
1365
- },
1366
- {
1367
- version: 34,
1368
- up: (db) => {
1369
- // E2 skill first-class object (docs/plans/2026-05-30-e2-skill-object.md).
1370
- // A skill is a reusable, agent-followable capability: an `instructions` body
1371
- // + an optional `trigger_text` (when to apply), evolving via the v32
1372
- // processes supersede machinery (superseded_by self-FK + supersede
1373
- // tenant-match trigger + version + change_summary). This table = the v32
1374
- // processes table MINUS `steps` (a skill's content is a single instructions
1375
- // body) PLUS `instructions` (NOT NULL) and `trigger_text`. "Executable" is
1376
- // scoped to an agent-followable instruction that EXPORTS into the agent's
1377
- // in-force rules (AGENTS.md / CLAUDE.md) via exportSkills; literal code
1378
- // execution is deferred. NOTE: the column is `trigger_text`, NOT `trigger`,
1379
- // because TRIGGER is a SQLite reserved keyword.
1380
- if (!tableExists(db, 'skills')) {
1381
- db.exec(`
1382
- CREATE TABLE skills (
1383
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1384
- memory_id TEXT,
1385
- tenant_id TEXT NOT NULL,
1386
- skill_name TEXT NOT NULL,
1387
- instructions TEXT NOT NULL,
1388
- trigger_text TEXT,
1389
- version INTEGER NOT NULL DEFAULT 1,
1390
- status TEXT NOT NULL DEFAULT 'active'
1391
- CHECK (status IN ('active', 'superseded', 'closed')),
1392
- superseded_by INTEGER,
1393
- superseded_at TEXT,
1394
- change_summary TEXT,
1395
- closed_at TEXT,
1396
- created_at TEXT NOT NULL,
1397
- FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
1398
- FOREIGN KEY (superseded_by) REFERENCES skills(id) ON DELETE SET NULL
1399
- )
1400
- `);
1401
- db.exec(`
1402
- CREATE INDEX IF NOT EXISTS idx_skills_tenant_status
1403
- ON skills(tenant_id, status)
1404
- `);
1405
- db.exec(`
1406
- CREATE INDEX IF NOT EXISTS idx_skills_memory
1407
- ON skills(memory_id) WHERE memory_id IS NOT NULL
1408
- `);
1409
- // Cross-tenant safety vs the referenced memory (verbatim mirror of the
1410
- // v32 processes tenant-match triggers).
1411
- db.exec(`
1412
- CREATE TRIGGER IF NOT EXISTS trg_skills_tenant_match_insert
1413
- BEFORE INSERT ON skills
1414
- WHEN NEW.memory_id IS NOT NULL
1415
- BEGIN
1416
- SELECT CASE
1417
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1418
- THEN RAISE(ABORT, 'skills.tenant_id must match memories.tenant_id for the referenced memory')
1419
- END;
1420
- END
1421
- `);
1422
- db.exec(`
1423
- CREATE TRIGGER IF NOT EXISTS trg_skills_tenant_match_update
1424
- BEFORE UPDATE ON skills
1425
- WHEN NEW.memory_id IS NOT NULL
1426
- AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
1427
- BEGIN
1428
- SELECT CASE
1429
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1430
- THEN RAISE(ABORT, 'skills.tenant_id must match memories.tenant_id for the referenced memory')
1431
- END;
1432
- END
1433
- `);
1434
- // Cross-tenant safety vs the successor skill (self-FK; verbatim mirror of
1435
- // the v32 processes supersede trigger).
1436
- db.exec(`
1437
- CREATE TRIGGER IF NOT EXISTS trg_skills_supersede_tenant_match_update
1438
- BEFORE UPDATE ON skills
1439
- WHEN NEW.superseded_by IS NOT NULL
1440
- AND NEW.superseded_by IS NOT OLD.superseded_by
1441
- BEGIN
1442
- SELECT CASE
1443
- WHEN NEW.tenant_id != (SELECT tenant_id FROM skills WHERE id = NEW.superseded_by)
1444
- THEN RAISE(ABORT, 'skills.superseded_by must reference a skill in the same tenant')
1445
- END;
1446
- END
1447
- `);
1448
- }
1449
- },
1450
- },
1451
- {
1452
- version: 35,
1453
- up: (db) => {
1454
- // E2 project_brief first-class object
1455
- // (docs/plans/2026-05-30-e2-project-brief-object.md). A project_brief is the
1456
- // living, repo-scoped summary of a repository's state: a `summary` body
1457
- // scoped to a `repo`, evolving via the v34 skills supersede machinery
1458
- // (superseded_by self-FK + supersede tenant-match trigger + version +
1459
- // change_summary). This table = the v34 skills table with
1460
- // skill_name/trigger_text replaced by `repo` (the repo-scoping dimension)
1461
- // PLUS `summary` (the brief body). The distinguishing op (refreshBrief, in
1462
- // src/project-briefs.ts) auto-assembles the summary from the repo's receipts
1463
- // (memory rows tagged path:<repo>); it needs no schema support beyond `repo`.
1464
- // All column names were checked against SQLite reserved words (skill-episode
1465
- // lesson re: `trigger`): repo/summary/version/status/etc. are non-reserved.
1466
- if (!tableExists(db, 'project_briefs')) {
1467
- db.exec(`
1468
- CREATE TABLE project_briefs (
1469
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1470
- memory_id TEXT,
1471
- tenant_id TEXT NOT NULL,
1472
- repo TEXT NOT NULL,
1473
- summary TEXT NOT NULL,
1474
- version INTEGER NOT NULL DEFAULT 1,
1475
- status TEXT NOT NULL DEFAULT 'active'
1476
- CHECK (status IN ('active', 'superseded', 'closed')),
1477
- superseded_by INTEGER,
1478
- superseded_at TEXT,
1479
- change_summary TEXT,
1480
- closed_at TEXT,
1481
- created_at TEXT NOT NULL,
1482
- FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
1483
- FOREIGN KEY (superseded_by) REFERENCES project_briefs(id) ON DELETE SET NULL
1484
- )
1485
- `);
1486
- db.exec(`
1487
- CREATE INDEX IF NOT EXISTS idx_project_briefs_tenant_status
1488
- ON project_briefs(tenant_id, status)
1489
- `);
1490
- db.exec(`
1491
- CREATE INDEX IF NOT EXISTS idx_project_briefs_memory
1492
- ON project_briefs(memory_id) WHERE memory_id IS NOT NULL
1493
- `);
1494
- db.exec(`
1495
- CREATE INDEX IF NOT EXISTS idx_project_briefs_repo
1496
- ON project_briefs(tenant_id, repo, status)
1497
- `);
1498
- // Cross-tenant safety vs the referenced memory (verbatim mirror of the
1499
- // v34 skills tenant-match triggers).
1500
- db.exec(`
1501
- CREATE TRIGGER IF NOT EXISTS trg_project_briefs_tenant_match_insert
1502
- BEFORE INSERT ON project_briefs
1503
- WHEN NEW.memory_id IS NOT NULL
1504
- BEGIN
1505
- SELECT CASE
1506
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1507
- THEN RAISE(ABORT, 'project_briefs.tenant_id must match memories.tenant_id for the referenced memory')
1508
- END;
1509
- END
1510
- `);
1511
- db.exec(`
1512
- CREATE TRIGGER IF NOT EXISTS trg_project_briefs_tenant_match_update
1513
- BEFORE UPDATE ON project_briefs
1514
- WHEN NEW.memory_id IS NOT NULL
1515
- AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
1516
- BEGIN
1517
- SELECT CASE
1518
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1519
- THEN RAISE(ABORT, 'project_briefs.tenant_id must match memories.tenant_id for the referenced memory')
1520
- END;
1521
- END
1522
- `);
1523
- // Cross-tenant safety vs the successor brief (self-FK; verbatim mirror of
1524
- // the v34 skills supersede trigger).
1525
- db.exec(`
1526
- CREATE TRIGGER IF NOT EXISTS trg_project_briefs_supersede_tenant_match_update
1527
- BEFORE UPDATE ON project_briefs
1528
- WHEN NEW.superseded_by IS NOT NULL
1529
- AND NEW.superseded_by IS NOT OLD.superseded_by
1530
- BEGIN
1531
- SELECT CASE
1532
- WHEN NEW.tenant_id != (SELECT tenant_id FROM project_briefs WHERE id = NEW.superseded_by)
1533
- THEN RAISE(ABORT, 'project_briefs.superseded_by must reference a project_brief in the same tenant')
1534
- END;
1535
- END
1536
- `);
1537
- }
1538
- },
1539
- },
1540
- {
1541
- version: 36,
1542
- up: (db) => {
1543
- // E2 customer_note first-class object (the LAST E2 object)
1544
- // (docs/plans/2026-06-01-e2-customer-note-object.md). A customer_note is a
1545
- // discrete note recorded against an account/customer entity, evolving via the
1546
- // v35 project_briefs supersede machinery (superseded_by self-FK + supersede
1547
- // tenant-match trigger + version + change_summary). This table = the v35
1548
- // project_briefs table with repo/summary replaced by `customer` (the
1549
- // entity-scoping dimension; a free-form account/customer id - the entities
1550
- // table is unbuilt E3.1, so a FK is deferred) PLUS `note` (the note body).
1551
- // MANY notes per customer (each its own supersede chain), unlike the
1552
- // one-summary-per-repo project_brief. All column names checked against SQLite
1553
- // reserved words (skill-episode lesson, codebase-audit rule 10): customer/note/
1554
- // version/status/etc. are non-reserved.
1555
- if (!tableExists(db, 'customer_notes')) {
1556
- db.exec(`
1557
- CREATE TABLE customer_notes (
1558
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1559
- memory_id TEXT,
1560
- tenant_id TEXT NOT NULL,
1561
- customer TEXT NOT NULL,
1562
- note TEXT NOT NULL,
1563
- version INTEGER NOT NULL DEFAULT 1,
1564
- status TEXT NOT NULL DEFAULT 'active'
1565
- CHECK (status IN ('active', 'superseded', 'closed')),
1566
- superseded_by INTEGER,
1567
- superseded_at TEXT,
1568
- change_summary TEXT,
1569
- closed_at TEXT,
1570
- created_at TEXT NOT NULL,
1571
- FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
1572
- FOREIGN KEY (superseded_by) REFERENCES customer_notes(id) ON DELETE SET NULL
1573
- )
1574
- `);
1575
- db.exec(`
1576
- CREATE INDEX IF NOT EXISTS idx_customer_notes_tenant_status
1577
- ON customer_notes(tenant_id, status)
1578
- `);
1579
- db.exec(`
1580
- CREATE INDEX IF NOT EXISTS idx_customer_notes_memory
1581
- ON customer_notes(memory_id) WHERE memory_id IS NOT NULL
1582
- `);
1583
- db.exec(`
1584
- CREATE INDEX IF NOT EXISTS idx_customer_notes_customer
1585
- ON customer_notes(tenant_id, customer, status)
1586
- `);
1587
- // Cross-tenant safety vs the referenced memory (verbatim mirror of the
1588
- // v35 project_briefs tenant-match triggers).
1589
- db.exec(`
1590
- CREATE TRIGGER IF NOT EXISTS trg_customer_notes_tenant_match_insert
1591
- BEFORE INSERT ON customer_notes
1592
- WHEN NEW.memory_id IS NOT NULL
1593
- BEGIN
1594
- SELECT CASE
1595
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1596
- THEN RAISE(ABORT, 'customer_notes.tenant_id must match memories.tenant_id for the referenced memory')
1597
- END;
1598
- END
1599
- `);
1600
- db.exec(`
1601
- CREATE TRIGGER IF NOT EXISTS trg_customer_notes_tenant_match_update
1602
- BEFORE UPDATE ON customer_notes
1603
- WHEN NEW.memory_id IS NOT NULL
1604
- AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
1605
- BEGIN
1606
- SELECT CASE
1607
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1608
- THEN RAISE(ABORT, 'customer_notes.tenant_id must match memories.tenant_id for the referenced memory')
1609
- END;
1610
- END
1611
- `);
1612
- // Cross-tenant safety vs the successor note (self-FK; verbatim mirror of the
1613
- // v35 project_briefs supersede trigger).
1614
- db.exec(`
1615
- CREATE TRIGGER IF NOT EXISTS trg_customer_notes_supersede_tenant_match_update
1616
- BEFORE UPDATE ON customer_notes
1617
- WHEN NEW.superseded_by IS NOT NULL
1618
- AND NEW.superseded_by IS NOT OLD.superseded_by
1619
- BEGIN
1620
- SELECT CASE
1621
- WHEN NEW.tenant_id != (SELECT tenant_id FROM customer_notes WHERE id = NEW.superseded_by)
1622
- THEN RAISE(ABORT, 'customer_notes.superseded_by must reference a customer_note in the same tenant')
1623
- END;
1624
- END
1625
- `);
1626
- }
1627
- },
1628
- },
1629
- {
1630
- version: 37,
1631
- up: (db) => {
1632
- // E3.3 graph-on-consolidated guard (docs/plans/2026-06-01-e3-graph-guard.md).
1633
- // The graph layer (entities + relations) sits ON TOP OF consolidated state and
1634
- // must NEVER index the raw layer. The substrate: entities + relations +
1635
- // graph_extraction_queue, each FK-ing to memories and guarded so they can only
1636
- // reference CONSOLIDATED memories (kind IN ('distilled','superseded')), never
1637
- // kind='raw'. New tables -> real CHECK constraints (unlike the ALTER'd memories,
1638
- // whose kind CHECK lives in triggers). The kind/source MATCH (source_kind ==
1639
- // the FK'd memory's actual kind) cannot be a CHECK (CHECK can't subquery), so it
1640
- // is a BEFORE INSERT *and* BEFORE UPDATE trigger (the subquery-capable pattern
1641
- // from the v30 decisions / predictions tenant-match triggers). Both INSERT and
1642
- // UPDATE are guarded: an INSERT-only guard is bypassable via a raw SQL UPDATE
1643
- // that moves a row onto a raw memory (plan-eng-critic 2026-06-01). All column
1644
- // names checked vs SQL reserved words (rule 10): rel_type avoids REFERENCES.
1645
- if (!tableExists(db, 'entities')) {
1646
- db.exec(`
1647
- CREATE TABLE entities (
1648
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1649
- tenant_id TEXT NOT NULL,
1650
- entity_type TEXT NOT NULL
1651
- CHECK (entity_type IN ('person', 'project', 'customer', 'system', 'policy', 'decision')),
1652
- name TEXT NOT NULL,
1653
- memory_id TEXT NOT NULL,
1654
- source_kind TEXT NOT NULL CHECK (source_kind IN ('distilled', 'superseded')),
1655
- created_at TEXT NOT NULL,
1656
- FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
1657
- )
1658
- `);
1659
- db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_tenant ON entities(tenant_id)`);
1660
- db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_memory ON entities(memory_id)`);
1661
- db.exec(`
1662
- CREATE TABLE relations (
1663
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1664
- tenant_id TEXT NOT NULL,
1665
- from_entity_id INTEGER NOT NULL,
1666
- to_entity_id INTEGER NOT NULL,
1667
- rel_type TEXT NOT NULL
1668
- CHECK (rel_type IN ('owns', 'supersedes', 'depends-on', 'blocked-by', 'references')),
1669
- memory_id TEXT NOT NULL,
1670
- source_kind TEXT NOT NULL CHECK (source_kind IN ('distilled', 'superseded')),
1671
- created_at TEXT NOT NULL,
1672
- FOREIGN KEY (from_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
1673
- FOREIGN KEY (to_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
1674
- FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
1675
- )
1676
- `);
1677
- db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_tenant ON relations(tenant_id)`);
1678
- db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity_id)`);
1679
- db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity_id)`);
1680
- db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_memory ON relations(memory_id)`);
1681
- db.exec(`
1682
- CREATE TABLE graph_extraction_queue (
1683
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1684
- tenant_id TEXT NOT NULL,
1685
- memory_id TEXT NOT NULL,
1686
- kind TEXT NOT NULL CHECK (kind IN ('distilled', 'superseded')),
1687
- status TEXT NOT NULL DEFAULT 'pending'
1688
- CHECK (status IN ('pending', 'processed', 'skipped')),
1689
- enqueued_at TEXT NOT NULL,
1690
- processed_at TEXT,
1691
- FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
1692
- )
1693
- `);
1694
- db.exec(`CREATE INDEX IF NOT EXISTS idx_graph_queue_status ON graph_extraction_queue(tenant_id, status)`);
1695
- // entities guard: source_kind must equal the FK'd memory's actual kind (so a
1696
- // raw memory or a lying source_kind both ABORT), and tenant must match. Both
1697
- // INSERT and UPDATE (UPDATE fires when memory_id/source_kind/tenant_id change).
1698
- db.exec(`
1699
- CREATE TRIGGER IF NOT EXISTS trg_entities_consolidated_only_insert
1700
- BEFORE INSERT ON entities
1701
- BEGIN
1702
- SELECT CASE
1703
- WHEN NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
1704
- THEN RAISE(ABORT, 'entities.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
1705
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1706
- THEN RAISE(ABORT, 'entities.tenant_id must match memories.tenant_id for the referenced memory')
1707
- END;
1708
- END
1709
- `);
1710
- db.exec(`
1711
- CREATE TRIGGER IF NOT EXISTS trg_entities_consolidated_only_update
1712
- BEFORE UPDATE ON entities
1713
- WHEN NEW.memory_id IS NOT OLD.memory_id
1714
- OR NEW.source_kind IS NOT OLD.source_kind
1715
- OR NEW.tenant_id IS NOT OLD.tenant_id
1716
- BEGIN
1717
- SELECT CASE
1718
- WHEN NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
1719
- THEN RAISE(ABORT, 'entities.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
1720
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1721
- THEN RAISE(ABORT, 'entities.tenant_id must match memories.tenant_id for the referenced memory')
1722
- END;
1723
- END
1724
- `);
1725
- // relations guard: source_kind must equal the FK'd memory's kind; tenant must
1726
- // match the memory AND both endpoint entities (no cross-tenant edges).
1727
- db.exec(`
1728
- CREATE TRIGGER IF NOT EXISTS trg_relations_consolidated_only_insert
1729
- BEFORE INSERT ON relations
1730
- BEGIN
1731
- SELECT CASE
1732
- WHEN NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
1733
- THEN RAISE(ABORT, 'relations.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
1734
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1735
- THEN RAISE(ABORT, 'relations.tenant_id must match memories.tenant_id for the referenced memory')
1736
- WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.from_entity_id)
1737
- THEN RAISE(ABORT, 'relations.tenant_id must match the from_entity tenant (no cross-tenant edges)')
1738
- WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.to_entity_id)
1739
- THEN RAISE(ABORT, 'relations.tenant_id must match the to_entity tenant (no cross-tenant edges)')
1740
- END;
1741
- END
1742
- `);
1743
- db.exec(`
1744
- CREATE TRIGGER IF NOT EXISTS trg_relations_consolidated_only_update
1745
- BEFORE UPDATE ON relations
1746
- WHEN NEW.memory_id IS NOT OLD.memory_id
1747
- OR NEW.source_kind IS NOT OLD.source_kind
1748
- OR NEW.tenant_id IS NOT OLD.tenant_id
1749
- OR NEW.from_entity_id IS NOT OLD.from_entity_id
1750
- OR NEW.to_entity_id IS NOT OLD.to_entity_id
1751
- BEGIN
1752
- SELECT CASE
1753
- WHEN NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
1754
- THEN RAISE(ABORT, 'relations.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
1755
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1756
- THEN RAISE(ABORT, 'relations.tenant_id must match memories.tenant_id for the referenced memory')
1757
- WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.from_entity_id)
1758
- THEN RAISE(ABORT, 'relations.tenant_id must match the from_entity tenant (no cross-tenant edges)')
1759
- WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.to_entity_id)
1760
- THEN RAISE(ABORT, 'relations.tenant_id must match the to_entity tenant (no cross-tenant edges)')
1761
- END;
1762
- END
1763
- `);
1764
- // graph_extraction_queue guard: kind must equal the FK'd memory's actual kind
1765
- // (so a raw memory ABORTs), and tenant must match. INSERT and UPDATE.
1766
- db.exec(`
1767
- CREATE TRIGGER IF NOT EXISTS trg_graph_queue_consolidated_only_insert
1768
- BEFORE INSERT ON graph_extraction_queue
1769
- BEGIN
1770
- SELECT CASE
1771
- WHEN NEW.kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
1772
- THEN RAISE(ABORT, 'graph_extraction_queue.kind must equal the referenced memory kind; only consolidated memories are queued (no raw)')
1773
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1774
- THEN RAISE(ABORT, 'graph_extraction_queue.tenant_id must match memories.tenant_id for the referenced memory')
1775
- END;
1776
- END
1777
- `);
1778
- db.exec(`
1779
- CREATE TRIGGER IF NOT EXISTS trg_graph_queue_consolidated_only_update
1780
- BEFORE UPDATE ON graph_extraction_queue
1781
- WHEN NEW.memory_id IS NOT OLD.memory_id
1782
- OR NEW.kind IS NOT OLD.kind
1783
- OR NEW.tenant_id IS NOT OLD.tenant_id
1784
- BEGIN
1785
- SELECT CASE
1786
- WHEN NEW.kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
1787
- THEN RAISE(ABORT, 'graph_extraction_queue.kind must equal the referenced memory kind; only consolidated memories are queued (no raw)')
1788
- WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1789
- THEN RAISE(ABORT, 'graph_extraction_queue.tenant_id must match memories.tenant_id for the referenced memory')
1790
- END;
1791
- END
1792
- `);
1793
- // Reverse guard (codex-review-critic 2026-06-01, P1): the graph-table triggers
1794
- // only fire on writes to the GRAPH tables. They do NOT fire when an
1795
- // already-indexed memory is later mutated. So 'UPDATE memories SET kind=raw'
1796
- // (or a tenant change) on a memory the graph references would silently leave
1797
- // entity/relation/queue rows pointing at a raw / cross-tenant memory while
1798
- // their source_kind stays 'distilled' - bypassing the central 'graph never
1799
- // indexes raw' invariant after insertion. This trigger closes that direction:
1800
- // a memory cannot be reclassified to raw, nor moved cross-tenant, WHILE the
1801
- // graph references it (rebuild/remove the graph rows first). Cheap: the EXISTS
1802
- // checks are only evaluated when kind actually becomes raw or tenant changes.
1803
- db.exec(`
1804
- CREATE TRIGGER IF NOT EXISTS trg_memories_graph_referenced_guard
1805
- BEFORE UPDATE ON memories
1806
- WHEN (NEW.kind IS NOT OLD.kind OR NEW.tenant_id IS NOT OLD.tenant_id)
1807
- AND (
1808
- EXISTS (SELECT 1 FROM entities WHERE memory_id = OLD.id)
1809
- OR EXISTS (SELECT 1 FROM relations WHERE memory_id = OLD.id)
1810
- OR EXISTS (SELECT 1 FROM graph_extraction_queue WHERE memory_id = OLD.id)
1811
- )
1812
- BEGIN
1813
- SELECT RAISE(ABORT, 'cannot change the kind or tenant of a memory while the graph references it (E3.3 graph-on-consolidated guard); a graph-referenced memory is immutable in kind/tenant - rebuild/remove the graph rows first, or rebuild them after supersession');
1814
- END
1815
- `);
1816
- // Reverse guard #2 (codex-review-critic 2026-06-01 retry, P2): an entity that is
1817
- // a relation endpoint cannot be moved cross-tenant. The entity UPDATE trigger
1818
- // validates the entity against its source memory, but an existing relation
1819
- // pointing at the entity is NOT re-validated, so a raw 'UPDATE entities SET
1820
- // tenant_id=?, memory_id=?' to another tenant would leave a tenant-A relation
1821
- // pointing at a tenant-B entity. Block the tenant move while the entity is
1822
- // referenced by any relation (rebuild the relations first).
1823
- db.exec(`
1824
- CREATE TRIGGER IF NOT EXISTS trg_entities_no_tenant_move_when_referenced
1825
- BEFORE UPDATE ON entities
1826
- WHEN NEW.tenant_id IS NOT OLD.tenant_id
1827
- AND EXISTS (SELECT 1 FROM relations WHERE from_entity_id = OLD.id OR to_entity_id = OLD.id)
1828
- BEGIN
1829
- SELECT RAISE(ABORT, 'cannot move an entity cross-tenant while a relation references it as an endpoint (E3.3 graph-on-consolidated guard); rebuild/remove the relations first');
1830
- END
1831
- `);
1832
- }
1833
- },
1834
- },
1835
- {
1836
- version: 38,
1837
- up: (db) => {
1838
- // E2-provenance: anchor graph entity/relation provenance to the authoritative E2
1839
- // object (decision/policy/customer-note/project-brief) instead of the decaying
1840
- // memory mirror (docs/plans/2026-06-03-graph-e2-provenance.md). An in-force E2
1841
- // object must STAY in the graph after its mirror memory is forgotten or
1842
- // consolidation-pruned. The graph is a PURE DERIVED CACHE (clearGraph + rebuild on
1843
- // every `graph extract` / `sleep`), so v38 DROPs+recreates entities/relations (no
1844
- // data copy) and the next extract repopulates. graph_extraction_queue is untouched.
1845
- //
1846
- // Two provenance paths, "at least one, no raw":
1847
- // - memory path (memory_id NOT NULL): source_kind must equal the FK'd memory's live
1848
- // kind and that kind is distilled|superseded (raw still ABORTs) + tenant-match.
1849
- // - object path (memory_id NULL): source_object_type/id must reference an EXISTING
1850
- // same-tenant E2 row whose status is active|superseded (not closed). E2 objects
1851
- // are consolidated BY CONSTRUCTION, so the no-raw invariant still holds.
1852
- // - all-null is rejected.
1853
- //
1854
- // memory_id is now NULLABLE with ON DELETE SET NULL (was NOT NULL / CASCADE), so a
1855
- // mirror forget/consolidate nulls the recall pointer without dropping the row. NOTE
1856
- // (empirically verified, contradicts the SQLite docs): node:sqlite DOES fire the
1857
- // BEFORE UPDATE guard from the FK SET NULL action even with recursive_triggers OFF.
1858
- // So the *_consolidated_only_UPDATE triggers deliberately OMIT the all-null ABORT
1859
- // (kept on INSERT) - otherwise a mirror delete of a memory-only row would be blocked.
1860
- // See the per-trigger comments below. A SET NULL leaves source_kind at its old value
1861
- // by design (the object path is distilled-by-construction; source_kind is only
1862
- // re-checked when memory_id NOT NULL).
1863
- // source_object_id is a SOFT (type,id) pointer (no hard FK) the rebuild re-validates,
1864
- // so a legitimate E2 hard-delete is never blocked; a `closed` E2 row drops at next
1865
- // extract. SQLite cannot parametrize a table name in a trigger, so the object-path
1866
- // validation is an explicit 4-way CASE (one arm per E2 table).
1867
- // Drop child (relations FK entities) first, then parent.
1868
- db.exec('DROP TABLE IF EXISTS relations');
1869
- db.exec('DROP TABLE IF EXISTS entities');
1870
- db.exec(`
1871
- CREATE TABLE entities (
1872
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1873
- tenant_id TEXT NOT NULL,
1874
- entity_type TEXT NOT NULL
1875
- CHECK (entity_type IN ('person', 'project', 'customer', 'system', 'policy', 'decision')),
1876
- name TEXT NOT NULL,
1877
- memory_id TEXT,
1878
- source_kind TEXT NOT NULL CHECK (source_kind IN ('distilled', 'superseded')),
1879
- source_object_type TEXT
1880
- CHECK (source_object_type IS NULL OR source_object_type IN ('decision', 'policy', 'customer', 'project')),
1881
- source_object_id INTEGER,
1882
- created_at TEXT NOT NULL,
1883
- FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL
1884
- )
1885
- `);
1886
- db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_tenant ON entities(tenant_id)`);
1887
- db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_memory ON entities(memory_id)`);
1888
- db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_source_object ON entities(source_object_type, source_object_id) WHERE source_object_id IS NOT NULL`);
1889
- db.exec(`
1890
- CREATE TABLE relations (
1891
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1892
- tenant_id TEXT NOT NULL,
1893
- from_entity_id INTEGER NOT NULL,
1894
- to_entity_id INTEGER NOT NULL,
1895
- rel_type TEXT NOT NULL
1896
- CHECK (rel_type IN ('owns', 'supersedes', 'depends-on', 'blocked-by', 'references')),
1897
- memory_id TEXT,
1898
- source_kind TEXT NOT NULL CHECK (source_kind IN ('distilled', 'superseded')),
1899
- source_object_type TEXT
1900
- CHECK (source_object_type IS NULL OR source_object_type IN ('decision', 'policy', 'customer', 'project')),
1901
- source_object_id INTEGER,
1902
- created_at TEXT NOT NULL,
1903
- FOREIGN KEY (from_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
1904
- FOREIGN KEY (to_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
1905
- FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL
1906
- )
1907
- `);
1908
- db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_tenant ON relations(tenant_id)`);
1909
- db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity_id)`);
1910
- db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity_id)`);
1911
- db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_memory ON relations(memory_id)`);
1912
- // Mirrors idx_entities_source_object: removeGraphEntitiesForObject (close-time cleanup)
1913
- // deletes relations by (source_object_type, source_object_id), so index that pair.
1914
- db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_source_object ON relations(source_object_type, source_object_id) WHERE source_object_id IS NOT NULL`);
1915
- // entities guard (dual-provenance): at least one valid provenance, no raw.
1916
- // - all-null -> ABORT.
1917
- // - memory path (memory_id NOT NULL): source_kind == the FK'd memory's kind
1918
- // (raw / lying source_kind ABORT) AND tenant-match.
1919
- // - object path (memory_id NULL): the (type,id) points at an EXISTING same-tenant
1920
- // E2 row whose status is active|superseded (explicit 4-way CASE per table).
1921
- db.exec(`
1922
- CREATE TRIGGER IF NOT EXISTS trg_entities_consolidated_only_insert
1923
- BEFORE INSERT ON entities
1924
- BEGIN
1925
- SELECT CASE
1926
- WHEN (NEW.source_object_type IS NULL AND NEW.source_object_id IS NOT NULL)
1927
- OR (NEW.source_object_type IS NOT NULL AND NEW.source_object_id IS NULL)
1928
- THEN RAISE(ABORT, 'source_object_type and source_object_id must be set together (all-or-none)')
1929
- WHEN NEW.memory_id IS NULL AND NEW.source_object_id IS NULL
1930
- THEN RAISE(ABORT, 'graph row needs a memory or a source object')
1931
- WHEN NEW.memory_id IS NOT NULL AND NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
1932
- THEN RAISE(ABORT, 'entities.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
1933
- WHEN NEW.memory_id IS NOT NULL AND NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1934
- THEN RAISE(ABORT, 'entities.tenant_id must match memories.tenant_id for the referenced memory')
1935
- WHEN NEW.source_object_type ='decision'
1936
- AND NOT EXISTS (SELECT 1 FROM decisions WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
1937
- THEN RAISE(ABORT, 'entities.source_object must reference an active/superseded decision in the same tenant')
1938
- WHEN NEW.source_object_type ='policy'
1939
- AND NOT EXISTS (SELECT 1 FROM policies WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
1940
- THEN RAISE(ABORT, 'entities.source_object must reference an active/superseded policy in the same tenant')
1941
- WHEN NEW.source_object_type ='customer'
1942
- AND NOT EXISTS (SELECT 1 FROM customer_notes WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
1943
- THEN RAISE(ABORT, 'entities.source_object must reference an active/superseded customer_note in the same tenant')
1944
- WHEN NEW.source_object_type ='project'
1945
- AND NOT EXISTS (SELECT 1 FROM project_briefs WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
1946
- THEN RAISE(ABORT, 'entities.source_object must reference an active/superseded project_brief in the same tenant')
1947
- END;
1948
- END
1949
- `);
1950
- db.exec(`
1951
- CREATE TRIGGER IF NOT EXISTS trg_entities_consolidated_only_update
1952
- BEFORE UPDATE ON entities
1953
- WHEN NEW.memory_id IS NOT OLD.memory_id
1954
- OR NEW.source_kind IS NOT OLD.source_kind
1955
- OR NEW.tenant_id IS NOT OLD.tenant_id
1956
- OR NEW.source_object_type IS NOT OLD.source_object_type
1957
- OR NEW.source_object_id IS NOT OLD.source_object_id
1958
- BEGIN
1959
- -- NO all-null ABORT on UPDATE: node:sqlite fires this BEFORE UPDATE trigger from
1960
- -- the FK ON DELETE SET NULL action even with recursive_triggers OFF (empirically
1961
- -- verified - the plan's "SET NULL does not fire the guard" premise is false for
1962
- -- this build). A mirror forget on a memory-only entity legitimately produces an
1963
- -- all-null row; aborting here would block the memory delete. The INSERT guard
1964
- -- still forbids CREATING a provenance-less row, and the next graph rebuild clears
1965
- -- any all-null orphan. A raw UPDATE to all-null is harmless (not recall-surfaced).
1966
- -- Object-path STATUS is validated only when the object columns THEMSELVES change
1967
- -- (an explicit re-point), NOT on the FK ON DELETE SET NULL transition (which changes
1968
- -- only memory_id). This threads the needle (codex P1 + P2 round 4): the SET NULL of a
1969
- -- since-closed object's mirror is NOT blocked (object cols unchanged -> object checks
1970
- -- skip), while an explicit raw UPDATE that re-points the row to a bad/closed/cross-
1971
- -- tenant object IS rejected. all-or-none + memory-path (raw/tenant) always apply.
1972
- SELECT CASE
1973
- WHEN (NEW.source_object_type IS NULL AND NEW.source_object_id IS NOT NULL)
1974
- OR (NEW.source_object_type IS NOT NULL AND NEW.source_object_id IS NULL)
1975
- THEN RAISE(ABORT, 'entities.source_object_type and source_object_id must be set together (all-or-none)')
1976
- WHEN NEW.memory_id IS NOT NULL AND NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
1977
- THEN RAISE(ABORT, 'entities.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
1978
- WHEN NEW.memory_id IS NOT NULL AND NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
1979
- THEN RAISE(ABORT, 'entities.tenant_id must match memories.tenant_id for the referenced memory')
1980
- WHEN (NEW.source_object_type IS NOT OLD.source_object_type OR NEW.source_object_id IS NOT OLD.source_object_id OR NEW.tenant_id IS NOT OLD.tenant_id)
1981
- AND NEW.source_object_type ='decision'
1982
- AND NOT EXISTS (SELECT 1 FROM decisions WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
1983
- THEN RAISE(ABORT, 'entities.source_object must reference an active/superseded decision in the same tenant')
1984
- WHEN (NEW.source_object_type IS NOT OLD.source_object_type OR NEW.source_object_id IS NOT OLD.source_object_id OR NEW.tenant_id IS NOT OLD.tenant_id)
1985
- AND NEW.source_object_type ='policy'
1986
- AND NOT EXISTS (SELECT 1 FROM policies WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
1987
- THEN RAISE(ABORT, 'entities.source_object must reference an active/superseded policy in the same tenant')
1988
- WHEN (NEW.source_object_type IS NOT OLD.source_object_type OR NEW.source_object_id IS NOT OLD.source_object_id OR NEW.tenant_id IS NOT OLD.tenant_id)
1989
- AND NEW.source_object_type ='customer'
1990
- AND NOT EXISTS (SELECT 1 FROM customer_notes WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
1991
- THEN RAISE(ABORT, 'entities.source_object must reference an active/superseded customer_note in the same tenant')
1992
- WHEN (NEW.source_object_type IS NOT OLD.source_object_type OR NEW.source_object_id IS NOT OLD.source_object_id OR NEW.tenant_id IS NOT OLD.tenant_id)
1993
- AND NEW.source_object_type ='project'
1994
- AND NOT EXISTS (SELECT 1 FROM project_briefs WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
1995
- THEN RAISE(ABORT, 'entities.source_object must reference an active/superseded project_brief in the same tenant')
1996
- END;
1997
- END
1998
- `);
1999
- // relations guard (dual-provenance) + the existing from/to endpoint same-tenant checks.
2000
- db.exec(`
2001
- CREATE TRIGGER IF NOT EXISTS trg_relations_consolidated_only_insert
2002
- BEFORE INSERT ON relations
2003
- BEGIN
2004
- SELECT CASE
2005
- WHEN (NEW.source_object_type IS NULL AND NEW.source_object_id IS NOT NULL)
2006
- OR (NEW.source_object_type IS NOT NULL AND NEW.source_object_id IS NULL)
2007
- THEN RAISE(ABORT, 'source_object_type and source_object_id must be set together (all-or-none)')
2008
- WHEN NEW.memory_id IS NULL AND NEW.source_object_id IS NULL
2009
- THEN RAISE(ABORT, 'graph row needs a memory or a source object')
2010
- WHEN NEW.memory_id IS NOT NULL AND NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
2011
- THEN RAISE(ABORT, 'relations.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
2012
- WHEN NEW.memory_id IS NOT NULL AND NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
2013
- THEN RAISE(ABORT, 'relations.tenant_id must match memories.tenant_id for the referenced memory')
2014
- WHEN NEW.source_object_type ='decision'
2015
- AND NOT EXISTS (SELECT 1 FROM decisions WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
2016
- THEN RAISE(ABORT, 'relations.source_object must reference an active/superseded decision in the same tenant')
2017
- WHEN NEW.source_object_type ='policy'
2018
- AND NOT EXISTS (SELECT 1 FROM policies WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
2019
- THEN RAISE(ABORT, 'relations.source_object must reference an active/superseded policy in the same tenant')
2020
- WHEN NEW.source_object_type ='customer'
2021
- AND NOT EXISTS (SELECT 1 FROM customer_notes WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
2022
- THEN RAISE(ABORT, 'relations.source_object must reference an active/superseded customer_note in the same tenant')
2023
- WHEN NEW.source_object_type ='project'
2024
- AND NOT EXISTS (SELECT 1 FROM project_briefs WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
2025
- THEN RAISE(ABORT, 'relations.source_object must reference an active/superseded project_brief in the same tenant')
2026
- WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.from_entity_id)
2027
- THEN RAISE(ABORT, 'relations.tenant_id must match the from_entity tenant (no cross-tenant edges)')
2028
- WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.to_entity_id)
2029
- THEN RAISE(ABORT, 'relations.tenant_id must match the to_entity tenant (no cross-tenant edges)')
2030
- END;
2031
- END
2032
- `);
2033
- db.exec(`
2034
- CREATE TRIGGER IF NOT EXISTS trg_relations_consolidated_only_update
2035
- BEFORE UPDATE ON relations
2036
- WHEN NEW.memory_id IS NOT OLD.memory_id
2037
- OR NEW.source_kind IS NOT OLD.source_kind
2038
- OR NEW.tenant_id IS NOT OLD.tenant_id
2039
- OR NEW.source_object_type IS NOT OLD.source_object_type
2040
- OR NEW.source_object_id IS NOT OLD.source_object_id
2041
- OR NEW.from_entity_id IS NOT OLD.from_entity_id
2042
- OR NEW.to_entity_id IS NOT OLD.to_entity_id
2043
- BEGIN
2044
- -- NO all-null ABORT on UPDATE (same reason as the entities UPDATE trigger): the FK
2045
- -- ON DELETE SET NULL fires this trigger in node:sqlite, and a mirror forget on a
2046
- -- memory-only relation legitimately nulls memory_id. The INSERT guard still forbids
2047
- -- creating a provenance-less relation; the rebuild clears any orphan.
2048
- -- Object-path STATUS validated only on an explicit object-column change, NOT on the
2049
- -- FK SET NULL transition (see the entities UPDATE trigger): SET NULL of a since-closed
2050
- -- object's mirror is not blocked (object cols unchanged), while an explicit re-point to
2051
- -- a bad object is rejected. all-or-none + memory-path + endpoint checks always apply.
2052
- SELECT CASE
2053
- WHEN (NEW.source_object_type IS NULL AND NEW.source_object_id IS NOT NULL)
2054
- OR (NEW.source_object_type IS NOT NULL AND NEW.source_object_id IS NULL)
2055
- THEN RAISE(ABORT, 'relations.source_object_type and source_object_id must be set together (all-or-none)')
2056
- WHEN NEW.memory_id IS NOT NULL AND NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
2057
- THEN RAISE(ABORT, 'relations.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
2058
- WHEN NEW.memory_id IS NOT NULL AND NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
2059
- THEN RAISE(ABORT, 'relations.tenant_id must match memories.tenant_id for the referenced memory')
2060
- WHEN (NEW.source_object_type IS NOT OLD.source_object_type OR NEW.source_object_id IS NOT OLD.source_object_id OR NEW.tenant_id IS NOT OLD.tenant_id)
2061
- AND NEW.source_object_type ='decision'
2062
- AND NOT EXISTS (SELECT 1 FROM decisions WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
2063
- THEN RAISE(ABORT, 'relations.source_object must reference an active/superseded decision in the same tenant')
2064
- WHEN (NEW.source_object_type IS NOT OLD.source_object_type OR NEW.source_object_id IS NOT OLD.source_object_id OR NEW.tenant_id IS NOT OLD.tenant_id)
2065
- AND NEW.source_object_type ='policy'
2066
- AND NOT EXISTS (SELECT 1 FROM policies WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
2067
- THEN RAISE(ABORT, 'relations.source_object must reference an active/superseded policy in the same tenant')
2068
- WHEN (NEW.source_object_type IS NOT OLD.source_object_type OR NEW.source_object_id IS NOT OLD.source_object_id OR NEW.tenant_id IS NOT OLD.tenant_id)
2069
- AND NEW.source_object_type ='customer'
2070
- AND NOT EXISTS (SELECT 1 FROM customer_notes WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
2071
- THEN RAISE(ABORT, 'relations.source_object must reference an active/superseded customer_note in the same tenant')
2072
- WHEN (NEW.source_object_type IS NOT OLD.source_object_type OR NEW.source_object_id IS NOT OLD.source_object_id OR NEW.tenant_id IS NOT OLD.tenant_id)
2073
- AND NEW.source_object_type ='project'
2074
- AND NOT EXISTS (SELECT 1 FROM project_briefs WHERE id = NEW.source_object_id AND tenant_id = NEW.tenant_id AND status IN ('active', 'superseded'))
2075
- THEN RAISE(ABORT, 'relations.source_object must reference an active/superseded project_brief in the same tenant')
2076
- WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.from_entity_id)
2077
- THEN RAISE(ABORT, 'relations.tenant_id must match the from_entity tenant (no cross-tenant edges)')
2078
- WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.to_entity_id)
2079
- THEN RAISE(ABORT, 'relations.tenant_id must match the to_entity tenant (no cross-tenant edges)')
2080
- END;
2081
- END
2082
- `);
2083
- // Recreated VERBATIM from v37 (logic unchanged): an entity that is a relation
2084
- // endpoint cannot be moved cross-tenant while referenced. trg_memories_graph_referenced_guard
2085
- // (on memories) and trg_graph_queue_* (on graph_extraction_queue) are NOT recreated
2086
- // here: those tables are not dropped by v38, so the triggers survive.
2087
- db.exec(`
2088
- CREATE TRIGGER IF NOT EXISTS trg_entities_no_tenant_move_when_referenced
2089
- BEFORE UPDATE ON entities
2090
- WHEN NEW.tenant_id IS NOT OLD.tenant_id
2091
- AND EXISTS (SELECT 1 FROM relations WHERE from_entity_id = OLD.id OR to_entity_id = OLD.id)
2092
- BEGIN
2093
- SELECT RAISE(ABORT, 'cannot move an entity cross-tenant while a relation references it as an endpoint (E3.3 graph-on-consolidated guard); rebuild/remove the relations first');
2094
- END
2095
- `);
2096
- },
2097
- },
2098
- {
2099
- version: 39,
2100
- up: (db, ctx) => {
2101
- // Memory scope isolation (docs/plans/2026-07-01-memory-scope-isolation.md).
2102
- // origin_project: '<name>' = owned by that project, '' = user-global,
2103
- // NULL = legacy/unknown (ambient context treats NULL as deny).
2104
- if (!tableHasColumn(db, 'memories', 'origin_project')) {
2105
- db.exec(`ALTER TABLE memories ADD COLUMN origin_project TEXT`);
2106
- }
2107
- // Backfill on store-location evidence only (no path-tag guessing):
2108
- // 1. Rows shared from a project carry source 'shared:<project>:<ts>' -
2109
- // take <project>; a share whose <project> is the home dir basename
2110
- // maps to '' (user-global).
2111
- // 2. Every other row was written into THIS store, so it takes the
2112
- // store's own origin: `<project>/.hippo` -> '<project>', the
2113
- // home/global store -> '' (user-global).
2114
- // Rows stay NULL only when no hippoRoot was provided.
2115
- const hippoRoot = ctx?.hippoRoot;
2116
- if (hippoRoot) {
2117
- // Provenance-source evidence first (shared:<project>: / promoted:<localRoot>,
2118
- // parsed by the same helper the markdown-import stamp uses), then the
2119
- // store's own location for everything else.
2120
- const homeName = path.basename(os.homedir()).toLowerCase();
2121
- // SAFETY: sourcedRows' shape matches the two columns (id, source)
2122
- // named in the SELECT above.
2123
- const sourcedRows = db.prepare(`SELECT id, source FROM memories WHERE origin_project IS NULL AND (source LIKE 'shared:%' OR source LIKE 'promoted:%')`).all();
2124
- const setOrigin = db.prepare(`UPDATE memories SET origin_project = ? WHERE id = ?`);
2125
- for (const row of sourcedRows) {
2126
- const origin = originFromSource(row.source, homeName);
2127
- if (origin === null)
2128
- continue;
2129
- setOrigin.run(origin, row.id);
2130
- }
2131
- // The global root itself is ALWAYS user-global (''), regardless of
2132
- // what surrounds it on disk - a HIPPO_HOME inside a dotfiles git
2133
- // repo must not stamp the whole corpus with that repo's name.
2134
- const storeOrigin = isGlobalStoreRoot(hippoRoot)
2135
- ? ''
2136
- : deriveOriginProject(path.dirname(hippoRoot));
2137
- db.prepare(`UPDATE memories SET origin_project = ? WHERE origin_project IS NULL`).run(storeOrigin);
2138
- }
2139
- // Rollback-safety guard (v24 precedent): a pre-isolation binary opening
2140
- // this DB would ignore origin_project and the secret veto and resume
2141
- // injecting cross-project rows. 1.24.0 is the first version with the
2142
- // isolation behavior. Forward-only - never lower an existing minimum.
2143
- // SAFETY: this get() result's shape matches the single `value` column
2144
- // named in the SELECT above.
2145
- const existingMin = db.prepare(`SELECT value FROM meta WHERE key = 'min_compatible_binary'`).get()?.value;
2146
- if (!existingMin || compareSemver('1.24.0', existingMin) > 0) {
2147
- db.prepare(`INSERT INTO meta(key, value) VALUES('min_compatible_binary', ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run('1.24.0');
2148
- }
2149
- },
2150
- },
2151
- {
2152
- version: 40,
2153
- up: (db) => {
2154
- // LC1 retrieval-trace persistence (docs/plans/2026-08-02-lc1-recall-trace-persistence.md).
2155
- // Follows the v18 goal_recall_log style: one parent trace row per
2156
- // recall, a WITHOUT ROWID child table for the ranked results, and a
2157
- // separate append-only outcomes table so audit_log pruning can never
2158
- // erase training data.
2159
- //
2160
- // F6 (deliberate, not an oversight): unlike v39, this migration does
2161
- // NOT bump `min_compatible_binary`. A pre-v40 binary opening this DB
2162
- // ignores the three new tables and keeps writing `last_retrieval_ids`
2163
- // exactly as before — it never touches `last_trace_id` (that key
2164
- // simply stays whatever it was). recordTraceOutcome's F4 consumer-
2165
- // side validation (recall-trace.ts) makes any resulting staleness
2166
- // harmless to linkage: it re-validates the named trace's tenant AND
2167
- // intersects credited ids against the trace's OWN result set before
2168
- // inserting, so a stale/mismatched trace id from an old-binary write
2169
- // gets silently skipped rather than mislinked. That preserves the
2170
- // plan's "drop the three tables + last_trace_id restores v39 behavior
2171
- // exactly" rollback promise — a min_compatible_binary bump would
2172
- // additionally lock old binaries out of the WHOLE store for what is
2173
- // purely an observability/training feature, which the rollback
2174
- // promise does not require.
2175
- db.exec(`
2176
- CREATE TABLE IF NOT EXISTS recall_traces (
2177
- id INTEGER PRIMARY KEY AUTOINCREMENT,
2178
- ts TEXT NOT NULL,
2179
- tenant_id TEXT NOT NULL DEFAULT 'default',
2180
- session_id TEXT,
2181
- pipeline TEXT NOT NULL CHECK (pipeline IN ('api','cli','context','mcp')),
2182
- -- 'mcp' reserved for the deferred MCP wire-up. Deliberate: SQLite
2183
- -- cannot ALTER a CHECK constraint, so extending it later means a
2184
- -- full table-rebuild migration. One unused enum value now is
2185
- -- cheaper than that rebuild.
2186
- query_hash TEXT NOT NULL, -- sha256/16, NEVER raw query (audit convention, cli.ts:1532)
2187
- query_length INTEGER NOT NULL,
2188
- result_count INTEGER NOT NULL,
2189
- explain_mode INTEGER NOT NULL DEFAULT 0
2190
- );
2191
- CREATE INDEX IF NOT EXISTS idx_recall_traces_tenant_ts ON recall_traces(tenant_id, ts DESC);
2192
-
2193
- CREATE TABLE IF NOT EXISTS recall_trace_results (
2194
- trace_id INTEGER NOT NULL REFERENCES recall_traces(id) ON DELETE CASCADE,
2195
- tenant_id TEXT NOT NULL DEFAULT 'default', -- denormalized like goal_recall_log (v18
2196
- -- precedent): tenant-scoped training queries
2197
- -- over the memory index must not need a join
2198
- -- back through recall_traces
2199
- memory_id TEXT NOT NULL, -- NO FK to memories: traces must OUTLIVE forgotten
2200
- -- memories (they are LC2's negative class). PRAGMA
2201
- -- foreign_keys=ON is real, so an FK would either
2202
- -- block inserts or cascade-delete exactly the rows
2203
- -- training needs. Deliberate.
2204
- result_rank INTEGER NOT NULL, -- NOT "rank" (SQL keyword; audit rule 10)
2205
- score REAL NOT NULL,
2206
- rerank_json TEXT, -- compact RerankStep[] when explain/trace present; else NULL
2207
- PRIMARY KEY (trace_id, result_rank)
2208
- ) WITHOUT ROWID;
2209
- CREATE INDEX IF NOT EXISTS idx_recall_trace_results_tenant_memory
2210
- ON recall_trace_results(tenant_id, memory_id);
2211
-
2212
- CREATE TABLE IF NOT EXISTS recall_trace_outcomes (
2213
- id INTEGER PRIMARY KEY AUTOINCREMENT,
2214
- trace_id INTEGER NOT NULL REFERENCES recall_traces(id) ON DELETE CASCADE,
2215
- ts TEXT NOT NULL,
2216
- tenant_id TEXT NOT NULL DEFAULT 'default',
2217
- outcome TEXT NOT NULL CHECK (outcome IN ('positive','negative')),
2218
- memory_ids_json TEXT NOT NULL -- ids actually credited by this outcome event
2219
- );
2220
- CREATE INDEX IF NOT EXISTS idx_recall_trace_outcomes_trace ON recall_trace_outcomes(trace_id);
2221
- `);
2222
- },
2223
- },
2224
- {
2225
- version: 41,
2226
- up: (db) => {
2227
- // AT1 rejected-value tombstone (docs/plans/2026-08-15-at1-rejected-value-tombstone.md).
2228
- // Additive table, template = v40 above. A human who rejects a fact gets
2229
- // a durable say: the write-path guard in upsertEntryRow refuses any
2230
- // write that would re-introduce a value whose normalized digest
2231
- // matches a row here.
2232
- //
2233
- // No raw content and no preview stored. Follows the raw_archive
2234
- // redaction precedent (raw-archive.ts payload is {redacted:true, ...}):
2235
- // a rejected value may itself be a secret or PII ("never store my key
2236
- // again") — persisting it in the tombstone would defeat the point. The
2237
- // human sees the content at reject time (CLI echoes it); afterwards
2238
- // `reason` is the human-readable identity.
2239
- //
2240
- // No FK on source_memory_id (v40 precedent above: tombstone outlives
2241
- // the row it was sourced from) — provenance only.
2242
- //
2243
- // Reserved-word check on column names (skill-episode lesson, rule 10):
2244
- // tenant/digest/reason/rejected/source/normalized/chars are non-reserved.
2245
- //
2246
- // No min_compatible_binary bump — a deliberate tradeoff (plan §2,
2247
- // flagged to the ship gate). An old binary sharing a synced store
2248
- // writes WITHOUT the guard until upgraded; documented in
2249
- // MEMORY_ENVELOPE.md rather than hard-locking every old binary out of
2250
- // the store, which is disproportionate for the dominant single-user
2251
- // single-binary deployment.
2252
- db.exec(`
2253
- CREATE TABLE IF NOT EXISTS rejected_values (
2254
- tenant_id TEXT NOT NULL,
2255
- digest TEXT NOT NULL, -- sha256/64 of normalized content
2256
- reason TEXT, -- human-supplied; the only description stored
2257
- rejected_by TEXT, -- actor ('user:<id>' / 'cli' / ...)
2258
- rejected_at TEXT NOT NULL,
2259
- source_memory_id TEXT, -- provenance only; NO FK (tombstone outlives the row)
2260
- normalized_chars INTEGER, -- weak sanity aid for humans listing tombstones
2261
- PRIMARY KEY (tenant_id, digest)
2262
- ) WITHOUT ROWID;
2263
- `);
2264
- },
2265
- },
2266
- {
2267
- version: 42,
2268
- up: (db) => {
2269
- // W1 handoff envelope (trajectories/01M2BQTM4AGFVMYY7G2XV5G7WY/plan.md).
2270
- // Five nullable columns so the envelope carries evidence and outcome
2271
- // and W2/W5 can filter on them without parsing JSON.
2272
- if (!tableHasColumn(db, 'session_handoffs', 'constraints_json')) {
2273
- db.exec(`ALTER TABLE session_handoffs ADD COLUMN constraints_json TEXT`);
2274
- }
2275
- if (!tableHasColumn(db, 'session_handoffs', 'evidence_json')) {
2276
- db.exec(`ALTER TABLE session_handoffs ADD COLUMN evidence_json TEXT`);
2277
- }
2278
- if (!tableHasColumn(db, 'session_handoffs', 'outcome')) {
2279
- db.exec(`ALTER TABLE session_handoffs ADD COLUMN outcome TEXT`);
2280
- // codex P2: backfill from session_complete so pre-existing handoffs don't
2281
- // all read as unfinished and get injected by the new 72h ambient fallback.
2282
- db.exec(`
2283
- UPDATE session_handoffs SET outcome = (
2284
- SELECT e.content FROM session_events e
2285
- WHERE e.tenant_id = session_handoffs.tenant_id AND e.session_id = session_handoffs.session_id
2286
- AND e.event_type = 'session_complete' AND e.content IN ('success','partial','failure')
2287
- ORDER BY e.created_at DESC, e.id DESC LIMIT 1
2288
- )
2289
- WHERE outcome IS NULL AND EXISTS (
2290
- SELECT 1 FROM session_events e
2291
- WHERE e.tenant_id = session_handoffs.tenant_id AND e.session_id = session_handoffs.session_id
2292
- AND e.event_type = 'session_complete' AND e.content IN ('success','partial','failure')
2293
- )
2294
- `);
2295
- }
2296
- if (!tableHasColumn(db, 'session_handoffs', 'target_runtime')) {
2297
- db.exec(`ALTER TABLE session_handoffs ADD COLUMN target_runtime TEXT`);
2298
- }
2299
- if (!tableHasColumn(db, 'session_handoffs', 'card_id')) {
2300
- db.exec(`ALTER TABLE session_handoffs ADD COLUMN card_id TEXT`);
2301
- }
2302
- db.exec(`CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_outcome ON session_handoffs(tenant_id, outcome, created_at DESC)`);
2303
- },
2304
- },
2305
- {
2306
- version: 43,
2307
- up: (db) => {
2308
- // W2a work-queue cards (trajectories/01M2D5VSYJFK4YXQ0RG2NGCPYJ/plan.md). Additive only, v41 precedent.
2309
- db.exec(`
2310
- CREATE TABLE IF NOT EXISTS cards (
2311
- id TEXT PRIMARY KEY,
2312
- title TEXT NOT NULL,
2313
- status TEXT NOT NULL DEFAULT 'backlog',
2314
- assignee_runtime TEXT,
2315
- repo TEXT,
2316
- contract TEXT,
2317
- budget INTEGER,
2318
- lease_until TEXT,
2319
- heartbeat_at TEXT,
2320
- created_at TEXT NOT NULL,
2321
- updated_at TEXT NOT NULL,
2322
- tenant_id TEXT NOT NULL DEFAULT 'default',
2323
- scope TEXT
2324
- ) WITHOUT ROWID;
2325
- CREATE INDEX IF NOT EXISTS idx_cards_tenant_status
2326
- ON cards(tenant_id, status, updated_at DESC);
2327
-
2328
- CREATE TABLE IF NOT EXISTS card_deps (
2329
- parent TEXT NOT NULL REFERENCES cards(id),
2330
- child TEXT NOT NULL REFERENCES cards(id),
2331
- tenant_id TEXT NOT NULL DEFAULT 'default',
2332
- created_at TEXT NOT NULL,
2333
- PRIMARY KEY (parent, child)
2334
- ) WITHOUT ROWID;
2335
- CREATE INDEX IF NOT EXISTS idx_card_deps_tenant_child
2336
- ON card_deps(tenant_id, child);
2337
- CREATE INDEX IF NOT EXISTS idx_card_deps_tenant_parent
2338
- ON card_deps(tenant_id, parent);
2339
-
2340
- CREATE TABLE IF NOT EXISTS card_runs (
2341
- id INTEGER PRIMARY KEY AUTOINCREMENT,
2342
- card TEXT NOT NULL REFERENCES cards(id),
2343
- runtime TEXT NOT NULL,
2344
- session_id TEXT,
2345
- started TEXT NOT NULL,
2346
- ended TEXT,
2347
- outcome TEXT,
2348
- created_at TEXT NOT NULL,
2349
- updated_at TEXT NOT NULL,
2350
- tenant_id TEXT NOT NULL DEFAULT 'default'
2351
- );
2352
- CREATE INDEX IF NOT EXISTS idx_card_runs_tenant_card
2353
- ON card_runs(tenant_id, card, started DESC);
2354
-
2355
- CREATE TABLE IF NOT EXISTS card_comments (
2356
- id INTEGER PRIMARY KEY AUTOINCREMENT,
2357
- card_id TEXT NOT NULL REFERENCES cards(id),
2358
- author TEXT NOT NULL,
2359
- body TEXT NOT NULL,
2360
- created_at TEXT NOT NULL,
2361
- tenant_id TEXT NOT NULL DEFAULT 'default'
2362
- );
2363
- CREATE INDEX IF NOT EXISTS idx_card_comments_tenant_card
2364
- ON card_comments(tenant_id, card_id, created_at DESC);
2365
-
2366
- CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_card
2367
- ON session_handoffs(tenant_id, card_id, created_at DESC);
2368
- `);
2369
- },
2370
- },
2371
- {
2372
- version: 44,
2373
- up: (db) => {
2374
- // Dormant memories (src/dormant.ts): with `dormant.enabled`, the sleep
2375
- // decay pass moves a faded memory here instead of deleting it. The row
2376
- // leaves `memories` in the same transaction, so recall, context and
2377
- // every sleep pass stop seeing it; entry_json is the full MemoryEntry
2378
- // snapshot `hippo dormant restore` writes back. No FK to memories (the
2379
- // memories row is gone by design). Additive only, v41 precedent: no
2380
- // min_compatible_binary bump. An older binary ignores the table and
2381
- // keeps deleting faded memories as it always did.
2382
- db.exec(`
2383
- CREATE TABLE IF NOT EXISTS dormant_memories (
2384
- tenant_id TEXT NOT NULL DEFAULT 'default',
2385
- id TEXT NOT NULL,
2386
- content TEXT NOT NULL,
2387
- entry_json TEXT NOT NULL,
2388
- reason TEXT NOT NULL,
2389
- strength REAL NOT NULL,
2390
- dormant_at TEXT NOT NULL,
2391
- PRIMARY KEY (tenant_id, id)
2392
- ) WITHOUT ROWID;
2393
- CREATE INDEX IF NOT EXISTS idx_dormant_memories_tenant_time
2394
- ON dormant_memories(tenant_id, dormant_at DESC);
2395
- `);
2396
- },
2397
- },
2398
- {
2399
- version: 45,
2400
- up: (db) => {
2401
- // Token ledger (src/token-ledger.ts, ROADMAP TE0): one row per block of
2402
- // memory text hippo hands an agent (hook, CLI, MCP, HTTP). `event` is 'inject'
2403
- // (sent), 'skip' (unchanged since the session's last inject, not sent), 'reset'
2404
- // (compaction dropped earlier injections, so the next one must be sent) or
2405
- // 'reread' (re-read by later calls, booked at session end per call day). block_hash
2406
- // lets the per-prompt hook skip an unchanged block. Rows older than the retention
2407
- // window are pruned on write. Additive only: no min_compatible_binary bump.
2408
- db.exec(`
2409
- CREATE TABLE IF NOT EXISTS token_ledger (
2410
- id INTEGER PRIMARY KEY AUTOINCREMENT,
2411
- ts TEXT NOT NULL,
2412
- tenant_id TEXT NOT NULL DEFAULT 'default',
2413
- session_id TEXT,
2414
- surface TEXT NOT NULL,
2415
- event TEXT NOT NULL,
2416
- items INTEGER NOT NULL DEFAULT 0,
2417
- tokens INTEGER NOT NULL DEFAULT 0,
2418
- block_hash TEXT
2419
- );
2420
- CREATE INDEX IF NOT EXISTS idx_token_ledger_session
2421
- ON token_ledger(tenant_id, session_id, surface, id DESC);
2422
- CREATE INDEX IF NOT EXISTS idx_token_ledger_ts
2423
- ON token_ledger(ts);
2424
- `);
2425
- },
2426
- },
2427
- {
2428
- version: 46,
2429
- up: (db) => {
2430
- // CD13 failure log (src/failure-log.ts): hashes only, since failure text can carry paths and secrets.
2431
- // Additive only: no min_compatible_binary bump.
2432
- db.exec(`
2433
- CREATE TABLE IF NOT EXISTS failure_log (
2434
- id INTEGER PRIMARY KEY AUTOINCREMENT,
2435
- ts TEXT NOT NULL,
2436
- tenant_id TEXT NOT NULL DEFAULT 'default',
2437
- session_id TEXT,
2438
- tool TEXT,
2439
- outcome TEXT NOT NULL,
2440
- skip_rule TEXT,
2441
- sig_hash TEXT,
2442
- detail_hash TEXT
2443
- );
2444
- CREATE INDEX IF NOT EXISTS idx_failure_log_tenant
2445
- ON failure_log(tenant_id, id);
2446
- CREATE INDEX IF NOT EXISTS idx_failure_log_ts
2447
- ON failure_log(ts);
2448
- `);
2449
- },
2450
- },
2451
- {
2452
- version: 47,
2453
- up: (db) => {
2454
- // Scope grants (src/auth.ts, ROADMAP EI2): a member key reads a
2455
- // restricted scope only by an explicit row here. Additive only: no
2456
- // min_compatible_binary bump.
2457
- db.exec(`
2458
- CREATE TABLE IF NOT EXISTS api_key_scope_grants (
2459
- key_id TEXT NOT NULL,
2460
- scope TEXT NOT NULL,
2461
- granted_at TEXT NOT NULL,
2462
- PRIMARY KEY(key_id, scope)
2463
- );
2464
- `);
2465
- },
2466
- },
2467
- {
2468
- version: 48,
2469
- up: (db) => {
2470
- // Quarantine (src/quarantine.ts, CD5): a poisoned or suspect memory sits here pending
2471
- // admin review instead of being hidden with no record. Additive only: no min_compatible_binary bump.
2472
- db.exec(MEMORY_QUARANTINE_DDL);
2473
- },
2474
- },
2475
- {
2476
- version: 49,
2477
- up: (db) => {
2478
- // Compaction record (src/compaction-record.ts): one row per Claude Code compaction, written before it (started)
2479
- // and after it (summarised, done). Not a memory row. Additive only: no min_compatible_binary bump.
2480
- db.exec(`
2481
- CREATE TABLE IF NOT EXISTS compactions (
2482
- tenant_id TEXT NOT NULL DEFAULT 'default',
2483
- id TEXT NOT NULL,
2484
- session_id TEXT NOT NULL,
2485
- origin_project TEXT NOT NULL DEFAULT '',
2486
- compact_trigger TEXT,
2487
- cwd TEXT,
2488
- transcript_path TEXT,
2489
- snapshot_saved INTEGER NOT NULL DEFAULT 0,
2490
- started_at TEXT NOT NULL,
2491
- summarised_at TEXT,
2492
- summary TEXT,
2493
- items_json TEXT,
2494
- items_written INTEGER NOT NULL DEFAULT 0,
2495
- status TEXT NOT NULL DEFAULT 'started' CHECK (status IN ('started','summarised','done','no-summary')),
2496
- PRIMARY KEY (tenant_id, id)
2497
- );
2498
- CREATE INDEX IF NOT EXISTS idx_compactions_session
2499
- ON compactions(tenant_id, session_id, started_at);
2500
- CREATE INDEX IF NOT EXISTS idx_compactions_status
2501
- ON compactions(tenant_id, status);
2502
- `);
2503
- },
2504
- },
2505
- {
2506
- version: 50,
2507
- up: (db) => {
2508
- // Per-turn delivery events (src/recall-trace.ts). Additive only: no min_compatible_binary bump; rollback drops both tables
2509
- // and sets schema_version back to 49. No CHECK on enum columns since SQLite cannot alter one; delivery-recorder.ts unions are the allowlist.
2510
- db.exec(`
2511
- CREATE TABLE IF NOT EXISTS delivery_events (
2512
- id INTEGER PRIMARY KEY AUTOINCREMENT,
2513
- ts TEXT NOT NULL,
2514
- ledger_version INTEGER NOT NULL,
2515
- tenant_id TEXT NOT NULL DEFAULT 'default',
2516
- runtime TEXT NOT NULL,
2517
- event_type TEXT NOT NULL,
2518
- surface TEXT NOT NULL,
2519
- store_hash TEXT NOT NULL,
2520
- write_store TEXT NOT NULL,
2521
- project_hash TEXT,
2522
- session_id TEXT,
2523
- session_state TEXT NOT NULL,
2524
- host_turn_id TEXT,
2525
- turn_seq INTEGER,
2526
- duplicate_of INTEGER,
2527
- prompt_hash TEXT,
2528
- prompt_length INTEGER NOT NULL DEFAULT 0,
2529
- query_hash TEXT,
2530
- recall_trace_id INTEGER,
2531
- block_state TEXT NOT NULL,
2532
- prompt_recall INTEGER NOT NULL DEFAULT 0,
2533
- considered_count INTEGER NOT NULL DEFAULT 0,
2534
- filtered_count INTEGER NOT NULL DEFAULT 0,
2535
- selected_count INTEGER NOT NULL DEFAULT 0,
2536
- emitted_count INTEGER NOT NULL DEFAULT 0,
2537
- rejected_count INTEGER NOT NULL DEFAULT 0,
2538
- rejected_unlisted INTEGER NOT NULL DEFAULT 0,
2539
- sections_shown INTEGER NOT NULL DEFAULT 0,
2540
- sections_dropped INTEGER NOT NULL DEFAULT 0,
2541
- budget_tokens INTEGER NOT NULL DEFAULT 0,
2542
- selected_tokens INTEGER NOT NULL DEFAULT 0,
2543
- injected_tokens INTEGER NOT NULL DEFAULT 0,
2544
- static_hash TEXT,
2545
- recall_hash TEXT,
2546
- emitted_hash TEXT,
2547
- elapsed_ms INTEGER NOT NULL DEFAULT 0
2548
- );
2549
- CREATE INDEX IF NOT EXISTS idx_delivery_events_session ON delivery_events(tenant_id, session_id, id);
2550
- CREATE INDEX IF NOT EXISTS idx_delivery_events_ts ON delivery_events(ts);
2551
- CREATE UNIQUE INDEX IF NOT EXISTS idx_delivery_events_turn
2552
- ON delivery_events(tenant_id, session_id, event_type, turn_seq) WHERE turn_seq IS NOT NULL;
2553
-
2554
- CREATE TABLE IF NOT EXISTS delivery_candidates (
2555
- event_id INTEGER NOT NULL REFERENCES delivery_events(id) ON DELETE CASCADE,
2556
- tenant_id TEXT NOT NULL DEFAULT 'default',
2557
- memory_id TEXT NOT NULL, -- no FK: events outlive forgotten memories, as recall traces do
2558
- source_store TEXT NOT NULL,
2559
- pool TEXT NOT NULL,
2560
- stage TEXT NOT NULL,
2561
- outcome TEXT NOT NULL,
2562
- reason TEXT,
2563
- cand_rank INTEGER,
2564
- score REAL,
2565
- tokens INTEGER,
2566
- PRIMARY KEY (event_id, memory_id)
2567
- ) WITHOUT ROWID;
2568
- `);
2569
- },
2570
- },
2571
- {
2572
- version: 51,
2573
- up: (db) => {
2574
- // Both indexes serve loadAmbientCandidates (src/store.ts); their WHERE text must match its SQL for the planner to pick them.
2575
- // The drift index stays empty on a healthy store, so the drift probe is one index seek instead of a tenant scan. Additive only.
2576
- db.exec(`
2577
- CREATE INDEX IF NOT EXISTS idx_memories_pinned
2578
- ON memories(tenant_id, created, id) WHERE pinned = 1;
2579
- CREATE INDEX IF NOT EXISTS idx_memories_created_drift
2580
- ON memories(tenant_id) WHERE superseded_by IS NULL AND (length(created) <> 24 OR created NOT LIKE '%Z');
2581
- `);
2582
- },
2583
- },
2584
- ];
2585
- function tableHasColumn(db, tableName, columnName) {
2586
- if (!/^[a-z_]+$/i.test(tableName))
2587
- throw new Error(`Invalid table name: ${tableName}`);
2588
- // SAFETY: rows' shape matches PRAGMA table_info's documented `name` column.
2589
- const rows = db.prepare(`PRAGMA table_info(${tableName})`).all();
2590
- return rows.some((row) => row.name === columnName);
2591
- }
2592
- function tableExists(db, tableName) {
2593
- if (!/^[a-z_]+$/i.test(tableName))
2594
- throw new Error(`Invalid table name: ${tableName}`);
2595
- // SAFETY: row's shape matches the single `name` column named in the
2596
- // SELECT above.
2597
- const row = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name = ?`).get(tableName);
2598
- return !!row?.name;
2599
- }
2600
- export function getHippoDbPath(hippoRoot) {
2601
- return path.join(hippoRoot, 'hippo.db');
2602
- }
2603
- export function getCurrentSchemaVersion() {
2604
- return CURRENT_SCHEMA_VERSION;
2605
- }
2606
- /** Thrown by {@link assertBinaryCompatible}; doctor uses it to pick the upgrade fix over a generic permissions fix. */
2607
- export class IncompatibleBinaryError extends Error {
2608
- }
2609
- /** Refuse a store stamped for a newer binary. Fails closed: runMigrations creates meta first, so a failed read is a real error. */
2610
- function assertBinaryCompatible(db) {
2611
- const minRequired = getMeta(db, 'min_compatible_binary');
2612
- if (minRequired && compareSemver(minRequired, PACKAGE_VERSION) > 0) {
2613
- throw new IncompatibleBinaryError(`hippo-memory: this database requires hippo-memory >= ${minRequired}, but the running binary is ${PACKAGE_VERSION}. ` +
2614
- `Upgrade hippo-memory to open it; an older binary does not know this schema and could expose private rows or damage the store.`);
2615
- }
2616
- }
2617
- export function isSqliteBusy(error) {
2618
- const code = error?.errcode;
2619
- return code === 5 || code === 6 || code === 517;
2620
- }
2621
- // busy_timeout covers neither of this file's two contended statements: SQLite
2622
- // skips the busy handler for `PRAGMA journal_mode` and for a write that upgrades
2623
- // a deferred read snapshot. Both need an explicit wait instead.
2624
- export function execWithBusyRetry(db, sql, timeoutMs = 30000) {
2625
- const deadline = Date.now() + timeoutMs;
2626
- const idle = new Int32Array(new SharedArrayBuffer(4));
2627
- for (;;) {
2628
- try {
2629
- db.exec(sql);
2630
- return;
2631
- }
2632
- catch (error) {
2633
- if (!isSqliteBusy(error) || Date.now() >= deadline)
2634
- throw error;
2635
- Atomics.wait(idle, 0, 0, 10 + Math.floor(Math.random() * 20));
2636
- }
2637
- }
2638
- }
2639
- // Hook commands run on every prompt, so inside withSharedStoreHandles each store pays its pragmas, migration check and mirror cleanup once.
2640
- const sharedHandles = new Map();
2641
- const sharedSet = new WeakSet();
2642
- let shareDepth = 0;
2643
- let shareBusyWaitMs;
2644
- /** Lock wait for hook commands: under the 5 s prompt-hook budget even after a few skipped writes, and far above a normal write's hold. */
2645
- export const HOOK_DB_WAIT_MS = 1000;
2646
- /** A busy store made a command skip work: warn once per process (the holder is usually `hippo sleep`). */
2647
- export function noteStoreBusy(skipped) {
2648
- log.once('store-busy', 'warn', `store busy (another hippo process holds the write lock); ${skipped}`);
2649
- // A lock held past one full wait belongs to a long transaction, so the hook's later writes skip at once.
2650
- for (const db of sharedHandles.values()) {
2651
- if (db.isOpen !== false)
2652
- db.exec('PRAGMA busy_timeout = 0');
2653
- }
2654
- }
2655
- function closeSharedStoreHandles() {
2656
- for (const db of sharedHandles.values()) {
2657
- sharedSet.delete(db);
2658
- if (db.isOpen !== false)
2659
- db.close();
2660
- }
2661
- sharedHandles.clear();
2662
- }
2663
- /** Runs `fn` with one handle per store: openHippoDb reuses it and closeHippoDb leaves it open until `fn` settles or the process exits.
2664
- * `busyWaitMs` is the lock wait of every open inside `fn` that does not pass its own. */
2665
- export async function withSharedStoreHandles(fn, opts) {
2666
- if (shareDepth++ === 0) {
2667
- process.once('exit', closeSharedStoreHandles);
2668
- shareBusyWaitMs = opts?.busyWaitMs;
2669
- }
2670
- try {
2671
- return await fn();
2672
- }
2673
- finally {
2674
- if (--shareDepth === 0) {
2675
- process.off('exit', closeSharedStoreHandles);
2676
- closeSharedStoreHandles();
2677
- shareBusyWaitMs = undefined;
2678
- }
2679
- }
2680
- }
2681
- /** `busyWaitMs` shortens every lock wait of this open, for a hook that must finish inside its own timeout. */
2682
- export function openHippoDb(hippoRoot, opts) {
2683
- if (shareDepth === 0)
2684
- return openOwnHippoDb(hippoRoot, opts);
2685
- const busyWaitMs = opts?.busyWaitMs ?? shareBusyWaitMs;
2686
- const key = `${path.resolve(getHippoDbPath(hippoRoot))}\0${busyWaitMs ?? ''}`;
2687
- const shared = sharedHandles.get(key);
2688
- if (shared?.isOpen && !shared.isTransaction)
2689
- return shared;
2690
- const db = openOwnHippoDb(hippoRoot, { busyWaitMs });
2691
- // An open nested inside a transaction gets its own connection, as it did before sharing.
2692
- if (!shared?.isOpen) {
2693
- sharedHandles.set(key, db);
2694
- sharedSet.add(db);
2695
- }
2696
- return db;
2697
- }
2698
- function openOwnHippoDb(hippoRoot, opts) {
2699
- fs.mkdirSync(hippoRoot, { recursive: true });
2700
- const db = new DatabaseSync(getHippoDbPath(hippoRoot));
2701
- const busyWaitMs = opts?.busyWaitMs;
2702
- try {
2703
- db.exec(`PRAGMA busy_timeout = ${busyWaitMs ?? 5000}`);
2704
- execWithBusyRetry(db, 'PRAGMA journal_mode = WAL', busyWaitMs);
2705
- db.exec('PRAGMA synchronous = NORMAL');
2706
- db.exec('PRAGMA wal_autocheckpoint = 100');
2707
- db.exec('PRAGMA foreign_keys = ON');
2708
- runMigrations(db, hippoRoot, busyWaitMs);
2709
- // Path A backfill: delete any orphan markdown mirrors for already-archived
2710
- // raw_archive rows. Idempotent via per-row raw_archive.mirror_cleaned_at
2711
- // (v21). Wrapped in try/catch — a filesystem failure must not prevent DB open.
2712
- try {
2713
- cleanupArchivedMirrors(hippoRoot, db);
2714
- }
2715
- catch (cleanupErr) {
2716
- log.error(`openHippoDb: cleanupArchivedMirrors failed (non-fatal): ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`);
2717
- }
2718
- return db;
2719
- }
2720
- catch (error) {
2721
- try {
2722
- db.close();
2723
- }
2724
- catch {
2725
- // Best effort only.
2726
- }
2727
- throw error;
2728
- }
2729
- }
2730
- /** Open an existing store without changing it: no mkdir, WAL switch, migration or mirror cleanup. Throws when hippo.db is missing. */
2731
- export function openHippoDbReadOnly(hippoRoot) {
2732
- const db = new DatabaseSync(getHippoDbPath(hippoRoot), { readOnly: true });
2733
- try {
2734
- db.exec('PRAGMA busy_timeout = 5000');
2735
- if (tableExists(db, 'meta'))
2736
- assertBinaryCompatible(db);
2737
- return db;
2738
- }
2739
- catch (error) {
2740
- try {
2741
- db.close();
2742
- }
2743
- catch {
2744
- // Best effort only.
2745
- }
2746
- throw error;
2747
- }
2748
- }
2749
- function runMigrations(db, hippoRoot, busyWaitMs) {
2750
- ensureMetaTable(db);
2751
- // Before anything writes, so a stale binary never repairs or migrates a store it does not understand.
2752
- assertBinaryCompatible(db);
2753
- let currentVersion = getSchemaVersion(db);
2754
- if (currentVersion > 0)
2755
- ensureContinuityTables(db);
2756
- for (const migration of MIGRATIONS) {
2757
- if (migration.version <= currentVersion)
2758
- continue;
2759
- execWithBusyRetry(db, 'BEGIN IMMEDIATE', busyWaitMs);
2760
- try {
2761
- // A newer binary may have migrated and raised the minimum while we waited for the lock.
2762
- assertBinaryCompatible(db);
2763
- // Re-read under the write lock: another process may have applied this
2764
- // migration while we waited, and re-running one is not idempotent.
2765
- const applied = getSchemaVersion(db);
2766
- if (applied >= migration.version) {
2767
- db.exec('COMMIT');
2768
- currentVersion = applied;
2769
- continue;
2770
- }
2771
- migration.up(db, { hippoRoot });
2772
- setSchemaVersion(db, migration.version);
2773
- db.exec('COMMIT');
2774
- currentVersion = migration.version;
2775
- }
2776
- catch (error) {
2777
- try {
2778
- db.exec('ROLLBACK');
2779
- }
2780
- catch { /* already rolled back; keep the original error */ }
2781
- throw error;
2782
- }
2783
- }
2784
- ensureContinuityIndexes(db);
2785
- ensureMetaDefaults(db);
2786
- ensureOptionalFts(db);
2787
- }
2788
- function ensureMetaTable(db) {
2789
- db.exec(`
2790
- CREATE TABLE IF NOT EXISTS meta (
2791
- key TEXT PRIMARY KEY,
2792
- value TEXT NOT NULL
2793
- );
2794
- `);
2795
- }
2796
- export function getSchemaVersion(db) {
2797
- if (!tableExists(db, 'meta'))
2798
- return 0;
2799
- // SAFETY: row's shape matches the single `value` column named in the
2800
- // SELECT above.
2801
- const row = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
2802
- const version = Number(row?.value ?? 0);
2803
- return Number.isFinite(version) ? version : 0;
2804
- }
2805
- function setSchemaVersion(db, version) {
2806
- db.prepare(`INSERT INTO meta(key, value) VALUES('schema_version', ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run(String(version));
2807
- db.exec(`PRAGMA user_version = ${Math.max(0, Math.trunc(version))}`);
2808
- }
2809
- // Shared by migration v48 and the stamped-store self-heal below.
2810
- const MEMORY_QUARANTINE_DDL = `
2811
- CREATE TABLE IF NOT EXISTS memory_quarantine (
2812
- tenant_id TEXT NOT NULL DEFAULT 'default',
2813
- memory_id TEXT NOT NULL,
2814
- original_scope TEXT,
2815
- reason TEXT NOT NULL,
2816
- status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','approved','rejected')),
2817
- quarantined_at TEXT NOT NULL,
2818
- decided_at TEXT,
2819
- decided_by TEXT,
2820
- PRIMARY KEY (tenant_id, memory_id)
2821
- ) WITHOUT ROWID;
2822
- CREATE INDEX IF NOT EXISTS idx_memory_quarantine_status
2823
- ON memory_quarantine(tenant_id, status, quarantined_at DESC);
2824
- `;
2825
- // Before the loop on stamped stores: a table lost after its migration stamped (2026-08-15
2826
- // incident) is never re-migrated, and v4/v16/v22 ALTER or read it. Fresh stores use the chain.
2827
- function ensureContinuityTables(db) {
2828
- db.exec(`
2829
- CREATE TABLE IF NOT EXISTS task_snapshots (
2830
- id INTEGER PRIMARY KEY AUTOINCREMENT,
2831
- task TEXT NOT NULL,
2832
- summary TEXT NOT NULL,
2833
- next_step TEXT NOT NULL,
2834
- status TEXT NOT NULL,
2835
- source TEXT NOT NULL,
2836
- created_at TEXT NOT NULL,
2837
- updated_at TEXT NOT NULL,
2838
- session_id TEXT,
2839
- tenant_id TEXT NOT NULL DEFAULT 'default',
2840
- scope TEXT
2841
- )
2842
- `);
2843
- db.exec(`
2844
- CREATE TABLE IF NOT EXISTS session_events (
2845
- id INTEGER PRIMARY KEY AUTOINCREMENT,
2846
- session_id TEXT NOT NULL,
2847
- task TEXT,
2848
- event_type TEXT NOT NULL,
2849
- content TEXT NOT NULL,
2850
- source TEXT NOT NULL,
2851
- metadata_json TEXT NOT NULL,
2852
- created_at TEXT NOT NULL,
2853
- tenant_id TEXT NOT NULL DEFAULT 'default',
2854
- scope TEXT
2855
- )
2856
- `);
2857
- db.exec(`
2858
- CREATE TABLE IF NOT EXISTS session_handoffs (
2859
- id INTEGER PRIMARY KEY AUTOINCREMENT,
2860
- session_id TEXT NOT NULL,
2861
- repo_root TEXT,
2862
- task_id TEXT,
2863
- summary TEXT NOT NULL,
2864
- next_action TEXT,
2865
- artifacts_json TEXT NOT NULL DEFAULT '[]',
2866
- created_at TEXT NOT NULL,
2867
- tenant_id TEXT NOT NULL DEFAULT 'default',
2868
- scope TEXT,
2869
- constraints_json TEXT,
2870
- evidence_json TEXT,
2871
- outcome TEXT,
2872
- target_runtime TEXT,
2873
- card_id TEXT
2874
- )
2875
- `);
2876
- db.exec(`
2877
- CREATE TABLE IF NOT EXISTS cards (
2878
- id TEXT PRIMARY KEY,
2879
- title TEXT NOT NULL,
2880
- status TEXT NOT NULL DEFAULT 'backlog',
2881
- assignee_runtime TEXT,
2882
- repo TEXT,
2883
- contract TEXT,
2884
- budget INTEGER,
2885
- lease_until TEXT,
2886
- heartbeat_at TEXT,
2887
- created_at TEXT NOT NULL,
2888
- updated_at TEXT NOT NULL,
2889
- tenant_id TEXT NOT NULL DEFAULT 'default',
2890
- scope TEXT
2891
- ) WITHOUT ROWID
2892
- `);
2893
- db.exec(`
2894
- CREATE TABLE IF NOT EXISTS card_deps (
2895
- parent TEXT NOT NULL REFERENCES cards(id),
2896
- child TEXT NOT NULL REFERENCES cards(id),
2897
- tenant_id TEXT NOT NULL DEFAULT 'default',
2898
- created_at TEXT NOT NULL,
2899
- PRIMARY KEY (parent, child)
2900
- ) WITHOUT ROWID
2901
- `);
2902
- db.exec(`
2903
- CREATE TABLE IF NOT EXISTS card_runs (
2904
- id INTEGER PRIMARY KEY AUTOINCREMENT,
2905
- card TEXT NOT NULL REFERENCES cards(id),
2906
- runtime TEXT NOT NULL,
2907
- session_id TEXT,
2908
- started TEXT NOT NULL,
2909
- ended TEXT,
2910
- outcome TEXT,
2911
- created_at TEXT NOT NULL,
2912
- updated_at TEXT NOT NULL,
2913
- tenant_id TEXT NOT NULL DEFAULT 'default'
2914
- )
2915
- `);
2916
- db.exec(`
2917
- CREATE TABLE IF NOT EXISTS card_comments (
2918
- id INTEGER PRIMARY KEY AUTOINCREMENT,
2919
- card_id TEXT NOT NULL REFERENCES cards(id),
2920
- author TEXT NOT NULL,
2921
- body TEXT NOT NULL,
2922
- created_at TEXT NOT NULL,
2923
- tenant_id TEXT NOT NULL DEFAULT 'default'
2924
- )
2925
- `);
2926
- db.exec(MEMORY_QUARANTINE_DDL);
2927
- }
2928
- // After the loop: tenant_id (v16) and scope (v23) do not exist yet on a genuine old store.
2929
- function ensureContinuityIndexes(db) {
2930
- db.exec(`CREATE INDEX IF NOT EXISTS idx_task_snapshots_status_updated ON task_snapshots(status, updated_at DESC, id DESC)`);
2931
- db.exec(`CREATE INDEX IF NOT EXISTS idx_task_snapshots_tenant_status ON task_snapshots(tenant_id, status, updated_at DESC)`);
2932
- db.exec(`CREATE INDEX IF NOT EXISTS idx_task_snapshots_tenant_scope ON task_snapshots(tenant_id, scope, status)`);
2933
- db.exec(`CREATE INDEX IF NOT EXISTS idx_session_events_session_created ON session_events(session_id, created_at DESC, id DESC)`);
2934
- db.exec(`CREATE INDEX IF NOT EXISTS idx_session_events_task_created ON session_events(task, created_at DESC, id DESC)`);
2935
- db.exec(`CREATE INDEX IF NOT EXISTS idx_session_events_tenant_session ON session_events(tenant_id, session_id, created_at DESC, id DESC)`);
2936
- db.exec(`CREATE INDEX IF NOT EXISTS idx_session_handoffs_session ON session_handoffs(session_id, created_at DESC)`);
2937
- db.exec(`CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_session ON session_handoffs(tenant_id, session_id, created_at DESC)`);
2938
- db.exec(`CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_outcome ON session_handoffs(tenant_id, outcome, created_at DESC)`);
2939
- db.exec(`CREATE INDEX IF NOT EXISTS idx_cards_tenant_status ON cards(tenant_id, status, updated_at DESC)`);
2940
- db.exec(`CREATE INDEX IF NOT EXISTS idx_card_deps_tenant_child ON card_deps(tenant_id, child)`);
2941
- db.exec(`CREATE INDEX IF NOT EXISTS idx_card_deps_tenant_parent ON card_deps(tenant_id, parent)`);
2942
- db.exec(`CREATE INDEX IF NOT EXISTS idx_card_runs_tenant_card ON card_runs(tenant_id, card, started DESC)`);
2943
- db.exec(`CREATE INDEX IF NOT EXISTS idx_card_comments_tenant_card ON card_comments(tenant_id, card_id, created_at DESC)`);
2944
- db.exec(`CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_card ON session_handoffs(tenant_id, card_id, created_at DESC)`);
2945
- }
2946
- function ensureMetaDefaults(db) {
2947
- const defaults = [
2948
- ['schema_version', String(CURRENT_SCHEMA_VERSION)],
2949
- ['last_retrieval_ids', '[]'],
2950
- ['last_trace_id', ''],
2951
- ['total_remembered', '0'],
2952
- ['total_recalled', '0'],
2953
- ['total_forgotten', '0'],
2954
- ['fts5_available', '0'],
2955
- ];
2956
- // Read-first: an already-current store has all 7 keys, so this is one
2957
- // SELECT and zero writes instead of 7 no-op INSERT OR IGNOREs, each of
2958
- // which takes a RESERVED lock even when nothing changes.
2959
- const keys = defaults.map(([key]) => key);
2960
- const present = new Set(db
2961
- .prepare(`SELECT key FROM meta WHERE key IN (${keys.map(() => '?').join(',')})`)
2962
- .all(...keys)
2963
- .map((r) => r.key));
2964
- if (present.size === defaults.length)
2965
- return;
2966
- const stmt = db.prepare(`INSERT OR IGNORE INTO meta(key, value) VALUES (?, ?)`);
2967
- for (const [key, value] of defaults) {
2968
- if (!present.has(key))
2969
- stmt.run(key, value);
2970
- }
2971
- }
2972
- function ensureOptionalFts(db) {
2973
- let available = false;
2974
- try {
2975
- db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(id UNINDEXED, content, tags)`);
2976
- backfillFtsIndex(db);
2977
- available = true;
2978
- }
2979
- catch (err) {
2980
- log.warn(`full-text index unavailable (${err instanceof Error ? err.message : String(err)}); search falls back to slower LIKE matching`);
2981
- }
2982
- // Read-first: only write when the flag actually changed, so a healthy
2983
- // store's open doesn't take a write lock for a same-value upsert.
2984
- const flag = available ? '1' : '0';
2985
- if (getMeta(db, 'fts5_available') !== flag)
2986
- setMeta(db, 'fts5_available', flag);
2987
- }
2988
- function backfillFtsIndex(db) {
2989
- // SAFETY: this get() result's shape matches the single aliased `c` column
2990
- // named in the SELECT above.
2991
- const memCount = db.prepare(`SELECT COUNT(*) AS c FROM memories`).get()?.c ?? 0;
2992
- // SAFETY: this get() result's shape matches the single aliased `c` column named in the SELECT above.
2993
- // memories_fts_docsize is FTS5's cheaper one-row-per-document shadow table; fall back for a foreign-built index.
2994
- const ftsTable = tableExists(db, 'memories_fts_docsize') ? 'memories_fts_docsize' : 'memories_fts';
2995
- const ftsCount = db.prepare(`SELECT COUNT(*) AS c FROM ${ftsTable}`).get()?.c ?? 0;
2996
- if (memCount === ftsCount)
2997
- return;
2998
- db.exec(`
2999
- INSERT INTO memories_fts(id, content, tags)
3000
- SELECT m.id, m.content, m.tags_json
3001
- FROM memories m
3002
- WHERE NOT EXISTS (
3003
- SELECT 1 FROM memories_fts f WHERE f.id = m.id
3004
- )
3005
- `);
3006
- db.exec(`
3007
- DELETE FROM memories_fts
3008
- WHERE id NOT IN (SELECT id FROM memories)
3009
- `);
3010
- }
3011
- export function closeHippoDb(db) {
3012
- if (sharedSet.has(db))
3013
- return;
3014
- db.close();
3015
- }
3016
- export function getMeta(db, key, fallback = '') {
3017
- // SAFETY: row's shape matches the single `value` column named in the
3018
- // SELECT above.
3019
- const row = db.prepare(`SELECT value FROM meta WHERE key = ?`).get(key);
3020
- return row?.value ?? fallback;
3021
- }
3022
- export function setMeta(db, key, value) {
3023
- db.prepare(`INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run(key, value);
3024
- }
3025
- /** Row count of one table; null when the table is missing or unreadable, which callers show as unknown. */
3026
- export function countTableRows(db, table) {
3027
- try {
3028
- // SAFETY: COUNT(*) returns one row with one numeric column.
3029
- const row = db.prepare(`SELECT COUNT(*) AS n FROM "${table.replace(/"/g, '""')}"`).get();
3030
- return Number(row?.n ?? 0);
3031
- }
3032
- catch {
3033
- // Callers treat an uncountable table as unknown; the bundle and doctor still finish.
3034
- return null;
3035
- }
3036
- }
3037
- export function isFtsAvailable(db) {
3038
- return getMeta(db, 'fts5_available', '0') === '1';
3039
- }
3040
- export function pruneConsolidationRuns(db, keep = 50) {
3041
- db.prepare(`
3042
- DELETE FROM consolidation_runs
3043
- WHERE id NOT IN (
3044
- SELECT id FROM consolidation_runs
3045
- ORDER BY timestamp DESC, id DESC
3046
- LIMIT ?
3047
- )
3048
- `).run(keep);
3049
- }
1
+ export { isSqliteBusy, STORE_BUSY_MESSAGE, execWithBusyRetry } from './db/busy.js';
2
+ export { getSchemaVersion, getMeta, setMeta, isFtsAvailable } from './db/meta.js';
3
+ export { countTableRows, pruneConsolidationRuns } from './db/tables.js';
4
+ export { getCurrentSchemaVersion, IncompatibleBinaryError } from './db/migrate.js';
5
+ export { getHippoDbPath, HOOK_DB_WAIT_MS, noteStoreBusy, withSharedStoreHandles, SERVER_DB_WAIT_MS, withBusyWait, openHippoDb, openHippoDbReadOnly, closeHippoDb, } from './db/open.js';
3050
6
  //# sourceMappingURL=db.js.map