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
@@ -38,9 +38,15 @@ export interface ResolveConflictOpts {
38
38
  * loser is kind='raw'). Defaults to a conflict-context string. */
39
39
  reason?: string;
40
40
  }
41
+ export interface ResolveConflictRequest {
42
+ readonly conflictId: number;
43
+ readonly keepId: string;
44
+ readonly forgetLoser?: boolean;
45
+ readonly tenantId?: string;
46
+ }
41
47
  /** Resolve a conflict by keeping one memory and halving the loser's half-life; `--forget` removes the loser, `opts.rejectLoserValue` also tombstones its
42
48
  * digest. Every path emits a `conflict_resolve` audit row. Returns the resolved conflict, or null if not found. */
43
- export declare function resolveConflict(hippoRoot: string, conflictId: number, keepId: string, forgetLoser?: boolean, tenantId?: string, opts?: ResolveConflictOpts): {
49
+ export declare function resolveConflict(hippoRoot: string, { conflictId, keepId, forgetLoser, tenantId }: ResolveConflictRequest, opts?: ResolveConflictOpts): {
44
50
  conflict: MemoryConflict;
45
51
  loserId: string;
46
52
  } | null;
@@ -2,7 +2,7 @@ import { DEFAULT_TENANT_ID } from '../util/env.js';
2
2
  import { closeHippoDb, withWriteScope } from '../db/index.js';
3
3
  import { rejectionDigest, insertRejectedValue, normalizeValueForRejection } from './rejection.js';
4
4
  import { archiveRawMemory } from './raw-archive.js';
5
- import { rowToMemoryConflict } from './rows.js';
5
+ import { parseJsonArray, rowToMemoryConflict } from './rows.js';
6
6
  import { audit } from './audit-event.js';
7
7
  import { syncChangedMirrors, purgeMirrorBestEffort } from './mirrors.js';
8
8
  import { selectEntriesByIds } from './entry-reads.js';
@@ -12,6 +12,7 @@ import { deleteEntryCore } from './delete-and-batch.js';
12
12
  import { BadRequestError } from '../core/api-errors.js';
13
13
  import { canTouchScope, isPersonalScope } from '../core/recall-scope.js';
14
14
  import { selectMemoryReach } from './tenant-lookup.js';
15
+ import { placeholders } from '../util/sql.js';
15
16
  // The one place MemoryConflictRow's columns are listed; CONFLICT_COLS_MC is the same list under the `mc` alias.
16
17
  const CONFLICT_COLS = 'id, memory_a_id, memory_b_id, reason, score, status, detected_at, updated_at';
17
18
  const CONFLICT_COLS_MC = CONFLICT_COLS.split(', ').map((c) => `mc.${c}`).join(', ');
@@ -132,7 +133,7 @@ function readConflictRefresh(db, detected) {
132
133
  }
133
134
  const given = [...new Set(detected.flatMap((c) => [c.memory_a_id, c.memory_b_id]))].filter((id) => !tenantById.has(id));
134
135
  for (const ids of chunked(given)) {
135
- const marks = ids.map(() => '?').join(',');
136
+ const marks = placeholders(ids.length);
136
137
  // SAFETY: rows' shape matches the two columns named in the SELECT.
137
138
  const found = db.prepare(`SELECT id, tenant_id FROM memories WHERE id IN (${marks})`).all(...ids);
138
139
  for (const r of found)
@@ -253,7 +254,7 @@ function findResolvableConflict(db, conflictId, keepId, tenantId) {
253
254
  }
254
255
  /** Resolve a conflict by keeping one memory and halving the loser's half-life; `--forget` removes the loser, `opts.rejectLoserValue` also tombstones its
255
256
  * digest. Every path emits a `conflict_resolve` audit row. Returns the resolved conflict, or null if not found. */
256
- export function resolveConflict(hippoRoot, conflictId, keepId, forgetLoser = false, tenantId, opts) {
257
+ export function resolveConflict(hippoRoot, { conflictId, keepId, forgetLoser = false, tenantId }, opts) {
257
258
  const db = openStore(hippoRoot);
258
259
  const scope = conflictMemScope(tenantId);
259
260
  try {
@@ -389,7 +390,7 @@ function stripConflictRefs(db, t, loserRemoved) {
389
390
  // SAFETY: keepRow's shape matches the single `conflicts_with_json` column selected above.
390
391
  const keepRow = db.prepare(`SELECT conflicts_with_json FROM memories WHERE id = ?${memScope}`).get(keepId, ...memArgs);
391
392
  if (keepRow) {
392
- const refs = JSON.parse(keepRow.conflicts_with_json || '[]');
393
+ const refs = parseJsonArray(keepRow.conflicts_with_json, { table: 'memories', id: keepId, column: 'conflicts_with_json' });
393
394
  const cleaned = refs.filter((r) => r !== loserId);
394
395
  db.prepare(`UPDATE memories SET conflicts_with_json = ?, updated_at = datetime('now') WHERE id = ?${memScope}`)
395
396
  .run(JSON.stringify(cleaned), keepId, ...memArgs);
@@ -399,7 +400,7 @@ function stripConflictRefs(db, t, loserRemoved) {
399
400
  // column named in the SELECT below.
400
401
  const loserRow = db.prepare(`SELECT conflicts_with_json FROM memories WHERE id = ?${memScope}`).get(loserId, ...memArgs);
401
402
  if (loserRow) {
402
- const refs = JSON.parse(loserRow.conflicts_with_json || '[]');
403
+ const refs = parseJsonArray(loserRow.conflicts_with_json, { table: 'memories', id: loserId, column: 'conflicts_with_json' });
403
404
  const cleaned = refs.filter((r) => r !== keepId);
404
405
  db.prepare(`UPDATE memories SET conflicts_with_json = ?, updated_at = datetime('now') WHERE id = ?${memScope}`)
405
406
  .run(JSON.stringify(cleaned), loserId, ...memArgs);
@@ -1,10 +1,11 @@
1
1
  // The dead-letter statements both connectors share; each connector names its table and columns and maps rows to its own item type.
2
+ import { placeholders } from '../../util/sql.js';
2
3
  /** Inserts one row, stamping received_at; `columns` pairs each column name with its value, in insert order. */
3
4
  export function insertDlqRow(db, table, columns) {
4
5
  const names = [...columns.map(([name]) => name), 'received_at'];
5
6
  const values = [...columns.map(([, value]) => value), new Date().toISOString()];
6
7
  const result = db
7
- .prepare(`INSERT INTO ${table} (${names.join(', ')}) VALUES (${names.map(() => '?').join(', ')})`)
8
+ .prepare(`INSERT INTO ${table} (${names.join(', ')}) VALUES (${placeholders(names.length)})`)
8
9
  .run(...values);
9
10
  return Number(result.lastInsertRowid);
10
11
  }
@@ -0,0 +1,13 @@
1
+ import type { DatabaseSyncLike } from '../../db/index.js';
2
+ import type { ConnectorEventRecord } from '../port.js';
3
+ /** Which log table a connector writes and the column its dedupe key lives in. */
4
+ export interface EventLogSpec {
5
+ readonly table: 'slack_event_log' | 'github_event_log';
6
+ readonly keyColumn: 'event_id' | 'idempotency_key';
7
+ }
8
+ export declare function eventSeenAt(db: DatabaseSyncLike, log: EventLogSpec, key: string): boolean;
9
+ export declare function eventMemoryAt(db: DatabaseSyncLike, log: EventLogSpec, key: string): string | null;
10
+ /** Records the key unless a row holds it already; false means another writer got there first. `extra` pairs connector-only columns with values. */
11
+ export declare function markEventSeenAt(db: DatabaseSyncLike, log: EventLogSpec, key: string, extra: ReadonlyArray<readonly [string, string]>, memoryId: string | null): boolean;
12
+ export declare function eventRecord(hippoRoot: string, log: EventLogSpec, key: string): ConnectorEventRecord;
13
+ //# sourceMappingURL=event-log.d.ts.map
@@ -0,0 +1,24 @@
1
+ // The event-log statements both connectors share; each connector names its table, key column and extra columns.
2
+ import { placeholders } from '../../util/sql.js';
3
+ import { onHandle } from '../open.js';
4
+ export function eventSeenAt(db, log, key) {
5
+ return !!db.prepare(`SELECT 1 FROM ${log.table} WHERE ${log.keyColumn} = ?`).get(key);
6
+ }
7
+ export function eventMemoryAt(db, log, key) {
8
+ // SAFETY: the SELECT projects only `memory_id`, a nullable column; .get() returns undefined when no row matches.
9
+ const row = db.prepare(`SELECT memory_id FROM ${log.table} WHERE ${log.keyColumn} = ?`).get(key);
10
+ return row?.memory_id ?? null;
11
+ }
12
+ /** Records the key unless a row holds it already; false means another writer got there first. `extra` pairs connector-only columns with values. */
13
+ export function markEventSeenAt(db, log, key, extra, memoryId) {
14
+ const names = [log.keyColumn, ...extra.map(([name]) => name), 'ingested_at', 'memory_id'];
15
+ const values = [key, ...extra.map(([, value]) => value), new Date().toISOString(), memoryId];
16
+ const inserted = db
17
+ .prepare(`INSERT OR IGNORE INTO ${log.table} (${names.join(', ')}) VALUES (${placeholders(names.length)})`)
18
+ .run(...values);
19
+ return Number(inserted.changes ?? 0) !== 0;
20
+ }
21
+ export function eventRecord(hippoRoot, log, key) {
22
+ return onHandle(hippoRoot, (db) => eventSeenAt(db, log, key) ? { seen: true, memoryId: eventMemoryAt(db, log, key) } : { seen: false });
23
+ }
24
+ //# sourceMappingURL=event-log.js.map
@@ -2,23 +2,21 @@
2
2
  import { withWriteScope } from '../../db/index.js';
3
3
  import { archiveRawMemory } from '../raw-archive.js';
4
4
  import { onHandle } from '../open.js';
5
+ import { eventMemoryAt, eventRecord, eventSeenAt, markEventSeenAt } from './event-log.js';
5
6
  import { getDlqRow, insertDlqRow, listDlqRows, markDlqRetried } from './dlq.js';
7
+ const GITHUB_LOG = { table: 'github_event_log', keyColumn: 'idempotency_key' };
6
8
  export function githubEventSeenAt(db, idempotencyKey) {
7
- const row = db.prepare(`SELECT 1 FROM github_event_log WHERE idempotency_key = ?`).get(idempotencyKey);
8
- return !!row;
9
+ return eventSeenAt(db, GITHUB_LOG, idempotencyKey);
9
10
  }
10
11
  export function githubEventMemoryAt(db, idempotencyKey) {
11
- // SAFETY: the SELECT projects exactly the memory_id column of github_event_log.
12
- const row = db.prepare(`SELECT memory_id FROM github_event_log WHERE idempotency_key = ?`).get(idempotencyKey);
13
- return row?.memory_id ?? null;
12
+ return eventMemoryAt(db, GITHUB_LOG, idempotencyKey);
14
13
  }
15
14
  /** False when the key was already logged, so a caller inside a write scope can roll its own row back. */
16
15
  export function markGitHubEventSeenAt(db, entry) {
17
- const inserted = db.prepare(`INSERT OR IGNORE INTO github_event_log (idempotency_key, delivery_id, event_name, ingested_at, memory_id) VALUES (?, ?, ?, ?, ?)`).run(entry.idempotencyKey, entry.deliveryId, entry.eventName, new Date().toISOString(), entry.memoryId);
18
- return Number(inserted.changes ?? 0) !== 0;
16
+ return markEventSeenAt(db, GITHUB_LOG, entry.idempotencyKey, [['delivery_id', entry.deliveryId], ['event_name', entry.eventName]], entry.memoryId);
19
17
  }
20
18
  export function githubEventRecord(hippoRoot, idempotencyKey) {
21
- return onHandle(hippoRoot, (db) => githubEventSeenAt(db, idempotencyKey) ? { seen: true, memoryId: githubEventMemoryAt(db, idempotencyKey) } : { seen: false });
19
+ return eventRecord(hippoRoot, GITHUB_LOG, idempotencyKey);
22
20
  }
23
21
  export function markGitHubEventSeen(hippoRoot, entry) {
24
22
  onHandle(hippoRoot, (db) => { markGitHubEventSeenAt(db, entry); });
@@ -1,6 +1,7 @@
1
1
  // Every statement the Slack connector runs on hippo.db: backfill cursors, the event log, the dead-letter queue and workspace routing.
2
2
  // `xAt(db, ...)` runs on the caller's handle; the same name without `At` opens hippo.db for that one call.
3
3
  import { onHandle } from '../open.js';
4
+ import { eventMemoryAt, eventRecord, eventSeenAt, markEventSeenAt } from './event-log.js';
4
5
  import { getDlqRow, insertDlqRow, listDlqRows, markDlqRetried } from './dlq.js';
5
6
  // Backfill cursors
6
7
  export function slackCursorAt(db, tenantId, channelId) {
@@ -21,24 +22,19 @@ export function saveSlackCursor(hippoRoot, tenantId, channelId, latestTs) {
21
22
  onHandle(hippoRoot, (db) => saveSlackCursorAt(db, tenantId, channelId, latestTs));
22
23
  }
23
24
  // Event log
25
+ const SLACK_LOG = { table: 'slack_event_log', keyColumn: 'event_id' };
24
26
  export function slackEventSeenAt(db, eventId) {
25
- const row = db.prepare(`SELECT 1 FROM slack_event_log WHERE event_id = ?`).get(eventId);
26
- return !!row;
27
+ return eventSeenAt(db, SLACK_LOG, eventId);
27
28
  }
28
29
  /** Records the event unless a row already holds its id; false means another writer got there first. */
29
30
  export function markSlackEventSeenAt(db, eventId, memoryId) {
30
- const inserted = db
31
- .prepare(`INSERT OR IGNORE INTO slack_event_log (event_id, ingested_at, memory_id) VALUES (?, ?, ?)`)
32
- .run(eventId, new Date().toISOString(), memoryId);
33
- return Number(inserted.changes ?? 0) !== 0;
31
+ return markEventSeenAt(db, SLACK_LOG, eventId, [], memoryId);
34
32
  }
35
33
  export function slackEventMemoryAt(db, eventId) {
36
- // SAFETY: the SELECT names only `memory_id`, a nullable column; .get() returns undefined when no row matches.
37
- const row = db.prepare(`SELECT memory_id FROM slack_event_log WHERE event_id = ?`).get(eventId);
38
- return row?.memory_id ?? null;
34
+ return eventMemoryAt(db, SLACK_LOG, eventId);
39
35
  }
40
36
  export function slackEventRecord(hippoRoot, eventId) {
41
- return onHandle(hippoRoot, (db) => slackEventSeenAt(db, eventId) ? { seen: true, memoryId: slackEventMemoryAt(db, eventId) } : { seen: false });
37
+ return eventRecord(hippoRoot, SLACK_LOG, eventId);
42
38
  }
43
39
  export function markSlackEventSeen(hippoRoot, eventId, memoryId) {
44
40
  onHandle(hippoRoot, (db) => markSlackEventSeenAt(db, eventId, memoryId));
@@ -8,6 +8,7 @@ import { storedVectorIds } from '../db/vector-store.js';
8
8
  import { onHandle, openStore } from './open.js';
9
9
  import { QUARANTINE_SCOPE_PREFIX } from './quarantine.js';
10
10
  import { parseJsonArray } from './rows.js';
11
+ // Full content stays: dashboard-snapshot's Fact.lower serves search over the whole text, so a preview would change which rows match.
11
12
  const LIVE_COLUMNS = 'id, created, last_retrieved, retrieval_count, half_life_days, layer, tags_json, emotional_valence, outcome_positive, ' +
12
13
  'outcome_negative, pinned, confidence, content, superseded_by, kind, scope, origin_project';
13
14
  // SQL twins of isQuarantineScope and of the two halves of isLiveMemory; substr compares bytes, as startsWith does, where LIKE would fold case.
@@ -14,6 +14,7 @@ import { purgeMirrorBestEffort, mirrorBestEffort, writeMarkdownMirror } from './
14
14
  import { onHandle, openStore } from './open.js';
15
15
  import { clock } from '../util/write-budget.js';
16
16
  import { chunked } from '../util/chunked.js';
17
+ import { placeholders } from '../util/sql.js';
17
18
  /** Tables whose rows keep a first-class object's backing memory in `memory_id` (ON DELETE SET NULL); tests/dormant-memories.test.ts pins it to the schema. */
18
19
  export const MEMORY_BACKED_TABLES = ['predictions', 'decisions', 'incidents', 'processes', 'policies', 'skills', 'project_briefs', 'customer_notes'];
19
20
  /** Deleting a memory that backs an object nulls the object's link, and no restore can repair it, so no automatic pass may. */
@@ -118,7 +119,7 @@ function selectDeleteTargets(db, ids) {
118
119
  const rows = new Map();
119
120
  for (const chunk of chunked([...new Set(ids)])) {
120
121
  // SAFETY: rows' shape matches the three columns named in the SELECT.
121
- const found = db.prepare(`SELECT id, tenant_id, dag_parent_id FROM memories WHERE id IN (${chunk.map(() => '?').join(',')})`)
122
+ const found = db.prepare(`SELECT id, tenant_id, dag_parent_id FROM memories WHERE id IN (${placeholders(chunk.length)})`)
122
123
  .all(...chunk);
123
124
  for (const row of found)
124
125
  rows.set(row.id, row);
@@ -248,7 +249,7 @@ function moveDormantAndDelete(db, dormantMoves, deletableIds, out) {
248
249
  /** The still auto-deletable rows among `ids`, recording each one's DAG parent as dirty. Placeholders come from `ids` itself. */
249
250
  function selectAutoDeletableRows(db, ids, dirty) {
250
251
  // SAFETY: rows' shape matches the three columns named in the SELECT.
251
- const rows = db.prepare(`SELECT id, dag_parent_id, tenant_id FROM memories WHERE id IN (${ids.map(() => '?').join(',')}) AND ${AUTOMATIC_DELETE_SQL}`).all(...ids);
252
+ const rows = db.prepare(`SELECT id, dag_parent_id, tenant_id FROM memories WHERE id IN (${placeholders(ids.length)}) AND ${AUTOMATIC_DELETE_SQL}`).all(...ids);
252
253
  for (const row of rows) {
253
254
  if (row.dag_parent_id) {
254
255
  dirty.parents.add(row.dag_parent_id);
@@ -287,13 +288,13 @@ function applyBatchWrites(db, stampedWrites, snapshot, out) {
287
288
  // Hook for writes: a new child, or a change to what its summary reads, marks the parent dirty; decay alone does not.
288
289
  if (row.dag_parent_id && (!live || SUMMARY_INPUTS.some((k) => row[k] !== live[k]))) {
289
290
  dirty.parents.add(row.dag_parent_id);
290
- dirty.tenantById.set(row.dag_parent_id, row.tenantId);
291
+ dirty.tenantById.set(row.dag_parent_id, row.tenant_id);
291
292
  }
292
293
  }
293
294
  }
294
295
  /** True, after auditing the refusal, when the write would introduce a rejected value. */
295
296
  function isRejectedBatchWrite(db, row) {
296
- const entryTenantId = row.tenantId ?? DEFAULT_TENANT_ID;
297
+ const entryTenantId = row.tenant_id ?? DEFAULT_TENANT_ID;
297
298
  // checkRejectionGuard, not a bare tombstone probe: a tombstone can coexist with a live same-content row,
298
299
  // and skipping every re-persist would starve it of decay/replay updates; only new or changed content is refused.
299
300
  try {
@@ -138,10 +138,10 @@ export interface DeliveryRecorderInit {
138
138
  runtime?: DeliveryRuntime;
139
139
  /** Set by hooks that are not prompt or context calls; without it the payload's hook event decides. */
140
140
  eventType?: DeliveryEventType;
141
+ /** Makes this recorder throw at one stage; only tests set it, and no CLI flag or environment variable reaches it. */
142
+ fault?: DeliveryFault;
141
143
  }
142
144
  export type DeliveryFault = 'observe' | 'build' | 'flush';
143
- /** Makes every recorder created afterwards throw at one stage. Only tests call it; the shipped CLI and server have no route here. */
144
- export declare function _setDeliveryFaultForTests(fault: DeliveryFault | null): void;
145
145
  /** A recorder for one call; every observer method is guarded, and a throw marks it broken instead of escaping. */
146
146
  export declare function createDeliveryRecorder(init: DeliveryRecorderInit): DeliveryRecorder;
147
147
  //# sourceMappingURL=delivery-recorder.d.ts.map
@@ -198,16 +198,11 @@ function candidateMethods(state, guard) {
198
198
  }),
199
199
  };
200
200
  }
201
- let injectedFault = null;
202
- /** Makes every recorder created afterwards throw at one stage. Only tests call it; the shipped CLI and server have no route here. */
203
- export function _setDeliveryFaultForTests(fault) {
204
- injectedFault = fault;
205
- }
206
201
  /** A recorder for one call; every observer method is guarded, and a throw marks it broken instead of escaping. */
207
202
  export function createDeliveryRecorder(init) {
208
203
  const startedMs = Date.now();
209
204
  const ts = evalNow().toISOString();
210
- const fault = injectedFault;
205
+ const fault = init.fault ?? null;
211
206
  const payload = readPayload(init);
212
207
  const state = {
213
208
  candidates: new Map(), picked: new Map(), filtered: new Set(), facts: null, shown: 0, dropped: 0,
@@ -1,7 +1,8 @@
1
1
  import { selectEntriesByIds, TENANT_IS } from './entry-reads.js';
2
2
  import { chunked } from '../util/chunked.js';
3
3
  import { MEMORY_SELECT_COLUMNS, rowToEntry } from './rows.js';
4
- const marks = (ids) => ids.map(() => '?').join(',');
4
+ import { placeholders } from '../util/sql.js';
5
+ const marks = (ids) => placeholders(ids.length);
5
6
  /** The walk `summaryWithDescendants` documents, with `levels` cut to the first `page.rows` rows and each level's full count in `sizes`. */
6
7
  export function pagedDescendants(db, walk, summaryAdmitted) {
7
8
  const sizes = [];
@@ -24,15 +24,19 @@ export function insertDormantRow(db, move) {
24
24
  reason = excluded.reason,
25
25
  strength = excluded.strength,
26
26
  dormant_at = excluded.dormant_at
27
- `).run(move.entry.tenantId, move.entry.id, move.entry.content, JSON.stringify(move.entry), move.reason, move.strength, move.dormantAt);
27
+ `).run(move.entry.tenant_id, move.entry.id, move.entry.content, JSON.stringify(move.entry), move.reason, move.strength, move.dormantAt);
28
+ }
29
+ function toCurrentEntry({ tenantId, summary_dirty, tenant_id, ...rest }) {
30
+ return { ...rest, tenant_id: tenant_id ?? tenantId ?? '', summary_dirty: summary_dirty === true || summary_dirty === 1 };
28
31
  }
29
32
  /** Parse a stored snapshot back into a MemoryEntry, or null when the row no longer matches it (edited by hand, or truncated). */
30
33
  function parseSnapshot(row) {
31
34
  try {
32
35
  // SAFETY: entry_json is only written by insertDormantRow from a MemoryEntry;
33
36
  // the id / tenant / content checks below reject a row edited out of shape.
34
- const entry = JSON.parse(row.entry_json);
35
- if (entry.id !== row.id || entry.tenantId !== row.tenant_id || entry.content !== row.content) {
37
+ // SAFETY: entry_json came from JSON.stringify of a MemoryEntry (2.0) or a 1.x one; the checks below reject a row edited out of shape.
38
+ const entry = toCurrentEntry(JSON.parse(row.entry_json));
39
+ if (entry.id !== row.id || entry.tenant_id !== row.tenant_id || entry.content !== row.content) {
36
40
  warnDamagedColumn({ table: 'dormant_memories', id: row.id, column: 'entry_json' }, 'wrong shape');
37
41
  return null;
38
42
  }
@@ -31,12 +31,12 @@ export declare function countAllEntries(hippoRoot: string): number;
31
31
  export declare function loadEntriesMentioning(hippoRoot: string, tokens: readonly string[], tag: string, tenantId?: string): MemoryEntry[];
32
32
  export interface EntryTextKey {
33
33
  id: string;
34
- tenantId: string;
34
+ tenant_id: string;
35
35
  content: string;
36
36
  source: string;
37
37
  }
38
- /** Every tenant's rows in loadAllEntries' order, with only the columns that say whether another store already holds each. */
39
- export declare function loadEntryTextKeys(hippoRoot: string): EntryTextKey[];
38
+ /** The rows in loadAllEntries' order (every tenant unless one is named), with only the columns that say whether another store already holds each. */
39
+ export declare function loadEntryTextKeys(hippoRoot: string, tenantId?: string): EntryTextKey[];
40
40
  export interface EntriesWithBase {
41
41
  entries: MemoryEntry[];
42
42
  base: {
@@ -45,8 +45,30 @@ export interface EntriesWithBase {
45
45
  }
46
46
  /** Every entry plus a by-id lookup of each as loaded, rebuilt from its row on first use, so no deep copy of the store is kept. */
47
47
  export declare function loadAllEntriesWithBase(hippoRoot: string): EntriesWithBase;
48
+ /** Where a page stopped: the `created`, `id` of its last row, in loadAllEntries' order. */
49
+ export interface EntryCursor {
50
+ created: string;
51
+ id: string;
52
+ }
53
+ export interface EntryPage {
54
+ entries: MemoryEntry[];
55
+ /** The cursor that reads the page after this one, or null when this was the last. */
56
+ next: EntryCursor | null;
57
+ }
58
+ /** Up to `limit` rows in loadAllEntries' order, starting after `after`; omitting `tenantId` reads every tenant. */
59
+ export declare function loadEntriesPage(hippoRoot: string, tenantId: string | undefined, page: {
60
+ limit: number;
61
+ after?: EntryCursor;
62
+ }): EntryPage;
48
63
  /** Every memory row on an open connection, so a caller can read inside its own transaction. */
49
64
  export declare function selectAllEntries(db: DatabaseSyncLike, tenantId?: string): MemoryEntry[];
65
+ /** The columns a text match needs, for every row of a tenant in loadAllEntries' order: callers read whole rows only for the ids that match. */
66
+ export declare function selectTenantTexts(db: DatabaseSyncLike, tenantId: string): Array<{
67
+ id: string;
68
+ content: string;
69
+ source: string;
70
+ scope: string | null;
71
+ }>;
50
72
  /** Live rows whose source starts with `prefix`, on the caller's handle; LIKE folds case, so the prefix is checked again exactly. */
51
73
  export declare function selectLiveEntriesBySourcePrefix(db: DatabaseSyncLike, tenantId: string, prefix: string): MemoryEntry[];
52
74
  export declare function loadContentsWithTag(hippoRoot: string, tenantId: string, tag: string, origins?: readonly string[]): string[];
@@ -6,6 +6,7 @@ import { originInSql } from '../core/project-identity.js';
6
6
  import { scopeAdmitSql } from './rule-sql.js';
7
7
  import { chunked } from '../util/chunked.js';
8
8
  import { DEFAULT_TENANT_ID } from '../util/env.js';
9
+ import { placeholders } from '../util/sql.js';
9
10
  const MAX_IDS_PER_READ = 500;
10
11
  // The plus keeps SQLite on the primary key for an id list: with a bare tenant_id it walks every row of the tenant instead.
11
12
  export const TENANT_IS = '+tenant_id = ?';
@@ -29,7 +30,7 @@ export function heldIdLookup(hippoRoot, tenantId, ids) {
29
30
  for (const id of chunk)
30
31
  held.set(id, false);
31
32
  // SAFETY: rows' shape matches the single `id` column selected.
32
- const rows = db.prepare(`SELECT id FROM memories WHERE id IN (${chunk.map(() => '?').join(',')}) AND ${TENANT_IS}`).all(...chunk, tenantId);
33
+ const rows = db.prepare(`SELECT id FROM memories WHERE id IN (${placeholders(chunk.length)}) AND ${TENANT_IS}`).all(...chunk, tenantId);
33
34
  for (const row of rows)
34
35
  held.set(row.id, true);
35
36
  }
@@ -49,9 +50,9 @@ export function selectEntriesByIds(db, ids, tenantId) {
49
50
  const tenantClause = tenantId !== undefined ? ` AND ${TENANT_IS}` : '';
50
51
  const tenantArgs = tenantId !== undefined ? [tenantId] : [];
51
52
  for (const chunk of chunked([...new Set(ids)])) {
52
- const placeholders = chunk.map(() => '?').join(',');
53
+ const marks = placeholders(chunk.length);
53
54
  // SAFETY: selects exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
54
- const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (${placeholders})${tenantClause}`).all(...chunk, ...tenantArgs);
55
+ const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (${marks})${tenantClause}`).all(...chunk, ...tenantArgs);
55
56
  for (const row of rows)
56
57
  byId.set(row.id, rowToEntry(row));
57
58
  }
@@ -63,9 +64,9 @@ export function selectChildrenByParent(db, parentIds, tenantId) {
63
64
  const tenantClause = tenantId !== undefined ? ` AND ${TENANT_IS}` : '';
64
65
  const tenantArgs = tenantId !== undefined ? [tenantId] : [];
65
66
  for (const chunk of chunked([...new Set(parentIds)])) {
66
- const placeholders = chunk.map(() => '?').join(',');
67
+ const marks = placeholders(chunk.length);
67
68
  // SAFETY: selects exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
68
- const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE dag_parent_id IN (${placeholders})${tenantClause} ORDER BY created ASC, id ASC`).all(...chunk, ...tenantArgs);
69
+ const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE dag_parent_id IN (${marks})${tenantClause} ORDER BY created ASC, id ASC`).all(...chunk, ...tenantArgs);
69
70
  for (const row of rows) {
70
71
  const entry = rowToEntry(row);
71
72
  const parentId = entry.dag_parent_id ?? '';
@@ -84,12 +85,12 @@ export function loadEntriesByIds(hippoRoot, ids, tenantId) {
84
85
  return [];
85
86
  const capped = ids.slice(0, MAX_IDS_PER_READ);
86
87
  return onHandle(hippoRoot, (db) => {
87
- const placeholders = capped.map(() => '?').join(',');
88
+ const marks = placeholders(capped.length);
88
89
  // Without ORDER BY, rows follow SQLite's IN(...) scan order, undefined w.r.t. `ids`.
89
90
  // SAFETY: both branches select exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
90
91
  const rows = tenantId !== undefined
91
- ? db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (${placeholders}) AND ${TENANT_IS} ORDER BY created ASC, content ASC, id ASC`).all(...capped, tenantId)
92
- : db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (${placeholders}) ORDER BY created ASC, content ASC, id ASC`).all(...capped);
92
+ ? db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (${marks}) AND ${TENANT_IS} ORDER BY created ASC, content ASC, id ASC`).all(...capped, tenantId)
93
+ : db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id IN (${marks}) ORDER BY created ASC, content ASC, id ASC`).all(...capped);
93
94
  return rows.map(rowToEntry);
94
95
  }, openStore);
95
96
  }
@@ -228,12 +229,14 @@ export function loadEntriesMentioning(hippoRoot, tokens, tag, tenantId) {
228
229
  return rows.map(rowToEntry);
229
230
  }, openStore);
230
231
  }
231
- /** Every tenant's rows in loadAllEntries' order, with only the columns that say whether another store already holds each. */
232
- export function loadEntryTextKeys(hippoRoot) {
232
+ /** The rows in loadAllEntries' order (every tenant unless one is named), with only the columns that say whether another store already holds each. */
233
+ export function loadEntryTextKeys(hippoRoot, tenantId) {
233
234
  return onHandle(hippoRoot, (db) => {
235
+ const where = tenantId !== undefined ? 'WHERE tenant_id = ? ' : '';
234
236
  // SAFETY: rows' shape matches the four columns named in the SELECT.
235
- const rows = db.prepare('SELECT id, tenant_id, content, source FROM memories ORDER BY created ASC, id ASC').all();
236
- return rows.map((row) => ({ id: row.id, tenantId: row.tenant_id ?? DEFAULT_TENANT_ID, content: row.content, source: row.source }));
237
+ const rows = db.prepare(`SELECT id, tenant_id, content, source FROM memories ${where}ORDER BY created ASC, id ASC`)
238
+ .all(...(tenantId !== undefined ? [tenantId] : []));
239
+ return rows.map((row) => ({ id: row.id, tenant_id: row.tenant_id ?? DEFAULT_TENANT_ID, content: row.content, source: row.source }));
237
240
  }, openStore);
238
241
  }
239
242
  /** Every entry plus a by-id lookup of each as loaded, rebuilt from its row on first use, so no deep copy of the store is kept. */
@@ -264,10 +267,38 @@ function selectAllRows(db, tenantId) {
264
267
  ? db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE tenant_id = ? ORDER BY created ASC, id ASC`).all(tenantId)
265
268
  : db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories ORDER BY created ASC, id ASC`).all();
266
269
  }
270
+ /** Up to `limit` rows in loadAllEntries' order, starting after `after`; omitting `tenantId` reads every tenant. */
271
+ export function loadEntriesPage(hippoRoot, tenantId, page) {
272
+ const where = [];
273
+ const params = [];
274
+ if (tenantId !== undefined) {
275
+ where.push('tenant_id = ?');
276
+ params.push(tenantId);
277
+ }
278
+ if (page.after) {
279
+ where.push('(created > ? OR (created = ? AND id > ?))');
280
+ params.push(page.after.created, page.after.created, page.after.id);
281
+ }
282
+ const clause = where.length > 0 ? `WHERE ${where.join(' AND ')} ` : '';
283
+ // One extra row says whether another page follows without a count.
284
+ params.push(page.limit + 1);
285
+ return onHandle(hippoRoot, (db) => {
286
+ // SAFETY: selects exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
287
+ const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories ${clause}ORDER BY created ASC, id ASC LIMIT ?`).all(...params);
288
+ const entries = rows.slice(0, page.limit).map(rowToEntry);
289
+ const last = entries[entries.length - 1];
290
+ return { entries, next: rows.length > page.limit && last ? { created: last.created, id: last.id } : null };
291
+ }, openStore);
292
+ }
267
293
  /** Every memory row on an open connection, so a caller can read inside its own transaction. */
268
294
  export function selectAllEntries(db, tenantId) {
269
295
  return selectAllRows(db, tenantId).map(rowToEntry);
270
296
  }
297
+ /** The columns a text match needs, for every row of a tenant in loadAllEntries' order: callers read whole rows only for the ids that match. */
298
+ export function selectTenantTexts(db, tenantId) {
299
+ // SAFETY: rows' shape matches the four columns named in the SELECT.
300
+ return db.prepare('SELECT id, content, source, scope FROM memories WHERE tenant_id = ? ORDER BY created ASC, id ASC').all(tenantId);
301
+ }
271
302
  /** Live rows whose source starts with `prefix`, on the caller's handle; LIKE folds case, so the prefix is checked again exactly. */
272
303
  export function selectLiveEntriesBySourcePrefix(db, tenantId, prefix) {
273
304
  // SAFETY: selects exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
@@ -3,8 +3,15 @@ import { closeHippoDb, ftsRowCounts, openHippoDb, isFtsAvailable, repairFtsDrift
3
3
  import { fallbackOrigin, originFromSource } from '../core/project-identity.js';
4
4
  import { checkRejectionGuard } from './rejection.js';
5
5
  import { errorMessage, log } from '../util/log.js';
6
+ /** A 1.x JS caller's `tenantId` would otherwise fall through to the default tenant without a sound. */
7
+ function rejectLegacyTenantKey(entry) {
8
+ if (Object.hasOwn(entry, 'tenantId') && entry.tenant_id === undefined) {
9
+ throw new TypeError('tenantId was renamed to tenant_id in 2.0');
10
+ }
11
+ }
6
12
  export function upsertEntryRow(db, entry) {
7
- checkRejectionGuard(db, entry.tenantId ?? DEFAULT_TENANT_ID, entry.id, entry.content);
13
+ rejectLegacyTenantKey(entry);
14
+ checkRejectionGuard(db, entry.tenant_id ?? DEFAULT_TENANT_ID, entry.id, entry.content);
8
15
  syncFtsRow(db, entry, upsertMemoryRow(db, entry));
9
16
  }
10
17
  /** The row alone, with no rejection guard or full-text row, for `upsertEntryRow` and for
@@ -104,7 +111,7 @@ function memoryRowValues(entry) {
104
111
  entry.scope ?? null,
105
112
  entry.owner ?? null,
106
113
  entry.artifact_ref ?? null,
107
- entry.tenantId ?? DEFAULT_TENANT_ID,
114
+ entry.tenant_id ?? DEFAULT_TENANT_ID,
108
115
  entry.origin_project ?? null,
109
116
  entry.descendant_count ?? 0,
110
117
  entry.earliest_at ?? null,
@@ -2,13 +2,19 @@ import { type MemoryEntry } from '../core/memory.js';
2
2
  import { type DatabaseSyncLike } from '../db/index.js';
3
3
  export interface WriteEntryOptions {
4
4
  actor?: string;
5
- afterWrite?: (db: DatabaseSyncLike, memoryId: string) => void;
6
5
  /** Runs after the row commits and before the mirrors, on an idle connection; keep it best-effort. */
7
6
  afterCommit?: () => void;
8
7
  }
9
- /** Write an entry to SQLite and refresh mirrors; `opts.afterWrite` runs in the same SAVEPOINT as the INSERT, so a throw rolls the row back.
10
- * `opts.actor` defaults to 'cli'; the api layer passes the resolved actor so the audit log gets one row per write. */
8
+ /** Store-side only, never exported from a package entry: `afterWrite` hands the write's own db handle to the hook. */
9
+ export interface WriteEntryInternalOptions extends WriteEntryOptions {
10
+ /** Runs in the same SAVEPOINT as the INSERT, so a throw rolls the row back. */
11
+ afterWrite?: (db: DatabaseSyncLike, memoryId: string) => void;
12
+ }
13
+ /** Write an entry to SQLite and refresh mirrors. `opts.actor` defaults to 'cli'; the api layer passes the resolved actor,
14
+ * so the audit log gets one row per write. */
11
15
  export declare function writeEntry(hippoRoot: string, entry: MemoryEntry, opts?: WriteEntryOptions): void;
16
+ /** writeEntry for store code that needs the write's own handle: `opts.afterWrite` shares the INSERT's SAVEPOINT. */
17
+ export declare function writeEntryWithHook(hippoRoot: string, entry: MemoryEntry, opts?: WriteEntryInternalOptions): void;
12
18
  /** writeEntry for each of `entries` in one transaction, so all land or none; each
13
19
  * keeps its audit row, and mirrors follow the commit. Returns how many were written; an empty list opens no store. */
14
20
  export declare function writeEntriesTogether(hippoRoot: string, entries: readonly MemoryEntry[]): number;
@@ -20,10 +26,7 @@ export declare function writeEntriesSkippingRejected(hippoRoot: string, fill: (p
20
26
  export declare function addTagToEntries(hippoRoot: string, tenantId: string, ids: readonly string[], tag: string): void;
21
27
  /** DB-only write path: upsert, afterWrite hook and audit row inside one withWriteScope; the caller owns `db` and must call `writeEntryMirrors` after any
22
28
  * outer tx commits, so a rolled-back tx never leaves orphan markdown. The audit row commits atomically with the INSERT. */
23
- export declare function writeEntryDbOnly(db: DatabaseSyncLike, entry: MemoryEntry, opts?: {
24
- actor?: string;
25
- afterWrite?: (db: DatabaseSyncLike, memoryId: string) => void;
26
- }): void;
29
+ export declare function writeEntryDbOnly(db: DatabaseSyncLike, entry: MemoryEntry, opts?: Pick<WriteEntryInternalOptions, 'actor' | 'afterWrite'>): void;
27
30
  /** What follows a row's upsert inside its write scope: the remember row, last of the write's audit rows, then the summary parent's dirty mark. */
28
31
  export declare function auditEntryWrite(db: DatabaseSyncLike, entry: MemoryEntry, actor: string): void;
29
32
  /** Markdown mirror path, invoked AFTER commit (a rolled-back tx must leave no orphan markdown). */