hippo-memory 1.61.0 → 1.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (377) hide show
  1. package/README.md +37 -53
  2. package/dist/agent-memories/apply.d.ts +3 -1
  3. package/dist/agent-memories/apply.js +23 -1
  4. package/dist/agent-memories/claude-code.d.ts +4 -1
  5. package/dist/agent-memories/claude-code.js +53 -9
  6. package/dist/agent-memories/report.d.ts +1 -0
  7. package/dist/agent-memories/report.js +2 -2
  8. package/dist/agent-memories/sync.d.ts +3 -3
  9. package/dist/agent-memories/sync.js +46 -24
  10. package/dist/agent-memories/types.d.ts +0 -2
  11. package/dist/ambient-store.d.ts +4 -4
  12. package/dist/ambient-store.js +4 -3
  13. package/dist/api/assemble.d.ts +7 -10
  14. package/dist/api/assemble.js +62 -66
  15. package/dist/api/audit.d.ts +2 -2
  16. package/dist/api/audit.js +2 -2
  17. package/dist/api/auth.d.ts +6 -6
  18. package/dist/api/auth.js +5 -7
  19. package/dist/api/context-select.d.ts +50 -0
  20. package/dist/api/context-select.js +342 -0
  21. package/dist/api/context-types.d.ts +12 -17
  22. package/dist/api/context.d.ts +5 -4
  23. package/dist/api/context.js +208 -535
  24. package/dist/api/dormant.js +1 -1
  25. package/dist/api/drill-down.d.ts +4 -4
  26. package/dist/api/drill-down.js +56 -41
  27. package/dist/api/outcome.d.ts +8 -13
  28. package/dist/api/outcome.js +13 -17
  29. package/dist/api/promote.d.ts +5 -8
  30. package/dist/api/promote.js +55 -66
  31. package/dist/api/quarantine.js +3 -2
  32. package/dist/api/recall-types.d.ts +43 -55
  33. package/dist/api/recall.d.ts +3 -3
  34. package/dist/api/recall.js +310 -456
  35. package/dist/api/remember.d.ts +2 -2
  36. package/dist/api/sleep.d.ts +7 -35
  37. package/dist/api/sleep.js +206 -220
  38. package/dist/api/tokens.d.ts +2 -2
  39. package/dist/api/tokens.js +2 -2
  40. package/dist/api/types.d.ts +6 -10
  41. package/dist/api/types.js +2 -4
  42. package/dist/audit-prune.d.ts +4 -6
  43. package/dist/audit-prune.js +3 -5
  44. package/dist/audit.js +11 -33
  45. package/dist/auth.d.ts +8 -9
  46. package/dist/auth.js +5 -7
  47. package/dist/autolearn.d.ts +1 -1
  48. package/dist/autolearn.js +1 -1
  49. package/dist/availability.js +3 -5
  50. package/dist/capture/command.d.ts +5 -4
  51. package/dist/capture/command.js +8 -22
  52. package/dist/capture/compact.d.ts +2 -2
  53. package/dist/capture/compact.js +11 -15
  54. package/dist/capture/extract.js +37 -115
  55. package/dist/capture-error.d.ts +1 -1
  56. package/dist/capture-error.js +1 -1
  57. package/dist/churn-git.d.ts +1 -1
  58. package/dist/churn-git.js +1 -1
  59. package/dist/cli/audit.js +3 -4
  60. package/dist/cli/auth.js +3 -6
  61. package/dist/cli/briefs.js +324 -306
  62. package/dist/cli/context.js +44 -34
  63. package/dist/cli/continuity.js +283 -271
  64. package/dist/cli/curate.d.ts +1 -1
  65. package/dist/cli/curate.js +43 -58
  66. package/dist/cli/dag.js +5 -9
  67. package/dist/cli/decisions.js +334 -345
  68. package/dist/cli/explain.js +68 -61
  69. package/dist/cli/goals.js +1 -1
  70. package/dist/cli/init.js +1 -1
  71. package/dist/cli/maintenance.js +62 -51
  72. package/dist/cli/playbooks.js +391 -379
  73. package/dist/cli/projects.js +11 -6
  74. package/dist/cli/recall.js +30 -44
  75. package/dist/cli/remember.js +118 -87
  76. package/dist/cli/session-hooks.js +106 -115
  77. package/dist/cli/setup.d.ts +1 -1
  78. package/dist/cli/setup.js +267 -250
  79. package/dist/cli/shared.js +3 -3
  80. package/dist/cli/slack.js +1 -1
  81. package/dist/cli/sleep.js +27 -1
  82. package/dist/cli/status.d.ts +4 -4
  83. package/dist/cli/status.js +80 -76
  84. package/dist/cli/transfer.js +88 -107
  85. package/dist/cli/usage.js +9 -6
  86. package/dist/cli.d.ts +1 -1
  87. package/dist/cli.js +4 -9
  88. package/dist/compaction-record.d.ts +2 -2
  89. package/dist/compaction-record.js +89 -68
  90. package/dist/compare.d.ts +11 -16
  91. package/dist/compare.js +11 -16
  92. package/dist/config.d.ts +19 -18
  93. package/dist/config.js +90 -67
  94. package/dist/connectors/github/backfill.d.ts +2 -2
  95. package/dist/connectors/github/backfill.js +8 -15
  96. package/dist/connectors/github/cli-impl.js +3 -8
  97. package/dist/connectors/github/deletion.d.ts +5 -12
  98. package/dist/connectors/github/deletion.js +5 -12
  99. package/dist/connectors/github/dlq.d.ts +6 -9
  100. package/dist/connectors/github/dlq.js +2 -3
  101. package/dist/connectors/github/ingest.d.ts +5 -7
  102. package/dist/connectors/github/ingest.js +8 -12
  103. package/dist/connectors/github/octokit-client.d.ts +3 -5
  104. package/dist/connectors/github/octokit-client.js +5 -6
  105. package/dist/connectors/github/signature.d.ts +9 -39
  106. package/dist/connectors/github/signature.js +9 -39
  107. package/dist/connectors/github/tenant-routing.d.ts +1 -1
  108. package/dist/connectors/github/tenant-routing.js +1 -1
  109. package/dist/connectors/github/transform.js +2 -2
  110. package/dist/connectors/github/types.d.ts +2 -10
  111. package/dist/connectors/github/types.js +1 -3
  112. package/dist/connectors/slack/deletion.d.ts +3 -8
  113. package/dist/connectors/slack/deletion.js +3 -8
  114. package/dist/connectors/slack/dlq.d.ts +1 -1
  115. package/dist/connectors/slack/ingest.d.ts +1 -1
  116. package/dist/connectors/slack/ingest.js +7 -16
  117. package/dist/connectors/slack/signature.d.ts +1 -1
  118. package/dist/connectors/slack/tenant-routing.d.ts +3 -5
  119. package/dist/connectors/slack/tenant-routing.js +3 -5
  120. package/dist/connectors/slack/transform.d.ts +5 -6
  121. package/dist/connectors/slack/transform.js +5 -6
  122. package/dist/connectors/slack/types.d.ts +2 -6
  123. package/dist/connectors/slack/types.js +1 -3
  124. package/dist/connectors/slack/web-client.js +10 -3
  125. package/dist/connectors/slack/workspaces.d.ts +3 -5
  126. package/dist/connectors/slack/workspaces.js +3 -5
  127. package/dist/consolidate/conflicts.js +3 -14
  128. package/dist/consolidate/decay.js +9 -29
  129. package/dist/consolidate/llm-passes.js +4 -5
  130. package/dist/consolidate/merge.js +8 -23
  131. package/dist/consolidate/run.d.ts +1 -8
  132. package/dist/consolidate/run.js +3 -25
  133. package/dist/consolidate/sleep.js +5 -17
  134. package/dist/consolidate/traces.js +9 -21
  135. package/dist/customer-notes.d.ts +5 -7
  136. package/dist/customer-notes.js +82 -76
  137. package/dist/dag.d.ts +10 -21
  138. package/dist/dag.js +189 -203
  139. package/dist/db/continuity.js +2 -2
  140. package/dist/db/migrations/v14.js +1 -1
  141. package/dist/db/migrations/v15.js +1 -2
  142. package/dist/db/migrations/v16.js +3 -4
  143. package/dist/db/migrations/v17.js +2 -3
  144. package/dist/db/migrations/v19.js +1 -1
  145. package/dist/db/migrations/v20.js +1 -1
  146. package/dist/db/migrations/v21.js +2 -6
  147. package/dist/db/migrations/v22.js +2 -4
  148. package/dist/db/migrations/v23.js +1 -1
  149. package/dist/db/migrations/v24.js +4 -6
  150. package/dist/db/migrations/v25.js +2 -3
  151. package/dist/db/migrations/v26.js +3 -3
  152. package/dist/db/migrations/v27.js +2 -10
  153. package/dist/db/migrations/v28.js +5 -8
  154. package/dist/db/migrations/v29.js +3 -4
  155. package/dist/db/migrations/v30.js +2 -2
  156. package/dist/db/migrations/v31.js +1 -1
  157. package/dist/db/migrations/v32.js +1 -1
  158. package/dist/db/migrations/v33.js +3 -3
  159. package/dist/db/migrations/v34.js +1 -1
  160. package/dist/db/migrations/v35.js +3 -4
  161. package/dist/db/migrations/v36.js +3 -4
  162. package/dist/db/migrations/v37.js +5 -5
  163. package/dist/db/migrations/v38.js +7 -8
  164. package/dist/db/migrations/v39.js +1 -1
  165. package/dist/db/migrations/v40.js +4 -16
  166. package/dist/db/migrations/v41.js +3 -4
  167. package/dist/db/migrations/v42.js +3 -4
  168. package/dist/db/migrations/v45.js +1 -1
  169. package/dist/db/migrations/v46.js +1 -1
  170. package/dist/db/migrations/v47.js +1 -1
  171. package/dist/db/migrations/v48.js +1 -1
  172. package/dist/decisions.d.ts +2 -2
  173. package/dist/decisions.js +97 -80
  174. package/dist/dedupe.js +86 -61
  175. package/dist/delivery-recorder.js +154 -135
  176. package/dist/doctor.js +129 -110
  177. package/dist/dormant.js +1 -4
  178. package/dist/embedding-provider.d.ts +4 -8
  179. package/dist/embedding-provider.js +4 -8
  180. package/dist/embeddings.js +55 -47
  181. package/dist/env.d.ts +1 -1
  182. package/dist/env.js +12 -12
  183. package/dist/escape.d.ts +5 -0
  184. package/dist/escape.js +10 -0
  185. package/dist/eval-stats.d.ts +1 -2
  186. package/dist/eval-stats.js +1 -2
  187. package/dist/eval-suite.js +27 -21
  188. package/dist/extract.js +4 -9
  189. package/dist/failure-log.d.ts +3 -3
  190. package/dist/failure-log.js +1 -1
  191. package/dist/forward-claim-detector.d.ts +2 -4
  192. package/dist/forward-claim-detector.js +6 -11
  193. package/dist/goals.d.ts +3 -3
  194. package/dist/goals.js +103 -91
  195. package/dist/graph/read.d.ts +2 -2
  196. package/dist/graph/read.js +5 -6
  197. package/dist/graph/types.d.ts +8 -8
  198. package/dist/graph/write.d.ts +7 -14
  199. package/dist/graph/write.js +16 -23
  200. package/dist/graph-extract.d.ts +7 -8
  201. package/dist/graph-extract.js +62 -72
  202. package/dist/graph-recall.d.ts +2 -2
  203. package/dist/graph-recall.js +55 -49
  204. package/dist/graph-stream.d.ts +5 -6
  205. package/dist/graph-stream.js +66 -57
  206. package/dist/graph-view.d.ts +2 -2
  207. package/dist/graph-view.js +7 -7
  208. package/dist/half-life-migration.d.ts +1 -2
  209. package/dist/half-life-migration.js +2 -3
  210. package/dist/hooks/codex-session.js +1 -1
  211. package/dist/hooks/codex-wrapper.d.ts +1 -1
  212. package/dist/hooks/codex-wrapper.js +3 -2
  213. package/dist/hooks/json-hooks.d.ts +2 -2
  214. package/dist/hooks/json-hooks.js +5 -4
  215. package/dist/hooks/opencode.d.ts +1 -1
  216. package/dist/hooks/opencode.js +5 -4
  217. package/dist/hooks/shared.d.ts +3 -7
  218. package/dist/hooks/shared.js +1 -8
  219. package/dist/http-util.d.ts +2 -3
  220. package/dist/http-util.js +3 -0
  221. package/dist/importers/core.d.ts +2 -9
  222. package/dist/importers/core.js +15 -30
  223. package/dist/importers/sources.js +2 -1
  224. package/dist/importers/vault.js +2 -20
  225. package/dist/incidents.d.ts +1 -1
  226. package/dist/incidents.js +46 -39
  227. package/dist/instruction-detect.d.ts +1 -1
  228. package/dist/instruction-detect.js +1 -1
  229. package/dist/invalidation.d.ts +3 -0
  230. package/dist/invalidation.js +160 -114
  231. package/dist/json.d.ts +5 -0
  232. package/dist/json.js +4 -0
  233. package/dist/judgment.js +1 -2
  234. package/dist/local-embedding.js +1 -1
  235. package/dist/mcp/admin-tools.js +7 -17
  236. package/dist/mcp/format.js +1 -1
  237. package/dist/mcp/framing.js +3 -6
  238. package/dist/mcp/protocol.d.ts +2 -5
  239. package/dist/mcp/protocol.js +1 -3
  240. package/dist/mcp/recall-tools.js +12 -15
  241. package/dist/mcp/request.js +4 -3
  242. package/dist/mcp/session-state.js +2 -3
  243. package/dist/mcp/stdio.js +2 -1
  244. package/dist/mcp/tools.js +9 -6
  245. package/dist/memory-value-weights.d.ts +5 -8
  246. package/dist/memory-value-weights.js +5 -8
  247. package/dist/memory-value.d.ts +13 -13
  248. package/dist/memory-value.js +26 -37
  249. package/dist/memory.d.ts +20 -22
  250. package/dist/memory.js +24 -48
  251. package/dist/multihop.d.ts +1 -1
  252. package/dist/multihop.js +3 -2
  253. package/dist/owner-validation.d.ts +4 -5
  254. package/dist/owner-validation.js +4 -5
  255. package/dist/physics.d.ts +4 -4
  256. package/dist/physics.js +7 -9
  257. package/dist/policies.d.ts +9 -10
  258. package/dist/policies.js +96 -81
  259. package/dist/postinstall.js +3 -6
  260. package/dist/predictions/planning-fallacy.d.ts +9 -14
  261. package/dist/predictions/planning-fallacy.js +10 -16
  262. package/dist/predictions/store.d.ts +15 -23
  263. package/dist/predictions/store.js +36 -33
  264. package/dist/processes.d.ts +2 -7
  265. package/dist/processes.js +88 -72
  266. package/dist/project-briefs.d.ts +2 -3
  267. package/dist/project-briefs.js +141 -118
  268. package/dist/project-identity.d.ts +22 -9
  269. package/dist/project-identity.js +47 -12
  270. package/dist/project-merge.d.ts +28 -5
  271. package/dist/project-merge.js +213 -46
  272. package/dist/project-remote.d.ts +12 -0
  273. package/dist/project-remote.js +138 -0
  274. package/dist/prompt-recall.js +1 -2
  275. package/dist/rate-limit.d.ts +1 -1
  276. package/dist/rate-limit.js +1 -1
  277. package/dist/raw-archive.d.ts +9 -0
  278. package/dist/raw-archive.js +70 -53
  279. package/dist/recall-history.d.ts +19 -20
  280. package/dist/recall-history.js +24 -42
  281. package/dist/recall-pipeline.js +4 -28
  282. package/dist/recall-scope.d.ts +7 -8
  283. package/dist/recall-scope.js +7 -8
  284. package/dist/recall-trace.d.ts +5 -9
  285. package/dist/recall-trace.js +6 -10
  286. package/dist/refine-llm.d.ts +1 -1
  287. package/dist/refine-llm.js +2 -2
  288. package/dist/reject-flow.d.ts +3 -4
  289. package/dist/reject-flow.js +122 -117
  290. package/dist/rejection.d.ts +5 -6
  291. package/dist/rejection.js +7 -15
  292. package/dist/rerankers/clef.d.ts +1 -1
  293. package/dist/rerankers/jev.d.ts +1 -2
  294. package/dist/rerankers/jev.js +4 -5
  295. package/dist/rerankers/llm.d.ts +1 -2
  296. package/dist/rerankers/llm.js +1 -2
  297. package/dist/rerankers/types.d.ts +1 -2
  298. package/dist/rrf.d.ts +2 -2
  299. package/dist/rrf.js +2 -2
  300. package/dist/search/bm25-search.d.ts +1 -1
  301. package/dist/search/bm25-search.js +2 -1
  302. package/dist/search/boosts.js +2 -1
  303. package/dist/search/hybrid.d.ts +1 -1
  304. package/dist/search/hybrid.js +2 -1
  305. package/dist/search/physics-search.d.ts +1 -1
  306. package/dist/search/physics-search.js +2 -1
  307. package/dist/search/types.d.ts +2 -0
  308. package/dist/search/types.js +3 -1
  309. package/dist/secret-detect.d.ts +4 -5
  310. package/dist/secret-detect.js +6 -10
  311. package/dist/server/auth.js +5 -5
  312. package/dist/server/client-ip.js +1 -1
  313. package/dist/server/cursor.js +2 -1
  314. package/dist/server/mcp-http.js +4 -4
  315. package/dist/server/request.d.ts +3 -6
  316. package/dist/server/request.js +6 -7
  317. package/dist/server/routes/admin.js +5 -4
  318. package/dist/server/routes/customer-notes.js +6 -5
  319. package/dist/server/routes/decisions.js +4 -3
  320. package/dist/server/routes/incidents.js +7 -5
  321. package/dist/server/routes/memories.js +7 -7
  322. package/dist/server/routes/policies.js +3 -2
  323. package/dist/server/routes/predictions.js +12 -15
  324. package/dist/server/routes/processes.js +3 -2
  325. package/dist/server/routes/project-briefs.js +8 -7
  326. package/dist/server/routes/recall.js +95 -93
  327. package/dist/server/routes/skills.js +6 -5
  328. package/dist/server/types.d.ts +1 -1
  329. package/dist/server/validation.d.ts +1 -2
  330. package/dist/server/validation.js +7 -14
  331. package/dist/server-detect.js +72 -58
  332. package/dist/server.d.ts +2 -2
  333. package/dist/server.js +131 -117
  334. package/dist/shared.d.ts +26 -17
  335. package/dist/shared.js +102 -104
  336. package/dist/skills.d.ts +3 -3
  337. package/dist/skills.js +88 -72
  338. package/dist/store/audit-event.d.ts +2 -2
  339. package/dist/store/audit-event.js +1 -1
  340. package/dist/store/candidates.d.ts +2 -2
  341. package/dist/store/candidates.js +4 -3
  342. package/dist/store/conflicts.js +30 -22
  343. package/dist/store/delete-and-batch.d.ts +11 -14
  344. package/dist/store/delete-and-batch.js +40 -91
  345. package/dist/store/entry-reads.d.ts +17 -25
  346. package/dist/store/entry-reads.js +59 -40
  347. package/dist/store/entry-row.d.ts +6 -24
  348. package/dist/store/entry-row.js +6 -24
  349. package/dist/store/entry-writes.d.ts +6 -7
  350. package/dist/store/entry-writes.js +13 -11
  351. package/dist/store/handoffs.d.ts +1 -1
  352. package/dist/store/handoffs.js +7 -10
  353. package/dist/store/index-and-stats.d.ts +2 -6
  354. package/dist/store/index-and-stats.js +4 -10
  355. package/dist/store/mirrors.d.ts +6 -19
  356. package/dist/store/mirrors.js +14 -39
  357. package/dist/store/open.js +9 -31
  358. package/dist/store/rows.d.ts +5 -11
  359. package/dist/store/rows.js +6 -11
  360. package/dist/store/search-rows.d.ts +17 -34
  361. package/dist/store/search-rows.js +34 -56
  362. package/dist/store/sessions.d.ts +4 -5
  363. package/dist/store/sessions.js +5 -6
  364. package/dist/store/summaries.d.ts +13 -17
  365. package/dist/store/summaries.js +26 -70
  366. package/dist/support-bundle.js +4 -8
  367. package/dist/tenant.d.ts +1 -5
  368. package/dist/token-ledger.d.ts +1 -1
  369. package/dist/token-ledger.js +3 -5
  370. package/dist/trace.js +1 -3
  371. package/dist/version.d.ts +1 -1
  372. package/dist/version.js +1 -1
  373. package/dist/working-memory.d.ts +1 -1
  374. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  375. package/extensions/openclaw-plugin/package.json +1 -1
  376. package/openclaw.plugin.json +1 -1
  377. package/package.json +1 -1
@@ -1,26 +1,8 @@
1
1
  import type { MemoryEntry } from '../memory.js';
2
2
  import { openHippoDb } from '../db.js';
3
3
  /**
4
- * `bypassRejectionGuard` (AT1, plan §3): ONLY `batchWriteAndDelete`'s call
5
- * site passes `true`. Consolidation merges are DETERMINISTIC CONCATENATION
6
- * (mergeContents, consolidate.ts:736-751), not LLM paraphrase — the bypass
7
- * is safe because the producer (consolidate.ts's merge pass) now checks the
8
- * merged content's rejection digest against the tenant's tombstones BEFORE
9
- * ever assembling a batch to write, and skips the merge entirely on a hit.
10
- * Every other caller (writeEntryDbOnly, bootstrapLegacyStore, rebuildIndex)
11
- * leaves this false and the guard runs live.
12
- *
13
- * AT1 P1 fix (codex, batch-transaction rejection race): the producer check
14
- * above runs on a DIFFERENT connection BEFORE this transaction opens — a
15
- * `hippo reject X` that commits in that window is invisible to it. This
16
- * parameter's contract is UNCHANGED (still the sole bypass, still trusted
17
- * by the producer-side check for the common case); what changed is that
18
- * `batchWriteAndDelete` no longer trusts it BLINDLY. It now runs its own
19
- * in-transaction point-probe (same connection, same digest lookup this
20
- * function's guard would have done) immediately before each upsert and
21
- * skips — rather than writes — any entry whose content matches a tombstone
22
- * that landed after the producer's check. See batchWriteAndDelete for the
23
- * skip logic.
4
+ * `bypassRejectionGuard`: ONLY `batchWriteAndDelete` passes `true`; its merges concatenate
5
+ * already-guarded facts, and it re-probes tombstones in-transaction before each upsert.
24
6
  */
25
7
  export declare function upsertEntryRow(db: ReturnType<typeof openHippoDb>, entry: MemoryEntry, bypassRejectionGuard?: boolean): void;
26
8
  export declare function syncFtsRow(db: ReturnType<typeof openHippoDb>, entry: MemoryEntry, isNewRow?: boolean): void;
@@ -29,14 +11,14 @@ export declare function deleteFtsRow(db: ReturnType<typeof openHippoDb>, id: str
29
11
  * Write a memory entry to SQLite and refresh compatibility mirrors.
30
12
  *
31
13
  * `opts.actor` defaults to 'cli' so unauthenticated direct-CLI callers still
32
- * get the right audit attribution. The HTTP server (A1) and api.* layer pass
14
+ * get the right audit attribution. The HTTP server and api.* layer pass
33
15
  * the resolved actor (`api_key:<key_id>` / `localhost:cli`) so audit events
34
16
  * land with one row per write, no double-emit.
35
17
  *
36
18
  * `opts.afterWrite` is invoked inside the same SAVEPOINT as the memories
37
19
  * INSERT (mirrors archiveRawMemory's shape in raw-archive.ts). On callback
38
20
  * throw, the SAVEPOINT rolls back — the memory row never lands, and the
39
- * filesystem mirrors / audit emit never run. Used by E1.3+ connectors to
21
+ * filesystem mirrors / audit emit never run. Used by connectors to
40
22
  * stamp idempotency rows atomically with the memory write.
41
23
  */
42
24
  /**
@@ -51,7 +33,7 @@ export declare function deleteFtsRow(db: ReturnType<typeof openHippoDb>, id: str
51
33
  * v39 migration found no evidence for" and is deny-by-default in ambient
52
34
  * context. A writeback (e.g. markRetrieved on a crossProject-included row)
53
35
  * must not launder it into an injectable origin - the migration is the only
54
- * evidence-based NULL converter (codex gating round 2 P1).
36
+ * evidence-based NULL converter.
55
37
  */
56
38
  export declare function stampOriginProject(hippoRoot: string, entry: MemoryEntry): MemoryEntry;
57
39
  /**
@@ -61,7 +43,7 @@ export declare function stampOriginProject(hippoRoot: string, entry: MemoryEntry
61
43
  * backfill. Same evidence order as the migration: the provenance source
62
44
  * (`shared:<project>:` / `promoted:<localRoot>`) wins over the destination
63
45
  * store's location, so a shared row imported into the global store keeps its
64
- * owning project instead of becoming user-global (codex gating round 3 P1).
46
+ * owning project instead of becoming user-global.
65
47
  */
66
48
  export declare function stampOriginProjectForImport(hippoRoot: string, entry: MemoryEntry): MemoryEntry;
67
49
  //# sourceMappingURL=entry-row.d.ts.map
@@ -4,26 +4,8 @@ import { deriveOriginProject, originFromSource } from '../project-identity.js';
4
4
  import { checkRejectionGuard } from '../rejection.js';
5
5
  import { log } from '../log.js';
6
6
  /**
7
- * `bypassRejectionGuard` (AT1, plan §3): ONLY `batchWriteAndDelete`'s call
8
- * site passes `true`. Consolidation merges are DETERMINISTIC CONCATENATION
9
- * (mergeContents, consolidate.ts:736-751), not LLM paraphrase — the bypass
10
- * is safe because the producer (consolidate.ts's merge pass) now checks the
11
- * merged content's rejection digest against the tenant's tombstones BEFORE
12
- * ever assembling a batch to write, and skips the merge entirely on a hit.
13
- * Every other caller (writeEntryDbOnly, bootstrapLegacyStore, rebuildIndex)
14
- * leaves this false and the guard runs live.
15
- *
16
- * AT1 P1 fix (codex, batch-transaction rejection race): the producer check
17
- * above runs on a DIFFERENT connection BEFORE this transaction opens — a
18
- * `hippo reject X` that commits in that window is invisible to it. This
19
- * parameter's contract is UNCHANGED (still the sole bypass, still trusted
20
- * by the producer-side check for the common case); what changed is that
21
- * `batchWriteAndDelete` no longer trusts it BLINDLY. It now runs its own
22
- * in-transaction point-probe (same connection, same digest lookup this
23
- * function's guard would have done) immediately before each upsert and
24
- * skips — rather than writes — any entry whose content matches a tombstone
25
- * that landed after the producer's check. See batchWriteAndDelete for the
26
- * skip logic.
7
+ * `bypassRejectionGuard`: ONLY `batchWriteAndDelete` passes `true`; its merges concatenate
8
+ * already-guarded facts, and it re-probes tombstones in-transaction before each upsert.
27
9
  */
28
10
  export function upsertEntryRow(db, entry, bypassRejectionGuard = false) {
29
11
  if (!bypassRejectionGuard) {
@@ -158,14 +140,14 @@ export function deleteFtsRow(db, id) {
158
140
  * Write a memory entry to SQLite and refresh compatibility mirrors.
159
141
  *
160
142
  * `opts.actor` defaults to 'cli' so unauthenticated direct-CLI callers still
161
- * get the right audit attribution. The HTTP server (A1) and api.* layer pass
143
+ * get the right audit attribution. The HTTP server and api.* layer pass
162
144
  * the resolved actor (`api_key:<key_id>` / `localhost:cli`) so audit events
163
145
  * land with one row per write, no double-emit.
164
146
  *
165
147
  * `opts.afterWrite` is invoked inside the same SAVEPOINT as the memories
166
148
  * INSERT (mirrors archiveRawMemory's shape in raw-archive.ts). On callback
167
149
  * throw, the SAVEPOINT rolls back — the memory row never lands, and the
168
- * filesystem mirrors / audit emit never run. Used by E1.3+ connectors to
150
+ * filesystem mirrors / audit emit never run. Used by connectors to
169
151
  * stamp idempotency rows atomically with the memory write.
170
152
  */
171
153
  /**
@@ -180,7 +162,7 @@ export function deleteFtsRow(db, id) {
180
162
  * v39 migration found no evidence for" and is deny-by-default in ambient
181
163
  * context. A writeback (e.g. markRetrieved on a crossProject-included row)
182
164
  * must not launder it into an injectable origin - the migration is the only
183
- * evidence-based NULL converter (codex gating round 2 P1).
165
+ * evidence-based NULL converter.
184
166
  */
185
167
  export function stampOriginProject(hippoRoot, entry) {
186
168
  if (entry.origin_project !== undefined)
@@ -194,7 +176,7 @@ export function stampOriginProject(hippoRoot, entry) {
194
176
  * backfill. Same evidence order as the migration: the provenance source
195
177
  * (`shared:<project>:` / `promoted:<localRoot>`) wins over the destination
196
178
  * store's location, so a shared row imported into the global store keeps its
197
- * owning project instead of becoming user-global (codex gating round 3 P1).
179
+ * owning project instead of becoming user-global.
198
180
  */
199
181
  export function stampOriginProjectForImport(hippoRoot, entry) {
200
182
  if (entry.origin_project !== undefined)
@@ -1,15 +1,14 @@
1
1
  import { type MemoryEntry } from '../memory.js';
2
2
  import { type DatabaseSyncLike } from '../db.js';
3
- export declare function writeEntry(hippoRoot: string, entry: MemoryEntry, opts?: {
3
+ export interface WriteEntryOptions {
4
4
  actor?: string;
5
5
  afterWrite?: (db: DatabaseSyncLike, memoryId: string) => void;
6
- /** Runs AFTER the DB row commits (RELEASE SAVEPOINT in writeEntryDbOnly) but
7
- * BEFORE the markdown mirrors are written. Lets a caller perform a post-commit
8
- * side effect (e.g. mark the graph dirty) that must still happen even if a
9
- * mirror write then throws. Keep it best-effort — it runs on a committed,
10
- * idle connection, so opening another handle inside it is safe. */
6
+ /** Runs after the row commits and before the mirrors, on an idle connection; keep it best-effort. */
11
7
  afterCommit?: () => void;
12
- }): void;
8
+ }
9
+ export declare function writeEntry(hippoRoot: string, entry: MemoryEntry, opts?: WriteEntryOptions): void;
10
+ /** writeEntry on the caller's open store, so a loop of writes opens the store once; each row still commits alone. */
11
+ export declare function writeEntryOn(db: DatabaseSyncLike, hippoRoot: string, entry: MemoryEntry, opts?: WriteEntryOptions): void;
13
12
  /**
14
13
  * DB-only write path. Caller owns the open `db` handle. Runs SAVEPOINT +
15
14
  * upsert + afterWrite hook + audit row inside the SAVEPOINT scope. Caller
@@ -10,6 +10,15 @@ import { mirrorBestEffort, writeMarkdownMirror } from './mirrors.js';
10
10
  import { openStore } from './open.js';
11
11
  export function writeEntry(hippoRoot, entry, opts) {
12
12
  const db = openStore(hippoRoot);
13
+ try {
14
+ writeEntryOn(db, hippoRoot, entry, opts);
15
+ }
16
+ finally {
17
+ closeHippoDb(db);
18
+ }
19
+ }
20
+ /** writeEntry on the caller's open store, so a loop of writes opens the store once; each row still commits alone. */
21
+ export function writeEntryOn(db, hippoRoot, entry, opts) {
13
22
  try {
14
23
  const stamped = stampOriginProject(hippoRoot, entry);
15
24
  writeEntryDbOnly(db, stamped, opts);
@@ -17,18 +26,13 @@ export function writeEntry(hippoRoot, entry, opts) {
17
26
  writeEntryMirrors(hippoRoot, stamped);
18
27
  }
19
28
  catch (error) {
20
- // AT1 (plan §3): writeEntryDbOnly's own SAVEPOINT has already unwound by
21
- // the time this catch runs, so the refusal audit lands post-rollback in
22
- // a fresh implicit transaction — then rethrow so the caller sees the
23
- // refusal.
29
+ // writeEntryDbOnly's SAVEPOINT has already unwound here, so the refusal audit lands
30
+ // post-rollback in a fresh implicit transaction; then rethrow so the caller sees it.
24
31
  if (error instanceof RejectedValueError) {
25
32
  auditRejectionRefusal(db, error, opts?.actor ?? 'cli');
26
33
  }
27
34
  throw error;
28
35
  }
29
- finally {
30
- closeHippoDb(db);
31
- }
32
36
  }
33
37
  /**
34
38
  * DB-only write path. Caller owns the open `db` handle. Runs SAVEPOINT +
@@ -58,10 +62,8 @@ export function writeEntryDbOnly(db, entry, opts) {
58
62
  kind: entry.kind ?? 'distilled',
59
63
  scope: entry.scope ?? null,
60
64
  }, opts?.actor ?? 'cli', entry.tenantId);
61
- // v0.30 / E2 — DAG live-coupling: child write under a level-2 summary
62
- // marks the parent dirty for E3 sleep-cycle rebuild. Early-exit on
63
- // null dag_parent_id (vast majority of writes); cost is one null check
64
- // on the hot path.
65
+ // A child write marks its summary parent dirty for the sleep-cycle rebuild; most writes
66
+ // have no parent, so the hot path pays one null check.
65
67
  if (entry.dag_parent_id) {
66
68
  markSummaryDirtyInTx(db, entry.dag_parent_id, entry.tenantId, opts?.actor ?? 'cli');
67
69
  }
@@ -19,7 +19,7 @@ export declare function loadLatestHandoff(hippoRoot: string, tenantId: string, s
19
19
  export declare function loadHandoffById(hippoRoot: string, tenantId: string, id: number): SessionHandoff | null;
20
20
  /** Stamp the outcome on a session's newest handoff, only if it has none yet. Returns rows changed. */
21
21
  export declare function stampHandoffOutcome(hippoRoot: string, tenantId: string, sessionId: string, outcome: HandoffOutcome): number;
22
- /** Auto-write a handoff at session-end (DF1 T3) from the session's active snapshot, else from `derived`, its transcript state.
22
+ /** Auto-write a handoff at session-end from the session's active snapshot, else from `derived`, its transcript state.
23
23
  * @param evidence best-effort git state; outcome comes from the newest session_complete event.
24
24
  * @returns null when neither source is the session's, a newer handoff covers the snapshot, or the session's latest handoff was not read off its transcript. */
25
25
  export declare function writeSessionEndHandoff(hippoRoot: string, tenantId: string, sessionId: string, evidence: HandoffEvidence | null, derived?: Pick<TaskSnapshot, 'task' | 'summary' | 'next_step'> | null): SessionHandoff | null;
@@ -4,8 +4,6 @@ import { RECALL_DEFAULT_DENY_SCOPES } from '../recall-scope.js';
4
4
  import { assertTenantId } from '../tenant.js';
5
5
  import { openStore } from './open.js';
6
6
  import { loadActiveTaskSnapshot } from './sessions.js';
7
- // W1: the nine-column SELECT was cloned four times (plan rule 8); one
8
- // definition so a sixth caller can't drift from the other five.
9
7
  /** Column list shared by every session_handoffs SELECT; store-cards.ts reuses it for the card handoff lookup. */
10
8
  export const HANDOFF_COLUMNS = 'id, session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, created_at, constraints_json, evidence_json, outcome, target_runtime, card_id';
11
9
  /**
@@ -15,8 +13,8 @@ export function saveSessionHandoff(hippoRoot, tenantId, handoff) {
15
13
  assertTenantId('saveSessionHandoff', tenantId);
16
14
  const db = openStore(hippoRoot);
17
15
  const now = new Date().toISOString();
18
- // v1.2: scope is wired through. Read-side default-deny in api.recall +
19
- // cmdRecall continuity excludes slack:private:* and 'unknown:legacy'.
16
+ // Scope is stored as given; read-side default-deny in api.recall + cmdRecall
17
+ // continuity excludes slack:private:* and 'unknown:legacy'.
20
18
  try {
21
19
  const result = db.prepare(`
22
20
  INSERT INTO session_handoffs(session_id, repo_root, task_id, summary, next_action, artifacts_json, scope, tenant_id, created_at, constraints_json, evidence_json, outcome, target_runtime, card_id)
@@ -54,7 +52,7 @@ export function loadLatestHandoff(hippoRoot, tenantId, sessionId, opts = {}) {
54
52
  params.push(opts.excludeSessionId);
55
53
  }
56
54
  if (opts.unfinishedOnly) {
57
- // codex P2: restrict to each session's newest revision first — stampHandoffOutcome
55
+ // Restrict to each session's newest revision first: stampHandoffOutcome
58
56
  // only stamps the newest row, so an older null-outcome revision must not resurrect.
59
57
  conditions.push(`id IN (SELECT MAX(id) FROM session_handoffs WHERE tenant_id = ? GROUP BY session_id)`);
60
58
  params.push(tenantId);
@@ -65,7 +63,7 @@ export function loadLatestHandoff(hippoRoot, tenantId, sessionId, opts = {}) {
65
63
  params.push(new Date(Date.now() - opts.maxAgeMs).toISOString());
66
64
  }
67
65
  if (opts.scopeFilter === 'default-deny') {
68
- // codex P2: admit scope before LIMIT 1, else a newer denied row hides an older eligible one.
66
+ // Admit scope before LIMIT 1, else a newer denied row hides an older eligible one.
69
67
  const placeholders = RECALL_DEFAULT_DENY_SCOPES.map(() => '?').join(', ');
70
68
  conditions.push(`(scope IS NULL OR (scope NOT IN (${placeholders}) AND scope NOT LIKE '%:private:%'))`);
71
69
  params.push(...RECALL_DEFAULT_DENY_SCOPES);
@@ -123,7 +121,7 @@ export function stampHandoffOutcome(hippoRoot, tenantId, sessionId, outcome) {
123
121
  closeHippoDb(db);
124
122
  }
125
123
  }
126
- /** Auto-write a handoff at session-end (DF1 T3) from the session's active snapshot, else from `derived`, its transcript state.
124
+ /** Auto-write a handoff at session-end from the session's active snapshot, else from `derived`, its transcript state.
127
125
  * @param evidence best-effort git state; outcome comes from the newest session_complete event.
128
126
  * @returns null when neither source is the session's, a newer handoff covers the snapshot, or the session's latest handoff was not read off its transcript. */
129
127
  export function writeSessionEndHandoff(hippoRoot, tenantId, sessionId, evidence, derived = null) {
@@ -160,9 +158,8 @@ export function writeSessionEndHandoff(hippoRoot, tenantId, sessionId, evidence,
160
158
  finally {
161
159
  closeHippoDb(db);
162
160
  }
163
- // codex P2: same-task refresh carries forward envelope fields nobody cleared,
164
- // rather than dropping them when the snapshot rewrite has no opinion on them.
165
- // codex P1: a scope mismatch must not leak private metadata into an unscoped envelope.
161
+ // A same-task refresh carries forward envelope fields nobody cleared; a scope
162
+ // mismatch must not leak private metadata into an unscoped envelope.
166
163
  const carryForward = existing != null && existing.taskId === snapshot.task
167
164
  && (existing.scope ?? null) === (snapshot.scope ?? null);
168
165
  return saveSessionHandoff(hippoRoot, tenantId, {
@@ -4,12 +4,8 @@ export declare function loadIndex(hippoRoot: string): HippoIndex;
4
4
  /**
5
5
  * Persist mutable index metadata. Entry rows themselves are derived from SQLite.
6
6
  *
7
- * LC1 F1(c) structural fix: `last_retrieval_ids` and `last_trace_id` must
8
- * land atomically — callers (getContext, cmdRecall) fold a freshly-written
9
- * trace id into `index.last_trace_id` before calling this, relying on BOTH
10
- * meta keys committing together. Wrapped in BEGIN/COMMIT so a crash or a
11
- * mid-write failure can never advance one key without the other. index.json
12
- * is left untouched; only `rebuildIndex` writes it.
7
+ * `last_retrieval_ids` and `last_trace_id` commit in one transaction: callers fold a fresh trace id
8
+ * into the index and rely on both keys moving together. index.json is left to `rebuildIndex`.
13
9
  */
14
10
  export declare function saveIndex(hippoRoot: string, index: HippoIndex): void;
15
11
  /**
@@ -18,12 +18,8 @@ export function loadIndex(hippoRoot) {
18
18
  /**
19
19
  * Persist mutable index metadata. Entry rows themselves are derived from SQLite.
20
20
  *
21
- * LC1 F1(c) structural fix: `last_retrieval_ids` and `last_trace_id` must
22
- * land atomically — callers (getContext, cmdRecall) fold a freshly-written
23
- * trace id into `index.last_trace_id` before calling this, relying on BOTH
24
- * meta keys committing together. Wrapped in BEGIN/COMMIT so a crash or a
25
- * mid-write failure can never advance one key without the other. index.json
26
- * is left untouched; only `rebuildIndex` writes it.
21
+ * `last_retrieval_ids` and `last_trace_id` commit in one transaction: callers fold a fresh trace id
22
+ * into the index and rely on both keys moving together. index.json is left to `rebuildIndex`.
27
23
  */
28
24
  export function saveIndex(hippoRoot, index) {
29
25
  const db = openStore(hippoRoot);
@@ -58,10 +54,8 @@ export function rebuildIndex(hippoRoot) {
58
54
  if (legacyEntries.length > 0) {
59
55
  db.exec('BEGIN');
60
56
  try {
61
- // AT1 (plan §3, round-3 redesign): same guard-with-per-row-skip as
62
- // bootstrapLegacyStore — rebuildIndex is the other channel through
63
- // which a stale markdown mirror could resurrect a rejected value.
64
- // Refusal audit written INLINE (nothing rolls back on a skip).
57
+ // Guard with per-row skip, like bootstrapLegacyStore: a stale markdown mirror could resurrect a
58
+ // rejected value here. Refusal audit is written inline because nothing rolls back on a skip.
65
59
  let rejectedCount = 0;
66
60
  for (const entry of legacyEntries) {
67
61
  // v39: same store-derived origin stamp as bootstrapLegacyStore.
@@ -9,28 +9,13 @@ export declare function writeRecentSessionMirror(hippoRoot: string, tenantId: st
9
9
  export declare function writeMarkdownMirror(hippoRoot: string, entry: MemoryEntry): void;
10
10
  export declare function removeEntryMirrors(hippoRoot: string, id: string): void;
11
11
  /**
12
- * AT1 mirror-purge honesty fix (docs/plans/2026-08-15-at1-rejected-value-tombstone.md):
13
- * the candidate markdown mirror paths still on disk for `id`, computed the
14
- * same way `removeEntryMirrors` walks them (one per layer: buffer/episodic/
15
- * semantic), filtered to the ones that still `fs.existsSync`. Used to report
16
- * an EXPLICIT path when a best-effort purge fails and no reaper exists to
17
- * retry it — plain `removeEntryMirrors` returns void, giving no way to name
18
- * which file is stuck.
12
+ * Mirror paths for `id` still on disk, walked like `removeEntryMirrors`, so a failed purge
13
+ * with no reaper to retry it can name the stuck file.
19
14
  */
20
15
  export declare function getExistingEntryMirrorPaths(hippoRoot: string, id: string): string[];
21
16
  /**
22
- * AT1 fix: best-effort markdown-mirror purge shared by `reject-flow.ts`'s
23
- * `rejectValue` and `resolveConflict`'s post-commit purge. Both used to log
24
- * "will retry via reaper on next open" for EVERY failure, but the reaper
25
- * (`cleanupArchivedMirrors`, raw-archive-mirror-cleanup.ts) only scans
26
- * `raw_archive` — that message was false for a non-raw id, which has no
27
- * reaper at all.
28
- *
29
- * Retries the unlink once synchronously (the common real-world failure is a
30
- * transient lock/AV-scanner false positive, not a permanent one). On a
31
- * second failure: raw ids still get the honest reaper message (true); non-raw
32
- * ids get the EXPLICIT leftover file path(s) and a manual-delete instruction,
33
- * since nothing will ever retry them automatically.
17
+ * Best-effort mirror purge. Retries the unlink once (most failures are transient locks); the reaper only
18
+ * scans `raw_archive`, so a non-raw id's second failure logs the leftover path(s) to delete by hand.
34
19
  *
35
20
  * Returns true if the mirror ended up purged (first or second attempt).
36
21
  */
@@ -46,4 +31,6 @@ export declare function writeStatsMirror(hippoRoot: string, stats: LegacyStats):
46
31
  /** Mirrors are derived from SQLite and written after COMMIT, so a failed write warns instead of failing a committed change. */
47
32
  export declare function mirrorBestEffort(what: string, write: () => void): void;
48
33
  export declare function syncMirrorFiles(hippoRoot: string, db: ReturnType<typeof openHippoDb>): void;
34
+ /** syncMirrorFiles for a pass that changed only `changed`: the other rows' markdown is already current. */
35
+ export declare function syncChangedMirrors(hippoRoot: string, db: ReturnType<typeof openHippoDb>, changed: readonly MemoryEntry[]): void;
49
36
  //# sourceMappingURL=mirrors.d.ts.map
@@ -145,15 +145,8 @@ export function writeMarkdownMirror(hippoRoot, entry) {
145
145
  fs.mkdirSync(dir, { recursive: true });
146
146
  fs.writeFileSync(path.join(dir, `${entry.id}.md`), serializeEntry(entry), 'utf8');
147
147
  }
148
- // AT1 P1 fix (codex): `writeMarkdownMirror` writes ANY layer's mirror,
149
- // including `trace/<id>.md` for Layer.Trace rows (auto-promoted traces,
150
- // consolidate.ts) — but this enumeration only walked
151
- // Buffer/Episodic/Semantic. A rejected/forgotten trace row's markdown
152
- // content survived on disk while the purge (and `hippo reject`/plain
153
- // `forget`) reported success, and a stale trace mirror is exactly the
154
- // resurrection channel bootstrapLegacyStore/rebuildIndex guard against.
155
- // Fixes BOTH the AT1 reject-flow purge and the pre-existing plain-`forget`
156
- // gap for trace rows (deleteEntry has always called this same function).
148
+ // Every layer, Trace included: `writeMarkdownMirror` writes any layer, and a stale trace mirror
149
+ // is the resurrection channel bootstrapLegacyStore/rebuildIndex guard against.
157
150
  export function removeEntryMirrors(hippoRoot, id) {
158
151
  for (const layer of [Layer.Buffer, Layer.Episodic, Layer.Semantic, Layer.Trace]) {
159
152
  const file = path.join(layerDir(hippoRoot, layer), `${id}.md`);
@@ -163,36 +156,18 @@ export function removeEntryMirrors(hippoRoot, id) {
163
156
  }
164
157
  }
165
158
  /**
166
- * AT1 mirror-purge honesty fix (docs/plans/2026-08-15-at1-rejected-value-tombstone.md):
167
- * the candidate markdown mirror paths still on disk for `id`, computed the
168
- * same way `removeEntryMirrors` walks them (one per layer: buffer/episodic/
169
- * semantic), filtered to the ones that still `fs.existsSync`. Used to report
170
- * an EXPLICIT path when a best-effort purge fails and no reaper exists to
171
- * retry it — plain `removeEntryMirrors` returns void, giving no way to name
172
- * which file is stuck.
159
+ * Mirror paths for `id` still on disk, walked like `removeEntryMirrors`, so a failed purge
160
+ * with no reaper to retry it can name the stuck file.
173
161
  */
174
162
  export function getExistingEntryMirrorPaths(hippoRoot, id) {
175
- // AT1 P1 fix (codex): same missing Layer.Trace as removeEntryMirrors above
176
- // — kept in lockstep with it since this function's whole purpose is
177
- // walking the mirror paths "the same way removeEntryMirrors walks them"
178
- // (see its own doc comment).
163
+ // Layer list kept in lockstep with removeEntryMirrors.
179
164
  return [Layer.Buffer, Layer.Episodic, Layer.Semantic, Layer.Trace]
180
165
  .map((layer) => path.join(layerDir(hippoRoot, layer), `${id}.md`))
181
166
  .filter((file) => fs.existsSync(file));
182
167
  }
183
168
  /**
184
- * AT1 fix: best-effort markdown-mirror purge shared by `reject-flow.ts`'s
185
- * `rejectValue` and `resolveConflict`'s post-commit purge. Both used to log
186
- * "will retry via reaper on next open" for EVERY failure, but the reaper
187
- * (`cleanupArchivedMirrors`, raw-archive-mirror-cleanup.ts) only scans
188
- * `raw_archive` — that message was false for a non-raw id, which has no
189
- * reaper at all.
190
- *
191
- * Retries the unlink once synchronously (the common real-world failure is a
192
- * transient lock/AV-scanner false positive, not a permanent one). On a
193
- * second failure: raw ids still get the honest reaper message (true); non-raw
194
- * ids get the EXPLICIT leftover file path(s) and a manual-delete instruction,
195
- * since nothing will ever retry them automatically.
169
+ * Best-effort mirror purge. Retries the unlink once (most failures are transient locks); the reaper only
170
+ * scans `raw_archive`, so a non-raw id's second failure logs the leftover path(s) to delete by hand.
196
171
  *
197
172
  * Returns true if the mirror ended up purged (first or second attempt).
198
173
  */
@@ -242,12 +217,8 @@ export function buildIndexFromDb(db) {
242
217
  pinned: Boolean(row.pinned),
243
218
  };
244
219
  }
245
- // LC1 codex round-2 med: the two lockstep keys must be read in ONE
246
- // statement. Two autocommit SELECTs leave a window where a concurrent
247
- // saveIndex (which commits both keys in one transaction) lands between
248
- // them, handing the reader mismatched last_retrieval_ids / last_trace_id
249
- // and re-opening the mislinkage hole saveIndex's BEGIN/COMMIT closed on
250
- // the write side. One SELECT = one SQLite read snapshot.
220
+ // Read both lockstep keys in ONE statement: two autocommit SELECTs could straddle a concurrent
221
+ // saveIndex and hand back a mismatched last_retrieval_ids / last_trace_id pair.
251
222
  // SAFETY: lockstepRows' shape matches the key/value columns named above.
252
223
  const lockstepRows = db.prepare(`SELECT key, value FROM meta WHERE key IN ('last_retrieval_ids', 'last_trace_id')`).all();
253
224
  const lockstep = new Map(lockstepRows.map((r) => [r.key, r.value]));
@@ -294,8 +265,12 @@ export function syncMirrorFiles(hippoRoot, db) {
294
265
  // SAFETY: this query selects exactly MEMORY_SELECT_COLUMNS, matching
295
266
  // MemoryRow's field set.
296
267
  const entries = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories ORDER BY created ASC, id ASC`).all();
268
+ syncChangedMirrors(hippoRoot, db, entries.map(rowToEntry));
269
+ }
270
+ /** syncMirrorFiles for a pass that changed only `changed`: the other rows' markdown is already current. */
271
+ export function syncChangedMirrors(hippoRoot, db, changed) {
297
272
  mirrorBestEffort('markdown mirrors', () => {
298
- for (const entry of entries.map(rowToEntry))
273
+ for (const entry of changed)
299
274
  writeMarkdownMirror(hippoRoot, entry);
300
275
  });
301
276
  // SAFETY: conflicts' shape matches the eight columns named in the SELECT
@@ -16,13 +16,8 @@ export function getHippoRoot(cwd = process.cwd(), opts) {
16
16
  return findHippoStoreDir(cwd, opts) ?? path.join(realpathOrResolve(cwd), '.hippo');
17
17
  }
18
18
  export function isInitialized(hippoRoot) {
19
- // A bare .hippo directory is not enough — autoInstallHooks /
20
- // setupDailySchedule can create it without ever calling initStore,
21
- // leaving a partial directory (integrations/, logs/, runs/) with no
22
- // hippo.db. Returning true in that state caused `hippo init` to skip
23
- // initStore and `hippo recall` to silently fall back to an empty store
24
- // (incident 2026-04-26: ingest_direct.py against a bare .hippo).
25
- // Treat the store as initialized only if hippo.db actually exists.
19
+ // autoInstallHooks / setupDailySchedule can create a bare .hippo with no hippo.db; counting
20
+ // that as initialized makes `hippo init` skip initStore, so only hippo.db counts.
26
21
  return fs.existsSync(path.join(hippoRoot, 'hippo.db'));
27
22
  }
28
23
  export function initStore(hippoRoot) {
@@ -75,15 +70,8 @@ function bootstrapLegacyStore(db, hippoRoot) {
75
70
  const memoryCount = Number(countRow?.count ?? 0);
76
71
  if (memoryCount > 0)
77
72
  return false;
78
- // AT1 P2 fix: memoryCount alone is not a reliable "already bootstrapped"
79
- // signal once the rejection guard exists. If EVERY legacy mirror row is
80
- // rejected, memories stays at 0 rows even after a successful bootstrap
81
- // pass, so the memoryCount>0 gate above never trips — every subsequent
82
- // initStore() call would re-run this whole function: re-scan the legacy
83
- // mirrors, re-attempt (and re-refuse, re-auditing) every row, and
84
- // re-INSERT the legacy consolidation_runs rows with no dedup, duplicating
85
- // them on each open. A dedicated meta flag marks bootstrap as
86
- // attempted-and-settled regardless of how many rows actually landed.
73
+ // memoryCount misses an all-rejected bootstrap (memories stays empty), which would re-run the
74
+ // import on every open and duplicate consolidation_runs; this meta flag settles it.
87
75
  if (getMeta(db, 'legacy_bootstrap_completed', '0') === '1')
88
76
  return false;
89
77
  const legacyEntries = loadLegacyEntriesFromMarkdown(hippoRoot);
@@ -93,8 +81,7 @@ function bootstrapLegacyStore(db, hippoRoot) {
93
81
  try {
94
82
  importLegacyEntries(db, hippoRoot, legacyEntries);
95
83
  importLegacyIndexAndStats(db, hippoRoot);
96
- // AT1 P2 fix: stamp completion regardless of how many rows actually
97
- // landed (all-rejected included) — see the gate comment above.
84
+ // Stamp completion even when every row was rejected; see the gate above.
98
85
  setMeta(db, 'legacy_bootstrap_completed', '1');
99
86
  db.exec('COMMIT');
100
87
  }
@@ -108,14 +95,8 @@ function bootstrapLegacyStore(db, hippoRoot) {
108
95
  return true;
109
96
  }
110
97
  function importLegacyEntries(db, hippoRoot, legacyEntries) {
111
- // AT1 (plan §3, round-3 redesign): run the guard LIVE per row rather
112
- // than bypassing it. bootstrapLegacyStore is exactly the channel through
113
- // which a stale/never-purged markdown mirror could resurrect a rejected
114
- // value; a skip-and-count here closes that structurally, independent of
115
- // mirror state. The refusal audit is written INLINE inside this
116
- // still-open loop transaction (plain audit() — nothing is rolled back
117
- // on a per-row skip, so the post-rollback auditRejectionRefusal helper
118
- // is the wrong tool here).
98
+ // Guard live per row: a stale markdown mirror could resurrect a rejected value. Plain audit()
99
+ // inline, because nothing rolls back on a per-row skip.
119
100
  let rejectedCount = 0;
120
101
  for (const entry of legacyEntries) {
121
102
  // v39: legacy markdown carries no origin_project; stamp from the store
@@ -140,11 +121,8 @@ function importLegacyEntries(db, hippoRoot, legacyEntries) {
140
121
  function importLegacyIndexAndStats(db, hippoRoot) {
141
122
  const legacyIndex = loadLegacyIndexFile(hippoRoot);
142
123
  setMeta(db, 'last_retrieval_ids', JSON.stringify(legacyIndex.last_retrieval_ids ?? []));
143
- // LC1: legacy index.json predates last_trace_id, so this is '' for every
144
- // pre-v40 store — harmless, matches the ensureMetaDefaults default.
145
- // Coerce like its neighbors below coerce theirs (independent-review-critic
146
- // LOW finding): accept only a clean digit string, else fall back to ''
147
- // rather than trusting whatever a hand-edited/corrupt index.json carries.
124
+ // Legacy index.json predates last_trace_id, so '' is normal; accept only a clean digit
125
+ // string rather than trusting a hand-edited or corrupt index.json.
148
126
  const legacyTraceId = String(legacyIndex.last_trace_id ?? '');
149
127
  setMeta(db, 'last_trace_id', /^\d+$/.test(legacyTraceId) ? legacyTraceId : '');
150
128
  const legacyStats = loadLegacyStatsFile(hippoRoot);
@@ -1,8 +1,5 @@
1
1
  import { Layer, type MemoryEntry, type ConfidenceLevel } from '../memory.js';
2
- /** A value that round-trips through JSON.stringify/JSON.parse unchanged. */
3
- export type JsonValue = string | number | boolean | null | JsonValue[] | {
4
- [key: string]: JsonValue;
5
- };
2
+ import type { JsonValue } from '../json.js';
6
3
  export interface IndexEntry {
7
4
  id: string;
8
5
  file: string;
@@ -17,11 +14,8 @@ export interface HippoIndex {
17
14
  version: number;
18
15
  entries: Record<string, IndexEntry>;
19
16
  last_retrieval_ids: string[];
20
- /** LC1 (docs/plans/2026-08-02-lc1-recall-trace-persistence.md): id of the
21
- * most recent recall_traces row written by getContext/cmdRecall, mirrored
22
- * from the `last_trace_id` meta key exactly like last_retrieval_ids. null
23
- * when no trace has been written yet (fresh store, pre-v40 flow, or
24
- * api.recall-only usage — api.recall never sets this). */
17
+ /** Id of the most recent recall_traces row written by getContext/cmdRecall, mirrored from the
18
+ * `last_trace_id` meta key like last_retrieval_ids. null when none was written (api.recall never sets it). */
25
19
  last_trace_id: string | null;
26
20
  }
27
21
  export interface MemoryRow {
@@ -151,8 +145,8 @@ export declare const DEFAULT_SEARCH_CANDIDATE_LIMIT = 200;
151
145
  export declare function rowToEntry(row: MemoryRow): MemoryEntry;
152
146
  export declare function parseJsonArray(raw: string | null | undefined): string[];
153
147
  /**
154
- * Strict parse for the `last_trace_id` meta value (LC1 F1(d) structural
155
- * fix). A bare Number(raw) would turn '', whitespace, or garbage into a
148
+ * Strict parse for the `last_trace_id` meta value.
149
+ * A bare Number(raw) would turn '', whitespace, or garbage into a
156
150
  * usable-looking 0/NaN — a consumer INSERTing recall_trace_outcomes with
157
151
  * trace_id=0 would hit a masked FK violation (row id 0 never exists).
158
152
  * Require a clean positive integer string; anything else is treated as
@@ -1,11 +1,8 @@
1
1
  import { log } from '../log.js';
2
2
  export const INDEX_VERSION = 3;
3
3
  export const MEMORY_SELECT_COLUMNS = `id, created, last_retrieved, retrieval_count, strength, half_life_days, layer, tags_json, emotional_valence, schema_fit, source, outcome_score, outcome_positive, outcome_negative, conflicts_with_json, pinned, confidence, content, parents_json, starred, trace_outcome, source_session_id, valid_from, superseded_by, extracted_from, dag_level, dag_parent_id, kind, scope, owner, artifact_ref, tenant_id, origin_project, descendant_count, earliest_at, latest_at, summary_dirty, last_rebuilt_at, rebuild_count, dag_level_3_built_at`;
4
- // F1 (v1.7.0): qualified-and-aliased columns for the FTS join in
5
- // loadSearchRows. Every column is `m.<col> AS <col>` so rowToEntry's
6
- // unqualified field reads keep working unchanged. The trailing
7
- // bm25(memories_fts) AS bm25_score adds the FTS rank as a result column.
8
- // Only used inside the FTS path; non-FTS paths keep MEMORY_SELECT_COLUMNS.
4
+ // FTS-join columns for loadSearchRows: each is `m.<col> AS <col>` so rowToEntry's unqualified
5
+ // reads still work, and the trailing bm25(memories_fts) AS bm25_score carries the FTS rank.
9
6
  export const MEMORY_SEARCH_COLUMNS = `m.id AS id, m.created AS created, m.last_retrieved AS last_retrieved, m.retrieval_count AS retrieval_count, m.strength AS strength, m.half_life_days AS half_life_days, m.layer AS layer, m.tags_json AS tags_json, m.emotional_valence AS emotional_valence, m.schema_fit AS schema_fit, m.source AS source, m.outcome_score AS outcome_score, m.outcome_positive AS outcome_positive, m.outcome_negative AS outcome_negative, m.conflicts_with_json AS conflicts_with_json, m.pinned AS pinned, m.confidence AS confidence, m.content AS content, m.parents_json AS parents_json, m.starred AS starred, m.trace_outcome AS trace_outcome, m.source_session_id AS source_session_id, m.valid_from AS valid_from, m.superseded_by AS superseded_by, m.extracted_from AS extracted_from, m.dag_level AS dag_level, m.dag_parent_id AS dag_parent_id, m.kind AS kind, m.scope AS scope, m.owner AS owner, m.artifact_ref AS artifact_ref, m.tenant_id AS tenant_id, m.origin_project AS origin_project, m.descendant_count AS descendant_count, m.earliest_at AS earliest_at, m.latest_at AS latest_at, m.summary_dirty AS summary_dirty, m.last_rebuilt_at AS last_rebuilt_at, m.rebuild_count AS rebuild_count, m.dag_level_3_built_at AS dag_level_3_built_at, bm25(memories_fts) AS bm25_score`;
10
7
  /**
11
8
  * Default candidate-pool size for `loadSearchEntries` when called with
@@ -55,15 +52,13 @@ export function rowToEntry(row) {
55
52
  descendant_count: Number(row.descendant_count ?? 0),
56
53
  earliest_at: row.earliest_at ?? null,
57
54
  latest_at: row.latest_at ?? null,
58
- // v0.30 / E1 of DAG live-coupling (schema v28). Symmetric with v25 cache.
59
55
  summary_dirty: (Number(row.summary_dirty ?? 0) === 1 ? 1 : 0),
60
56
  last_rebuilt_at: row.last_rebuilt_at ?? null,
61
57
  rebuild_count: Number(row.rebuild_count ?? 0),
62
58
  dag_level_3_built_at: row.dag_level_3_built_at ?? null,
63
59
  };
64
- // F1 (v1.7.0): preserve bm25_score from the FTS path. `'bm25_score' in row`
65
- // distinguishes "absent column" (non-FTS path) from "column present but
66
- // value 0" — though FTS5 bm25() never returns 0, this is defensive.
60
+ // Preserve bm25_score from the FTS path; `'bm25_score' in row` tells an absent column
61
+ // (non-FTS path) from a present one.
67
62
  if ('bm25_score' in row && row.bm25_score !== undefined && row.bm25_score !== null) {
68
63
  entry.bm25_score = Number(row.bm25_score);
69
64
  }
@@ -82,8 +77,8 @@ export function parseJsonArray(raw) {
82
77
  }
83
78
  }
84
79
  /**
85
- * Strict parse for the `last_trace_id` meta value (LC1 F1(d) structural
86
- * fix). A bare Number(raw) would turn '', whitespace, or garbage into a
80
+ * Strict parse for the `last_trace_id` meta value.
81
+ * A bare Number(raw) would turn '', whitespace, or garbage into a
87
82
  * usable-looking 0/NaN — a consumer INSERTing recall_trace_outcomes with
88
83
  * trace_id=0 would hit a masked FK violation (row id 0 never exists).
89
84
  * Require a clean positive integer string; anything else is treated as