hippo-memory 1.62.0 → 1.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (370) hide show
  1. package/README.md +9 -0
  2. package/dist/agent-memories/apply.d.ts +2 -0
  3. package/dist/agent-memories/apply.js +23 -1
  4. package/dist/agent-memories/claude-code.d.ts +2 -1
  5. package/dist/agent-memories/claude-code.js +4 -4
  6. package/dist/agent-memories/report.d.ts +1 -0
  7. package/dist/agent-memories/report.js +2 -2
  8. package/dist/agent-memories/sync.js +37 -23
  9. package/dist/ambient-store.d.ts +4 -4
  10. package/dist/ambient-store.js +4 -3
  11. package/dist/api/assemble.d.ts +7 -10
  12. package/dist/api/assemble.js +7 -12
  13. package/dist/api/audit.d.ts +2 -2
  14. package/dist/api/audit.js +2 -2
  15. package/dist/api/auth.d.ts +6 -6
  16. package/dist/api/auth.js +5 -7
  17. package/dist/api/context-select.d.ts +4 -3
  18. package/dist/api/context-select.js +9 -11
  19. package/dist/api/context-types.d.ts +12 -17
  20. package/dist/api/context.d.ts +3 -2
  21. package/dist/api/context.js +20 -20
  22. package/dist/api/dormant.js +1 -1
  23. package/dist/api/drill-down.d.ts +4 -4
  24. package/dist/api/drill-down.js +22 -10
  25. package/dist/api/outcome.d.ts +8 -13
  26. package/dist/api/outcome.js +13 -17
  27. package/dist/api/promote.d.ts +5 -8
  28. package/dist/api/quarantine.js +3 -2
  29. package/dist/api/recall-types.d.ts +43 -55
  30. package/dist/api/recall.d.ts +3 -3
  31. package/dist/api/recall.js +7 -18
  32. package/dist/api/remember.d.ts +2 -2
  33. package/dist/api/sleep.d.ts +7 -35
  34. package/dist/api/sleep.js +3 -2
  35. package/dist/api/tokens.d.ts +2 -2
  36. package/dist/api/tokens.js +2 -2
  37. package/dist/api/types.d.ts +6 -10
  38. package/dist/api/types.js +2 -4
  39. package/dist/audit-prune.d.ts +4 -6
  40. package/dist/audit-prune.js +3 -5
  41. package/dist/audit.js +11 -33
  42. package/dist/auth.d.ts +8 -9
  43. package/dist/auth.js +5 -7
  44. package/dist/autolearn.d.ts +1 -1
  45. package/dist/autolearn.js +1 -1
  46. package/dist/availability.js +3 -5
  47. package/dist/capture/command.d.ts +5 -4
  48. package/dist/capture/command.js +8 -22
  49. package/dist/capture/compact.d.ts +1 -1
  50. package/dist/capture/compact.js +9 -13
  51. package/dist/capture/extract.js +37 -115
  52. package/dist/capture-error.d.ts +1 -1
  53. package/dist/capture-error.js +1 -1
  54. package/dist/churn-git.d.ts +1 -1
  55. package/dist/churn-git.js +1 -1
  56. package/dist/cli/audit.js +3 -4
  57. package/dist/cli/auth.js +3 -6
  58. package/dist/cli/curate.d.ts +1 -1
  59. package/dist/cli/curate.js +8 -24
  60. package/dist/cli/dag.js +5 -9
  61. package/dist/cli/decisions.js +10 -21
  62. package/dist/cli/explain.js +2 -1
  63. package/dist/cli/goals.js +1 -1
  64. package/dist/cli/init.js +1 -1
  65. package/dist/cli/playbooks.js +4 -9
  66. package/dist/cli/projects.js +3 -1
  67. package/dist/cli/recall.js +2 -1
  68. package/dist/cli/remember.js +6 -18
  69. package/dist/cli/session-hooks.js +21 -40
  70. package/dist/cli/setup.d.ts +1 -1
  71. package/dist/cli/setup.js +2 -6
  72. package/dist/cli/shared.js +3 -3
  73. package/dist/cli/slack.js +1 -1
  74. package/dist/cli/sleep.js +27 -1
  75. package/dist/cli/status.d.ts +4 -4
  76. package/dist/cli/status.js +7 -12
  77. package/dist/cli/transfer.js +8 -13
  78. package/dist/cli/usage.js +9 -6
  79. package/dist/cli.d.ts +1 -1
  80. package/dist/cli.js +4 -9
  81. package/dist/compaction-record.d.ts +2 -0
  82. package/dist/compaction-record.js +89 -68
  83. package/dist/compare.d.ts +11 -16
  84. package/dist/compare.js +11 -16
  85. package/dist/config.d.ts +19 -18
  86. package/dist/config.js +90 -67
  87. package/dist/connectors/github/backfill.d.ts +2 -2
  88. package/dist/connectors/github/backfill.js +8 -15
  89. package/dist/connectors/github/cli-impl.js +3 -8
  90. package/dist/connectors/github/deletion.d.ts +5 -12
  91. package/dist/connectors/github/deletion.js +5 -12
  92. package/dist/connectors/github/dlq.d.ts +6 -9
  93. package/dist/connectors/github/dlq.js +2 -3
  94. package/dist/connectors/github/ingest.d.ts +5 -7
  95. package/dist/connectors/github/ingest.js +8 -12
  96. package/dist/connectors/github/octokit-client.d.ts +3 -5
  97. package/dist/connectors/github/octokit-client.js +5 -6
  98. package/dist/connectors/github/signature.d.ts +9 -39
  99. package/dist/connectors/github/signature.js +9 -39
  100. package/dist/connectors/github/tenant-routing.d.ts +1 -1
  101. package/dist/connectors/github/tenant-routing.js +1 -1
  102. package/dist/connectors/github/transform.js +2 -2
  103. package/dist/connectors/github/types.d.ts +2 -10
  104. package/dist/connectors/github/types.js +1 -3
  105. package/dist/connectors/slack/deletion.d.ts +3 -8
  106. package/dist/connectors/slack/deletion.js +3 -8
  107. package/dist/connectors/slack/dlq.d.ts +1 -1
  108. package/dist/connectors/slack/ingest.d.ts +1 -1
  109. package/dist/connectors/slack/ingest.js +7 -16
  110. package/dist/connectors/slack/signature.d.ts +1 -1
  111. package/dist/connectors/slack/tenant-routing.d.ts +3 -5
  112. package/dist/connectors/slack/tenant-routing.js +3 -5
  113. package/dist/connectors/slack/transform.d.ts +5 -6
  114. package/dist/connectors/slack/transform.js +5 -6
  115. package/dist/connectors/slack/types.d.ts +2 -6
  116. package/dist/connectors/slack/types.js +1 -3
  117. package/dist/connectors/slack/web-client.js +10 -3
  118. package/dist/connectors/slack/workspaces.d.ts +3 -5
  119. package/dist/connectors/slack/workspaces.js +3 -5
  120. package/dist/consolidate/conflicts.js +3 -14
  121. package/dist/consolidate/decay.js +9 -29
  122. package/dist/consolidate/llm-passes.js +4 -5
  123. package/dist/consolidate/merge.js +8 -23
  124. package/dist/consolidate/run.d.ts +1 -8
  125. package/dist/consolidate/run.js +3 -25
  126. package/dist/consolidate/sleep.js +5 -17
  127. package/dist/consolidate/traces.js +9 -21
  128. package/dist/customer-notes.d.ts +5 -7
  129. package/dist/customer-notes.js +6 -9
  130. package/dist/dag.d.ts +10 -21
  131. package/dist/dag.js +23 -73
  132. package/dist/db/continuity.js +2 -2
  133. package/dist/db/migrations/v14.js +1 -1
  134. package/dist/db/migrations/v15.js +1 -2
  135. package/dist/db/migrations/v16.js +3 -4
  136. package/dist/db/migrations/v17.js +2 -3
  137. package/dist/db/migrations/v19.js +1 -1
  138. package/dist/db/migrations/v20.js +1 -1
  139. package/dist/db/migrations/v21.js +2 -6
  140. package/dist/db/migrations/v22.js +2 -4
  141. package/dist/db/migrations/v23.js +1 -1
  142. package/dist/db/migrations/v24.js +4 -6
  143. package/dist/db/migrations/v25.js +2 -3
  144. package/dist/db/migrations/v26.js +3 -3
  145. package/dist/db/migrations/v27.js +2 -10
  146. package/dist/db/migrations/v28.js +5 -8
  147. package/dist/db/migrations/v29.js +3 -4
  148. package/dist/db/migrations/v30.js +2 -2
  149. package/dist/db/migrations/v31.js +1 -1
  150. package/dist/db/migrations/v32.js +1 -1
  151. package/dist/db/migrations/v33.js +3 -3
  152. package/dist/db/migrations/v34.js +1 -1
  153. package/dist/db/migrations/v35.js +3 -4
  154. package/dist/db/migrations/v36.js +3 -4
  155. package/dist/db/migrations/v37.js +5 -5
  156. package/dist/db/migrations/v38.js +7 -8
  157. package/dist/db/migrations/v39.js +1 -1
  158. package/dist/db/migrations/v40.js +4 -16
  159. package/dist/db/migrations/v41.js +3 -4
  160. package/dist/db/migrations/v42.js +3 -4
  161. package/dist/db/migrations/v45.js +1 -1
  162. package/dist/db/migrations/v46.js +1 -1
  163. package/dist/db/migrations/v47.js +1 -1
  164. package/dist/db/migrations/v48.js +1 -1
  165. package/dist/decisions.d.ts +2 -2
  166. package/dist/decisions.js +6 -6
  167. package/dist/dedupe.js +86 -61
  168. package/dist/delivery-recorder.js +154 -135
  169. package/dist/doctor.js +119 -104
  170. package/dist/dormant.js +1 -4
  171. package/dist/embedding-provider.d.ts +4 -8
  172. package/dist/embedding-provider.js +4 -8
  173. package/dist/embeddings.js +55 -47
  174. package/dist/env.d.ts +1 -1
  175. package/dist/env.js +12 -12
  176. package/dist/escape.d.ts +5 -0
  177. package/dist/escape.js +10 -0
  178. package/dist/eval-stats.d.ts +1 -2
  179. package/dist/eval-stats.js +1 -2
  180. package/dist/eval-suite.js +27 -21
  181. package/dist/extract.js +4 -9
  182. package/dist/failure-log.d.ts +3 -3
  183. package/dist/failure-log.js +1 -1
  184. package/dist/forward-claim-detector.d.ts +2 -4
  185. package/dist/forward-claim-detector.js +6 -11
  186. package/dist/goals.d.ts +3 -3
  187. package/dist/goals.js +5 -6
  188. package/dist/graph/read.d.ts +2 -2
  189. package/dist/graph/read.js +5 -6
  190. package/dist/graph/types.d.ts +8 -8
  191. package/dist/graph/write.d.ts +7 -14
  192. package/dist/graph/write.js +16 -23
  193. package/dist/graph-extract.d.ts +7 -8
  194. package/dist/graph-extract.js +62 -72
  195. package/dist/graph-recall.d.ts +2 -2
  196. package/dist/graph-recall.js +55 -49
  197. package/dist/graph-stream.d.ts +5 -6
  198. package/dist/graph-stream.js +66 -57
  199. package/dist/graph-view.d.ts +2 -2
  200. package/dist/graph-view.js +7 -7
  201. package/dist/half-life-migration.d.ts +1 -2
  202. package/dist/half-life-migration.js +2 -3
  203. package/dist/hooks/codex-session.js +1 -1
  204. package/dist/hooks/codex-wrapper.d.ts +1 -1
  205. package/dist/hooks/codex-wrapper.js +3 -2
  206. package/dist/hooks/json-hooks.d.ts +2 -2
  207. package/dist/hooks/json-hooks.js +5 -4
  208. package/dist/hooks/opencode.d.ts +1 -1
  209. package/dist/hooks/opencode.js +5 -4
  210. package/dist/hooks/shared.d.ts +3 -7
  211. package/dist/hooks/shared.js +1 -8
  212. package/dist/http-util.d.ts +2 -3
  213. package/dist/http-util.js +3 -0
  214. package/dist/importers/core.d.ts +2 -9
  215. package/dist/importers/core.js +15 -30
  216. package/dist/importers/sources.js +2 -1
  217. package/dist/importers/vault.js +2 -20
  218. package/dist/incidents.d.ts +1 -1
  219. package/dist/incidents.js +1 -1
  220. package/dist/instruction-detect.d.ts +1 -1
  221. package/dist/instruction-detect.js +1 -1
  222. package/dist/invalidation.d.ts +3 -0
  223. package/dist/invalidation.js +160 -114
  224. package/dist/json.d.ts +5 -0
  225. package/dist/json.js +4 -0
  226. package/dist/judgment.js +1 -2
  227. package/dist/local-embedding.js +1 -1
  228. package/dist/mcp/admin-tools.js +7 -17
  229. package/dist/mcp/format.js +1 -1
  230. package/dist/mcp/framing.js +3 -6
  231. package/dist/mcp/protocol.d.ts +2 -5
  232. package/dist/mcp/protocol.js +1 -3
  233. package/dist/mcp/recall-tools.js +12 -15
  234. package/dist/mcp/request.js +4 -3
  235. package/dist/mcp/session-state.js +2 -3
  236. package/dist/mcp/stdio.js +2 -1
  237. package/dist/mcp/tools.js +9 -6
  238. package/dist/memory-value-weights.d.ts +5 -8
  239. package/dist/memory-value-weights.js +5 -8
  240. package/dist/memory-value.d.ts +13 -13
  241. package/dist/memory-value.js +26 -37
  242. package/dist/memory.d.ts +20 -22
  243. package/dist/memory.js +24 -48
  244. package/dist/multihop.d.ts +1 -1
  245. package/dist/multihop.js +3 -2
  246. package/dist/owner-validation.d.ts +4 -5
  247. package/dist/owner-validation.js +4 -5
  248. package/dist/physics.d.ts +4 -4
  249. package/dist/physics.js +7 -9
  250. package/dist/policies.d.ts +9 -10
  251. package/dist/policies.js +12 -14
  252. package/dist/postinstall.js +3 -6
  253. package/dist/predictions/planning-fallacy.d.ts +9 -14
  254. package/dist/predictions/planning-fallacy.js +10 -16
  255. package/dist/predictions/store.d.ts +15 -23
  256. package/dist/predictions/store.js +36 -33
  257. package/dist/processes.d.ts +2 -7
  258. package/dist/processes.js +3 -3
  259. package/dist/project-briefs.d.ts +2 -3
  260. package/dist/project-briefs.js +9 -13
  261. package/dist/project-identity.d.ts +22 -9
  262. package/dist/project-identity.js +47 -12
  263. package/dist/project-merge.d.ts +17 -2
  264. package/dist/project-merge.js +109 -27
  265. package/dist/project-remote.d.ts +12 -0
  266. package/dist/project-remote.js +138 -0
  267. package/dist/prompt-recall.js +1 -2
  268. package/dist/rate-limit.d.ts +1 -1
  269. package/dist/rate-limit.js +1 -1
  270. package/dist/raw-archive.d.ts +9 -0
  271. package/dist/raw-archive.js +70 -53
  272. package/dist/recall-history.d.ts +19 -20
  273. package/dist/recall-history.js +24 -42
  274. package/dist/recall-pipeline.js +4 -28
  275. package/dist/recall-scope.d.ts +7 -8
  276. package/dist/recall-scope.js +7 -8
  277. package/dist/recall-trace.d.ts +5 -9
  278. package/dist/recall-trace.js +6 -10
  279. package/dist/refine-llm.d.ts +1 -1
  280. package/dist/refine-llm.js +2 -2
  281. package/dist/reject-flow.d.ts +3 -4
  282. package/dist/reject-flow.js +122 -117
  283. package/dist/rejection.d.ts +5 -6
  284. package/dist/rejection.js +7 -15
  285. package/dist/rerankers/clef.d.ts +1 -1
  286. package/dist/rerankers/jev.d.ts +1 -2
  287. package/dist/rerankers/jev.js +4 -5
  288. package/dist/rerankers/llm.d.ts +1 -2
  289. package/dist/rerankers/llm.js +1 -2
  290. package/dist/rerankers/types.d.ts +1 -2
  291. package/dist/rrf.d.ts +2 -2
  292. package/dist/rrf.js +2 -2
  293. package/dist/search/bm25-search.d.ts +1 -1
  294. package/dist/search/bm25-search.js +2 -1
  295. package/dist/search/boosts.js +2 -1
  296. package/dist/search/hybrid.d.ts +1 -1
  297. package/dist/search/hybrid.js +2 -1
  298. package/dist/search/physics-search.d.ts +1 -1
  299. package/dist/search/physics-search.js +2 -1
  300. package/dist/search/types.d.ts +2 -0
  301. package/dist/search/types.js +3 -1
  302. package/dist/secret-detect.d.ts +4 -5
  303. package/dist/secret-detect.js +6 -10
  304. package/dist/server/auth.js +5 -5
  305. package/dist/server/client-ip.js +1 -1
  306. package/dist/server/cursor.js +2 -1
  307. package/dist/server/mcp-http.js +4 -4
  308. package/dist/server/request.d.ts +3 -6
  309. package/dist/server/request.js +6 -7
  310. package/dist/server/routes/admin.js +5 -4
  311. package/dist/server/routes/customer-notes.js +6 -5
  312. package/dist/server/routes/decisions.js +4 -3
  313. package/dist/server/routes/incidents.js +7 -5
  314. package/dist/server/routes/memories.js +7 -7
  315. package/dist/server/routes/policies.js +3 -2
  316. package/dist/server/routes/predictions.js +12 -15
  317. package/dist/server/routes/processes.js +3 -2
  318. package/dist/server/routes/project-briefs.js +8 -7
  319. package/dist/server/routes/recall.js +95 -93
  320. package/dist/server/routes/skills.js +6 -5
  321. package/dist/server/types.d.ts +1 -1
  322. package/dist/server/validation.d.ts +1 -2
  323. package/dist/server/validation.js +7 -14
  324. package/dist/server-detect.js +72 -58
  325. package/dist/server.d.ts +2 -2
  326. package/dist/server.js +131 -117
  327. package/dist/shared.d.ts +17 -17
  328. package/dist/shared.js +93 -97
  329. package/dist/skills.d.ts +3 -3
  330. package/dist/skills.js +8 -8
  331. package/dist/store/audit-event.d.ts +2 -2
  332. package/dist/store/audit-event.js +1 -1
  333. package/dist/store/candidates.d.ts +2 -2
  334. package/dist/store/candidates.js +4 -3
  335. package/dist/store/conflicts.js +30 -22
  336. package/dist/store/delete-and-batch.d.ts +11 -14
  337. package/dist/store/delete-and-batch.js +40 -91
  338. package/dist/store/entry-reads.d.ts +17 -25
  339. package/dist/store/entry-reads.js +59 -40
  340. package/dist/store/entry-row.d.ts +6 -24
  341. package/dist/store/entry-row.js +6 -24
  342. package/dist/store/entry-writes.d.ts +6 -7
  343. package/dist/store/entry-writes.js +13 -11
  344. package/dist/store/handoffs.d.ts +1 -1
  345. package/dist/store/handoffs.js +7 -10
  346. package/dist/store/index-and-stats.d.ts +2 -6
  347. package/dist/store/index-and-stats.js +4 -10
  348. package/dist/store/mirrors.d.ts +6 -19
  349. package/dist/store/mirrors.js +14 -39
  350. package/dist/store/open.js +9 -31
  351. package/dist/store/rows.d.ts +5 -11
  352. package/dist/store/rows.js +6 -11
  353. package/dist/store/search-rows.d.ts +17 -34
  354. package/dist/store/search-rows.js +31 -59
  355. package/dist/store/sessions.d.ts +4 -5
  356. package/dist/store/sessions.js +5 -6
  357. package/dist/store/summaries.d.ts +13 -17
  358. package/dist/store/summaries.js +26 -70
  359. package/dist/support-bundle.js +4 -8
  360. package/dist/tenant.d.ts +1 -5
  361. package/dist/token-ledger.d.ts +1 -1
  362. package/dist/token-ledger.js +3 -5
  363. package/dist/trace.js +1 -3
  364. package/dist/version.d.ts +1 -1
  365. package/dist/version.js +1 -1
  366. package/dist/working-memory.d.ts +1 -1
  367. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  368. package/extensions/openclaw-plugin/package.json +1 -1
  369. package/openclaw.plugin.json +1 -1
  370. package/package.json +1 -1
@@ -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
@@ -1,29 +1,17 @@
1
1
  import type { MemoryEntry } from '../memory.js';
2
2
  import { type DatabaseSyncLike } from '../db.js';
3
3
  /**
4
- * v1.7.2 — recall-mode scope filter shape, exported so callers
5
- * (`loadRecallSearchEntries`) and tests can refer to it symbolically without
6
- * `Parameters<typeof loadSearchRows>[N]` indirection.
7
- *
8
- * Three modes:
9
- * - 'default-deny' — exclude scopes in `RECALL_DEFAULT_DENY_SCOPES` (T2).
10
- * - 'exact' — exact match on `m.scope = value` (api.recall's explicit-scope
11
- * request semantics).
12
- * - 'default-deny-or-exact' (v1.25.0) — the default-admitted set PLUS rows
13
- * whose scope equals `value`. This is the CLI `--scope` semantics: the
14
- * flag predates the envelope column as a TAG-boost ranking hint
15
- * (`scope:<v>` tags, HIPPO_SCOPE), so an explicit flag must UNLOCK the
16
- * named envelope scope in addition to the normal set rather than narrow
17
- * the result to it — narrowing would return zero rows for every
18
- * tag-scoped workflow (envelope scope NULL). Strictly safer than the
19
- * pre-v1.25.0 CLI behavior (no filter at all): other private scopes and
20
- * quarantine buckets stay denied.
4
+ * Recall-mode scope filter shape, exported so callers and tests can name it. Three modes:
5
+ * - 'default-deny': exclude scopes in `RECALL_DEFAULT_DENY_SCOPES`.
6
+ * - 'exact': exact match on `m.scope = value` (api.recall's explicit-scope request semantics).
7
+ * - 'default-deny-or-exact': the default-admitted set PLUS rows whose scope equals `value` (CLI `--scope`).
8
+ * The flag began as a tag-boost hint, so narrowing would return zero rows for tag-scoped workflows.
21
9
  *
22
10
  * Background pipelines (`consolidate`, `embeddings`, `refine-llm`, ...) call
23
11
  * `loadSearchEntries` (no scopeFilter arg) and see all rows including
24
12
  * quarantine.
25
13
  */
26
- /** @internal v1.7.2 — internal SQL-builder shape; not on the public API
14
+ /** @internal Internal SQL-builder shape; not on the public API
27
15
  * surface (not re-exported from `src/index.ts`). Subject to change. */
28
16
  export type RecallScopeFilter = {
29
17
  mode: 'default-deny';
@@ -34,6 +22,8 @@ export type RecallScopeFilter = {
34
22
  mode: 'default-deny-or-exact';
35
23
  value: string;
36
24
  };
25
+ /** One project name, or every name a project's rows carry. */
26
+ export type OriginFilter = string | readonly string[];
37
27
  /** Scope rule for recall: none requested is default-deny; 'exact' narrows to the request; 'additive' adds it to the default set. */
38
28
  export declare function recallScopeFilter(requestedScope: string | undefined, mode: 'exact' | 'additive'): RecallScopeFilter;
39
29
  /**
@@ -45,31 +35,24 @@ export declare function recallScopeFilter(requestedScope: string | undefined, mo
45
35
  */
46
36
  export declare function loadSearchEntries(hippoRoot: string, query: string, limit?: number, tenantId?: string): MemoryEntry[];
47
37
  /**
48
- * v1.7.1 — recall-mode loader. Pushes the recall-side scope predicate into
49
- * SQL so `unknown:legacy` cannot leak via any consumer that hasn't remembered
50
- * to re-filter (root-cause-over-patches: codex flagged this on v1.6.5 review).
38
+ * Recall-mode loader. Pushes the recall-side scope predicate into SQL so
39
+ * `unknown:legacy` cannot leak via any consumer that hasn't remembered to re-filter.
51
40
  *
52
41
  * - `requestedScope` undefined / '': default-deny on `unknown:legacy`.
53
42
  * - `requestedScope` non-empty string: exact match on `m.scope = requestedScope`.
54
43
  *
55
- * Private-scope (`<source>:private:*`) exclusion: SQL applies a conservative
56
- * pre-window approximation (`NOT LIKE '%:private:%'`, v1.25.0 — codex P2:
57
- * post-window-only filtering let private rows starve admitted candidates out
58
- * of the LIMIT window); the exact anchored regex
59
- * (`passesScopeFilterForRecall`) remains the authoritative JS post-filter in
60
- * the recall consumers.
44
+ * Private scopes: SQL applies a conservative `NOT LIKE '%:private:%'` before the LIMIT window so private
45
+ * rows cannot starve admitted ones; `passesScopeFilterForRecall` stays the exact JS post-filter.
61
46
  *
62
- * Consumers: `api.recall` (v1.7.1+), `cmdRecall`/`cmdExplain` direct CLI paths
63
- * and `searchBothHybrid` recall mode (v1.25.0). Background pipelines
47
+ * Consumers: `api.recall`, `cmdRecall`/`cmdExplain` direct CLI paths
48
+ * and `searchBothHybrid` recall mode. Background pipelines
64
49
  * (`consolidate`, `embeddings`, `refine-llm`, ...) keep using
65
50
  * `loadSearchEntries` so they can see quarantined rows when needed.
66
51
  *
67
- * `tenantId` widened to optional in v1.25.0 for the searchBothHybrid recall
68
- * mode (its `tenantId` option is optional); `loadSearchRows` already treats
69
- * undefined as "no tenant filter" for legacy callers.
52
+ * `tenantId` is optional because searchBothHybrid's is; undefined means no tenant filter.
70
53
  */
71
- export declare function loadRecallSearchEntries(hippoRoot: string, query: string, limit?: number, tenantId?: string, requestedScope?: string, explicitScopeMode?: 'exact' | 'additive', includeSuperseded?: boolean, originProject?: string): MemoryEntry[];
72
- export declare function loadRecallSearchEntriesFromDb(db: DatabaseSyncLike, query: string, limit?: number, tenantId?: string, requestedScope?: string, explicitScopeMode?: 'exact' | 'additive', includeSuperseded?: boolean, originProject?: string): MemoryEntry[];
54
+ export declare function loadRecallSearchEntries(hippoRoot: string, query: string, limit?: number, tenantId?: string, requestedScope?: string, explicitScopeMode?: 'exact' | 'additive', includeSuperseded?: boolean, originProjects?: OriginFilter): MemoryEntry[];
55
+ export declare function loadRecallSearchEntriesFromDb(db: DatabaseSyncLike, query: string, limit?: number, tenantId?: string, requestedScope?: string, explicitScopeMode?: 'exact' | 'additive', includeSuperseded?: boolean, originProjects?: OriginFilter): MemoryEntry[];
73
56
  /** Which rows the vector arm of hybrid search may add: the same tenant, scope and superseded rules as the lexical load. */
74
57
  export interface VectorCandidateSpec {
75
58
  tenantId?: string;