hippo-memory 1.52.0 → 1.52.2

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.
package/dist/store.js CHANGED
@@ -13,6 +13,7 @@ import { rowToSessionHandoff, isHandoffOutcome } from './handoff.js';
13
13
  import { CARD_TRANSITIONS, CARD_LEASE_MS } from './card.js';
14
14
  import { tokenize, markRetrieved } from './search.js';
15
15
  import { isRecallBoostAblated } from './ablation.js';
16
+ import { rarestPromptTerms, RAREST_TERM_COUNT } from './prompt-recall.js';
16
17
  import { appendAuditEvent } from './audit.js';
17
18
  import { resolveTenantId } from './tenant.js';
18
19
  import { deriveOriginProject, originFromSource, findHippoStoreDir, realpathOrResolve } from './project-identity.js';
@@ -113,6 +114,10 @@ export function isInitialized(hippoRoot) {
113
114
  return fs.existsSync(path.join(hippoRoot, 'hippo.db'));
114
115
  }
115
116
  export function initStore(hippoRoot) {
117
+ closeHippoDb(openStore(hippoRoot));
118
+ }
119
+ /** One open connection with init done on it, for callers who used to pay for `initStore` + a second `openHippoDb`. */
120
+ export function openStore(hippoRoot) {
116
121
  ensureMirrorDirectories(hippoRoot);
117
122
  const db = openHippoDb(hippoRoot);
118
123
  try {
@@ -121,9 +126,16 @@ export function initStore(hippoRoot) {
121
126
  syncMirrorFiles(hippoRoot, db);
122
127
  }
123
128
  recordHalfLifeBaseForNewStore(db);
129
+ return db;
124
130
  }
125
- finally {
126
- closeHippoDb(db);
131
+ catch (error) {
132
+ try {
133
+ closeHippoDb(db);
134
+ }
135
+ catch {
136
+ // Best effort only; surface the original init error.
137
+ }
138
+ throw error;
127
139
  }
128
140
  }
129
141
  /** `meta` key holding the default half-life base a store's memories are on (src/half-life-migration.ts). */
@@ -1111,8 +1123,7 @@ function syncMirrorFiles(hippoRoot, db) {
1111
1123
  }
1112
1124
  /** Load the derived index from SQLite. Read-only: index.json is only ever written by `rebuildIndex`. */
1113
1125
  export function loadIndex(hippoRoot) {
1114
- initStore(hippoRoot);
1115
- const db = openHippoDb(hippoRoot);
1126
+ const db = openStore(hippoRoot);
1116
1127
  try {
1117
1128
  return buildIndexFromDb(db);
1118
1129
  }
@@ -1131,8 +1142,7 @@ export function loadIndex(hippoRoot) {
1131
1142
  * is left untouched; only `rebuildIndex` writes it.
1132
1143
  */
1133
1144
  export function saveIndex(hippoRoot, index) {
1134
- initStore(hippoRoot);
1135
- const db = openHippoDb(hippoRoot);
1145
+ const db = openStore(hippoRoot);
1136
1146
  try {
1137
1147
  db.exec('BEGIN');
1138
1148
  try {
@@ -1204,10 +1214,9 @@ function stampOriginProjectForImport(hippoRoot, entry) {
1204
1214
  };
1205
1215
  }
1206
1216
  export function writeEntry(hippoRoot, entry, opts) {
1207
- initStore(hippoRoot);
1208
- const stamped = stampOriginProject(hippoRoot, entry);
1209
- const db = openHippoDb(hippoRoot);
1217
+ const db = openStore(hippoRoot);
1210
1218
  try {
1219
+ const stamped = stampOriginProject(hippoRoot, entry);
1211
1220
  writeEntryDbOnly(db, stamped, opts);
1212
1221
  opts?.afterCommit?.();
1213
1222
  writeEntryMirrors(hippoRoot, stamped);
@@ -1286,8 +1295,7 @@ export function writeEntryMirrors(hippoRoot, entry) {
1286
1295
  * legacy single-tenant callers and the writeEntry/readEntry round-trip.
1287
1296
  */
1288
1297
  export function readEntry(hippoRoot, id, tenantId) {
1289
- initStore(hippoRoot);
1290
- const db = openHippoDb(hippoRoot);
1298
+ const db = openStore(hippoRoot);
1291
1299
  try {
1292
1300
  // SAFETY: both branches select exactly MEMORY_SELECT_COLUMNS, matching
1293
1301
  // MemoryRow's field set.
@@ -1310,8 +1318,7 @@ export function loadEntriesByIds(hippoRoot, ids, tenantId) {
1310
1318
  if (ids.length === 0)
1311
1319
  return [];
1312
1320
  const capped = ids.slice(0, 500);
1313
- initStore(hippoRoot);
1314
- const db = openHippoDb(hippoRoot);
1321
+ const db = openStore(hippoRoot);
1315
1322
  try {
1316
1323
  const placeholders = capped.map(() => '?').join(',');
1317
1324
  // T2: no ORDER BY meant row order followed SQLite's IN(...) scan order
@@ -1383,8 +1390,7 @@ export function strengthenRetrieved(hippoRoot, ids, tenantId) {
1383
1390
  export function loadSessionRawMemories(hippoRoot, sessionId, tenantId, cap) {
1384
1391
  if (!sessionId)
1385
1392
  return [];
1386
- initStore(hippoRoot);
1387
- const db = openHippoDb(hippoRoot);
1393
+ const db = openStore(hippoRoot);
1388
1394
  try {
1389
1395
  const params = [];
1390
1396
  let sql = `SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE kind = 'raw' AND source_session_id = ? AND superseded_by IS NULL`;
@@ -1429,8 +1435,7 @@ export function loadSessionRawMemories(hippoRoot, sessionId, tenantId, cap) {
1429
1435
  export function countSessionRawMemories(hippoRoot, sessionId, tenantId, scope) {
1430
1436
  if (!sessionId)
1431
1437
  return 0;
1432
- initStore(hippoRoot);
1433
- const db = openHippoDb(hippoRoot);
1438
+ const db = openStore(hippoRoot);
1434
1439
  try {
1435
1440
  const params = [];
1436
1441
  let sql = `SELECT COUNT(*) AS c FROM memories WHERE kind = 'raw' AND source_session_id = ? AND superseded_by IS NULL`;
@@ -1483,8 +1488,7 @@ export function loadFreshRawMemories(hippoRoot, count, tenantId, sessionId) {
1483
1488
  if (count <= 0)
1484
1489
  return [];
1485
1490
  const capped = Math.min(count, 200);
1486
- initStore(hippoRoot);
1487
- const db = openHippoDb(hippoRoot);
1491
+ const db = openStore(hippoRoot);
1488
1492
  try {
1489
1493
  const params = [];
1490
1494
  let sql = `SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE kind = 'raw' AND superseded_by IS NULL`;
@@ -1517,8 +1521,7 @@ export function loadFreshRawMemories(hippoRoot, count, tenantId, sessionId) {
1517
1521
  * Used by `drillDown` (Task 3).
1518
1522
  */
1519
1523
  export function loadChildrenOf(hippoRoot, parentId, tenantId) {
1520
- initStore(hippoRoot);
1521
- const db = openHippoDb(hippoRoot);
1524
+ const db = openStore(hippoRoot);
1522
1525
  try {
1523
1526
  // SAFETY: both branches select exactly MEMORY_SELECT_COLUMNS, matching
1524
1527
  // MemoryRow's field set.
@@ -1586,8 +1589,7 @@ export function deleteEntryCore(db, id, opts) {
1586
1589
  * existing caller.
1587
1590
  */
1588
1591
  export function deleteEntry(hippoRoot, id, opts) {
1589
- initStore(hippoRoot);
1590
- const db = openHippoDb(hippoRoot);
1592
+ const db = openStore(hippoRoot);
1591
1593
  try {
1592
1594
  const result = deleteEntryCore(db, id, opts);
1593
1595
  if (!result)
@@ -1621,8 +1623,7 @@ export function batchWriteAndDelete(hippoRoot, toWrite, toDeleteIds, opts) {
1621
1623
  const dormantMoves = opts?.dormant ?? [];
1622
1624
  if (toWrite.length === 0 && toDeleteIds.length === 0 && dormantMoves.length === 0)
1623
1625
  return [];
1624
- initStore(hippoRoot);
1625
- const db = openHippoDb(hippoRoot);
1626
+ const db = openStore(hippoRoot);
1626
1627
  try {
1627
1628
  // BEGIN IMMEDIATE (codex delta-review P2): the AT1 tombstone probes below
1628
1629
  // READ before the first write. Under a deferred BEGIN, that read pins a
@@ -1788,8 +1789,7 @@ export function batchWriteAndDelete(hippoRoot, toWrite, toDeleteIds, opts) {
1788
1789
  * paths that surface results to a user MUST pass a resolved tenant.
1789
1790
  */
1790
1791
  export function loadAllEntries(hippoRoot, tenantId) {
1791
- initStore(hippoRoot);
1792
- const db = openHippoDb(hippoRoot);
1792
+ const db = openStore(hippoRoot);
1793
1793
  try {
1794
1794
  return selectAllEntries(db, tenantId);
1795
1795
  }
@@ -1806,15 +1806,26 @@ export function selectAllEntries(db, tenantId) {
1806
1806
  : db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories ORDER BY created ASC, id ASC`).all();
1807
1807
  return rows.map(rowToEntry);
1808
1808
  }
1809
+ // Content of every tenant row tagged `tag`, without reading the rest of the store.
1810
+ // `instr` is a substring prefilter over the raw JSON; `includes` below re-checks exactly.
1811
+ export function loadContentsWithTag(hippoRoot, tenantId, tag) {
1812
+ const db = openStore(hippoRoot);
1813
+ try {
1814
+ /** SAFETY: rows' shape matches the two columns named in the SELECT below. */
1815
+ const rows = db.prepare(`SELECT content, tags_json FROM memories WHERE tenant_id = ? AND instr(tags_json, ?) > 0`).all(tenantId, JSON.stringify(tag));
1816
+ return rows.filter((r) => parseJsonArray(r.tags_json).includes(tag)).map((r) => r.content);
1817
+ }
1818
+ finally {
1819
+ closeHippoDb(db);
1820
+ }
1821
+ }
1809
1822
  // The pins plus the `recentNeeded` newest rows that pass `admit`, for ambient
1810
- // injection. One connection: opening one costs ~5.8ms on a warm 1896-row store,
1811
- // so a second handle loses more than the narrower scan saves.
1812
- export function loadAmbientCandidates(hippoRoot, tenantId, recentNeeded, admit) {
1813
- initStore(hippoRoot);
1823
+ // injection. One connection; `recall` piggybacks the Z1 FTS query on it too.
1824
+ export function loadAmbientCandidates(hippoRoot, tenantId, recentNeeded, admit, recall) {
1814
1825
  // A SQL LIMIT takes an integer; the Array.slice this replaced truncated one,
1815
1826
  // and include_recent is any non-negative finite number at the HTTP edge.
1816
1827
  const needed = Math.trunc(recentNeeded);
1817
- const db = openHippoDb(hippoRoot);
1828
+ const db = openStore(hippoRoot);
1818
1829
  try {
1819
1830
  // SAFETY: every `where` below starts from MEMORY_SELECT_COLUMNS' table.
1820
1831
  const run = (where, params) => db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE ${where}`).all(...params).map(rowToEntry);
@@ -1842,10 +1853,17 @@ export function loadAmbientCandidates(hippoRoot, tenantId, recentNeeded, admit)
1842
1853
  }
1843
1854
  // loadAllEntries' order: rankedPinned's comparator can tie and Array.sort
1844
1855
  // is stable, so input order is load-bearing downstream.
1845
- return [...byId.values()].sort((a, b) => {
1856
+ const entries = [...byId.values()].sort((a, b) => {
1846
1857
  const byCreated = a.created.localeCompare(b.created);
1847
1858
  return byCreated !== 0 ? byCreated : a.id.localeCompare(b.id);
1848
1859
  });
1860
+ if (!recall)
1861
+ return { entries };
1862
+ const ftsQuery = pickRarestFtsQuery(db, recall.terms);
1863
+ const recallEntries = ftsQuery
1864
+ ? loadRecallSearchEntriesFromDb(db, ftsQuery, recall.limit, tenantId, undefined, 'exact', false)
1865
+ : [];
1866
+ return { entries, recall: recallEntries };
1849
1867
  }
1850
1868
  finally {
1851
1869
  closeHippoDb(db);
@@ -1859,8 +1877,7 @@ export function loadAmbientCandidates(hippoRoot, tenantId, recentNeeded, admit)
1859
1877
  * by tenant_id. Cross-tenant memories never surface. Omitted = no filter.
1860
1878
  */
1861
1879
  export function loadSearchEntries(hippoRoot, query, limit = DEFAULT_SEARCH_CANDIDATE_LIMIT, tenantId) {
1862
- initStore(hippoRoot);
1863
- const db = openHippoDb(hippoRoot);
1880
+ const db = openStore(hippoRoot);
1864
1881
  try {
1865
1882
  return loadSearchRows(db, query, limit, tenantId).map(rowToEntry);
1866
1883
  }
@@ -1893,30 +1910,53 @@ export function loadSearchEntries(hippoRoot, query, limit = DEFAULT_SEARCH_CANDI
1893
1910
  * undefined as "no tenant filter" for legacy callers.
1894
1911
  */
1895
1912
  export function loadRecallSearchEntries(hippoRoot, query, limit = DEFAULT_SEARCH_CANDIDATE_LIMIT, tenantId, requestedScope, explicitScopeMode = 'exact', includeSuperseded = true) {
1896
- initStore(hippoRoot);
1897
- const db = openHippoDb(hippoRoot);
1913
+ const db = openStore(hippoRoot);
1898
1914
  try {
1899
- // explicitScopeMode only matters when requestedScope is set:
1900
- // 'exact' — api.recall semantics: narrow to m.scope = requested.
1901
- // 'additive' — CLI --scope semantics (v1.25.0): default-admitted set
1902
- // PLUS the requested scope; see RecallScopeFilter docs.
1903
- const scopeFilter = requestedScope && requestedScope !== ''
1904
- ? explicitScopeMode === 'additive'
1905
- ? { mode: 'default-deny-or-exact', value: requestedScope }
1906
- : { mode: 'exact', value: requestedScope }
1907
- : { mode: 'default-deny' };
1908
- return loadSearchRows(db, query, limit, tenantId, scopeFilter, includeSuperseded).map(rowToEntry);
1915
+ return loadRecallSearchEntriesFromDb(db, query, limit, tenantId, requestedScope, explicitScopeMode, includeSuperseded);
1909
1916
  }
1910
1917
  finally {
1911
1918
  closeHippoDb(db);
1912
1919
  }
1913
1920
  }
1921
+ // Split out so callers with an already-open db (Z1 prompt-recall path) skip
1922
+ // the initStore+open/close cycle per store per call.
1923
+ export function loadRecallSearchEntriesFromDb(db, query, limit = DEFAULT_SEARCH_CANDIDATE_LIMIT, tenantId, requestedScope, explicitScopeMode = 'exact', includeSuperseded = true) {
1924
+ // 'exact' narrows to requestedScope; 'additive' adds it to the default-admitted set.
1925
+ const scopeFilter = requestedScope && requestedScope !== ''
1926
+ ? explicitScopeMode === 'additive'
1927
+ ? { mode: 'default-deny-or-exact', value: requestedScope }
1928
+ : { mode: 'exact', value: requestedScope }
1929
+ : { mode: 'default-deny' };
1930
+ return loadSearchRows(db, query, limit, tenantId, scopeFilter, includeSuperseded).map(rowToEntry);
1931
+ }
1932
+ /** Rarest-K prompt terms for this connection's FTS index, as a space-joined query string.
1933
+ * Without FTS, returns the first 32 terms, as before rarest-term selection. */
1934
+ export function pickRarestFtsQuery(db, terms, maxTerms = RAREST_TERM_COUNT) {
1935
+ // The LIKE path has no bm25 ranking to bound, so it keeps the pre-rarest 32-term query.
1936
+ if (!isFtsAvailable(db))
1937
+ return terms.slice(0, 32).join(' ');
1938
+ db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS temp.z1_rarest_vocab USING fts5vocab(main, 'memories_fts', 'row')`);
1939
+ // unicode61 splits `journal_mode` into two vocab terms; a term's count is its rarest part's (an upper bound).
1940
+ const partsOf = (t) => t.split(/[^\p{L}\p{N}]+/u).filter(Boolean);
1941
+ const vocab = Array.from(new Set(terms.flatMap(partsOf)));
1942
+ if (vocab.length === 0)
1943
+ return '';
1944
+ // SAFETY: rows' shape matches the two columns named in the SELECT.
1945
+ const rows = db
1946
+ .prepare(`SELECT term, doc FROM temp.z1_rarest_vocab WHERE term IN (${vocab.map(() => '?').join(', ')})`)
1947
+ .all(...vocab);
1948
+ const counts = new Map(rows.map((r) => [r.term, r.doc]));
1949
+ const docCount = (t) => {
1950
+ const parts = partsOf(t);
1951
+ return parts.length === 0 ? 0 : Math.min(...parts.map((x) => counts.get(x) ?? 0));
1952
+ };
1953
+ return rarestPromptTerms(terms, docCount, maxTerms).join(' ');
1954
+ }
1914
1955
  /**
1915
1956
  * Rebuild mirrors from SQLite, importing any legacy markdown files not already present.
1916
1957
  */
1917
1958
  export function rebuildIndex(hippoRoot) {
1918
- initStore(hippoRoot);
1919
- const db = openHippoDb(hippoRoot);
1959
+ const db = openStore(hippoRoot);
1920
1960
  try {
1921
1961
  // SAFETY: rows' shape matches the single `id` column selected above.
1922
1962
  const existingIds = new Set(db.prepare(`SELECT id FROM memories`).all().map((row) => row.id));
@@ -1967,8 +2007,7 @@ export function rebuildIndex(hippoRoot) {
1967
2007
  }
1968
2008
  }
1969
2009
  export function updateStats(hippoRoot, delta) {
1970
- initStore(hippoRoot);
1971
- const db = openHippoDb(hippoRoot);
2010
+ const db = openStore(hippoRoot);
1972
2011
  try {
1973
2012
  // One atomic statement per counter, and only for counters the caller
1974
2013
  // named: the read-modify-write this replaces both lost increments to a
@@ -1995,8 +2034,7 @@ export function updateStats(hippoRoot, delta) {
1995
2034
  }
1996
2035
  }
1997
2036
  export function loadStats(hippoRoot) {
1998
- initStore(hippoRoot);
1999
- const db = openHippoDb(hippoRoot);
2037
+ const db = openStore(hippoRoot);
2000
2038
  try {
2001
2039
  return buildStatsFromDb(db);
2002
2040
  }
@@ -2005,8 +2043,7 @@ export function loadStats(hippoRoot) {
2005
2043
  }
2006
2044
  }
2007
2045
  export function appendConsolidationRun(hippoRoot, run) {
2008
- initStore(hippoRoot);
2009
- const db = openHippoDb(hippoRoot);
2046
+ const db = openStore(hippoRoot);
2010
2047
  try {
2011
2048
  db.prepare(`INSERT INTO consolidation_runs(timestamp, decayed, merged, removed) VALUES (?, ?, ?, ?)`).run(run.timestamp, run.decayed, run.merged, run.removed);
2012
2049
  pruneConsolidationRuns(db, 50);
@@ -2018,8 +2055,7 @@ export function appendConsolidationRun(hippoRoot, run) {
2018
2055
  }
2019
2056
  /** Rows a tenant created since the last sleep (runs are host-wide), looking back at most 24 hours. */
2020
2057
  export function countCreatedSinceLastSleep(hippoRoot, tenantId, now = new Date()) {
2021
- initStore(hippoRoot);
2022
- const db = openHippoDb(hippoRoot);
2058
+ const db = openStore(hippoRoot);
2023
2059
  try {
2024
2060
  const dayAgo = new Date(now.getTime() - 86_400_000).toISOString();
2025
2061
  const row = db.prepare(`SELECT COUNT(*) AS n FROM memories WHERE tenant_id = ?
@@ -2035,8 +2071,7 @@ export function countCreatedSinceLastSleep(hippoRoot, tenantId, now = new Date()
2035
2071
  * Uses consolidation_runs timestamps to compute session intervals.
2036
2072
  */
2037
2073
  export function loadSessionDecayContext(hippoRoot) {
2038
- initStore(hippoRoot);
2039
- const db = openHippoDb(hippoRoot);
2074
+ const db = openStore(hippoRoot);
2040
2075
  try {
2041
2076
  // Get recent consolidation timestamps (last 20)
2042
2077
  // SAFETY: rows' shape matches the single `timestamp` column above.
@@ -2063,8 +2098,7 @@ export function loadSessionDecayContext(hippoRoot) {
2063
2098
  * Increment the sleep counter. Called after each consolidation run.
2064
2099
  */
2065
2100
  export function incrementSleepCount(hippoRoot) {
2066
- initStore(hippoRoot);
2067
- const db = openHippoDb(hippoRoot);
2101
+ const db = openStore(hippoRoot);
2068
2102
  try {
2069
2103
  const current = Number(getMeta(db, 'sleep_count', '0')) || 0;
2070
2104
  setMeta(db, 'sleep_count', String(current + 1));
@@ -2101,8 +2135,7 @@ export function assertTenantId(fnName, value) {
2101
2135
  }
2102
2136
  export function saveActiveTaskSnapshot(hippoRoot, tenantId, snapshot) {
2103
2137
  assertTenantId('saveActiveTaskSnapshot', tenantId);
2104
- initStore(hippoRoot);
2105
- const db = openHippoDb(hippoRoot);
2138
+ const db = openStore(hippoRoot);
2106
2139
  const now = new Date().toISOString();
2107
2140
  try {
2108
2141
  db.exec('BEGIN');
@@ -2141,8 +2174,7 @@ export function saveActiveTaskSnapshot(hippoRoot, tenantId, snapshot) {
2141
2174
  }
2142
2175
  export function loadActiveTaskSnapshot(hippoRoot, tenantId) {
2143
2176
  assertTenantId('loadActiveTaskSnapshot', tenantId);
2144
- initStore(hippoRoot);
2145
- const db = openHippoDb(hippoRoot);
2177
+ const db = openStore(hippoRoot);
2146
2178
  try {
2147
2179
  // SAFETY: row's shape matches the ten columns named in the SELECT above.
2148
2180
  const row = db.prepare(`
@@ -2217,8 +2249,7 @@ export function loadFreshActiveTaskSnapshot(hippoRoot, tenantId, opts = {}) {
2217
2249
  }
2218
2250
  export function clearActiveTaskSnapshot(hippoRoot, tenantId, clearedStatus = 'cleared') {
2219
2251
  assertTenantId('clearActiveTaskSnapshot', tenantId);
2220
- initStore(hippoRoot);
2221
- const db = openHippoDb(hippoRoot);
2252
+ const db = openStore(hippoRoot);
2222
2253
  const now = new Date().toISOString();
2223
2254
  try {
2224
2255
  // SAFETY: active's shape matches the single `id` column selected above.
@@ -2246,8 +2277,7 @@ export function clearActiveTaskSnapshot(hippoRoot, tenantId, clearedStatus = 'cl
2246
2277
  */
2247
2278
  export function closeTaskSnapshotsForSession(hippoRoot, tenantId, sessionId, status = 'session-ended') {
2248
2279
  assertTenantId('closeTaskSnapshotsForSession', tenantId);
2249
- initStore(hippoRoot);
2250
- const db = openHippoDb(hippoRoot);
2280
+ const db = openStore(hippoRoot);
2251
2281
  const now = new Date().toISOString();
2252
2282
  try {
2253
2283
  const result = db.prepare(`UPDATE task_snapshots SET status = ?, updated_at = ? WHERE status = 'active' AND tenant_id = ? AND session_id = ?`).run(status, now, tenantId, sessionId);
@@ -2259,8 +2289,7 @@ export function closeTaskSnapshotsForSession(hippoRoot, tenantId, sessionId, sta
2259
2289
  }
2260
2290
  export function appendSessionEvent(hippoRoot, tenantId, event) {
2261
2291
  assertTenantId('appendSessionEvent', tenantId);
2262
- initStore(hippoRoot);
2263
- const db = openHippoDb(hippoRoot);
2292
+ const db = openStore(hippoRoot);
2264
2293
  const now = new Date().toISOString();
2265
2294
  // v1.2: scope is wired through. Default-deny in api.recall + cmdRecall
2266
2295
  // continuity reads applies to slack:private:* and 'unknown:legacy' rows.
@@ -2300,8 +2329,7 @@ export function appendSessionEvent(hippoRoot, tenantId, event) {
2300
2329
  }
2301
2330
  export function listSessionEvents(hippoRoot, tenantId, options = {}) {
2302
2331
  assertTenantId('listSessionEvents', tenantId);
2303
- initStore(hippoRoot);
2304
- const db = openHippoDb(hippoRoot);
2332
+ const db = openStore(hippoRoot);
2305
2333
  try {
2306
2334
  const clauses = ['tenant_id = ?'];
2307
2335
  const params = [tenantId];
@@ -2337,8 +2365,7 @@ export function listSessionEvents(hippoRoot, tenantId, options = {}) {
2337
2365
  */
2338
2366
  export function findPromotableSessions(hippoRoot, tenantId, sinceMs) {
2339
2367
  assertTenantId('findPromotableSessions', tenantId);
2340
- initStore(hippoRoot);
2341
- const db = openHippoDb(hippoRoot);
2368
+ const db = openStore(hippoRoot);
2342
2369
  try {
2343
2370
  // SAFETY: rows' shape matches the single `session_id` column selected
2344
2371
  // above.
@@ -2358,8 +2385,7 @@ export function findPromotableSessions(hippoRoot, tenantId, sinceMs) {
2358
2385
  */
2359
2386
  export function traceExistsForSession(hippoRoot, tenantId, session_id) {
2360
2387
  assertTenantId('traceExistsForSession', tenantId);
2361
- initStore(hippoRoot);
2362
- const db = openHippoDb(hippoRoot);
2388
+ const db = openStore(hippoRoot);
2363
2389
  try {
2364
2390
  const row = db.prepare(`
2365
2391
  SELECT 1 FROM memories
@@ -2373,8 +2399,7 @@ export function traceExistsForSession(hippoRoot, tenantId, session_id) {
2373
2399
  }
2374
2400
  }
2375
2401
  export function listMemoryConflicts(hippoRoot, status = 'open', tenantId) {
2376
- initStore(hippoRoot);
2377
- const db = openHippoDb(hippoRoot);
2402
+ const db = openStore(hippoRoot);
2378
2403
  try {
2379
2404
  // v0.28 — '*' is a sentinel meaning "no status filter, return all rows".
2380
2405
  // Pre-v0.28 callers (cli/mcp/dashboard) always passed 'open' or default,
@@ -2432,8 +2457,7 @@ export function listMemoryConflicts(hippoRoot, status = 'open', tenantId) {
2432
2457
  }
2433
2458
  }
2434
2459
  export function replaceDetectedConflicts(hippoRoot, detected, detectedAt = new Date().toISOString()) {
2435
- initStore(hippoRoot);
2436
- const db = openHippoDb(hippoRoot);
2460
+ const db = openStore(hippoRoot);
2437
2461
  try {
2438
2462
  db.exec('BEGIN');
2439
2463
  // Tenant guard (E2): a conflict is meaningful only within one tenant.
@@ -2548,8 +2572,7 @@ export function replaceDetectedConflicts(hippoRoot, detected, detectedAt = new D
2548
2572
  * Returns the resolved conflict, or null if not found.
2549
2573
  */
2550
2574
  export function resolveConflict(hippoRoot, conflictId, keepId, forgetLoser = false, tenantId, opts) {
2551
- initStore(hippoRoot);
2552
- const db = openHippoDb(hippoRoot);
2575
+ const db = openStore(hippoRoot);
2553
2576
  // When tenantId is set, the conflict lookup requires BOTH members in-tenant
2554
2577
  // and every memories mutation carries AND tenant_id = ?. A cross-tenant probe
2555
2578
  // then returns null, indistinguishable from a bad id. Omitted tenantId =
@@ -2772,8 +2795,7 @@ const HANDOFF_COLUMNS = 'id, session_id, repo_root, task_id, summary, next_actio
2772
2795
  */
2773
2796
  export function saveSessionHandoff(hippoRoot, tenantId, handoff) {
2774
2797
  assertTenantId('saveSessionHandoff', tenantId);
2775
- initStore(hippoRoot);
2776
- const db = openHippoDb(hippoRoot);
2798
+ const db = openStore(hippoRoot);
2777
2799
  const now = new Date().toISOString();
2778
2800
  // v1.2: scope is wired through. Read-side default-deny in api.recall +
2779
2801
  // cmdRecall continuity excludes slack:private:* and 'unknown:legacy'.
@@ -2801,8 +2823,7 @@ export function saveSessionHandoff(hippoRoot, tenantId, handoff) {
2801
2823
  /** Load the most recent handoff, optionally filtered by session ID. */
2802
2824
  export function loadLatestHandoff(hippoRoot, tenantId, sessionId, opts = {}) {
2803
2825
  assertTenantId('loadLatestHandoff', tenantId);
2804
- initStore(hippoRoot);
2805
- const db = openHippoDb(hippoRoot);
2826
+ const db = openStore(hippoRoot);
2806
2827
  try {
2807
2828
  const conditions = ['tenant_id = ?'];
2808
2829
  const params = [tenantId];
@@ -2846,8 +2867,7 @@ export function loadLatestHandoff(hippoRoot, tenantId, sessionId, opts = {}) {
2846
2867
  */
2847
2868
  export function loadHandoffById(hippoRoot, tenantId, id) {
2848
2869
  assertTenantId('loadHandoffById', tenantId);
2849
- initStore(hippoRoot);
2850
- const db = openHippoDb(hippoRoot);
2870
+ const db = openStore(hippoRoot);
2851
2871
  try {
2852
2872
  // SAFETY: row's shape matches HANDOFF_COLUMNS.
2853
2873
  const row = db.prepare(`
@@ -2864,8 +2884,7 @@ export function loadHandoffById(hippoRoot, tenantId, id) {
2864
2884
  /** Stamp the outcome on a session's newest handoff, only if it has none yet. Returns rows changed. */
2865
2885
  export function stampHandoffOutcome(hippoRoot, tenantId, sessionId, outcome) {
2866
2886
  assertTenantId('stampHandoffOutcome', tenantId);
2867
- initStore(hippoRoot);
2868
- const db = openHippoDb(hippoRoot);
2887
+ const db = openStore(hippoRoot);
2869
2888
  try {
2870
2889
  const result = db.prepare(`
2871
2890
  UPDATE session_handoffs SET outcome = ?
@@ -3033,8 +3052,7 @@ export function createCard(hippoRoot, tenantId, input) {
3033
3052
  if (input.budget !== undefined && !(Number.isSafeInteger(input.budget) && input.budget > 0)) {
3034
3053
  throw new Error(`Invalid budget: ${input.budget} (expected a positive integer)`);
3035
3054
  }
3036
- initStore(hippoRoot);
3037
- const db = openHippoDb(hippoRoot);
3055
+ const db = openStore(hippoRoot);
3038
3056
  try {
3039
3057
  const dependsOn = [...new Set(input.dependsOn ?? [])];
3040
3058
  let id = '';
@@ -3086,8 +3104,7 @@ export function createCard(hippoRoot, tenantId, input) {
3086
3104
  /** Returns the card row for id, or null if it does not exist under this tenant. */
3087
3105
  export function loadCard(hippoRoot, tenantId, id) {
3088
3106
  assertTenantId('loadCard', tenantId);
3089
- initStore(hippoRoot);
3090
- const db = openHippoDb(hippoRoot);
3107
+ const db = openStore(hippoRoot);
3091
3108
  try {
3092
3109
  return loadCardRow(db, tenantId, id);
3093
3110
  }
@@ -3098,8 +3115,7 @@ export function loadCard(hippoRoot, tenantId, id) {
3098
3115
  /** Lists cards for this tenant, optionally filtered to one status, newest-updated first. */
3099
3116
  export function listCards(hippoRoot, tenantId, opts = {}) {
3100
3117
  assertTenantId('listCards', tenantId);
3101
- initStore(hippoRoot);
3102
- const db = openHippoDb(hippoRoot);
3118
+ const db = openStore(hippoRoot);
3103
3119
  try {
3104
3120
  const conditions = ['tenant_id = ?'];
3105
3121
  const params = [tenantId];
@@ -3120,8 +3136,7 @@ export function listCards(hippoRoot, tenantId, opts = {}) {
3120
3136
  /** Returns this card's parent and child ids from card_deps. */
3121
3137
  export function loadCardDeps(hippoRoot, tenantId, id) {
3122
3138
  assertTenantId('loadCardDeps', tenantId);
3123
- initStore(hippoRoot);
3124
- const db = openHippoDb(hippoRoot);
3139
+ const db = openStore(hippoRoot);
3125
3140
  try {
3126
3141
  // SAFETY: rows' shape matches the single `parent` column named in the SELECT below.
3127
3142
  const parents = db.prepare(`SELECT parent FROM card_deps WHERE tenant_id = ? AND child = ?`).all(tenantId, id).map((r) => r.parent);
@@ -3136,8 +3151,7 @@ export function loadCardDeps(hippoRoot, tenantId, id) {
3136
3151
  /** Returns this card's run history, most recent first. */
3137
3152
  export function loadCardRuns(hippoRoot, tenantId, id) {
3138
3153
  assertTenantId('loadCardRuns', tenantId);
3139
- initStore(hippoRoot);
3140
- const db = openHippoDb(hippoRoot);
3154
+ const db = openStore(hippoRoot);
3141
3155
  try {
3142
3156
  // SAFETY: rows' shape matches CardRunRow.
3143
3157
  const rows = db.prepare(`
@@ -3153,8 +3167,7 @@ export function loadCardRuns(hippoRoot, tenantId, id) {
3153
3167
  /** Returns this card's comments, most recent first. */
3154
3168
  export function loadCardComments(hippoRoot, tenantId, id) {
3155
3169
  assertTenantId('loadCardComments', tenantId);
3156
- initStore(hippoRoot);
3157
- const db = openHippoDb(hippoRoot);
3170
+ const db = openStore(hippoRoot);
3158
3171
  try {
3159
3172
  // SAFETY: rows' shape matches CardCommentRow.
3160
3173
  const rows = db.prepare(`
@@ -3170,8 +3183,7 @@ export function loadCardComments(hippoRoot, tenantId, id) {
3170
3183
  /** Read side of the card <-> handoff round trip: the newest handoff filed against this card. */
3171
3184
  export function loadLatestHandoffForCard(hippoRoot, tenantId, cardId) {
3172
3185
  assertTenantId('loadLatestHandoffForCard', tenantId);
3173
- initStore(hippoRoot);
3174
- const db = openHippoDb(hippoRoot);
3186
+ const db = openStore(hippoRoot);
3175
3187
  try {
3176
3188
  // SAFETY: row's shape matches HANDOFF_COLUMNS.
3177
3189
  const row = db.prepare(`
@@ -3190,8 +3202,7 @@ export function claimCard(hippoRoot, tenantId, id, runtime, sessionId) {
3190
3202
  if (runtime.trim() === '') {
3191
3203
  throw new Error('runtime must not be empty');
3192
3204
  }
3193
- initStore(hippoRoot);
3194
- const db = openHippoDb(hippoRoot);
3205
+ const db = openStore(hippoRoot);
3195
3206
  try {
3196
3207
  db.exec('BEGIN IMMEDIATE');
3197
3208
  let runId = 0;
@@ -3233,8 +3244,7 @@ export function claimCard(hippoRoot, tenantId, id, runtime, sessionId) {
3233
3244
  export function heartbeatCard(hippoRoot, tenantId, id, runId) {
3234
3245
  assertTenantId('heartbeatCard', tenantId);
3235
3246
  assertRunId(runId);
3236
- initStore(hippoRoot);
3237
- const db = openHippoDb(hippoRoot);
3247
+ const db = openStore(hippoRoot);
3238
3248
  try {
3239
3249
  db.exec('BEGIN IMMEDIATE');
3240
3250
  try {
@@ -3272,8 +3282,7 @@ export function blockCard(hippoRoot, tenantId, id, reason, runId) {
3272
3282
  }
3273
3283
  if (runId !== undefined)
3274
3284
  assertRunId(runId);
3275
- initStore(hippoRoot);
3276
- const db = openHippoDb(hippoRoot);
3285
+ const db = openStore(hippoRoot);
3277
3286
  try {
3278
3287
  db.exec('BEGIN IMMEDIATE');
3279
3288
  try {
@@ -3310,8 +3319,7 @@ export function reviewCard(hippoRoot, tenantId, id, runId) {
3310
3319
  assertTenantId('reviewCard', tenantId);
3311
3320
  if (runId !== undefined)
3312
3321
  assertRunId(runId);
3313
- initStore(hippoRoot);
3314
- const db = openHippoDb(hippoRoot);
3322
+ const db = openStore(hippoRoot);
3315
3323
  try {
3316
3324
  db.exec('BEGIN IMMEDIATE');
3317
3325
  try {
@@ -3347,8 +3355,7 @@ export function completeCard(hippoRoot, tenantId, id, outcome, runId) {
3347
3355
  }
3348
3356
  if (runId !== undefined)
3349
3357
  assertRunId(runId);
3350
- initStore(hippoRoot);
3351
- const db = openHippoDb(hippoRoot);
3358
+ const db = openStore(hippoRoot);
3352
3359
  try {
3353
3360
  db.exec('BEGIN IMMEDIATE');
3354
3361
  let promotedChildren = [];
@@ -3404,8 +3411,7 @@ export function completeCard(hippoRoot, tenantId, id, outcome, runId) {
3404
3411
  /** Returns to ready every running card of the tenant whose lease has expired or is missing: clears its assignee, closes its live run as 'reclaimed' and leaves its handoffs alone, all in one write transaction. Returns the reclaimed card ids in id order. */
3405
3412
  export function reclaimExpiredCards(hippoRoot, tenantId) {
3406
3413
  assertTenantId('reclaimExpiredCards', tenantId);
3407
- initStore(hippoRoot);
3408
- const db = openHippoDb(hippoRoot);
3414
+ const db = openStore(hippoRoot);
3409
3415
  try {
3410
3416
  db.exec('BEGIN IMMEDIATE');
3411
3417
  try {
@@ -3442,8 +3448,7 @@ export function addCardComment(hippoRoot, tenantId, cardId, author, body) {
3442
3448
  if (body.trim() === '') {
3443
3449
  throw new Error('body must not be empty');
3444
3450
  }
3445
- initStore(hippoRoot);
3446
- const db = openHippoDb(hippoRoot);
3451
+ const db = openStore(hippoRoot);
3447
3452
  try {
3448
3453
  return insertCardComment(db, tenantId, cardId, author, body);
3449
3454
  }
@@ -3470,8 +3475,7 @@ export function addCardComment(hippoRoot, tenantId, cardId, author, body) {
3470
3475
  */
3471
3476
  export function loadDirtySummaries(hippoRoot, tenantId) {
3472
3477
  assertTenantId('loadDirtySummaries', tenantId);
3473
- initStore(hippoRoot);
3474
- const db = openHippoDb(hippoRoot);
3478
+ const db = openStore(hippoRoot);
3475
3479
  try {
3476
3480
  // SAFETY: this query selects exactly MEMORY_SELECT_COLUMNS, matching
3477
3481
  // MemoryRow's field set.
@@ -3540,8 +3544,7 @@ export function markSummaryDirtyInTx(db, summaryId, tenantId, actor) {
3540
3544
  */
3541
3545
  export function markSummaryDirty(hippoRoot, summaryId, tenantId, actor = 'cli') {
3542
3546
  assertTenantId('markSummaryDirty', tenantId);
3543
- initStore(hippoRoot);
3544
- const db = openHippoDb(hippoRoot);
3547
+ const db = openStore(hippoRoot);
3545
3548
  try {
3546
3549
  // v0.30 / E5: widened dag_level=2 -> IN (2, 3). RETURNING dag_level reads
3547
3550
  // actual level in same round trip.
@@ -3588,8 +3591,7 @@ export function markSummaryDirty(hippoRoot, summaryId, tenantId, actor = 'cli')
3588
3591
  * tenant-scoped via summary.tenantId.
3589
3592
  */
3590
3593
  export function loadAllL2Summaries(hippoRoot) {
3591
- initStore(hippoRoot);
3592
- const db = openHippoDb(hippoRoot);
3594
+ const db = openStore(hippoRoot);
3593
3595
  try {
3594
3596
  // SAFETY: this query selects exactly MEMORY_SELECT_COLUMNS, matching
3595
3597
  // MemoryRow's field set.
@@ -3618,8 +3620,7 @@ export function loadAllL2Summaries(hippoRoot) {
3618
3620
  * HIPPO_DAG_REBUILD_CAP takes most-recently-changed summaries first.
3619
3621
  */
3620
3622
  export function loadAllDirtySummaries(hippoRoot) {
3621
- initStore(hippoRoot);
3622
- const db = openHippoDb(hippoRoot);
3623
+ const db = openStore(hippoRoot);
3623
3624
  try {
3624
3625
  // SAFETY: this query selects exactly MEMORY_SELECT_COLUMNS, matching
3625
3626
  // MemoryRow's field set.
@@ -3645,8 +3646,7 @@ export function loadAllDirtySummaries(hippoRoot) {
3645
3646
  */
3646
3647
  export function loadChildrenOfSummary(hippoRoot, summaryId, tenantId) {
3647
3648
  assertTenantId('loadChildrenOfSummary', tenantId);
3648
- initStore(hippoRoot);
3649
- const db = openHippoDb(hippoRoot);
3649
+ const db = openStore(hippoRoot);
3650
3650
  try {
3651
3651
  // SAFETY: this query selects exactly MEMORY_SELECT_COLUMNS, matching
3652
3652
  // MemoryRow's field set.
@@ -3679,8 +3679,7 @@ export function loadChildrenOfSummary(hippoRoot, summaryId, tenantId) {
3679
3679
  */
3680
3680
  export function applyRebuildResult(hippoRoot, summary, patch) {
3681
3681
  assertTenantId('applyRebuildResult', summary.tenantId);
3682
- initStore(hippoRoot);
3683
- const db = openHippoDb(hippoRoot);
3682
+ const db = openStore(hippoRoot);
3684
3683
  try {
3685
3684
  db.exec('SAVEPOINT rebuild_summary');
3686
3685
  try {
@@ -3827,8 +3826,7 @@ export function applyRebuildResult(hippoRoot, summary, patch) {
3827
3826
  */
3828
3827
  export function clearSummaryDirtyAfterBuild(hippoRoot, summaryId, tenantId, actor = 'cli', source = 'buildDag-clean') {
3829
3828
  assertTenantId('clearSummaryDirtyAfterBuild', tenantId);
3830
- initStore(hippoRoot);
3831
- const db = openHippoDb(hippoRoot);
3829
+ const db = openStore(hippoRoot);
3832
3830
  try {
3833
3831
  // v0.30 / E5: widened dag_level=2 -> IN (2, 3). RETURNING dag_level reads
3834
3832
  // actual level so audit metadata stays accurate without an extra SELECT.