hippo-memory 1.72.0 → 2.0.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 (259) hide show
  1. package/README.md +6 -4
  2. package/dist/agent-memories/apply.js +5 -5
  3. package/dist/api/assemble.js +2 -1
  4. package/dist/api/audit.d.ts +7 -7
  5. package/dist/api/audit.js +19 -17
  6. package/dist/api/auth.d.ts +5 -6
  7. package/dist/api/auth.js +28 -33
  8. package/dist/api/conflicts.js +1 -1
  9. package/dist/api/context-auto.js +4 -2
  10. package/dist/api/context-select.js +8 -20
  11. package/dist/api/context.js +12 -10
  12. package/dist/api/drill-down.js +2 -1
  13. package/dist/api/forget.d.ts +2 -2
  14. package/dist/api/forget.js +6 -7
  15. package/dist/api/graph.js +2 -1
  16. package/dist/api/learn.d.ts +1 -1
  17. package/dist/api/learn.js +45 -44
  18. package/dist/api/memories.d.ts +4 -2
  19. package/dist/api/memories.js +16 -5
  20. package/dist/api/objects.js +2 -1
  21. package/dist/api/outcome.d.ts +5 -5
  22. package/dist/api/outcome.js +16 -15
  23. package/dist/api/predictions.js +2 -1
  24. package/dist/api/promote.d.ts +3 -3
  25. package/dist/api/promote.js +25 -27
  26. package/dist/api/prompt-hook.d.ts +3 -1
  27. package/dist/api/prompt-hook.js +1 -0
  28. package/dist/api/quarantine.js +2 -1
  29. package/dist/api/recall-core.js +8 -3
  30. package/dist/api/recall-pipeline.js +17 -17
  31. package/dist/api/recall-types.d.ts +3 -1
  32. package/dist/api/recall.js +32 -29
  33. package/dist/api/remember-local.d.ts +5 -2
  34. package/dist/api/remember-local.js +14 -6
  35. package/dist/api/remember.d.ts +5 -2
  36. package/dist/api/remember.js +16 -14
  37. package/dist/api/session-end.d.ts +24 -0
  38. package/dist/api/session-end.js +31 -1
  39. package/dist/api/sleep-run.js +5 -4
  40. package/dist/api/sleep.js +4 -3
  41. package/dist/api/tokens.js +1 -1
  42. package/dist/api/types.d.ts +1 -7
  43. package/dist/capture/command.js +1 -1
  44. package/dist/capture/compaction-spool.d.ts +3 -5
  45. package/dist/capture/compaction-spool.js +41 -46
  46. package/dist/capture/session-end-caller.js +1 -1
  47. package/dist/capture/session-worker.js +19 -6
  48. package/dist/cli/audit.js +10 -10
  49. package/dist/cli/auth.js +4 -4
  50. package/dist/cli/client.d.ts +1 -2
  51. package/dist/cli/client.js +13 -13
  52. package/dist/cli/context.js +1 -1
  53. package/dist/cli/curate.d.ts +1 -1
  54. package/dist/cli/curate.js +9 -9
  55. package/dist/cli/decisions.js +4 -5
  56. package/dist/cli/hook-runtime.d.ts +3 -1
  57. package/dist/cli/hook-runtime.js +2 -2
  58. package/dist/cli/init.d.ts +1 -1
  59. package/dist/cli/init.js +9 -9
  60. package/dist/cli/object-verbs.d.ts +2 -2
  61. package/dist/cli/object-verbs.js +4 -4
  62. package/dist/cli/predictions.js +2 -3
  63. package/dist/cli/quality-repair-once.d.ts +1 -1
  64. package/dist/cli/quality-repair-once.js +2 -2
  65. package/dist/cli/refine-llm.js +2 -1
  66. package/dist/cli/remember.js +5 -5
  67. package/dist/cli/serve.js +9 -4
  68. package/dist/cli/session-hooks.d.ts +0 -4
  69. package/dist/cli/session-hooks.js +57 -66
  70. package/dist/cli/setup.d.ts +1 -1
  71. package/dist/cli/setup.js +2 -2
  72. package/dist/cli/shared.d.ts +1 -1
  73. package/dist/cli/shared.js +2 -2
  74. package/dist/cli/sleep.js +2 -2
  75. package/dist/cli/transfer.d.ts +3 -3
  76. package/dist/cli/transfer.js +34 -15
  77. package/dist/cli/usage.d.ts +1 -1
  78. package/dist/cli/usage.js +1 -1
  79. package/dist/cli/verb-row.d.ts +2 -0
  80. package/dist/cli/verbs/health.js +2 -0
  81. package/dist/cli/verbs/memory.js +43 -51
  82. package/dist/cli/verbs/objects.js +6 -3
  83. package/dist/cli/verbs/sessions.js +4 -2
  84. package/dist/cli.js +1 -2
  85. package/dist/connectors/dlq.js +2 -1
  86. package/dist/connectors/github/deletion.js +2 -1
  87. package/dist/connectors/github/ingest.js +2 -1
  88. package/dist/connectors/github/tenant-routing.js +2 -1
  89. package/dist/connectors/github/webhook.js +4 -2
  90. package/dist/connectors/ingest.js +2 -1
  91. package/dist/connectors/slack/deletion.js +2 -1
  92. package/dist/connectors/slack/ingest.js +2 -1
  93. package/dist/connectors/slack/tenant-routing.js +2 -1
  94. package/dist/consolidate/conflicts.js +1 -1
  95. package/dist/consolidate/dag.js +14 -9
  96. package/dist/consolidate/dedupe.js +1 -1
  97. package/dist/consolidate/memory-value.js +1 -1
  98. package/dist/consolidate/merge.js +6 -6
  99. package/dist/consolidate/sleep.js +1 -1
  100. package/dist/consolidate/traces.js +2 -2
  101. package/dist/core/config.js +15 -16
  102. package/dist/core/markdown.js +2 -2
  103. package/dist/core/memory.d.ts +4 -4
  104. package/dist/core/memory.js +10 -5
  105. package/dist/core/physics.d.ts +3 -1
  106. package/dist/core/physics.js +17 -2
  107. package/dist/core/project-identity.js +2 -1
  108. package/dist/core/search-types.d.ts +1 -1
  109. package/dist/dashboard/dashboard-actions.d.ts +2 -2
  110. package/dist/dashboard/dashboard-actions.js +5 -5
  111. package/dist/dashboard/dashboard-queries.js +2 -7
  112. package/dist/dashboard/dashboard-snapshot.js +1 -3
  113. package/dist/dashboard/dashboard.js +16 -5
  114. package/dist/db/connect.d.ts +2 -1
  115. package/dist/db/connect.js +2 -1
  116. package/dist/db/migrate.js +14 -7
  117. package/dist/db/migrations/v11.js +1 -0
  118. package/dist/db/migrations/v14.js +1 -0
  119. package/dist/db/physics-state.d.ts +0 -4
  120. package/dist/db/physics-state.js +5 -31
  121. package/dist/db/request-stores.d.ts +1 -0
  122. package/dist/db/request-stores.js +2 -1
  123. package/dist/db/vector-store.js +2 -1
  124. package/dist/doctor.d.ts +3 -0
  125. package/dist/doctor.js +7 -7
  126. package/dist/embeddings/local.js +2 -2
  127. package/dist/embeddings/provider.js +1 -10
  128. package/dist/entry/store-kit.d.ts +37 -0
  129. package/dist/entry/store-kit.js +25 -0
  130. package/dist/entry/testing.d.ts +6 -0
  131. package/dist/entry/testing.js +7 -0
  132. package/dist/eval/judgment.js +1 -1
  133. package/dist/hooks/json-hooks.js +6 -2
  134. package/dist/importers/core.js +3 -3
  135. package/dist/importers/vault.d.ts +1 -1
  136. package/dist/importers/vault.js +18 -18
  137. package/dist/index.d.ts +2 -2
  138. package/dist/index.js +1 -1
  139. package/dist/learn/autolearn.d.ts +6 -2
  140. package/dist/learn/autolearn.js +8 -6
  141. package/dist/learn/extract.js +1 -1
  142. package/dist/learn/invalidation.d.ts +2 -1
  143. package/dist/learn/invalidation.js +10 -5
  144. package/dist/mcp/memory-tools.d.ts +1 -1
  145. package/dist/mcp/memory-tools.js +6 -9
  146. package/dist/mcp/request.js +20 -19
  147. package/dist/mcp/stdio.js +13 -8
  148. package/dist/rerankers/cross-encoder.js +10 -3
  149. package/dist/search/embed-memories.d.ts +5 -2
  150. package/dist/search/embed-memories.js +40 -28
  151. package/dist/search/engine.d.ts +8 -0
  152. package/dist/search/engine.js +10 -0
  153. package/dist/search/vector.js +5 -5
  154. package/dist/server/auth.d.ts +1 -1
  155. package/dist/server/auth.js +7 -8
  156. package/dist/server/boot.d.ts +2 -1
  157. package/dist/server/boot.js +28 -19
  158. package/dist/server/client-ip.d.ts +9 -4
  159. package/dist/server/client-ip.js +35 -17
  160. package/dist/server/route-table.js +5 -6
  161. package/dist/server/routes/memories.js +4 -2
  162. package/dist/server/sleep-offload.js +3 -3
  163. package/dist/server/tls.d.ts +7 -2
  164. package/dist/server/tls.js +17 -5
  165. package/dist/server/types.d.ts +7 -2
  166. package/dist/server.d.ts +4 -41
  167. package/dist/server.js +3 -30
  168. package/dist/sharing/global-store.d.ts +4 -5
  169. package/dist/sharing/global-store.js +6 -11
  170. package/dist/sharing/global-sync.js +1 -1
  171. package/dist/sharing/search-both.js +9 -2
  172. package/dist/sharing/share.js +2 -4
  173. package/dist/store/ambient.js +2 -1
  174. package/dist/store/candidates.js +18 -8
  175. package/dist/store/cards.js +5 -4
  176. package/dist/store/compactions-record.js +8 -7
  177. package/dist/store/conflicts.d.ts +7 -1
  178. package/dist/store/conflicts.js +6 -5
  179. package/dist/store/connectors/dlq.js +2 -1
  180. package/dist/store/connectors/event-log.d.ts +13 -0
  181. package/dist/store/connectors/event-log.js +24 -0
  182. package/dist/store/connectors/github.js +6 -8
  183. package/dist/store/connectors/slack.js +6 -10
  184. package/dist/store/dashboard-reads.js +1 -0
  185. package/dist/store/delete-and-batch.js +5 -4
  186. package/dist/store/delivery-recorder.d.ts +2 -2
  187. package/dist/store/delivery-recorder.js +1 -6
  188. package/dist/store/descendant-page.js +2 -1
  189. package/dist/store/dormant.js +7 -3
  190. package/dist/store/entry-reads.d.ts +25 -3
  191. package/dist/store/entry-reads.js +43 -12
  192. package/dist/store/entry-row.js +9 -2
  193. package/dist/store/entry-writes.d.ts +10 -7
  194. package/dist/store/entry-writes.js +23 -11
  195. package/dist/store/goals.js +3 -2
  196. package/dist/store/graph-reads.js +5 -4
  197. package/dist/store/graph-view-rows.js +2 -1
  198. package/dist/store/half-life.js +2 -2
  199. package/dist/store/handoffs.d.ts +9 -3
  200. package/dist/store/handoffs.js +4 -3
  201. package/dist/store/index.d.ts +2 -2
  202. package/dist/store/index.js +1 -1
  203. package/dist/store/mirrors.js +9 -4
  204. package/dist/store/open.d.ts +2 -1
  205. package/dist/store/open.js +2 -1
  206. package/dist/store/port.d.ts +59 -3
  207. package/dist/store/quality-repair.js +1 -1
  208. package/dist/store/recall-trace.js +2 -1
  209. package/dist/store/rejected-values.js +14 -9
  210. package/dist/store/rows.js +2 -2
  211. package/dist/store/rule-sql.d.ts +2 -0
  212. package/dist/store/rule-sql.js +12 -6
  213. package/dist/store/search-rows.d.ts +5 -5
  214. package/dist/store/search-rows.js +9 -12
  215. package/dist/store/set-aside.js +1 -1
  216. package/dist/store/sqlite/connector-writes-group.js +3 -1
  217. package/dist/store/sqlite/entry-writes-group.d.ts +3 -3
  218. package/dist/store/sqlite/entry-writes-group.js +1 -1
  219. package/dist/store/sqlite/held-connection.d.ts +2 -0
  220. package/dist/store/sqlite/held-connection.js +1 -1
  221. package/dist/store/sqlite/last-recall-group.d.ts +13 -0
  222. package/dist/store/sqlite/last-recall-group.js +57 -0
  223. package/dist/store/sqlite/local.d.ts +0 -17
  224. package/dist/store/sqlite/local.js +0 -31
  225. package/dist/store/sqlite/maintenance-groups.d.ts +6 -0
  226. package/dist/store/sqlite/maintenance-groups.js +60 -0
  227. package/dist/store/sqlite/objects-group.js +14 -6
  228. package/dist/store/sqlite/store.d.ts +1 -8
  229. package/dist/store/sqlite/store.js +13 -24
  230. package/dist/store/summaries.d.ts +7 -0
  231. package/dist/store/summaries.js +49 -7
  232. package/dist/store/token-ledger-rows.js +3 -2
  233. package/dist/store/vector-index.js +5 -2
  234. package/dist/util/anthropic-messages.d.ts +3 -1
  235. package/dist/util/anthropic-messages.js +4 -2
  236. package/dist/util/atomic-write.d.ts +5 -2
  237. package/dist/util/atomic-write.js +12 -8
  238. package/dist/util/env.d.ts +10 -1
  239. package/dist/util/env.js +10 -1
  240. package/dist/util/format.d.ts +2 -0
  241. package/dist/util/format.js +4 -0
  242. package/dist/util/request-scope.d.ts +6 -2
  243. package/dist/util/request-scope.js +7 -3
  244. package/dist/util/spawn-argv.d.ts +9 -0
  245. package/dist/util/spawn-argv.js +57 -0
  246. package/dist/util/sql.d.ts +3 -0
  247. package/dist/util/sql.js +6 -0
  248. package/dist/util/vector-codec.d.ts +4 -0
  249. package/dist/util/vector-codec.js +15 -0
  250. package/dist/util/version.d.ts +1 -1
  251. package/dist/util/version.js +1 -1
  252. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  253. package/extensions/openclaw-plugin/package.json +1 -1
  254. package/openclaw.plugin.json +1 -1
  255. package/package.json +3 -1
  256. package/dist/api/on-store.d.ts +0 -16
  257. package/dist/api/on-store.js +0 -38
  258. package/dist/cli/lenient-id.d.ts +0 -2
  259. package/dist/cli/lenient-id.js +0 -13
@@ -9,6 +9,7 @@ import { topVectorMatches } from '../db/vector-store.js';
9
9
  import { MEMORY_SELECT_COLUMNS, MEMORY_SEARCH_COLUMNS, DEFAULT_SEARCH_CANDIDATE_LIMIT, rowToEntry, } from './rows.js';
10
10
  import { onHandle, openStore } from './open.js';
11
11
  import { escapeLike } from '../util/escape.js';
12
+ import { placeholders } from '../util/sql.js';
12
13
  /** The recall scope rule for a table column prefix (`m.` or none); `passesScopeFilterForRecall` in recall-scope.ts is its JS twin. */
13
14
  function recallScopeClause(col, scopeFilter) {
14
15
  if (scopeFilter === undefined)
@@ -40,11 +41,6 @@ export function recallScopeFilter(requestedScope, mode, ownScope) {
40
41
  return mode === 'additive' ? { mode: 'default-deny-or-exact', value: requestedScope, ownScope } : { mode: 'exact', value: requestedScope };
41
42
  }
42
43
  const FTS_QUERY_SYNTAX_RE = /fts5: syntax error|unterminated string/i;
43
- let forceLikePath = false;
44
- /** Sends term queries down the LIKE path on a build that has FTS. Only tests call it. */
45
- export function _forceLikePathForTests(on) {
46
- forceLikePath = on;
47
- }
48
44
  function loadSearchRows(db, query, limit, tenantId, options = {}) {
49
45
  const { scopeFilter, includeSuperseded = true, originProjects } = options;
50
46
  const p = searchPredicates(tenantId, scopeFilter, includeSuperseded, originProjects);
@@ -53,8 +49,7 @@ function loadSearchRows(db, query, limit, tenantId, options = {}) {
53
49
  // LIMIT here too, so every candidate path honours the caller's cap.
54
50
  return selectAllCandidates(db, p, limit);
55
51
  }
56
- // The test switch is read here, not in `isFtsAvailable`, so writes never skip FTS index sync.
57
- if (!forceLikePath && isFtsAvailable(db)) {
52
+ if (isFtsAvailable(db)) {
58
53
  const rows = selectFtsCandidates(db, terms, p, limit);
59
54
  if (rows.length > 0)
60
55
  return rows;
@@ -152,10 +147,12 @@ export function loadSearchEntries(hippoRoot, query, limit = DEFAULT_SEARCH_CANDI
152
147
  }
153
148
  /** Recall-mode loader: scope predicate in SQL so `unknown:legacy` cannot leak. Empty `requestedScope` default-denies (admits `ownScope`); else exact match.
154
149
  * SQL `NOT LIKE '%:private:%'` runs before the LIMIT so private rows cannot starve admitted ones; `passesScopeFilterForRecall` is the exact post-filter. */
150
+ export function loadRecallEntries(hippoRoot, query, options = {}) {
151
+ return onHandle(hippoRoot, (db) => loadRecallSearchEntriesFromDb(db, query, options), openStore);
152
+ }
153
+ /** Positional form of `loadRecallEntries`, kept internal because the frozen q3b-ranker-floor benchmark imports it from dist. */
155
154
  export function loadRecallSearchEntries(hippoRoot, query, limit = DEFAULT_SEARCH_CANDIDATE_LIMIT, tenantId, requestedScope, explicitScopeMode = 'exact', includeSuperseded = true, originProjects, ownScope) {
156
- return onHandle(hippoRoot, (db) => {
157
- return loadRecallSearchEntriesFromDb(db, query, { limit, tenantId, requestedScope, explicitScopeMode, includeSuperseded, originProjects, ownScope });
158
- }, openStore);
155
+ return loadRecallEntries(hippoRoot, query, { limit, tenantId, requestedScope, explicitScopeMode, includeSuperseded, originProjects, ownScope });
159
156
  }
160
157
  // Split out so callers with an already-open db (the prompt-recall path) skip
161
158
  // the initStore+open/close cycle per store per call.
@@ -176,7 +173,7 @@ export async function loadVectorCandidateEntries(hippoRoot, queryVector, spec) {
176
173
  if (matches.length === 0)
177
174
  return [];
178
175
  // SAFETY: the SELECT names exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
179
- const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (${matches.map(() => '?').join(', ')})`)
176
+ const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (${placeholders(matches.length)})`)
180
177
  .all(...matches.map((m) => m.id));
181
178
  const byId = new Map(rows.map((r) => [r.id, rowToEntry(r)]));
182
179
  return matches.flatMap((m) => byId.get(m.id) ?? []);
@@ -197,7 +194,7 @@ export function pickRarestFtsQuery(db, terms, maxTerms = RAREST_TERM_COUNT) {
197
194
  return '';
198
195
  // SAFETY: rows' shape matches the two columns named in the SELECT.
199
196
  const rows = db
200
- .prepare(`SELECT term, doc FROM temp.z1_rarest_vocab WHERE term IN (${vocab.map(() => '?').join(', ')})`)
197
+ .prepare(`SELECT term, doc FROM temp.z1_rarest_vocab WHERE term IN (${placeholders(vocab.length)})`)
201
198
  .all(...vocab);
202
199
  const counts = new Map(rows.map((r) => [r.term, r.doc]));
203
200
  return rarestFtsQuery(terms, (part) => counts.get(part) ?? 0, maxTerms);
@@ -7,7 +7,7 @@ export const SYNC_ACTOR = 'agent-memories';
7
7
  /** Design 6's set-aside on the caller's transaction: a pinned row only loses the tag, any other goes dormant, restorable. */
8
8
  export function setAsideRow(db, tag, row, why) {
9
9
  const untagged = { ...row, tags: row.tags.filter((t) => t !== tag) };
10
- const audit = (metadata) => appendAuditEvent(db, { tenantId: row.tenantId, actor: SYNC_ACTOR, op: 'agent_memory_set_aside', targetId: row.id, metadata });
10
+ const audit = (metadata) => appendAuditEvent(db, { tenantId: row.tenant_id, actor: SYNC_ACTOR, op: 'agent_memory_set_aside', targetId: row.id, metadata });
11
11
  if (row.pinned) {
12
12
  setEntryTagsInTx(db, untagged);
13
13
  audit({ why, untagged: true });
@@ -22,7 +22,9 @@ function loggedMemory(db, event) {
22
22
  function writeWithCompanions(db, { entry, actor, event, quarantine }) {
23
23
  writeInOwnTenant(db, entry, actor, (handle, memoryId) => {
24
24
  if (quarantine)
25
- recordQuarantine(handle, { tenantId: entry.tenantId, memoryId, originalScope: quarantine.originalScope, reason: quarantine.reason, actor });
25
+ recordQuarantine(handle, {
26
+ tenantId: entry.tenant_id, memoryId, originalScope: quarantine.originalScope, reason: quarantine.reason, actor,
27
+ });
26
28
  if (event && !logEvent(handle, event, memoryId))
27
29
  throw new EventAlreadyLogged();
28
30
  });
@@ -2,11 +2,11 @@ import { type DatabaseSyncLike } from '../../db/index.js';
2
2
  import { type MemoryEntry } from '../../core/memory.js';
3
3
  import { type ArchiveOpts } from '../raw-archive.js';
4
4
  import type { EntryWrite, EntryWrites, OutcomeWrite, RawArchive, Sync } from '../port.js';
5
- import { type WriteEntryOptions } from '../entry-writes.js';
5
+ import { type WriteEntryInternalOptions } from '../entry-writes.js';
6
6
  /** Each call on its own handle; mirrors and the forgotten counter follow the commit, so a rolled-back write leaves neither. */
7
7
  export declare function sqliteEntryWrites(hippoRoot: string): Sync<EntryWrites>;
8
8
  /** A store-side hook writes on the row's handle inside its write scope, so its throw undoes the row. */
9
- export declare function writeEntryAt(hippoRoot: string, { entry, actor }: EntryWrite, afterWrite?: WriteEntryOptions['afterWrite']): void;
9
+ export declare function writeEntryAt(hippoRoot: string, { entry, actor }: EntryWrite, afterWrite?: WriteEntryInternalOptions['afterWrite']): void;
10
10
  /** A trace link follows the commit on the same handle and names only the ids applied, so a rolled-back outcome leaves none. */
11
11
  export declare function applyOutcomeAt(hippoRoot: string, outcome: OutcomeWrite, traceId?: number): string[];
12
12
  /** Reach is checked inside the archive's write scope. A store-side hook writes on the same handle inside that scope, so its throw undoes the archive. */
@@ -14,5 +14,5 @@ export declare function archiveRawAt(hippoRoot: string, archive: RawArchive, aft
14
14
  /** The refusal row lands after `write` has rolled back, in a fresh implicit transaction the aborted one cannot undo. */
15
15
  export declare function auditingRefusal<T>(db: DatabaseSyncLike, actor: string, write: () => T): T;
16
16
  /** writeEntryAt on the caller's handle, so a sibling group can read on it after the scope has unwound. */
17
- export declare function writeInOwnTenant(db: DatabaseSyncLike, entry: MemoryEntry, actor: string, afterWrite?: WriteEntryOptions['afterWrite']): void;
17
+ export declare function writeInOwnTenant(db: DatabaseSyncLike, entry: MemoryEntry, actor: string, afterWrite?: WriteEntryInternalOptions['afterWrite']): void;
18
18
  //# sourceMappingURL=entry-writes-group.d.ts.map
@@ -91,7 +91,7 @@ function assertInReach(db, target, id) {
91
91
  /** hippo.db's upsert would move the row into the writer's tenant, so a store write refuses an id another tenant holds. */
92
92
  function assertIdInTenant(db, entry) {
93
93
  const holder = selectMemoryReach(db, entry.id)?.tenantId;
94
- if (holder !== undefined && holder !== entry.tenantId)
94
+ if (holder !== undefined && holder !== entry.tenant_id)
95
95
  throw new ConflictError(`Memory ${entry.id} belongs to another tenant`);
96
96
  }
97
97
  /** The refusal row lands after `write` has rolled back, in a fresh implicit transaction the aborted one cannot undo. */
@@ -3,6 +3,8 @@ export interface StoreHolder {
3
3
  hold: () => void;
4
4
  afterResponse: () => void;
5
5
  release: () => Promise<void>;
6
+ /** Runs one request, inside the fence that keeps hippo.db shut when the store is not it. */
7
+ runRequest: (run: () => Promise<void>) => Promise<void>;
6
8
  }
7
9
  /** Holds a connection to the hippo.db under `hippoRoot` once it exists, so serving never creates one. */
8
10
  export declare function holdSqliteConnection(hippoRoot: string): StoreHolder;
@@ -36,6 +36,6 @@ export function holdSqliteConnection(hippoRoot) {
36
36
  closeHippoDb(heldDb);
37
37
  heldDb = undefined;
38
38
  };
39
- return { hold, afterResponse, release };
39
+ return { hold, afterResponse, release, runRequest: (run) => run() };
40
40
  }
41
41
  //# sourceMappingURL=held-connection.js.map
@@ -0,0 +1,13 @@
1
+ import type { LastRecall, RecallWrites, Sync } from '../port.js';
2
+ /** What a finished recall wrote after its audit rows: the trace's id, null when none was asked or its write failed, and the ids strengthened. */
3
+ interface RecallFinish {
4
+ readonly traceId: number | null;
5
+ readonly strengthened: ReadonlySet<string>;
6
+ }
7
+ /** sqliteStore's finishRecall, for the synchronous recall that cannot await the port. */
8
+ export declare function finishRecallAt(hippoRoot: string, writes: RecallWrites): RecallFinish;
9
+ export declare function sqliteLastRecall(hippoRoot: string): Sync<LastRecall>;
10
+ /** The group as a served store answers it: the call runs at once and resolves through a Promise, so a throw rejects as another store's would. */
11
+ export declare function servedLastRecall(sync: Sync<LastRecall>): LastRecall;
12
+ export {};
13
+ //# sourceMappingURL=last-recall-group.d.ts.map
@@ -0,0 +1,57 @@
1
+ // hippo.db's LastRecall group: the last recall sits in its meta table, and a traced outcome links on the outcome's own handle.
2
+ import { withWriteScope } from '../../db/index.js';
3
+ import { appendAuditEvent } from '../audit.js';
4
+ import { strengthenRetrieved, strengthenRetrievedInOwnTx } from '../entry-writes.js';
5
+ import { localGoalRecallRows, writeGoalRecallLog } from '../goals.js';
6
+ import { loadLastRecall, saveIndex } from '../index-and-stats.js';
7
+ import { onHandle } from '../open.js';
8
+ import { writeRecallTrace } from '../recall-trace.js';
9
+ import { applyOutcomeAt } from './entry-writes-group.js';
10
+ /** sqliteStore's finishRecall, for the synchronous recall that cannot await the port. */
11
+ export function finishRecallAt(hippoRoot, writes) {
12
+ return onHandle(hippoRoot, (db) => {
13
+ const traceAndStrengthen = () => ({
14
+ traceId: writes.trace ? writeRecallTrace(db, writes.trace) : null,
15
+ strengthened: writes.strengthen ? strengthenRetrievedInOwnTx(db, writes.strengthen.ids, writes.strengthen.opts) : new Set(),
16
+ });
17
+ // Alone, the fail-soft trace and strengthen keep their own scopes so a busy lock loses them, never the read.
18
+ if (writes.goalLog.length === 0 && writes.audit.length === 0)
19
+ return traceAndStrengthen();
20
+ // Inside this scope both run as savepoints, so one recall commits once and a failed trace or strengthen still unwinds alone.
21
+ return withWriteScope(db, 'finish_recall', () => {
22
+ writeGoalRecallLog(db, localGoalRecallRows(db, writes.goalLog));
23
+ for (const event of writes.audit)
24
+ appendAuditEvent(db, event);
25
+ return traceAndStrengthen();
26
+ });
27
+ });
28
+ }
29
+ export function sqliteLastRecall(hippoRoot) {
30
+ return {
31
+ applyTracedOutcome: (outcome, traceId) => applyOutcomeAt(hippoRoot, outcome, traceId),
32
+ applyOutcomeToLast(target, good) {
33
+ const { last_retrieval_ids: ids, last_trace_id: trace } = loadLastRecall(hippoRoot);
34
+ if (ids.length === 0)
35
+ return [];
36
+ return applyOutcomeAt(hippoRoot, { ...target, ids, good }, trace === null ? undefined : Number(trace));
37
+ },
38
+ finishRecall(writes, globalRoot) {
39
+ const { ids, opts } = writes.strengthen;
40
+ const { traceId, strengthened } = finishRecallAt(hippoRoot, writes);
41
+ if (globalRoot !== undefined)
42
+ strengthenRetrieved(globalRoot, ids.filter((id) => !strengthened.has(id)), opts);
43
+ // One write for both keys, so an outcome never pairs these ids with an older trace; a lost trace saves null.
44
+ saveIndex(hippoRoot, { last_retrieval_ids: [...ids], last_trace_id: traceId === null ? null : String(traceId) });
45
+ return traceId;
46
+ },
47
+ };
48
+ }
49
+ /** The group as a served store answers it: the call runs at once and resolves through a Promise, so a throw rejects as another store's would. */
50
+ export function servedLastRecall(sync) {
51
+ return {
52
+ applyTracedOutcome: async (outcome, traceId) => sync.applyTracedOutcome(outcome, traceId),
53
+ applyOutcomeToLast: async (target, good) => sync.applyOutcomeToLast(target, good),
54
+ finishRecall: async (writes, globalRoot) => sync.finishRecall(writes, globalRoot),
55
+ };
56
+ }
57
+ //# sourceMappingURL=last-recall-group.js.map
@@ -1,21 +1,4 @@
1
1
  import { type ScopeGrantChange } from '../key-writes.js';
2
- import type { EntryTarget, OutcomeWrite, RecallWrites } from '../port.js';
3
- /** A recall's writes when hippo.db keeps it as its last one: `strengthen.ids` are the ids it returned. */
4
- type LastRecallWrites = RecallWrites & Required<Pick<RecallWrites, 'strengthen'>>;
5
- /** The port's writes with a part that needs hippo.db's own handle or its meta table, which no other store has. */
6
- export interface SqliteLocal {
7
- /** entryWrites.applyOutcome, then a link from the recall trace to the ids applied, on the same handle after the commit. */
8
- applyOutcome(outcome: OutcomeWrite, traceId: number): string[];
9
- /** applyOutcome on the ids of the last recall, which hippo.db's meta table holds with that recall's trace; answers the ids applied. */
10
- applyOutcomeToLastRecall(target: EntryTarget, good: boolean): string[];
11
- /** finishRecall, then the ids this root lacks strengthened under `globalRoot`, then the ids and their trace saved as the last recall;
12
- * answers the trace id. A store of another kind keeps no last recall, so its stand-in is its own finishRecall, answering null. */
13
- finishLastRecall(writes: LastRecallWrites, globalRoot: string | undefined): number | null | Promise<number | null>;
14
- }
15
- export declare function sqliteLocal(hippoRoot: string): SqliteLocal;
16
- /** What a served store answers for each write that needs hippo.db's own handle: a refusal. */
17
- export declare const REFUSED_ON_A_STORE: Pick<SqliteLocal, 'applyOutcome'>;
18
2
  /** Off the port: only the local CLI changes grants, through synchronous published functions a store's Promise cannot answer. */
19
3
  export declare function changeScopeGrant(hippoRoot: string, change: ScopeGrantChange): void;
20
- export {};
21
4
  //# sourceMappingURL=local.d.ts.map
@@ -1,37 +1,6 @@
1
1
  // What only hippo.db does: operations no port method covers, so they never run on another store.
2
- import { strengthenRetrieved } from '../entry-writes.js';
3
- import { loadLastRecall, saveIndex } from '../index-and-stats.js';
4
2
  import { changeScopeGrantAt } from '../key-writes.js';
5
3
  import { onHandle } from '../open.js';
6
- import { applyOutcomeAt } from './entry-writes-group.js';
7
- import { finishRecallAt } from './store.js';
8
- export function sqliteLocal(hippoRoot) {
9
- return {
10
- applyOutcome: (outcome, traceId) => applyOutcomeAt(hippoRoot, outcome, traceId),
11
- // The ids and the trace come from one statement, so the outcome is linked to the recall that returned those ids.
12
- applyOutcomeToLastRecall(target, good) {
13
- const { last_retrieval_ids: ids, last_trace_id: trace } = loadLastRecall(hippoRoot);
14
- if (ids.length === 0)
15
- return [];
16
- return applyOutcomeAt(hippoRoot, { ...target, ids, good }, trace === null ? undefined : Number(trace));
17
- },
18
- finishLastRecall(writes, globalRoot) {
19
- const { ids, opts } = writes.strengthen;
20
- const { traceId, strengthened } = finishRecallAt(hippoRoot, writes);
21
- if (globalRoot !== undefined)
22
- strengthenRetrieved(globalRoot, ids.filter((id) => !strengthened.has(id)), opts);
23
- // One write for both keys, so an outcome never pairs these ids with an older trace; a lost trace saves null.
24
- saveIndex(hippoRoot, { last_retrieval_ids: [...ids], last_trace_id: traceId === null ? null : String(traceId) });
25
- return traceId;
26
- },
27
- };
28
- }
29
- /** What a served store answers for each write that needs hippo.db's own handle: a refusal. */
30
- export const REFUSED_ON_A_STORE = {
31
- applyOutcome() {
32
- throw new Error('an outcome links its recall trace on hippo.db only, never through a store');
33
- },
34
- };
35
4
  /** Off the port: only the local CLI changes grants, through synchronous published functions a store's Promise cannot answer. */
36
5
  export function changeScopeGrant(hippoRoot, change) {
37
6
  onHandle(hippoRoot, (db) => changeScopeGrantAt(db, change));
@@ -0,0 +1,6 @@
1
+ import type { AuditMaintenance, LessonWrites, Sync } from '../port.js';
2
+ export declare function sqliteAuditMaintenance(hippoRoot: string): Sync<AuditMaintenance>;
3
+ export declare function servedAuditMaintenance(sync: Sync<AuditMaintenance>): AuditMaintenance;
4
+ export declare function sqliteLessonWrites(hippoRoot: string): Sync<LessonWrites>;
5
+ export declare function servedLessonWrites(sync: Sync<LessonWrites>): LessonWrites;
6
+ //# sourceMappingURL=maintenance-groups.d.ts.map
@@ -0,0 +1,60 @@
1
+ // hippo.db's half of the AuditMaintenance and LessonWrites store groups: the quality audit, audit prune and git learn's batch.
2
+ import { pruneAuditRows } from '../audit.js';
3
+ import { loadTextsHoldingWords } from '../candidates.js';
4
+ import { deleteEntriesAutomatically, memoriesBackingObjects } from '../delete-and-batch.js';
5
+ import { loadAllEntries } from '../entry-reads.js';
6
+ import { writeEntriesSkippingRejected, writeEntriesTogether } from '../entry-writes.js';
7
+ import { updateStats } from '../index-and-stats.js';
8
+ import { repairAutomaticMemories, repairQualityOnce } from '../quality-repair.js';
9
+ import { touchableScopeSql } from '../rule-sql.js';
10
+ export function sqliteAuditMaintenance(hippoRoot) {
11
+ return {
12
+ tenantEntries: (tenantId) => loadAllEntries(hippoRoot, tenantId),
13
+ objectBackedIds: () => memoriesBackingObjects(hippoRoot),
14
+ deleteAutomatically: (removals, actor) => deleteEntriesAutomatically(hippoRoot, removals, actor),
15
+ pruneAuditRows: (prune) => pruneAuditRows(hippoRoot, prune),
16
+ repairQuality: (tenantId, apply) => repairAutomaticMemories(hippoRoot, { tenantId, apply }),
17
+ repairQualityOnce: (tenantId) => repairQualityOnce(hippoRoot, tenantId),
18
+ };
19
+ }
20
+ export function servedAuditMaintenance(sync) {
21
+ return {
22
+ tenantEntries: async (tenantId) => sync.tenantEntries(tenantId),
23
+ objectBackedIds: async () => sync.objectBackedIds(),
24
+ deleteAutomatically: async (removals, actor) => sync.deleteAutomatically(removals, actor),
25
+ pruneAuditRows: async (prune) => sync.pruneAuditRows(prune),
26
+ repairQuality: async (tenantId, apply) => sync.repairQuality(tenantId, apply),
27
+ repairQualityOnce: async (tenantId) => sync.repairQualityOnce(tenantId),
28
+ };
29
+ }
30
+ export function sqliteLessonWrites(hippoRoot) {
31
+ return {
32
+ tenantEntries: (tenantId) => loadAllEntries(hippoRoot, tenantId),
33
+ textsHoldingWords: (tenantId, words, ownScope) => loadTextsHoldingWords(hippoRoot, tenantId, words, undefined, touchableScopeSql('', ownScope)),
34
+ writeSkippingRejected(entries, actor) {
35
+ const written = [];
36
+ writeEntriesSkippingRejected(hippoRoot, (put) => {
37
+ for (const entry of entries)
38
+ if (put(entry))
39
+ written.push(entry.id);
40
+ }, actor);
41
+ return written;
42
+ },
43
+ writeTogether(entries) {
44
+ writeEntriesTogether(hippoRoot, entries);
45
+ },
46
+ bumpRemembered(count) {
47
+ updateStats(hippoRoot, { remembered: count });
48
+ },
49
+ };
50
+ }
51
+ export function servedLessonWrites(sync) {
52
+ return {
53
+ tenantEntries: async (tenantId) => sync.tenantEntries(tenantId),
54
+ textsHoldingWords: async (tenantId, words, ownScope) => sync.textsHoldingWords(tenantId, words, ownScope),
55
+ writeSkippingRejected: async (entries, actor) => sync.writeSkippingRejected(entries, actor),
56
+ writeTogether: async (entries) => sync.writeTogether(entries),
57
+ bumpRemembered: async (count) => sync.bumpRemembered(count),
58
+ };
59
+ }
60
+ //# sourceMappingURL=maintenance-groups.js.map
@@ -3,6 +3,7 @@ import { appendAuditEvent } from '../audit.js';
3
3
  import { getMeta, withWriteScopeOr } from '../../db/index.js';
4
4
  import { escapeLike } from '../../util/escape.js';
5
5
  import { keysetAfter } from '../../util/keyset.js';
6
+ import { chunked } from '../../util/chunked.js';
6
7
  import { calculateStrength, deriveHalfLife } from '../../core/memory.js';
7
8
  import { scopeAdmitSql } from '../rule-sql.js';
8
9
  import { stampOriginProject, upsertEntryRow } from '../entry-row.js';
@@ -13,6 +14,7 @@ import { LEGACY_TYPED_HALF_LIFE, onHandle, openStore, TYPED_HALF_LIFE_META_KEY }
13
14
  import { isObjectRefusal, } from '../port.js';
14
15
  import { auditingRefusal } from './entry-writes-group.js';
15
16
  import { insertSpec, rowSpec } from './object-rows.js';
17
+ import { placeholders } from '../../util/sql.js';
16
18
  export function sqliteObjects(hippoRoot) {
17
19
  return {
18
20
  listObjects: (tenantId, kind, query) => onHandle(hippoRoot, (db) => listRows(db, tenantId, kind, query)),
@@ -80,7 +82,7 @@ function listRows(db, tenantId, kind, query) {
80
82
  function closeRow(db, tenantId, kind, id, close) {
81
83
  const spec = rowSpec(kind);
82
84
  return withWriteScopeOr(db, 'close_object', (rollback) => {
83
- const marks = close.from.map(() => '?').join(', ');
85
+ const marks = placeholders(close.from.length);
84
86
  const updated = db.prepare(`UPDATE ${spec.table} SET status = 'closed', closed_at = ? WHERE id = ? AND tenant_id = ? AND status IN (${marks})`).run(close.at, id, tenantId, ...close.from);
85
87
  if (updated.changes === 0) {
86
88
  const existing = db.prepare(`SELECT status FROM ${spec.table} WHERE id = ? AND tenant_id = ?`)
@@ -126,7 +128,7 @@ function insertRow(db, row) {
126
128
  }
127
129
  const result = db.prepare(`
128
130
  INSERT INTO ${rowSpec(row.kind).table}(${cols.join(', ')}, status, superseded_by, superseded_at, closed_at, created_at)
129
- VALUES (${cols.map(() => '?').join(', ')}, 'active', NULL, NULL, NULL, ?)
131
+ VALUES (${placeholders(cols.length)}, 'active', NULL, NULL, NULL, ?)
130
132
  `).run(...values, row.save.at);
131
133
  return Number(result.lastInsertRowid ?? 0);
132
134
  }
@@ -217,11 +219,17 @@ function saveAt(hippoRoot, tenantId, kind, save) {
217
219
  /** The linked ids are checked once the mirror is in and before the INSERT, so a refusal undoes the mirror with it. */
218
220
  function openIncidentRow(db, tenantId, memoryId, open) {
219
221
  const { incidentText, context, linkedMemoryIds } = open.fields;
220
- for (const linkId of linkedMemoryIds) {
221
- const held = db.prepare(`SELECT id FROM memories WHERE id = ? AND tenant_id = ?`).get(linkId, tenantId);
222
- if (!held)
223
- return { refused: 'unlinked', memoryId: linkId };
222
+ const held = new Set();
223
+ for (const ids of chunked(linkedMemoryIds)) {
224
+ const marks = placeholders(ids.length);
225
+ // SAFETY: rows' shape matches the one column named in the SELECT.
226
+ const found = db.prepare(`SELECT id FROM memories WHERE tenant_id = ? AND id IN (${marks})`).all(tenantId, ...ids);
227
+ for (const r of found)
228
+ held.add(r.id);
224
229
  }
230
+ const missing = linkedMemoryIds.find((linkId) => !held.has(linkId));
231
+ if (missing !== undefined)
232
+ return { refused: 'unlinked', memoryId: missing };
225
233
  const result = db.prepare(`
226
234
  INSERT INTO incidents(
227
235
  memory_id, tenant_id, incident_text, context,
@@ -1,4 +1,4 @@
1
- import type { HippoStore, RecallWrites, StoreGroups, Sync } from '../port.js';
1
+ import type { HippoStore, StoreGroups, Sync } from '../port.js';
2
2
  /** The store a request runs on: the served one, else hippo.db under its root, as the CLI and SDK callers have it. */
3
3
  export declare function storeFor(ctx: {
4
4
  readonly hippoRoot: string;
@@ -12,12 +12,5 @@ export type SqliteSyncStore = Sync<Omit<HippoStore, AsyncOnly>>;
12
12
  export declare function sqliteSyncStore(hippoRoot: string): SqliteSyncStore & Sync<Omit<StoreGroups, AsyncOnly>>;
13
13
  /** `sqliteSyncStore` as a served store: each method runs at once and answers through a Promise, so a throw rejects as another store's would. */
14
14
  export declare function sqliteStore(hippoRoot: string): HippoStore & StoreGroups;
15
- /** What a finished recall wrote after its audit rows: the trace's id, null when none was asked or its write failed, and the ids strengthened. */
16
- interface RecallFinish {
17
- readonly traceId: number | null;
18
- readonly strengthened: ReadonlySet<string>;
19
- }
20
- /** sqliteStore's finishRecall, for the synchronous recall that cannot await the port. */
21
- export declare function finishRecallAt(hippoRoot: string, writes: RecallWrites): RecallFinish;
22
15
  export {};
23
16
  //# sourceMappingURL=store.d.ts.map
@@ -4,17 +4,17 @@ import { listApiKeyRows, readApiKeyRecord } from '../auth.js';
4
4
  import { existsSync } from 'node:fs';
5
5
  import { appendAuditEvent, listAuditEventsAfter, queryAuditEvents } from '../audit.js';
6
6
  import { getHippoDbPath, withWriteScope } from '../../db/index.js';
7
- import { activeGoalsWithPolicies, localGoalRecallRows, writeGoalRecallLog } from '../goals.js';
7
+ import { activeGoalsWithPolicies } from '../goals.js';
8
8
  import { planningFallacyEvidenceAt } from '../planning-fallacy-evidence.js';
9
- import { writeRecallTrace } from '../recall-trace.js';
10
9
  import { recordTokenUse } from '../token-ledger.js';
11
10
  import { loadAmbientCandidates, loadContextCandidates } from '../candidates.js';
12
11
  import { loadEntriesByIds, loadFreshRawMemories } from '../entry-reads.js';
13
- import { strengthenRetrievedInOwnTx } from '../entry-writes.js';
14
12
  import { servedConnectorEvents, sqliteConnectorEvents } from './connector-events-group.js';
15
13
  import { servedConnectorWrites, sqliteConnectorWrites } from './connector-writes-group.js';
16
14
  import { sqliteDagReads } from './dag-reads-group.js';
17
15
  import { sqliteEntryWrites } from './entry-writes-group.js';
16
+ import { servedAuditMaintenance, servedLessonWrites, sqliteAuditMaintenance, sqliteLessonWrites } from './maintenance-groups.js';
17
+ import { finishRecallAt, servedLastRecall, sqliteLastRecall } from './last-recall-group.js';
18
18
  import { servedGraphReads, sqliteGraphReads } from './graph-reads-group.js';
19
19
  import { servedObjects, sqliteObjects } from './objects-group.js';
20
20
  import { servedPredictions, sqlitePredictions } from './predictions-group.js';
@@ -25,7 +25,7 @@ import { updateStats } from '../index-and-stats.js';
25
25
  import { auditHighIdAt, revokeKeyAt } from '../key-audit.js';
26
26
  import { createKeyAt, createSelfKeyAt } from '../key-writes.js';
27
27
  import { onHandle } from '../open.js';
28
- import { loadRecallSearchEntries } from '../search-rows.js';
28
+ import { loadRecallEntries } from '../search-rows.js';
29
29
  import { listSessionEvents, loadActiveTaskSnapshot } from '../sessions.js';
30
30
  import { entriesWithoutVectorAt, loadStoredVectorViews, writeVectorsAt } from '../vector-index.js';
31
31
  /** The store a request runs on: the served one, else hippo.db under its root, as the CLI and SDK callers have it. */
@@ -36,6 +36,7 @@ export function storeFor(ctx) {
36
36
  export function sqliteSyncStore(hippoRoot) {
37
37
  return {
38
38
  kind: 'sqlite',
39
+ localFiles: { hippoRoot },
39
40
  ...sqliteBaseReads(hippoRoot),
40
41
  ...sqliteRecallWrites(hippoRoot),
41
42
  keyAudit: sqliteKeyAudit(hippoRoot),
@@ -53,6 +54,9 @@ export function sqliteSyncStore(hippoRoot) {
53
54
  connectorEvents: sqliteConnectorEvents(hippoRoot),
54
55
  objects: sqliteObjects(hippoRoot),
55
56
  readiness: sqliteReadiness(hippoRoot),
57
+ lastRecall: sqliteLastRecall(hippoRoot),
58
+ auditMaintenance: sqliteAuditMaintenance(hippoRoot),
59
+ lessonWrites: sqliteLessonWrites(hippoRoot),
56
60
  close() { },
57
61
  };
58
62
  }
@@ -62,7 +66,7 @@ function sqliteBaseReads(hippoRoot) {
62
66
  return onHandle(hippoRoot, (db) => readApiKeyRecord(db, keyId));
63
67
  },
64
68
  searchRecallEntries(query, args) {
65
- return loadRecallSearchEntries(hippoRoot, query, args.limit, args.tenantId, args.requestedScope, args.explicitScopeMode, args.includeSuperseded, args.originProjects, args.ownScope);
69
+ return loadRecallEntries(hippoRoot, query, args);
66
70
  },
67
71
  entriesByIds(ids, tenantId) {
68
72
  return loadEntriesByIds(hippoRoot, ids, tenantId);
@@ -141,6 +145,7 @@ export function sqliteStore(hippoRoot) {
141
145
  const sync = sqliteSyncStore(hippoRoot);
142
146
  return {
143
147
  ...servedBaseMethods(sync),
148
+ localFiles: sync.localFiles,
144
149
  vectors: sqliteVectorReads(hippoRoot),
145
150
  vectorViews: { storedVectorViews: async (ids) => sync.vectorViews.storedVectorViews(ids) },
146
151
  ...servedKeyGroups(sync),
@@ -153,6 +158,9 @@ export function sqliteStore(hippoRoot) {
153
158
  connectorEvents: servedConnectorEvents(sync.connectorEvents),
154
159
  objects: servedObjects(sqliteObjects(hippoRoot)),
155
160
  readiness: { ping: async () => sync.readiness.ping() },
161
+ lastRecall: servedLastRecall(sync.lastRecall),
162
+ auditMaintenance: servedAuditMaintenance(sync.auditMaintenance),
163
+ lessonWrites: servedLessonWrites(sync.lessonWrites),
156
164
  close: async () => sync.close(),
157
165
  };
158
166
  }
@@ -274,23 +282,4 @@ function continuityAt(hippoRoot, tenantId, eventLimit, key) {
274
282
  recentSessionEvents: sessionId ? listSessionEvents(hippoRoot, tenantId, { session_id: sessionId, limit: eventLimit }) : [],
275
283
  };
276
284
  }
277
- /** sqliteStore's finishRecall, for the synchronous recall that cannot await the port. */
278
- export function finishRecallAt(hippoRoot, writes) {
279
- return onHandle(hippoRoot, (db) => {
280
- const traceAndStrengthen = () => ({
281
- traceId: writes.trace ? writeRecallTrace(db, writes.trace) : null,
282
- strengthened: writes.strengthen ? strengthenRetrievedInOwnTx(db, writes.strengthen.ids, writes.strengthen.opts) : new Set(),
283
- });
284
- // Alone, the fail-soft trace and strengthen keep their own scopes so a busy lock loses them, never the read.
285
- if (writes.goalLog.length === 0 && writes.audit.length === 0)
286
- return traceAndStrengthen();
287
- // Inside this scope both run as savepoints, so one recall commits once and a failed trace or strengthen still unwinds alone.
288
- return withWriteScope(db, 'finish_recall', () => {
289
- writeGoalRecallLog(db, localGoalRecallRows(db, writes.goalLog));
290
- for (const event of writes.audit)
291
- appendAuditEvent(db, event);
292
- return traceAndStrengthen();
293
- });
294
- });
295
- }
296
285
  //# sourceMappingURL=store.js.map
@@ -7,6 +7,13 @@ export declare function loadAllL2Summaries(hippoRoot: string): MemoryEntry[];
7
7
  export declare function loadAllDirtySummaries(hippoRoot: string, limit?: number): MemoryEntry[];
8
8
  /** Live (non-archived) children of a DAG summary, so a rebuild regenerates from the CURRENT child set; tenant-scoped as defence in depth. */
9
9
  export declare function loadChildrenOfSummary(hippoRoot: string, summaryId: string, tenantId: string): MemoryEntry[];
10
+ /** loadChildrenOfSummary for many summaries in one IN query per tenant chunk; the map key is `${tenantId}\u0000${summaryId}`, each list in created order. */
11
+ export declare function loadChildrenOfSummaries(hippoRoot: string, summaries: ReadonlyArray<{
12
+ id: string;
13
+ tenant_id: string;
14
+ }>): Map<string, MemoryEntry[]>;
15
+ /** Key into the map loadChildrenOfSummaries returns. */
16
+ export declare function childrenKey(tenantId: string, summaryId: string): string;
10
17
  /** Patch applied by applyRebuildResult; `bumpRebuildCount` is false for the zero-child case, true for a normal rebuild. */
11
18
  export interface RebuildPatch {
12
19
  content: string;
@@ -6,7 +6,9 @@ import { MEMORY_SELECT_COLUMNS, rowToEntry } from './rows.js';
6
6
  import { audit } from './audit-event.js';
7
7
  import { syncFtsRow } from './entry-row.js';
8
8
  import { onHandle, openStore } from './open.js';
9
+ import { chunked } from '../util/chunked.js';
9
10
  import { DIGEST_DISPLAY_CHARS } from '../util/token-text.js';
11
+ import { placeholders } from '../util/sql.js';
10
12
  // Sleep-cycle rebuild surface: lives here, not in src/consolidate/dag.ts, because it needs module-private MEMORY_SELECT_COLUMNS, MemoryRow, rowToEntry,
11
13
  // audit, syncFtsRow and assertTenantId. dag.ts keeps only the thin orchestrator rebuildDirtySummaries().
12
14
  /** Host-wide loader for L2 topic summaries without an L3 parent, for consolidate phase 1.9 (buildEntityProfiles); SQL-level filter, since in-memory
@@ -62,10 +64,50 @@ export function loadChildrenOfSummary(hippoRoot, summaryId, tenantId) {
62
64
  return rows.map(rowToEntry);
63
65
  }, openStore);
64
66
  }
67
+ /** loadChildrenOfSummary for many summaries in one IN query per tenant chunk; the map key is `${tenantId}\u0000${summaryId}`, each list in created order. */
68
+ export function loadChildrenOfSummaries(hippoRoot, summaries) {
69
+ const byTenant = new Map();
70
+ for (const s of summaries) {
71
+ assertTenantId('loadChildrenOfSummaries', s.tenant_id);
72
+ const ids = byTenant.get(s.tenant_id) ?? [];
73
+ ids.push(s.id);
74
+ byTenant.set(s.tenant_id, ids);
75
+ }
76
+ const out = new Map();
77
+ onHandle(hippoRoot, (db) => {
78
+ for (const [tenantId, ids] of byTenant) {
79
+ for (const slice of chunked(ids)) {
80
+ // SAFETY: this query selects exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
81
+ const rows = db.prepare(`
82
+ SELECT ${MEMORY_SELECT_COLUMNS}
83
+ FROM memories
84
+ WHERE dag_parent_id IN (${placeholders(slice.length)})
85
+ AND tenant_id = ?
86
+ AND kind != 'archived'
87
+ AND superseded_by IS NULL
88
+ ORDER BY created ASC
89
+ `).all(...slice, tenantId);
90
+ for (const entry of rows.map(rowToEntry)) {
91
+ const key = childrenKey(tenantId, entry.dag_parent_id ?? '');
92
+ const list = out.get(key);
93
+ if (list)
94
+ list.push(entry);
95
+ else
96
+ out.set(key, [entry]);
97
+ }
98
+ }
99
+ }
100
+ }, openStore);
101
+ return out;
102
+ }
103
+ /** Key into the map loadChildrenOfSummaries returns. */
104
+ export function childrenKey(tenantId, summaryId) {
105
+ return `${tenantId}\u0000${summaryId}`;
106
+ }
65
107
  /** Apply a rebuild result to a dirty summary in one write scope (UPDATE plus syncFtsRow); `AND summary_dirty = 1` makes a concurrent race-loser a no-op.
66
108
  * Returns `{ changed, refused }`: `refused` only when a tombstone hit suppressed the content write while the metadata UPDATE still landed. */
67
109
  export function applyRebuildResult(hippoRoot, summary, patch) {
68
- assertTenantId('applyRebuildResult', summary.tenantId);
110
+ assertTenantId('applyRebuildResult', summary.tenant_id);
69
111
  return onHandle(hippoRoot, (db) => {
70
112
  return withWriteScope(db, 'rebuild_summary', () => applyRebuildInSavepoint(db, summary, patch));
71
113
  }, openStore);
@@ -99,14 +141,14 @@ function applyRebuildInSavepoint(db, summary, patch) {
99
141
  // Check tombstones before choosing the UPDATE: this direct UPDATE bypasses upsertEntryRow's guard, and
100
142
  // a deterministic rebuild would otherwise re-assert a rejected value every sleep cycle.
101
143
  const tombstone = patch.bumpRebuildCount
102
- ? findRejectedValue(db, summary.tenantId, rejectionDigest(patch.content))
144
+ ? findRejectedValue(db, summary.tenant_id, rejectionDigest(patch.content))
103
145
  : null;
104
146
  // On a tombstone hit, write no content: take the zero-child metadata-only path (descendant_count/earliest_at/latest_at, summary_dirty cleared, no
105
147
  // rebuild_count bump). Clearing dirty is deliberate: leaving it set would make every later sleep re-attempt and re-refuse the same rebuild forever.
106
148
  const applyContentWrite = patch.bumpRebuildCount && !tombstone;
107
149
  const result = applyContentWrite
108
- ? db.prepare(REBUILD_CONTENT_SQL).run(patch.content, patch.descendant_count, patch.earliest_at, patch.latest_at, nowIso, summary.id, summary.tenantId)
109
- : db.prepare(REBUILD_METADATA_SQL).run(patch.descendant_count, patch.earliest_at, patch.latest_at, summary.id, summary.tenantId);
150
+ ? db.prepare(REBUILD_CONTENT_SQL).run(patch.content, patch.descendant_count, patch.earliest_at, patch.latest_at, nowIso, summary.id, summary.tenant_id)
151
+ : db.prepare(REBUILD_METADATA_SQL).run(patch.descendant_count, patch.earliest_at, patch.latest_at, summary.id, summary.tenant_id);
110
152
  // `changed` means THIS call's UPDATE hit a row, so a refusal still settles the cycle (false would retry
111
153
  // the doomed rebuild forever); `refused` lets the caller count it as refused rather than rebuilt.
112
154
  const changed = (result.changes ?? 0) > 0;
@@ -125,7 +167,7 @@ function auditRefusedRebuild(db, summary, patch, tombstone) {
125
167
  targetId: summary.id,
126
168
  metadata: { digest: tombstone.digest, reason: tombstone.reason },
127
169
  actor: patch.actor,
128
- tenantId: summary.tenantId
170
+ tenantId: summary.tenant_id
129
171
  });
130
172
  log.warn(`applyRebuildResult: refused rebuild content for ${summary.id} — matches a rejected value ` +
131
173
  `(digest ${tombstone.digest.slice(0, DIGEST_DISPLAY_CHARS)}...); metadata updated, content unchanged`);
@@ -139,7 +181,7 @@ function syncRebuiltSummary(db, summary, patch, applyContentWrite, nowIso) {
139
181
  descendant_count: patch.descendant_count,
140
182
  earliest_at: patch.earliest_at,
141
183
  latest_at: patch.latest_at,
142
- summary_dirty: 0,
184
+ summary_dirty: false,
143
185
  last_rebuilt_at: applyContentWrite ? nowIso : summary.last_rebuilt_at,
144
186
  rebuild_count: applyContentWrite
145
187
  ? (summary.rebuild_count ?? 0) + 1
@@ -152,7 +194,7 @@ function syncRebuiltSummary(db, summary, patch, applyContentWrite, nowIso) {
152
194
  source: 'E3-rebuild',
153
195
  zero_children: patch.zeroChildren,
154
196
  descendant_count: patch.descendant_count,
155
- }, actor: patch.actor, tenantId: summary.tenantId });
197
+ }, actor: patch.actor, tenantId: summary.tenant_id });
156
198
  }
157
199
  /** Clear summary_dirty on a new summary after buildDag's child-link loop (each member's writeEntry marks the parent dirty, so the same cycle would rebuild it).
158
200
  * Idempotent (no-op, no audit if not dirty); audit source='buildDag-clean' distinguishes it from the rebuild's. */