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
package/dist/api/learn.js CHANGED
@@ -2,23 +2,20 @@
2
2
  import { ForbiddenError } from '../core/api-errors.js';
3
3
  import { fetchGitLog, extractLessons, partitionLessons, isGitRepo } from '../learn/autolearn.js';
4
4
  import { loadConfig } from '../core/config.js';
5
- import { withRequestStoresSync } from '../db/request-stores.js';
5
+ import { runWithRequestStores } from '../db/request-stores.js';
6
6
  import { embedMemory } from '../search/embed-memories.js';
7
7
  import { extractInvalidationTarget, invalidateMatching, invalidateMatchingAmong, } from '../learn/invalidation.js';
8
8
  import { createMemory, Layer, schemaFitFrom, schemaFitSourceOf } from '../core/memory.js';
9
9
  import { extractPathTags } from '../search/path-context.js';
10
10
  import { canReadScope, personalScopeOf } from '../core/recall-scope.js';
11
- import { touchableScopeSql } from '../store/rule-sql.js';
12
11
  import { duplicateKey, longestWord, storedTextKeys } from '../util/same-text.js';
13
- import { loadTextsHoldingWords } from '../store/candidates.js';
14
- import { loadAllEntries } from '../store/entry-reads.js';
15
- import { writeEntriesSkippingRejected } from '../store/entry-writes.js';
16
- import { updateStats } from '../store/index-and-stats.js';
12
+ import { requireGroup } from '../store/index.js';
13
+ import { storeFor } from '../store/sqlite/store.js';
17
14
  // SHORTCUT: two profiles keep each surface's rows as they were; fold MCP onto the CLI profile once it may invalidate.
18
15
  export const CLI_LEARN = Object.freeze({ tags: Object.freeze(['error', 'git-learned']), source: 'git-learn', full: true });
19
16
  export const MCP_LEARN = Object.freeze({ tags: Object.freeze(['git-learned']), source: 'git', full: false });
20
17
  /** Learn from `repoPath`'s last `days` of commits into the caller's tenant. Host admin only. */
21
- export function learn(ctx, opts) {
18
+ export async function learn(ctx, opts) {
22
19
  // It reads the host's own git history, which belongs to the host, not to the caller's tenant.
23
20
  if (!ctx.actor.hostAdmin)
24
21
  throw new ForbiddenError('hippo_learn requires a host admin');
@@ -34,67 +31,71 @@ export function learn(ctx, opts) {
34
31
  return { status: 'no-lessons', ...none };
35
32
  // The gate filters the loop input, so a dropped lesson neither stores nor invalidates; docs/decisions git-learn-gate-placement says why.
36
33
  const { kept, dropped } = partitionLessons(parsed);
37
- return { status: 'scanned', lowInfo: dropped.length, ...writeLessons(ctx, kept, opts, config) };
34
+ return { status: 'scanned', lowInfo: dropped.length, ...await writeLessons(ctx, kept, opts, config) };
38
35
  }
39
- /** Writes `batch` in one transaction; a refused lesson is dropped from `live` and `keys` and must not abort the rest of the git-log scan. */
40
- function landLessons(ctx, batch, live, keys, counts, full) {
41
- if (batch.length === 0)
36
+ /** Writes `queued` in one transaction; a refused lesson is dropped from `live` and `keys` and must not abort the rest of the git-log scan. */
37
+ async function landLessons(batch, queued) {
38
+ if (queued.length === 0)
42
39
  return;
43
- // SHORTCUT: hippo.db only, as the port has no batch write, stats bump or whole-tenant read, so another store kind answers 501 at the
44
- // open; port groups for them once a served store must learn.
40
+ const { ctx, writes, live, keys, counts } = batch;
41
+ const written = new Set(await writes.writeSkippingRejected(queued.map((q) => q.entry), ctx.actor.subject));
45
42
  const landed = [];
46
- writeEntriesSkippingRejected(ctx.hippoRoot, (put) => {
47
- for (const { entry, key } of batch) {
48
- if (put(entry)) {
49
- landed.push(entry);
50
- continue;
51
- }
52
- counts.rejected++;
53
- keys.delete(key);
54
- const at = live.indexOf(entry);
55
- if (at !== -1)
56
- live.splice(at, 1);
43
+ for (const { entry, key } of queued) {
44
+ if (written.has(entry.id)) {
45
+ landed.push(entry);
46
+ continue;
57
47
  }
58
- }, ctx.actor.subject);
48
+ counts.rejected++;
49
+ keys.delete(key);
50
+ const at = live.indexOf(entry);
51
+ if (at !== -1)
52
+ live.splice(at, 1);
53
+ }
59
54
  counts.added += landed.length;
60
- if (!full)
55
+ if (!batch.full)
61
56
  return;
62
57
  if (landed.length > 0)
63
- updateStats(ctx.hippoRoot, { remembered: landed.length });
58
+ await writes.bumpRemembered(landed.length);
64
59
  for (const entry of landed)
65
- void embedMemory(ctx.hippoRoot, entry);
60
+ void embedMemory(ctx.hippoRoot, entry, undefined, ctx.store);
61
+ }
62
+ /** The duplicate keys a batch skips: every readable row's with `existing`, else only the rows holding a lesson's longest word. */
63
+ async function knownKeys(ctx, writes, lessons, existing) {
64
+ return storedTextKeys(existing ?? await writes.textsHoldingWords(ctx.tenantId, lessons.map(longestWord), personalScopeOf(ctx.actor)));
66
65
  }
67
- function writeLessons(ctx, lessons, opts, config) {
68
- const { hippoRoot, tenantId } = ctx;
66
+ async function writeLessons(ctx, lessons, opts, config) {
67
+ const { tenantId } = ctx;
69
68
  const { profile } = opts;
69
+ const store = storeFor(ctx);
70
+ const writes = requireGroup(store, 'lessonWrites');
70
71
  // `hippo init` calls this outside any request, so the batch brings its own scope: every read and write below shares one handle.
71
- return withRequestStoresSync(() => {
72
+ return runWithRequestStores(async () => {
72
73
  // One read serves the batch: invalidation matches against `live`, which follows every write below, as a reload per lesson did.
73
- const live = profile.full ? loadAllEntries(hippoRoot, tenantId) : [];
74
+ const live = profile.full ? await writes.tenantEntries(tenantId) : [];
74
75
  // Schema fit needs every row; without it, only rows holding a lesson's longest word can be its copy. Learn is host-admin only, so
75
76
  // both read what an admin can.
76
77
  const existing = profile.full ? live.filter((e) => e.scope == null || canReadScope(ctx.actor, e.scope)) : undefined;
77
78
  // Each lesson is scored against `existing` as it was before the batch, so its tag counts and texts are read once.
78
79
  const fitSource = existing && schemaFitSourceOf(existing);
79
- const readable = touchableScopeSql('', personalScopeOf(ctx.actor));
80
- const keys = storedTextKeys(existing ?? loadTextsHoldingWords(hippoRoot, tenantId, lessons.map(longestWord), undefined, readable));
80
+ const batch = {
81
+ ctx, writes, live, keys: await knownKeys(ctx, writes, lessons, existing), full: profile.full,
82
+ counts: { added: 0, skipped: 0, rejected: 0, invalidations: [] },
83
+ };
81
84
  const pathTags = profile.full ? extractPathTags(opts.repoPath) : [];
82
- const counts = { added: 0, skipped: 0, rejected: 0, invalidations: [] };
83
85
  // Lessons queue and land in one transaction; an invalidation reads the store, so it flushes the queue first.
84
86
  const queue = [];
85
- const flush = () => landLessons(ctx, queue.splice(0), live, keys, counts, profile.full);
86
87
  for (const lesson of lessons) {
87
88
  const key = duplicateKey(lesson);
88
- if (keys.has(key)) {
89
- counts.skipped++;
89
+ if (batch.keys.has(key)) {
90
+ batch.counts.skipped++;
90
91
  continue;
91
92
  }
92
93
  const target = profile.full ? extractInvalidationTarget(lesson) : null;
93
94
  if (target) {
94
- flush();
95
- const { invalidated } = invalidateMatchingAmong(hippoRoot, live, target, tenantId);
95
+ await landLessons(batch, queue.splice(0));
96
+ const { invalidated } = await invalidateMatchingAmong(store, live, target, tenantId);
96
97
  if (invalidated > 0)
97
- counts.invalidations.push({ from: target.from, count: invalidated });
98
+ batch.counts.invalidations.push({ from: target.from, count: invalidated });
98
99
  }
99
100
  const entry = createMemory(lesson, {
100
101
  layer: Layer.Episodic,
@@ -111,10 +112,10 @@ function writeLessons(ctx, lessons, opts, config) {
111
112
  queue.push({ entry, key });
112
113
  if (profile.full)
113
114
  live.push(entry);
114
- keys.add(key);
115
+ batch.keys.add(key);
115
116
  }
116
- flush();
117
- return counts;
117
+ await landLessons(batch, queue.splice(0));
118
+ return batch.counts;
118
119
  });
119
120
  }
120
121
  /** `hippo invalidate`: weakens the caller's tenant's memories that reference `target.from`, or only the one `options.onlyId` names. */
@@ -14,8 +14,10 @@ export declare function getMemoryStatus(ctx: Context, now: Date, atRiskBelow: nu
14
14
  export interface TenantReach {
15
15
  everyTenant?: boolean;
16
16
  }
17
- /** Every memory row in reach, oldest first. */
18
- export declare function listMemories(ctx: Context, reach?: TenantReach): MemoryEntry[];
17
+ /** Every memory row in reach, oldest first, read `pageSize` rows per query so no single read grows with the store. */
18
+ export declare function listMemories(ctx: Context, reach?: TenantReach, opts?: {
19
+ pageSize?: number;
20
+ }): MemoryEntry[];
19
21
  /** How many memory rows the whole store holds, counted in SQL. */
20
22
  export declare function countAllMemories(ctx: Context): number;
21
23
  /** The rows in reach among `ids`; a missing id or one out of reach is absent. */
@@ -2,8 +2,8 @@ import { loadStrengthTallies } from '../store/candidates.js';
2
2
  import { countOpenConflicts } from '../store/conflicts.js';
3
3
  import { NotFoundError } from '../core/api-errors.js';
4
4
  import { canTouchScope } from '../core/recall-scope.js';
5
- import { storeFor } from '../store/index.js';
6
- import { countAllEntries, loadAllEntries, loadEntriesByIds } from '../store/entry-reads.js';
5
+ import { storeFor } from '../store/sqlite/store.js';
6
+ import { countAllEntries, loadEntriesByIds, loadEntriesPage } from '../store/entry-reads.js';
7
7
  import { requireHostAdmin } from './types.js';
8
8
  /** The row `id` in the caller's tenant, or null when it is missing or another tenant's. */
9
9
  export async function getMemory(ctx, id) {
@@ -30,9 +30,20 @@ function tenantFilter(ctx, reach) {
30
30
  requireHostAdmin(ctx, 'Reading every tenant');
31
31
  return undefined;
32
32
  }
33
- /** Every memory row in reach, oldest first. */
34
- export function listMemories(ctx, reach = {}) {
35
- return loadAllEntries(ctx.hippoRoot, tenantFilter(ctx, reach));
33
+ /** Rows a listing reads per query by default; the verbs still receive every row. */
34
+ const LIST_PAGE_SIZE = 1000;
35
+ /** Every memory row in reach, oldest first, read `pageSize` rows per query so no single read grows with the store. */
36
+ export function listMemories(ctx, reach = {}, opts = {}) {
37
+ const tenant = tenantFilter(ctx, reach);
38
+ const limit = Math.max(1, Math.trunc(opts.pageSize ?? LIST_PAGE_SIZE));
39
+ const all = [];
40
+ let after;
41
+ do {
42
+ const page = loadEntriesPage(ctx.hippoRoot, tenant, { limit, after });
43
+ all.push(...page.entries);
44
+ after = page.next ?? undefined;
45
+ } while (after);
46
+ return all;
36
47
  }
37
48
  /** How many memory rows the whole store holds, counted in SQL. */
38
49
  export function countAllMemories(ctx) {
@@ -1,5 +1,6 @@
1
1
  import { saveObject } from '../objects/lifecycle.js';
2
- import { requireGroup, storeFor } from '../store/index.js';
2
+ import { requireGroup } from '../store/index.js';
3
+ import { storeFor } from '../store/sqlite/store.js';
3
4
  /** The request store's object group; the dispatcher has already answered 501 for a store without it. */
4
5
  export function objectsOf(ctx) {
5
6
  return requireGroup(storeFor(ctx), 'objects');
@@ -1,4 +1,4 @@
1
- import type { Context, StoreReply } from './types.js';
1
+ import type { Context } from './types.js';
2
2
  /** An id outside ctx.tenantId, or in someone else's personal scope, is skipped, so a stale id never fails the call.
3
3
  * `appliedIds` holds only the ids applied: a caller that answers across tenants returns it, never the ids it was sent. */
4
4
  export interface OutcomeResult {
@@ -6,14 +6,14 @@ export interface OutcomeResult {
6
6
  appliedIds: string[];
7
7
  }
8
8
  /** `opts.traceId` links the outcome to the recall trace it judges. Only outcomeForLastRecall sends one: explicit ids may follow no recall at all. */
9
- export declare function outcome<C extends Context>(ctx: C, ids: ReadonlyArray<string>, good: boolean, opts?: {
9
+ export declare function outcome(ctx: Context, ids: ReadonlyArray<string>, good: boolean, opts?: {
10
10
  traceId?: number;
11
- }): StoreReply<C, OutcomeResult>;
12
- /** The last recall's ids and its trace sit in hippo.db's meta table, which only the CLI and context write, so a store of another kind is refused.
11
+ }): Promise<OutcomeResult>;
12
+ /** The last recall's ids and its trace sit with the store's lastRecall group, which only the CLI and context write; a store without it is refused.
13
13
  * `ids` holds only the ids applied in ctx.tenantId: the index is not tenant-scoped, so the ids read from it are never returned. */
14
14
  export interface OutcomeForLastRecallResult {
15
15
  applied: number;
16
16
  ids: string[];
17
17
  }
18
- export declare function outcomeForLastRecall<C extends Context>(ctx: C, good: boolean): StoreReply<C, OutcomeForLastRecallResult>;
18
+ export declare function outcomeForLastRecall(ctx: Context, good: boolean): Promise<OutcomeForLastRecallResult>;
19
19
  //# sourceMappingURL=outcome.d.ts.map
@@ -1,21 +1,22 @@
1
1
  // Outcome feedback on recalled memories.
2
2
  import { personalScopeOf } from '../core/recall-scope.js';
3
- import { andThen, notPorted, onStore } from './on-store.js';
3
+ import { requireGroup } from '../store/index.js';
4
+ import { storeFor } from '../store/sqlite/store.js';
4
5
  /** `opts.traceId` links the outcome to the recall trace it judges. Only outcomeForLastRecall sends one: explicit ids may follow no recall at all. */
5
- export function outcome(ctx, ids, good, opts) {
6
- return onStore(ctx, (port, local) => {
7
- const entryWrites = port.entryWrites ?? notPorted(port, 'entryWrites');
8
- const write = { tenantId: ctx.tenantId, actor: ctx.actor.subject, ownScope: personalScopeOf(ctx.actor), ids, good };
9
- const traceId = opts?.traceId;
10
- // The trace link is written on hippo.db's own handle after the commit, so a served store refuses a traceId.
11
- const applied = traceId === undefined ? entryWrites.applyOutcome(write) : local.applyOutcome(write, traceId);
12
- return andThen(applied, (appliedIds) => ({ applied: appliedIds.length, appliedIds }));
13
- });
6
+ export async function outcome(ctx, ids, good, opts) {
7
+ const store = storeFor(ctx);
8
+ const entryWrites = requireGroup(store, 'entryWrites');
9
+ const write = { tenantId: ctx.tenantId, actor: ctx.actor.subject, ownScope: personalScopeOf(ctx.actor), ids, good };
10
+ const traceId = opts?.traceId;
11
+ // Recall traces live beside the last recall, so only a store with the lastRecall group can link one.
12
+ const appliedIds = traceId === undefined
13
+ ? await entryWrites.applyOutcome(write)
14
+ : await requireGroup(store, 'lastRecall').applyTracedOutcome(write, traceId);
15
+ return { applied: appliedIds.length, appliedIds };
14
16
  }
15
- export function outcomeForLastRecall(ctx, good) {
16
- return onStore(ctx, (_port, local) => {
17
- const ids = local.applyOutcomeToLastRecall({ tenantId: ctx.tenantId, actor: ctx.actor.subject, ownScope: personalScopeOf(ctx.actor) }, good);
18
- return { applied: ids.length, ids };
19
- });
17
+ export async function outcomeForLastRecall(ctx, good) {
18
+ const lastRecall = requireGroup(storeFor(ctx), 'lastRecall');
19
+ const ids = await lastRecall.applyOutcomeToLast({ tenantId: ctx.tenantId, actor: ctx.actor.subject, ownScope: personalScopeOf(ctx.actor) }, good);
20
+ return { applied: ids.length, ids };
20
21
  }
21
22
  //# sourceMappingURL=outcome.js.map
@@ -1,6 +1,7 @@
1
1
  import { predictionMirror } from '../store/predictions.js';
2
2
  import { loadConfig } from '../core/config.js';
3
- import { requireGroup, storeFor } from '../store/index.js';
3
+ import { requireGroup } from '../store/index.js';
4
+ import { storeFor } from '../store/sqlite/store.js';
4
5
  const predictionsOf = (ctx) => requireGroup(storeFor(ctx), 'predictions');
5
6
  export async function savePrediction(ctx, input) {
6
7
  return predictionsOf(ctx).savePrediction(ctx.tenantId, input, ctx.actor.subject);
@@ -1,6 +1,6 @@
1
1
  import type { ConnectorEvent } from '../store/port.js';
2
2
  import { type Layer } from '../core/memory.js';
3
- import type { Context, StoreReply } from './types.js';
3
+ import type { Context } from './types.js';
4
4
  /** Copies a local memory into the global store; the inner writeEntry emits 'remember' there, and we add a 'promote' audit event so intent stays distinct.
5
5
  * promoteToGlobal reads via `readEntry` with no tenant filter and keeps the entry's existing tenantId on the global side. */
6
6
  export interface PromoteResult {
@@ -21,7 +21,7 @@ export interface SupersedeOverrides {
21
21
  tags?: string[];
22
22
  pinned?: boolean;
23
23
  }
24
- export declare function supersede<C extends Context>(ctx: C, oldId: string, newContent: string, overrides?: SupersedeOverrides): StoreReply<C, SupersedeResult>;
24
+ export declare function supersede(ctx: Context, oldId: string, newContent: string, overrides?: SupersedeOverrides): Promise<SupersedeResult>;
25
25
  /** Archive a kind='raw' memory: its metadata moves to raw_archive and the row is
26
26
  * deleted. The store writes the one archive_raw audit row, under the caller's subject. */
27
27
  export interface ArchiveRawOpts {
@@ -32,5 +32,5 @@ export interface ArchiveRawResult {
32
32
  ok: true;
33
33
  archivedAt: string;
34
34
  }
35
- export declare function archiveRaw<C extends Context>(ctx: C, id: string, reason: string, opts?: ArchiveRawOpts): StoreReply<C, ArchiveRawResult>;
35
+ export declare function archiveRaw(ctx: Context, id: string, reason: string, opts?: ArchiveRawOpts): Promise<ArchiveRawResult>;
36
36
  //# sourceMappingURL=promote.d.ts.map
@@ -4,7 +4,8 @@ import { stampOriginProject } from '../store/entry-row.js';
4
4
  import { createSuccessor } from '../core/memory.js';
5
5
  import { promoteToGlobal } from '../sharing/global-store.js';
6
6
  import { loadConfig } from '../core/config.js';
7
- import { andThen, notPorted, onStore } from './on-store.js';
7
+ import { requireGroup } from '../store/index.js';
8
+ import { storeFor } from '../store/sqlite/store.js';
8
9
  import { memoryReach } from '../store/tenant-lookup.js';
9
10
  import { canTouchScope, personalScopeOf } from '../core/recall-scope.js';
10
11
  export function promote(ctx, id) {
@@ -22,23 +23,21 @@ export function promote(ctx, id) {
22
23
  });
23
24
  return { ok: true, sourceId: id, globalId: globalEntry.id };
24
25
  }
25
- export function supersede(ctx, oldId, newContent, overrides = {}) {
26
- return onStore(ctx, (port) => {
27
- const entryWrites = port.entryWrites ?? notPorted(port, 'entryWrites');
28
- return andThen(port.entriesByIds([oldId], ctx.tenantId), ([old]) => {
29
- const newEntry = createSuccessor(assertSupersedable(ctx, oldId, old ?? null), newContent, {
30
- tenantId: ctx.tenantId,
31
- baseHalfLifeDays: loadConfig(ctx.hippoRoot).defaultHalfLifeDays,
32
- layer: overrides.layer,
33
- tags: overrides.tags,
34
- pinned: overrides.pinned,
35
- });
36
- // A store writes origin_project as given, so the served folder's fallback is stamped here.
37
- const successor = stampOriginProject(ctx.hippoRoot, newEntry);
38
- const write = { tenantId: ctx.tenantId, actor: ctx.actor.subject, ownScope: personalScopeOf(ctx.actor), oldId, successor };
39
- return andThen(entryWrites.supersede(write), () => ({ ok: true, oldId, newId: successor.id }));
40
- });
26
+ export async function supersede(ctx, oldId, newContent, overrides = {}) {
27
+ const store = storeFor(ctx);
28
+ const entryWrites = requireGroup(store, 'entryWrites');
29
+ const [old] = await store.entriesByIds([oldId], ctx.tenantId);
30
+ const newEntry = createSuccessor(assertSupersedable(ctx, oldId, old ?? null), newContent, {
31
+ tenantId: ctx.tenantId,
32
+ baseHalfLifeDays: loadConfig(ctx.hippoRoot).defaultHalfLifeDays,
33
+ layer: overrides.layer,
34
+ tags: overrides.tags,
35
+ pinned: overrides.pinned,
41
36
  });
37
+ // A store writes origin_project as given, so the served folder's fallback is stamped here.
38
+ const successor = stampOriginProject(ctx.hippoRoot, newEntry);
39
+ await entryWrites.supersede({ tenantId: ctx.tenantId, actor: ctx.actor.subject, ownScope: personalScopeOf(ctx.actor), oldId, successor });
40
+ return { ok: true, oldId, newId: successor.id };
42
41
  }
43
42
  /** The tenant-scoped row to supersede; another tenant's id, or someone else's personal row, reads as not found. */
44
43
  function assertSupersedable(ctx, oldId, old) {
@@ -51,15 +50,14 @@ function assertSupersedable(ctx, oldId, old) {
51
50
  }
52
51
  return old;
53
52
  }
54
- export function archiveRaw(ctx, id, reason, opts = {}) {
55
- return onStore(ctx, (port) => {
56
- const entryWrites = port.entryWrites ?? notPorted(port, 'entryWrites');
57
- const archive = { tenantId: ctx.tenantId, actor: ctx.actor.subject, ownScope: personalScopeOf(ctx.actor), id, reason };
58
- const { event } = opts;
59
- const archived = event
60
- ? (port.connectorWrites ?? notPorted(port, 'connectorWrites')).archiveConnectorEntry({ ...archive, event })
61
- : entryWrites.archiveRaw(archive);
62
- return andThen(archived, (archivedAt) => ({ ok: true, archivedAt }));
63
- });
53
+ export async function archiveRaw(ctx, id, reason, opts = {}) {
54
+ const store = storeFor(ctx);
55
+ const entryWrites = requireGroup(store, 'entryWrites');
56
+ const archive = { tenantId: ctx.tenantId, actor: ctx.actor.subject, ownScope: personalScopeOf(ctx.actor), id, reason };
57
+ const { event } = opts;
58
+ const archivedAt = event
59
+ ? await requireGroup(store, 'connectorWrites').archiveConnectorEntry({ ...archive, event })
60
+ : await entryWrites.archiveRaw(archive);
61
+ return { ok: true, archivedAt };
64
62
  }
65
63
  //# sourceMappingURL=promote.js.map
@@ -1,5 +1,5 @@
1
1
  import { type Context, type ContextResult, type ContextResultEntry } from './index.js';
2
- import { type DeliveryEventType, type DeliveryRecorder } from '../store/delivery-recorder.js';
2
+ import { type DeliveryEventType, type DeliveryFault, type DeliveryRecorder } from '../store/delivery-recorder.js';
3
3
  import type { HookRuntime } from '../core/capture-contract.js';
4
4
  import { type JsonValue } from '../util/json.js';
5
5
  import type { MemoryEntry } from '../core/memory.js';
@@ -12,6 +12,8 @@ export interface DeliveryStart {
12
12
  eventType?: DeliveryEventType;
13
13
  /** The host's own session id from its environment, used when the payload names none. */
14
14
  envSessionId: string | undefined;
15
+ /** Passed through to the recorder; only tests set it. */
16
+ fault?: DeliveryFault;
15
17
  }
16
18
  /** A delivery recorder for a hook on `ctx.hippoRoot` when the ledger store enables one, else null; never throws. */
17
19
  export declare function startDeliveryRecorder(ctx: Context, start: DeliveryStart): DeliveryRecorder | null;
@@ -35,6 +35,7 @@ export function startDeliveryRecorder(ctx, start) {
35
35
  envSessionId: start.envSessionId,
36
36
  runtime: start.runtime === 'copilot' ? 'copilot' : undefined,
37
37
  eventType: start.eventType,
38
+ fault: start.fault,
38
39
  });
39
40
  }
40
41
  catch (error) {
@@ -1,6 +1,7 @@
1
1
  // Quarantine review: list, approve and reject held memories.
2
2
  import { ConflictError, ForbiddenError, NotFoundError } from '../core/api-errors.js';
3
- import { requireGroup, storeFor } from '../store/index.js';
3
+ import { requireGroup } from '../store/index.js';
4
+ import { storeFor } from '../store/sqlite/store.js';
4
5
  const QUARANTINE_PREVIEW_CHARS = 200;
5
6
  /** A tenant's quarantined memories, newest first. Default `status` is 'pending' (the review queue). */
6
7
  export async function quarantineList(ctx, opts = {}) {
@@ -5,7 +5,7 @@ import { decidePlanningFallacy, detectPlanningClaim } from '../predictions/plann
5
5
  import { rankRecall } from './recall-pipeline.js';
6
6
  import { writeRecallTraceAtRoot } from '../store/recall-trace.js';
7
7
  import { saveIndex } from '../store/index-and-stats.js';
8
- import { sqliteStore, storeFor } from '../store/index.js';
8
+ import { storeFor } from '../store/sqlite/store.js';
9
9
  import { estimateTokens } from '../util/token-text.js';
10
10
  import { callerOf, recallAuditMetadata, recallAuditRow, strengthenOf } from './recall-record.js';
11
11
  import { RECALL_RECORDING, recordShownRecall } from './recall-finish.js';
@@ -58,7 +58,7 @@ async function recordShown(ctx, store, opts, core, done) {
58
58
  const { ranking, shown, planningAudit } = done;
59
59
  const ids = shown.results.map((r) => r.entry.id);
60
60
  const recallRow = recallAuditRow(callerOf(ctx), 'recall', undefined, recallAuditMetadata(opts.query, ids.length));
61
- const second = core.sources?.globalRoot !== undefined && core.sources.globalRoot !== ctx.hippoRoot ? core.sources.globalRoot : undefined;
61
+ const second = secondStore(ctx, core);
62
62
  const heldElsewhere = second !== undefined ? await idsNotHeld(store, ids, ctx.tenantId) : [];
63
63
  const audited = await finishOrReport(store, {
64
64
  goalLog: ranking.goalRecallLog,
@@ -68,7 +68,7 @@ async function recordShown(ctx, store, opts, core, done) {
68
68
  if (audited) {
69
69
  // The second store logs that it was searched, and strengthens only the rows the first does not hold.
70
70
  if (second !== undefined)
71
- await finishOrReport(sqliteStore(second), { goalLog: [], audit: [recallRow], strengthen: strengthenOf(ctx, heldElsewhere) });
71
+ await finishOrReport(second, { goalLog: [], audit: [recallRow], strengthen: strengthenOf(ctx, heldElsewhere) });
72
72
  traceAndMark(ctx, opts, core, shown.results);
73
73
  }
74
74
  // The caller prints whether or not the record was written, so the ring and the ledger still take this list; `written` holds back the count.
@@ -83,6 +83,11 @@ async function recordShown(ctx, store, opts, core, done) {
83
83
  written: audited,
84
84
  });
85
85
  }
86
+ /** The second store's writes go where the first's do: the store named for its root, else hippo.db under that root. */
87
+ function secondStore(ctx, core) {
88
+ const root = core.sources?.globalRoot;
89
+ return root !== undefined && root !== ctx.hippoRoot ? storeFor({ hippoRoot: root, store: core.sources?.globalStore }) : undefined;
90
+ }
86
91
  // The caller has ranked and is about to print, so a failed audit write is logged and counted, never thrown.
87
92
  async function finishOrReport(store, writes) {
88
93
  try {
@@ -13,10 +13,10 @@ import { multihopSearch } from '../search/multihop.js';
13
13
  import { passesCliRecallScopeFilter } from '../core/recall-scope.js';
14
14
  import { currentEntries } from '../search/as-of.js';
15
15
  import { STRENGTH_RANK_FLOOR, STRENGTH_RANK_SPAN } from '../search/boosts.js';
16
- import { hybridSearch } from '../search/hybrid.js';
17
- import { physicsSearch } from '../search/physics-search.js';
16
+ import { hybridSearch, vectorSourceOf } from '../search/hybrid.js';
17
+ import { searchByEngine } from '../search/engine.js';
18
18
  import { rankBothStores } from '../sharing/search-both.js';
19
- import { loadRecallSearchEntries, recallScopeFilter } from '../store/search-rows.js';
19
+ import { loadRecallEntries, recallScopeFilter } from '../store/search-rows.js';
20
20
  import { isJsonNumber } from '../util/json.js';
21
21
  import { textOverlap, tokenize as tokenizeQuery } from '../util/tokenize.js';
22
22
  /** Ranks memories for a query as `hippo recall` does, through the `limit` slice. Reads stores, embeddings and physics state; writes and prints nothing
@@ -65,10 +65,9 @@ function loadRecallPool(ctx, opts) {
65
65
  // An explicit --scope unlocks that envelope scope; the regex-only `<source>:private:*` deny is the JS half below.
66
66
  const requested = opts.explicitScope || undefined;
67
67
  const loadSuperseded = opts.includeSuperseded || Boolean(opts.asOf);
68
- let local = loadRecallSearchEntries(ctx.hippoRoot, opts.query, undefined, ctx.tenantId, requested, 'additive', loadSuperseded);
69
- let global = ctx.globalRoot
70
- ? loadRecallSearchEntries(ctx.globalRoot, opts.query, undefined, ctx.tenantId, requested, 'additive', loadSuperseded)
71
- : [];
68
+ const load = { tenantId: ctx.tenantId, requestedScope: requested, explicitScopeMode: 'additive', includeSuperseded: loadSuperseded };
69
+ let local = loadRecallEntries(ctx.hippoRoot, opts.query, load);
70
+ let global = ctx.globalRoot ? loadRecallEntries(ctx.globalRoot, opts.query, load) : [];
72
71
  // SQL-excluded rows are pre-candidate, so the total is taken before the JS filters, which normally drop nothing.
73
72
  const total = local.length + global.length;
74
73
  const passes = (e) => passesCliRecallScopeFilter(e.scope ?? null, requested);
@@ -109,13 +108,6 @@ async function searchPool(ctx, opts, pool) {
109
108
  includeSuperseded: includeSuperseded || Boolean(asOf),
110
109
  admit: (e) => passesCliRecallScopeFilter(e.scope ?? null, requested),
111
110
  };
112
- if (search.usePhysics && !globalRoot) {
113
- // Explain leaves minResults and includeSuperseded out to hold its output steady; asOf must reach physics or later rows leak.
114
- const temporal = explain ? { asOf } : { minResults, includeSuperseded, asOf };
115
- return physicsSearch(query, pool.local, {
116
- budget, cost, hippoRoot: ctx.hippoRoot, physicsConfig: search.physicsConfig, scope, explain, vectorCandidates, ...temporal
117
- });
118
- }
119
111
  if (globalRoot) {
120
112
  // The pool already holds searchBothHybrid's additive-scope load for each store, so rank it rather than read both stores again.
121
113
  return rankBothStores(query, { local: ctx.hippoRoot, global: globalRoot }, { local: pool.local, global: pool.global }, vectorCandidates, {
@@ -123,9 +115,17 @@ async function searchPool(ctx, opts, pool) {
123
115
  includeSuperseded, asOf,
124
116
  });
125
117
  }
126
- return hybridSearch(query, pool.local, {
127
- budget, cost, hippoRoot: ctx.hippoRoot, explain, mmr, mmrLambda, minResults, scope, includeSuperseded, asOf, vectorCandidates
128
- });
118
+ const physics = search.usePhysics ? search.physicsConfig : null;
119
+ // Physics takes no MMR; explain drops minResults and includeSuperseded to hold its output steady; asOf must reach physics or later rows leak.
120
+ const tuning = !physics ? { mmr, mmrLambda, minResults, includeSuperseded, asOf } : explain ? { asOf } : { minResults, includeSuperseded, asOf };
121
+ return searchByEngine(hippoDbSource(ctx.hippoRoot), query, pool.local, physics, { budget, cost, scope, explain, vectorCandidates, ...tuning });
122
+ }
123
+ /** The CLI core holds no store: it ranks on hippo.db under its root, as its loads above do. */
124
+ function hippoDbSource(hippoRoot) {
125
+ const source = vectorSourceOf({ hippoRoot });
126
+ if (!source)
127
+ throw new Error('The CLI ranking core needs a hippo root to rank on');
128
+ return source;
129
129
  }
130
130
  /** Adds memories reached by walking the entity graph out from the lexical seeds. */
131
131
  function expandGraph(ctx, opts, graph, state) {
@@ -5,6 +5,7 @@ import type { PlanningFallacyHint, PlanningFallacyOutput, PlanningFallacyWatchin
5
5
  import type { AnchoringHint, RecallHistorySnapshot } from './recall-history.js';
6
6
  import type { AvailabilityHint } from './availability.js';
7
7
  import type { AppendAuditOpts } from '../store/audit.js';
8
+ import type { HippoStore } from '../store/port.js';
8
9
  import type { CallerProject } from './prompt-hook.js';
9
10
  import type { RankRecallOpts, RankRecallResult } from './recall-pipeline.js';
10
11
  import type { RecallRecording } from './recall-finish.js';
@@ -60,9 +61,10 @@ export interface RecallOpts {
60
61
  }
61
62
  interface CliCoreRanking {
62
63
  rank: Omit<RankRecallOpts, 'query' | 'sessionId' | 'goalTag'>;
63
- /** A second store searched beside `ctx.hippoRoot`. */
64
+ /** A second store searched beside `ctx.hippoRoot`; its recall rows go to `globalStore`, else hippo.db under `globalRoot`. */
64
65
  sources?: {
65
66
  globalRoot?: string;
67
+ globalStore?: HippoStore;
66
68
  };
67
69
  /** Receives each operator note when ranking reaches it. */
68
70
  note?: (line: string) => void;