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
@@ -4,9 +4,11 @@ import { tokenize } from '../tokenize.js';
4
4
  import { RECALL_DEFAULT_DENY_SCOPES } from '../recall-scope.js';
5
5
  import { RAREST_TERM_COUNT, rarestPromptTerms } from '../prompt-recall.js';
6
6
  import { log } from '../log.js';
7
+ import { originInSql } from '../project-identity.js';
7
8
  import { topVectorMatches } from '../vector-store.js';
8
9
  import { MEMORY_SELECT_COLUMNS, MEMORY_SEARCH_COLUMNS, DEFAULT_SEARCH_CANDIDATE_LIMIT, rowToEntry, } from './rows.js';
9
10
  import { openStore } from './open.js';
11
+ import { escapeLike } from '../escape.js';
10
12
  /** The recall scope rule for a table column prefix (`m.` or none); `passesScopeFilterForRecall` in recall-scope.ts is its JS twin. */
11
13
  function recallScopeClause(col, scopeFilter) {
12
14
  if (scopeFilter === undefined)
@@ -23,10 +25,12 @@ function recallScopeClause(col, scopeFilter) {
23
25
  return { sql: ` AND (${admitted} OR ${col}scope = ?)`, params: [...RECALL_DEFAULT_DENY_SCOPES, scopeFilter.value] };
24
26
  }
25
27
  // In SQL, not after the window cut, so other projects' matches cannot crowd the project's own rows out of the LIMIT.
26
- function withProject(scope, col, originProject) {
27
- if (originProject === undefined)
28
+ function withProject(scope, col, origin) {
29
+ if (origin === undefined)
28
30
  return scope;
29
- return { sql: `${scope.sql} AND (${col}origin_project = '' OR ${col}origin_project = ?)`, params: [...scope.params, originProject] };
31
+ // A string is the shape published callers passed before a project could carry several names.
32
+ const originProjects = [origin].flat();
33
+ return { sql: `${scope.sql} AND (${col}origin_project = '' OR ${originInSql(originProjects, `${col}origin_project`)})`, params: [...scope.params, ...originProjects] };
30
34
  }
31
35
  /** Scope rule for recall: none requested is default-deny; 'exact' narrows to the request; 'additive' adds it to the default set. */
32
36
  export function recallScopeFilter(requestedScope, mode) {
@@ -35,22 +39,15 @@ export function recallScopeFilter(requestedScope, mode) {
35
39
  return mode === 'additive' ? { mode: 'default-deny-or-exact', value: requestedScope } : { mode: 'exact', value: requestedScope };
36
40
  }
37
41
  const FTS_QUERY_SYNTAX_RE = /fts5: syntax error|unterminated string/i;
38
- function loadSearchRows(db, query, limit, tenantId, scopeFilter, includeSuperseded = true, originProject) {
39
- const p = searchPredicates(tenantId, scopeFilter, includeSuperseded, originProject);
42
+ function loadSearchRows(db, query, limit, tenantId, scopeFilter, includeSuperseded = true, originProjects) {
43
+ const p = searchPredicates(tenantId, scopeFilter, includeSuperseded, originProjects);
40
44
  const terms = Array.from(new Set(tokenize(query)));
41
45
  if (terms.length === 0) {
42
- // F3 (v1.7.0) self-review: empty-query path is the second uncapped
43
- // path (codex diff-pass caught the full-store fallback at the bottom;
44
- // this no-terms path had the same shape). Apply LIMIT so all four
45
- // candidate paths honour the caller's cap when set.
46
+ // LIMIT here too, so every candidate path honours the caller's cap.
46
47
  return selectAllCandidates(db, p, limit);
47
48
  }
48
- // v1.7.1 — test/diagnostic hook: `HIPPO_FORCE_LIKE_PATH=1` forces the
49
- // LIKE-fallback path here only. Gated at the read-call site so writes
50
- // (`syncFtsRow`, `deleteFtsRow`, `raw-archive.ts::archiveRaw`) keep using
51
- // `isFtsAvailable` honestly and never silently skip FTS index sync.
52
- // Lets tests exercise the LIKE branch deterministically without
53
- // poisoning the on-disk FTS state.
49
+ // `HIPPO_FORCE_LIKE_PATH=1` forces the LIKE path for tests; gated at this read site so
50
+ // writes keep using `isFtsAvailable` and never silently skip FTS index sync.
54
51
  const forceLikePath = envForceLikePath();
55
52
  if (!forceLikePath && isFtsAvailable(db)) {
56
53
  const rows = selectFtsCandidates(db, terms, p, limit);
@@ -60,14 +57,11 @@ function loadSearchRows(db, query, limit, tenantId, scopeFilter, includeSupersed
60
57
  const rows = selectLikeCandidates(db, terms, p, limit);
61
58
  if (rows.length > 0)
62
59
  return rows;
63
- // F3 (v1.7.0) codex P1: pre-v1.7.0 the full-store fallback ignored
64
- // `limit` and could return the whole tenant store. With scorerWindow
65
- // now reported on RecallResult, an unbounded fallback would lie about
66
- // candidate-pool size. Apply LIMIT here so all four paths honour the
67
- // caller's cap.
60
+ // LIMIT the full-store fallback too: RecallResult reports the scorer window, so an
61
+ // unbounded fallback would misstate the candidate-pool size.
68
62
  return selectAllCandidates(db, p, limit);
69
63
  }
70
- function searchPredicates(tenantId, scopeFilter, includeSuperseded, originProject) {
64
+ function searchPredicates(tenantId, scopeFilter, includeSuperseded, originProjects) {
71
65
  // tenantId undefined = no tenant filter (legacy callers / cross-deployment
72
66
  // helpers). tenantId set = strict tenant isolation, leveraging the composite
73
67
  // idx_memories_tenant_created (leading column tenant_id, O(log n) lookup).
@@ -75,19 +69,8 @@ function searchPredicates(tenantId, scopeFilter, includeSuperseded, originProjec
75
69
  const tenantPredicateNoAlias = tenantId !== undefined ? ` AND tenant_id = ?` : '';
76
70
  const tenantOnlyPredicate = tenantId !== undefined ? ` WHERE tenant_id = ?` : '';
77
71
  const tenantParams = tenantId !== undefined ? [tenantId] : [];
78
- // v1.12.6 — belt-and-suspenders against `kind='archived'` leaking into recall.
79
- // `kind='archived'` is a transient sentinel inside `archiveRawMemory`'s
80
- // SAVEPOINT (src/raw-archive.ts:56): UPDATE kind = 'archived' immediately
81
- // followed by DELETE, both inside one savepoint that commits or rolls back
82
- // atomically. SQLite atomicity guarantees no concurrent reader sees the
83
- // intermediate state. This filter is defensive-only against:
84
- // (a) future bugs that drop the SAVEPOINT,
85
- // (b) future bugs that introduce kind='archived' as a persisted state,
86
- // (c) external direct-SQL writes that bypass archiveRawMemory.
87
- // tenantOnlyPredicate starts with " WHERE tenant_id = ?" when tenant is set;
88
- // when unset, we have no WHERE yet, so the archived clause needs both AND
89
- // and WHERE forms. The "tenant-only" path always has WHERE (from tenant or
90
- // we synthesize one).
72
+ // Defensive: kind='archived' is a transient sentinel inside archiveRawMemory's SAVEPOINT, so this only
73
+ // guards against a dropped SAVEPOINT, a persisted 'archived' state, or direct-SQL writes.
91
74
  const archivedClauseAlias = ` AND m.kind != 'archived'`;
92
75
  const archivedClauseNoAlias = ` AND kind != 'archived'`;
93
76
  // For the "tenant-only" path: if no tenant set, tenantOnlyPredicate is '',
@@ -95,8 +78,8 @@ function searchPredicates(tenantId, scopeFilter, includeSuperseded, originProjec
95
78
  // by always joining `tenantOnlyPredicate + archivedClauseTenantOnly` where
96
79
  // the latter switches between " AND" and " WHERE" based on caller context.
97
80
  const archivedClauseTenantOnly = tenantId !== undefined ? ` AND kind != 'archived'` : ` WHERE kind != 'archived'`;
98
- const aliasScope = withProject(recallScopeClause('m.', scopeFilter), 'm.', originProject);
99
- const plainScope = withProject(recallScopeClause('', scopeFilter), '', originProject);
81
+ const aliasScope = withProject(recallScopeClause('m.', scopeFilter), 'm.', originProjects);
82
+ const plainScope = withProject(recallScopeClause('', scopeFilter), '', originProjects);
100
83
  const scopeParams = aliasScope.params;
101
84
  const currentAlias = includeSuperseded ? '' : ' AND m.superseded_by IS NULL';
102
85
  const currentNoAlias = includeSuperseded ? '' : ' AND superseded_by IS NULL';
@@ -120,9 +103,6 @@ function selectFtsCandidates(db, terms, p, limit) {
120
103
  // memories_fts virtual table has no tenant_id column; filter via the
121
104
  // joined memories row (cheap with idx_memories_tenant_created leading
122
105
  // on tenant_id).
123
- // F1 (v1.7.0): MEMORY_SEARCH_COLUMNS adds bm25_score as the trailing
124
- // result column. Every other column is m.<col> AS <col> so rowToEntry
125
- // sees the same shape it always has.
126
106
  // SAFETY: MEMORY_SEARCH_COLUMNS aliases every column to the same name
127
107
  // MEMORY_SELECT_COLUMNS uses (plus bm25_score), matching MemoryRow.
128
108
  return db.prepare(`
@@ -143,7 +123,6 @@ function selectFtsCandidates(db, terms, p, limit) {
143
123
  }
144
124
  }
145
125
  function selectLikeCandidates(db, terms, p, limit) {
146
- const escapeLike = (term) => term.replace(/[%_\\]/g, '\\$&');
147
126
  const where = terms.map(() => `(LOWER(content) LIKE ? ESCAPE '\\' OR LOWER(tags_json) LIKE ? ESCAPE '\\')`).join(' OR ');
148
127
  const params = terms.flatMap((term) => {
149
128
  const like = `%${escapeLike(term)}%`;
@@ -176,42 +155,35 @@ export function loadSearchEntries(hippoRoot, query, limit = DEFAULT_SEARCH_CANDI
176
155
  }
177
156
  }
178
157
  /**
179
- * v1.7.1 — recall-mode loader. Pushes the recall-side scope predicate into
180
- * SQL so `unknown:legacy` cannot leak via any consumer that hasn't remembered
181
- * to re-filter (root-cause-over-patches: codex flagged this on v1.6.5 review).
158
+ * Recall-mode loader. Pushes the recall-side scope predicate into SQL so
159
+ * `unknown:legacy` cannot leak via any consumer that hasn't remembered to re-filter.
182
160
  *
183
161
  * - `requestedScope` undefined / '': default-deny on `unknown:legacy`.
184
162
  * - `requestedScope` non-empty string: exact match on `m.scope = requestedScope`.
185
163
  *
186
- * Private-scope (`<source>:private:*`) exclusion: SQL applies a conservative
187
- * pre-window approximation (`NOT LIKE '%:private:%'`, v1.25.0 — codex P2:
188
- * post-window-only filtering let private rows starve admitted candidates out
189
- * of the LIMIT window); the exact anchored regex
190
- * (`passesScopeFilterForRecall`) remains the authoritative JS post-filter in
191
- * the recall consumers.
164
+ * Private scopes: SQL applies a conservative `NOT LIKE '%:private:%'` before the LIMIT window so private
165
+ * rows cannot starve admitted ones; `passesScopeFilterForRecall` stays the exact JS post-filter.
192
166
  *
193
- * Consumers: `api.recall` (v1.7.1+), `cmdRecall`/`cmdExplain` direct CLI paths
194
- * and `searchBothHybrid` recall mode (v1.25.0). Background pipelines
167
+ * Consumers: `api.recall`, `cmdRecall`/`cmdExplain` direct CLI paths
168
+ * and `searchBothHybrid` recall mode. Background pipelines
195
169
  * (`consolidate`, `embeddings`, `refine-llm`, ...) keep using
196
170
  * `loadSearchEntries` so they can see quarantined rows when needed.
197
171
  *
198
- * `tenantId` widened to optional in v1.25.0 for the searchBothHybrid recall
199
- * mode (its `tenantId` option is optional); `loadSearchRows` already treats
200
- * undefined as "no tenant filter" for legacy callers.
172
+ * `tenantId` is optional because searchBothHybrid's is; undefined means no tenant filter.
201
173
  */
202
- export function loadRecallSearchEntries(hippoRoot, query, limit = DEFAULT_SEARCH_CANDIDATE_LIMIT, tenantId, requestedScope, explicitScopeMode = 'exact', includeSuperseded = true, originProject) {
174
+ export function loadRecallSearchEntries(hippoRoot, query, limit = DEFAULT_SEARCH_CANDIDATE_LIMIT, tenantId, requestedScope, explicitScopeMode = 'exact', includeSuperseded = true, originProjects) {
203
175
  const db = openStore(hippoRoot);
204
176
  try {
205
- return loadRecallSearchEntriesFromDb(db, query, limit, tenantId, requestedScope, explicitScopeMode, includeSuperseded, originProject);
177
+ return loadRecallSearchEntriesFromDb(db, query, limit, tenantId, requestedScope, explicitScopeMode, includeSuperseded, originProjects);
206
178
  }
207
179
  finally {
208
180
  closeHippoDb(db);
209
181
  }
210
182
  }
211
- // Split out so callers with an already-open db (Z1 prompt-recall path) skip
183
+ // Split out so callers with an already-open db (the prompt-recall path) skip
212
184
  // the initStore+open/close cycle per store per call.
213
- export function loadRecallSearchEntriesFromDb(db, query, limit = DEFAULT_SEARCH_CANDIDATE_LIMIT, tenantId, requestedScope, explicitScopeMode = 'exact', includeSuperseded = true, originProject) {
214
- return loadSearchRows(db, query, limit, tenantId, recallScopeFilter(requestedScope, explicitScopeMode), includeSuperseded, originProject).map(rowToEntry);
185
+ export function loadRecallSearchEntriesFromDb(db, query, limit = DEFAULT_SEARCH_CANDIDATE_LIMIT, tenantId, requestedScope, explicitScopeMode = 'exact', includeSuperseded = true, originProjects) {
186
+ return loadSearchRows(db, query, limit, tenantId, recallScopeFilter(requestedScope, explicitScopeMode), includeSuperseded, originProjects).map(rowToEntry);
215
187
  }
216
188
  /** The rows nearest `queryVector` that pass `spec`, nearest first. */
217
189
  export function loadVectorCandidateEntries(hippoRoot, queryVector, spec) {
@@ -9,8 +9,7 @@ export declare function saveActiveTaskSnapshot(hippoRoot: string, tenantId: stri
9
9
  }): TaskSnapshot;
10
10
  export declare function loadActiveTaskSnapshot(hippoRoot: string, tenantId: string): TaskSnapshot | null;
11
11
  /**
12
- * Default freshness bound for AMBIENT active-task-snapshot reads (DF1,
13
- * docs/plans/2026-08-23-df1-snapshot-lifecycle.md): 72h, chosen over 48h so
12
+ * Default freshness bound for AMBIENT active-task-snapshot reads: 72h, chosen over 48h so
14
13
  * a Friday-evening orphan still offers continuity on Monday morning.
15
14
  * Exported so callers can override via `loadFreshActiveTaskSnapshot`'s
16
15
  * `opts.maxAgeMs`; deliberately no env knob (Simplicity First).
@@ -18,7 +17,7 @@ export declare function loadActiveTaskSnapshot(hippoRoot: string, tenantId: stri
18
17
  export declare const SNAPSHOT_AMBIENT_MAX_AGE_MS: number;
19
18
  /**
20
19
  * Bounded read for AMBIENT active-task-snapshot surfaces (UserPromptSubmit
21
- * hook context, MCP recall block) — the never-expires fix for DF1. A
20
+ * hook context, MCP recall block), so snapshots expire. A
22
21
  * snapshot written by `hippo pre-compact` has no death path tied to the
23
22
  * session that owns it, so an orphaned row would otherwise inject into
24
23
  * every prompt of every later session forever. Wraps `loadActiveTaskSnapshot`
@@ -45,8 +44,8 @@ export declare function loadFreshActiveTaskSnapshot(hippoRoot: string, tenantId:
45
44
  }): TaskSnapshot | null;
46
45
  export declare function clearActiveTaskSnapshot(hippoRoot: string, tenantId: string, clearedStatus?: string): boolean;
47
46
  /**
48
- * Close the `active` task snapshot(s) owned by `sessionId`, for the T3
49
- * session-end death path (DF1, docs/plans/2026-08-23-df1-snapshot-lifecycle.md).
47
+ * Close the `active` task snapshot(s) owned by `sessionId`, for the
48
+ * session-end death path.
50
49
  * Only one `active` row exists per tenant in practice (supersession happens
51
50
  * at save), but the WHERE clause scopes on `session_id` too — not just
52
51
  * `status='active' AND tenant_id=?` — so an ending session can never close a
@@ -68,8 +68,7 @@ export function loadActiveTaskSnapshot(hippoRoot, tenantId) {
68
68
  }
69
69
  }
70
70
  /**
71
- * Default freshness bound for AMBIENT active-task-snapshot reads (DF1,
72
- * docs/plans/2026-08-23-df1-snapshot-lifecycle.md): 72h, chosen over 48h so
71
+ * Default freshness bound for AMBIENT active-task-snapshot reads: 72h, chosen over 48h so
73
72
  * a Friday-evening orphan still offers continuity on Monday morning.
74
73
  * Exported so callers can override via `loadFreshActiveTaskSnapshot`'s
75
74
  * `opts.maxAgeMs`; deliberately no env knob (Simplicity First).
@@ -83,7 +82,7 @@ function isNonEmptySessionId(value) {
83
82
  }
84
83
  /**
85
84
  * Bounded read for AMBIENT active-task-snapshot surfaces (UserPromptSubmit
86
- * hook context, MCP recall block) — the never-expires fix for DF1. A
85
+ * hook context, MCP recall block), so snapshots expire. A
87
86
  * snapshot written by `hippo pre-compact` has no death path tied to the
88
87
  * session that owns it, so an orphaned row would otherwise inject into
89
88
  * every prompt of every later session forever. Wraps `loadActiveTaskSnapshot`
@@ -138,8 +137,8 @@ export function clearActiveTaskSnapshot(hippoRoot, tenantId, clearedStatus = 'cl
138
137
  }
139
138
  }
140
139
  /**
141
- * Close the `active` task snapshot(s) owned by `sessionId`, for the T3
142
- * session-end death path (DF1, docs/plans/2026-08-23-df1-snapshot-lifecycle.md).
140
+ * Close the `active` task snapshot(s) owned by `sessionId`, for the
141
+ * session-end death path.
143
142
  * Only one `active` row exists per tenant in practice (supersession happens
144
143
  * at save), but the WHERE clause scopes on `session_id` too — not just
145
144
  * `status='active' AND tenant_id=?` — so an ending session can never close a
@@ -162,7 +161,7 @@ export function appendSessionEvent(hippoRoot, tenantId, event) {
162
161
  assertTenantId('appendSessionEvent', tenantId);
163
162
  const db = openStore(hippoRoot);
164
163
  const now = new Date().toISOString();
165
- // v1.2: scope is wired through. Default-deny in api.recall + cmdRecall
164
+ // Scope is stored as given; default-deny in api.recall + cmdRecall
166
165
  // continuity reads applies to slack:private:* and 'unknown:legacy' rows.
167
166
  try {
168
167
  const result = db.prepare(`
@@ -1,7 +1,7 @@
1
1
  import type { MemoryEntry } from '../memory.js';
2
2
  /**
3
3
  * Load summaries flagged dirty for the given tenant. Sorted by latest_at
4
- * DESC (NULLS LAST) so E3's rebuild cap (HIPPO_DAG_REBUILD_CAP, default 20)
4
+ * DESC (NULLS LAST) so the rebuild cap (HIPPO_DAG_REBUILD_CAP, default 20)
5
5
  * takes the most-recently-changed summaries first.
6
6
  *
7
7
  * Returns full MemoryEntry shape via MEMORY_SELECT_COLUMNS + rowToEntry
@@ -11,21 +11,20 @@ export declare function loadDirtySummaries(hippoRoot: string, tenantId: string):
11
11
  /**
12
12
  * Mark a summary as dirty. Idempotent (re-marking dirty is a no-op + no
13
13
  * second audit row). Tenant-scoped to prevent cross-tenant writes via
14
- * parent-lookup. Called by E2 from invalidation.ts / writeEntry /
14
+ * parent-lookup. Called from invalidation.ts / writeEntry /
15
15
  * forgetMemory / archiveRawMemory whenever a child is invalidated,
16
16
  * superseded, forgotten, or archived.
17
17
  *
18
- * Quietly no-ops if the target row doesn't exist or isn't a level-2
19
- * summary (E5 will widen the dag_level guard to IN (2, 3) when level-3
20
- * build path lands). Emits a 'summary_marked_dirty' audit row on actual
18
+ * Quietly no-ops if the target row doesn't exist or isn't a level-2/3
19
+ * summary. Emits a 'summary_marked_dirty' audit row on actual
21
20
  * state transitions (0 -> 1) via the audit() helper, which try/catches
22
21
  * for missing audit_log (the v27 self-heal scenario).
23
22
  */
24
23
  export declare function markSummaryDirty(hippoRoot: string, summaryId: string, tenantId: string, actor?: string): void;
25
24
  /**
26
- * v0.30 / E5 — host-wide loader for L2 topic summaries without an L3 parent.
25
+ * Host-wide loader for L2 topic summaries without an L3 parent.
27
26
  * Used by consolidate phase 1.9 (buildEntityProfiles) to cluster L2s into
28
- * L3 entity profiles. Mirrors loadAllDirtySummaries pattern (E3): SQL-level
27
+ * L3 entity profiles. Mirrors loadAllDirtySummaries: SQL-level
29
28
  * filter is cheaper than reusing in-memory `survivors` (which doesn't
30
29
  * contain L2s freshly created by phase 1.7 buildDag).
31
30
  *
@@ -34,7 +33,7 @@ export declare function markSummaryDirty(hippoRoot: string, summaryId: string, t
34
33
  */
35
34
  export declare function loadAllL2Summaries(hippoRoot: string): MemoryEntry[];
36
35
  /**
37
- * v0.30 / E3 — host-wide variant of loadDirtySummaries. Iterates all tenants
36
+ * Host-wide variant of loadDirtySummaries. Iterates all tenants
38
37
  * in one query so consolidate.ts (host-wide per L106-109) does not need a
39
38
  * per-tenant loop. Each returned MemoryEntry carries its own tenantId (via
40
39
  * rowToEntry), so per-summary children + rebuild UPDATE stay tenant-scoped.
@@ -44,7 +43,7 @@ export declare function loadAllL2Summaries(hippoRoot: string): MemoryEntry[];
44
43
  */
45
44
  export declare function loadAllDirtySummaries(hippoRoot: string): MemoryEntry[];
46
45
  /**
47
- * v0.30 / E3 — load live children of a DAG summary. Used by
46
+ * Load live children of a DAG summary. Used by
48
47
  * rebuildDirtySummaries to regenerate content from the CURRENT child set
49
48
  * (not the children at create-time). Skips archived. Tenant-scoped
50
49
  * (defence in depth — dag_parent_id is unique-ish but tenant guard is
@@ -52,7 +51,7 @@ export declare function loadAllDirtySummaries(hippoRoot: string): MemoryEntry[];
52
51
  */
53
52
  export declare function loadChildrenOfSummary(hippoRoot: string, summaryId: string, tenantId: string): MemoryEntry[];
54
53
  /**
55
- * v0.30 / E3 — patch applied by applyRebuildResult. Two-branch shape
54
+ * Patch applied by applyRebuildResult. Two-branch shape
56
55
  * (bumpRebuildCount false for zero-child case, true for normal rebuild).
57
56
  */
58
57
  export interface RebuildPatch {
@@ -65,7 +64,7 @@ export interface RebuildPatch {
65
64
  actor: string;
66
65
  }
67
66
  /**
68
- * v0.30 / E3 — apply a rebuild result to a dirty summary. Atomic: one
67
+ * Apply a rebuild result to a dirty summary. Atomic: one
69
68
  * prepared UPDATE statement plus syncFtsRow inside one SAVEPOINT.
70
69
  * WHERE includes `AND summary_dirty = 1` so concurrent sleep's race-loser
71
70
  * becomes a no-op (no rebuild_count bump, no audit row).
@@ -81,14 +80,11 @@ export declare function applyRebuildResult(hippoRoot: string, summary: MemoryEnt
81
80
  refused: boolean;
82
81
  };
83
82
  /**
84
- * v0.30 / E3 — clear summary_dirty on a freshly-built summary. Called by
85
- * buildDag immediately after the child-link loop finishes. Without this,
86
- * each member's writeEntry call fires markSummaryDirtyInTx on the just-
87
- * created parent (E2 hook at store.ts:1214), and the same sleep cycle's
88
- * E3 rebuild phase would re-rebuild every new summary (2x LLM cost).
83
+ * Clear summary_dirty on a freshly-built summary. Called by buildDag right after the child-link loop:
84
+ * each member's writeEntry marks the new parent dirty, so the same cycle's rebuild would redo every new summary.
89
85
  *
90
86
  * Idempotent: no-op + no audit if summary isn't dirty. Audit
91
- * source='buildDag-clean' distinguishes from E3-rebuild source.
87
+ * source='buildDag-clean' distinguishes it from the rebuild's source.
92
88
  */
93
89
  export declare function clearSummaryDirtyAfterBuild(hippoRoot: string, summaryId: string, tenantId: string, actor?: string, source?: string): void;
94
90
  //# sourceMappingURL=summaries.d.ts.map
@@ -7,17 +7,12 @@ import { audit } from './audit-event.js';
7
7
  import { syncFtsRow } from './entry-row.js';
8
8
  import { openStore } from './open.js';
9
9
  // ---------------------------------------------------------------------------
10
- // v0.30 / E1 of DAG live-coupling — dirty-flag helpers for the existing
11
- // DAG layer's level-2 summaries.
12
- //
13
- // Used by E2 (child-write propagation in invalidation.ts / writeEntry /
14
- // forgetMemory / archiveRawMemory) to mark a summary dirty when one of its
15
- // children changes, and by E3's sleep-cycle rebuildDirtySummaries phase to
16
- // enumerate candidates without scanning every memory row.
10
+ // Dirty-flag helpers for DAG summaries: child writes mark a summary dirty, and the
11
+ // sleep-cycle rebuild enumerates candidates without scanning every memory row.
17
12
  // ---------------------------------------------------------------------------
18
13
  /**
19
14
  * Load summaries flagged dirty for the given tenant. Sorted by latest_at
20
- * DESC (NULLS LAST) so E3's rebuild cap (HIPPO_DAG_REBUILD_CAP, default 20)
15
+ * DESC (NULLS LAST) so the rebuild cap (HIPPO_DAG_REBUILD_CAP, default 20)
21
16
  * takes the most-recently-changed summaries first.
22
17
  *
23
18
  * Returns full MemoryEntry shape via MEMORY_SELECT_COLUMNS + rowToEntry
@@ -46,13 +41,12 @@ export function loadDirtySummaries(hippoRoot, tenantId) {
46
41
  /**
47
42
  * Mark a summary as dirty. Idempotent (re-marking dirty is a no-op + no
48
43
  * second audit row). Tenant-scoped to prevent cross-tenant writes via
49
- * parent-lookup. Called by E2 from invalidation.ts / writeEntry /
44
+ * parent-lookup. Called from invalidation.ts / writeEntry /
50
45
  * forgetMemory / archiveRawMemory whenever a child is invalidated,
51
46
  * superseded, forgotten, or archived.
52
47
  *
53
- * Quietly no-ops if the target row doesn't exist or isn't a level-2
54
- * summary (E5 will widen the dag_level guard to IN (2, 3) when level-3
55
- * build path lands). Emits a 'summary_marked_dirty' audit row on actual
48
+ * Quietly no-ops if the target row doesn't exist or isn't a level-2/3
49
+ * summary. Emits a 'summary_marked_dirty' audit row on actual
56
50
  * state transitions (0 -> 1) via the audit() helper, which try/catches
57
51
  * for missing audit_log (the v27 self-heal scenario).
58
52
  */
@@ -60,8 +54,7 @@ export function markSummaryDirty(hippoRoot, summaryId, tenantId, actor = 'cli')
60
54
  assertTenantId('markSummaryDirty', tenantId);
61
55
  const db = openStore(hippoRoot);
62
56
  try {
63
- // v0.30 / E5: widened dag_level=2 -> IN (2, 3). RETURNING dag_level reads
64
- // actual level in same round trip.
57
+ // RETURNING dag_level reads the actual level in the same round trip.
65
58
  // SAFETY: result's shape matches the single `dag_level` column returned
66
59
  // above.
67
60
  const result = db.prepare(`
@@ -76,8 +69,7 @@ export function markSummaryDirty(hippoRoot, summaryId, tenantId, actor = 'cli')
76
69
  `).get(summaryId, tenantId);
77
70
  if (result) {
78
71
  // audit() wraps appendAuditEvent in try/catch (v27 heal scenario).
79
- // metadata.source=E1 leaves a breadcrumb so E2-E5 debugging can
80
- // distinguish dirty-marks across the arc's wiring layers.
72
+ // metadata.source tells this dirty-mark apart from the other wiring layers' marks.
81
73
  audit(db, 'summary_marked_dirty', summaryId, { dag_level: result.dag_level, source: 'E1' }, actor, tenantId);
82
74
  }
83
75
  }
@@ -86,7 +78,7 @@ export function markSummaryDirty(hippoRoot, summaryId, tenantId, actor = 'cli')
86
78
  }
87
79
  }
88
80
  // ---------------------------------------------------------------------------
89
- // v0.30 / E3 of DAG live-coupling — sleep-cycle rebuild surface.
81
+ // Sleep-cycle rebuild surface.
90
82
  //
91
83
  // loadAllDirtySummaries / loadChildrenOfSummary / applyRebuildResult /
92
84
  // clearSummaryDirtyAfterBuild live HERE (not in dag.ts) because they need
@@ -95,9 +87,9 @@ export function markSummaryDirty(hippoRoot, summaryId, tenantId, actor = 'cli')
95
87
  // rebuildDirtySummaries() that calls into these.
96
88
  // ---------------------------------------------------------------------------
97
89
  /**
98
- * v0.30 / E5 — host-wide loader for L2 topic summaries without an L3 parent.
90
+ * Host-wide loader for L2 topic summaries without an L3 parent.
99
91
  * Used by consolidate phase 1.9 (buildEntityProfiles) to cluster L2s into
100
- * L3 entity profiles. Mirrors loadAllDirtySummaries pattern (E3): SQL-level
92
+ * L3 entity profiles. Mirrors loadAllDirtySummaries: SQL-level
101
93
  * filter is cheaper than reusing in-memory `survivors` (which doesn't
102
94
  * contain L2s freshly created by phase 1.7 buildDag).
103
95
  *
@@ -125,7 +117,7 @@ export function loadAllL2Summaries(hippoRoot) {
125
117
  }
126
118
  }
127
119
  /**
128
- * v0.30 / E3 — host-wide variant of loadDirtySummaries. Iterates all tenants
120
+ * Host-wide variant of loadDirtySummaries. Iterates all tenants
129
121
  * in one query so consolidate.ts (host-wide per L106-109) does not need a
130
122
  * per-tenant loop. Each returned MemoryEntry carries its own tenantId (via
131
123
  * rowToEntry), so per-summary children + rebuild UPDATE stay tenant-scoped.
@@ -152,7 +144,7 @@ export function loadAllDirtySummaries(hippoRoot) {
152
144
  }
153
145
  }
154
146
  /**
155
- * v0.30 / E3 — load live children of a DAG summary. Used by
147
+ * Load live children of a DAG summary. Used by
156
148
  * rebuildDirtySummaries to regenerate content from the CURRENT child set
157
149
  * (not the children at create-time). Skips archived. Tenant-scoped
158
150
  * (defence in depth — dag_parent_id is unique-ish but tenant guard is
@@ -180,7 +172,7 @@ export function loadChildrenOfSummary(hippoRoot, summaryId, tenantId) {
180
172
  }
181
173
  }
182
174
  /**
183
- * v0.30 / E3 — apply a rebuild result to a dirty summary. Atomic: one
175
+ * Apply a rebuild result to a dirty summary. Atomic: one
184
176
  * prepared UPDATE statement plus syncFtsRow inside one SAVEPOINT.
185
177
  * WHERE includes `AND summary_dirty = 1` so concurrent sleep's race-loser
186
178
  * becomes a no-op (no rebuild_count bump, no audit row).
@@ -217,7 +209,6 @@ export function applyRebuildResult(hippoRoot, summary, patch) {
217
209
  }
218
210
  }
219
211
  // ONE prepared UPDATE per branch. Test #8 inspects the SQL string.
220
- // v0.30 / E5: widened dag_level=2 -> IN (2, 3) on both branches.
221
212
  const REBUILD_CONTENT_SQL = `UPDATE memories
222
213
  SET content = ?,
223
214
  descendant_count = ?,
@@ -243,17 +234,8 @@ const REBUILD_METADATA_SQL = `UPDATE memories
243
234
  AND kind != 'archived'`;
244
235
  function applyRebuildInSavepoint(db, summary, patch) {
245
236
  const nowIso = new Date().toISOString();
246
- // AT1 P1a fix (docs/plans/2026-08-15-at1-rejected-value-tombstone.md):
247
- // applyRebuildResult's bumpRebuildCount branch wrote patch.content via
248
- // a direct UPDATE, bypassing the rejection guard entirely (the guard
249
- // lives in upsertEntryRow's INSERT path, which this function never
250
- // calls). A rebuild that regenerates byte-identical content to an
251
- // already-rejected value (e.g. deterministic summarization of an
252
- // unchanged child set) would silently re-assert it every sleep cycle.
253
- // Check BEFORE choosing which UPDATE to run — only the
254
- // bumpRebuildCount branch ever writes content, so a miss or a
255
- // zero-child call is a no-op here (one indexed point query, guarded
256
- // path only).
237
+ // Check tombstones before choosing the UPDATE: this direct UPDATE bypasses upsertEntryRow's guard, and
238
+ // a deterministic rebuild would otherwise re-assert a rejected value every sleep cycle.
257
239
  const tombstone = patch.bumpRebuildCount
258
240
  ? findRejectedValue(db, summary.tenantId, rejectionDigest(patch.content))
259
241
  : null;
@@ -268,23 +250,8 @@ function applyRebuildInSavepoint(db, summary, patch) {
268
250
  const result = applyContentWrite
269
251
  ? db.prepare(REBUILD_CONTENT_SQL).run(patch.content, patch.descendant_count, patch.earliest_at, patch.latest_at, nowIso, summary.id, summary.tenantId)
270
252
  : db.prepare(REBUILD_METADATA_SQL).run(patch.descendant_count, patch.earliest_at, patch.latest_at, summary.id, summary.tenantId);
271
- // Return-value semantics (v0.30/T4 split): `changed` reflects whether
272
- // THIS call's UPDATE (content or metadata-only) affected a row — NOT
273
- // whether patch.content specifically landed. On a refusal, metadata
274
- // still applies, so changed=true even though content did not change.
275
- // This preserves the pre-T4 no-infinite-retry choice: the caller
276
- // (dag.ts rebuildDirtySummaries) treats changed=false as "race lost,
277
- // silently retry next cycle" — returning false on a refusal would
278
- // retry the same doomed LLM rebuild forever, so changed=true settles
279
- // this cycle (dirty cleared) regardless of refusal.
280
- // `refused` is the T4 addition: true only when a tombstone hit AND
281
- // the metadata UPDATE landed (changed=true) — a refusal that loses
282
- // the race to a concurrent writer reports refused=false too, since
283
- // nothing from this call took effect. Before T4, a refusal also
284
- // counted toward the caller's `rebuilt` stat because `changed` alone
285
- // could not distinguish it; the caller now increments `refused`
286
- // instead of `rebuilt` when this is true, so the stat reflects what
287
- // happened without changing dirty-clearing or retry behavior.
253
+ // `changed` means THIS call's UPDATE hit a row, so a refusal still settles the cycle (false would retry
254
+ // the doomed rebuild forever); `refused` lets the caller count it as refused rather than rebuilt.
288
255
  const changed = (result.changes ?? 0) > 0;
289
256
  const refused = Boolean(tombstone) && changed;
290
257
  if (tombstone && changed)
@@ -307,13 +274,8 @@ function auditRefusedRebuild(db, summary, patch, tombstone) {
307
274
  `(digest ${tombstone.digest.slice(0, 12)}...); metadata updated, content unchanged`);
308
275
  }
309
276
  function syncRebuiltSummary(db, summary, patch, applyContentWrite, nowIso) {
310
- // FTS sync — bare UPDATE on memories does NOT update memories_fts.
311
- // R1 HIGH must-fix from plan-eng-r1. Construct the patched entry in
312
- // memory and reuse the existing syncFtsRow helper (delete-then-insert).
313
- // earliest_at/latest_at preserve null semantics (R2 must-fix).
314
- // AT1: content stays summary.content (unchanged) when the write was
315
- // refused — applyContentWrite is false, so patch.content was never
316
- // written to the row FTS must mirror.
277
+ // A bare UPDATE on memories does NOT update memories_fts, so resync from the patched entry;
278
+ // content stays summary.content when the write was refused, since FTS must mirror the row.
317
279
  const patchedEntry = {
318
280
  ...summary,
319
281
  content: applyContentWrite ? patch.content : summary.content,
@@ -328,8 +290,7 @@ function syncRebuiltSummary(db, summary, patch, applyContentWrite, nowIso) {
328
290
  };
329
291
  syncFtsRow(db, patchedEntry);
330
292
  audit(db, 'summary_rebuilt', summary.id, {
331
- // v0.30 / E5: read actual level from the summary in scope
332
- // (NOT hardcoded 2). L2 -> 2, L3 -> 3.
293
+ // Actual level from the summary in scope, never hardcoded: L2 -> 2, L3 -> 3.
333
294
  dag_level: summary.dag_level,
334
295
  source: 'E3-rebuild',
335
296
  zero_children: patch.zeroChildren,
@@ -337,21 +298,17 @@ function syncRebuiltSummary(db, summary, patch, applyContentWrite, nowIso) {
337
298
  }, patch.actor, summary.tenantId);
338
299
  }
339
300
  /**
340
- * v0.30 / E3 — clear summary_dirty on a freshly-built summary. Called by
341
- * buildDag immediately after the child-link loop finishes. Without this,
342
- * each member's writeEntry call fires markSummaryDirtyInTx on the just-
343
- * created parent (E2 hook at store.ts:1214), and the same sleep cycle's
344
- * E3 rebuild phase would re-rebuild every new summary (2x LLM cost).
301
+ * Clear summary_dirty on a freshly-built summary. Called by buildDag right after the child-link loop:
302
+ * each member's writeEntry marks the new parent dirty, so the same cycle's rebuild would redo every new summary.
345
303
  *
346
304
  * Idempotent: no-op + no audit if summary isn't dirty. Audit
347
- * source='buildDag-clean' distinguishes from E3-rebuild source.
305
+ * source='buildDag-clean' distinguishes it from the rebuild's source.
348
306
  */
349
307
  export function clearSummaryDirtyAfterBuild(hippoRoot, summaryId, tenantId, actor = 'cli', source = 'buildDag-clean') {
350
308
  assertTenantId('clearSummaryDirtyAfterBuild', tenantId);
351
309
  const db = openStore(hippoRoot);
352
310
  try {
353
- // v0.30 / E5: widened dag_level=2 -> IN (2, 3). RETURNING dag_level reads
354
- // actual level so audit metadata stays accurate without an extra SELECT.
311
+ // RETURNING dag_level reads the actual level so audit metadata stays accurate without an extra SELECT.
355
312
  // SAFETY: result's shape matches the single `dag_level` column returned
356
313
  // below.
357
314
  const result = db.prepare(`
@@ -365,8 +322,7 @@ export function clearSummaryDirtyAfterBuild(hippoRoot, summaryId, tenantId, acto
365
322
  RETURNING dag_level
366
323
  `).get(summaryId, tenantId);
367
324
  if (result) {
368
- // v0.30 / E5: source param distinguishes buildDag-clean (L2) from
369
- // buildEntityProfiles-clean (L3) and any future build path.
325
+ // source tells buildDag-clean (L2) from buildEntityProfiles-clean (L3) and any future build path.
370
326
  audit(db, 'summary_marked_clean', summaryId, { dag_level: result.dag_level, source }, actor, tenantId);
371
327
  }
372
328
  }
@@ -11,6 +11,8 @@ import { openHippoDbReadOnly, closeHippoDb, getSchemaVersion, getMeta, countTabl
11
11
  import { runDoctor } from './doctor.js';
12
12
  import { loadConfig } from './config.js';
13
13
  import { redactSecretsStrict } from './secret-detect.js';
14
+ import { isJsonString } from './json.js';
15
+ import { escapeRegex } from './escape.js';
14
16
  const TAIL_MAX_BYTES = 256 * 1024;
15
17
  export const TAIL_MAX_LINES = 200;
16
18
  const TAIL_MAX_LINE_CHARS = 2000;
@@ -28,9 +30,6 @@ const CONFIG_SECRET_KEY_RE = /key|token|secret|passw|credential|auth|cookie|bear
28
30
  function isJsonObject(v) {
29
31
  return typeof v === 'object' && v !== null && !Array.isArray(v);
30
32
  }
31
- function isJsonString(v) {
32
- return typeof v === 'string';
33
- }
34
33
  function buildRuntime() {
35
34
  return {
36
35
  node: process.versions.node,
@@ -177,9 +176,6 @@ function buildLogsSection(opts) {
177
176
  }
178
177
  return logs;
179
178
  }
180
- function escapeRegExp(s) {
181
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
182
- }
183
179
  // The native realpath is the only form that exposes a short-name (8.3) or symlinked alias for what it is.
184
180
  function nativeRealPathKey(p) {
185
181
  try {
@@ -232,12 +228,12 @@ function mountedDrive(letter) {
232
228
  /** Regex source for one spelling of the home, or null when too little of it is left to swap safely. */
233
229
  function spellingPattern(spelling, deep) {
234
230
  if (process.platform !== 'win32')
235
- return spelling.length >= 3 ? escapeRegExp(spelling) : null;
231
+ return spelling.length >= 3 ? escapeRegex(spelling) : null;
236
232
  // Each tool that mounts a drive writes it its own way, so a home two or more folders below its drive (\Users\<name>)
237
233
  // matches after any prefix, with \, / or JSON's \\ between folders. A drive written a known way goes into the swap with it.
238
234
  const drive = /^([A-Za-z])[:-]/.exec(spelling);
239
235
  const below = drive === null ? spelling : spelling.slice(2);
240
- const body = below.split(/[\\/]+/).map(escapeRegExp).join('[\\\\/]+');
236
+ const body = below.split(/[\\/]+/).map(escapeRegex).join('[\\\\/]+');
241
237
  if (drive === null) {
242
238
  if (spelling.length < 3)
243
239
  return null;
package/dist/tenant.d.ts CHANGED
@@ -1,13 +1,10 @@
1
1
  import type { DatabaseSyncLike } from './db.js';
2
+ import type { JsonValue } from './json.js';
2
3
  export interface ResolveOpts {
3
4
  db?: DatabaseSyncLike;
4
5
  apiKey?: string;
5
6
  }
6
7
  export declare function resolveTenantId(opts: ResolveOpts): string;
7
- /** A value that round-trips through JSON.stringify/JSON.parse unchanged. */
8
- type JsonValue = string | number | boolean | null | JsonValue[] | {
9
- [key: string]: JsonValue;
10
- };
11
8
  /**
12
9
  * Defensive runtime guard for tenant id arguments.
13
10
  *
@@ -25,5 +22,4 @@ type JsonValue = string | number | boolean | null | JsonValue[] | {
25
22
  * Acceptable tradeoff for catching the silent-leak class.
26
23
  */
27
24
  export declare function assertTenantId(fnName: string, value: JsonValue): asserts value is string;
28
- export {};
29
25
  //# sourceMappingURL=tenant.d.ts.map
@@ -2,7 +2,7 @@ import type { DatabaseSyncLike } from './db.js';
2
2
  /**
3
3
  * Where a block of memory text was sent.
4
4
  * - `hook`: the per-prompt `UserPromptSubmit` hook (`hippo context --pinned-only`).
5
- * - `hook_recall`: the same hook's Z1 prompt-recall section (docs/plans/2026-09-26-z1-prompt-recall.md).
5
+ * - `hook_recall`: the same hook's prompt-recall section.
6
6
  * - `compact_resume`: the snapshot the SessionStart(compact) hook prints (`hippo compact-resume`).
7
7
  * - `context`, `recall`: the CLI commands.
8
8
  * - `mcp_recall`, `mcp_context`: the MCP tools.