hippo-memory 1.64.0 → 1.65.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 (64) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-memories/sync.js +3 -2
  3. package/dist/api/recall-record.d.ts +46 -0
  4. package/dist/api/recall-record.js +98 -0
  5. package/dist/api/recall-request.d.ts +32 -0
  6. package/dist/api/recall-request.js +121 -0
  7. package/dist/api/recall-types.d.ts +7 -1
  8. package/dist/api/recall.d.ts +3 -2
  9. package/dist/api/recall.js +174 -181
  10. package/dist/api/tokens.d.ts +1 -1
  11. package/dist/api/tokens.js +13 -17
  12. package/dist/cli/maintenance.d.ts +2 -1
  13. package/dist/cli/maintenance.js +17 -11
  14. package/dist/cli/recall.d.ts +1 -1
  15. package/dist/cli/recall.js +18 -49
  16. package/dist/cli/shared.js +2 -2
  17. package/dist/cli.d.ts +16 -0
  18. package/dist/cli.js +18 -1
  19. package/dist/config.d.ts +2 -0
  20. package/dist/config.js +4 -0
  21. package/dist/db/connect.d.ts +5 -0
  22. package/dist/db/connect.js +52 -0
  23. package/dist/db/open.d.ts +9 -11
  24. package/dist/db/open.js +23 -116
  25. package/dist/db/request-stores.d.ts +30 -0
  26. package/dist/db/request-stores.js +100 -0
  27. package/dist/db/sqlite-blocked.d.ts +6 -0
  28. package/dist/db/sqlite-blocked.js +11 -0
  29. package/dist/db.d.ts +3 -1
  30. package/dist/db.js +3 -1
  31. package/dist/embedding-provider.js +4 -3
  32. package/dist/embeddings.js +2 -1
  33. package/dist/goals.d.ts +18 -11
  34. package/dist/goals.js +38 -36
  35. package/dist/http-util.d.ts +2 -0
  36. package/dist/http-util.js +5 -0
  37. package/dist/local-embedding.d.ts +2 -0
  38. package/dist/local-embedding.js +20 -4
  39. package/dist/mcp/memory-tools.js +4 -1
  40. package/dist/mcp/recall-tools.js +28 -133
  41. package/dist/mcp/request.d.ts +7 -0
  42. package/dist/mcp/request.js +63 -41
  43. package/dist/mcp/session-state.d.ts +1 -3
  44. package/dist/mcp/session-state.js +3 -5
  45. package/dist/predictions/planning-fallacy.d.ts +52 -0
  46. package/dist/predictions/planning-fallacy.js +70 -55
  47. package/dist/recall-trace.d.ts +1 -1
  48. package/dist/recall-trace.js +5 -4
  49. package/dist/search/physics-search.js +4 -1
  50. package/dist/search/vector.js +2 -0
  51. package/dist/server/routes/recall.d.ts +1 -1
  52. package/dist/server/routes/recall.js +30 -218
  53. package/dist/server.d.ts +12 -1
  54. package/dist/server.js +33 -22
  55. package/dist/store/entry-writes.d.ts +2 -0
  56. package/dist/store/entry-writes.js +24 -16
  57. package/dist/store-port.d.ts +70 -1
  58. package/dist/store-port.js +87 -9
  59. package/dist/version.d.ts +1 -1
  60. package/dist/version.js +1 -1
  61. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  62. package/extensions/openclaw-plugin/package.json +1 -1
  63. package/openclaw.plugin.json +1 -1
  64. package/package.json +1 -1
@@ -3,7 +3,6 @@ import { envAnthropicApiKey } from '../env.js';
3
3
  import { loadAllEntries } from '../store/entry-reads.js';
4
4
  import { deduplicateStore } from '../dedupe.js';
5
5
  import { embedAll, loadEmbeddingIndex } from '../embeddings.js';
6
- import { resolveEmbeddingModel } from '../local-embedding.js';
7
6
  import { resolveEmbeddingProvider } from '../embedding-provider.js';
8
7
  import { resetAllPhysicsState } from '../physics-state.js';
9
8
  import { loadConfig } from '../config.js';
@@ -93,7 +92,7 @@ export function cmdDedup(hippoRoot, flags) {
93
92
  // ---------------------------------------------------------------------------
94
93
  // Embed command
95
94
  // ---------------------------------------------------------------------------
96
- export async function cmdEmbed(hippoRoot, flags) {
95
+ export async function cmdEmbed(hippoRoot, flags, given) {
97
96
  // --global mirrors resolveAuthRoot (cli.ts:6900): initGlobal() + the global
98
97
  // root, skipping the local requireInit entirely, so this is the healing
99
98
  // path for pre-1.27.0 global stores from a directory with no local .hippo.
@@ -109,12 +108,13 @@ export async function cmdEmbed(hippoRoot, flags) {
109
108
  printEmbedStatus(root);
110
109
  return;
111
110
  }
112
- if (!embedProviderReady(root))
111
+ const provider = readyEmbedProvider(root, given);
112
+ if (!provider)
113
113
  return;
114
114
  console.log('Embedding all memories (this may take a moment on first run to download model)...');
115
115
  let count;
116
116
  try {
117
- count = await embedAll(root, resolveEmbeddingModel(root));
117
+ count = await embedAll(root, undefined, provider);
118
118
  }
119
119
  catch (err) {
120
120
  printError(`Embedding failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -126,6 +126,11 @@ export async function cmdEmbed(hippoRoot, flags) {
126
126
  const entriesAfter = loadAllEntries(root);
127
127
  const embIndexAfter = loadEmbeddingIndex(root);
128
128
  console.log(`Done. ${count} new embeddings created. ${Object.keys(embIndexAfter).length}/${entriesAfter.length} total.`);
129
+ const unembedded = entriesAfter.filter((e) => !embIndexAfter[e.id]).length;
130
+ if (unembedded > 0) {
131
+ printError(`${unembedded} memories are still not embedded (the warnings above name them). Re-run \`hippo embed\` to retry.`);
132
+ process.exitCode = 1;
133
+ }
129
134
  }
130
135
  function resetPhysics(root) {
131
136
  const entries = loadAllEntries(root);
@@ -154,10 +159,10 @@ function printEmbedStatus(root) {
154
159
  console.log(` ${missing.length} memories need embedding (run \`hippo embed\` to embed them)`);
155
160
  }
156
161
  }
157
- /** False, after saying why, when no usable provider exists. */
158
- function embedProviderReady(root) {
162
+ /** The provider to embed with, or null after saying why none is usable. */
163
+ function readyEmbedProvider(root, given) {
159
164
  // Embedding (unlike status/reset) needs an available provider.
160
- const embedProvider = (() => {
165
+ const embedProvider = given ?? (() => {
161
166
  try {
162
167
  return resolveEmbeddingProvider(root);
163
168
  }
@@ -168,23 +173,24 @@ function embedProviderReady(root) {
168
173
  })();
169
174
  if (!embedProvider) {
170
175
  process.exitCode = 1;
171
- return false;
176
+ return null;
172
177
  }
173
178
  if (embedProvider.isAvailable())
174
- return true;
179
+ return embedProvider;
175
180
  if (loadConfig(root).embeddings.enabled === false) {
176
181
  console.log('Embeddings are disabled in config (embeddings.enabled = false). Set it to true or "auto" to enable.');
177
- return false;
182
+ return null;
178
183
  }
179
184
  if (embedProvider.kind === 'local') {
180
185
  console.log('Embeddings not available. Install @huggingface/transformers to enable:');
181
186
  console.log(' npm install @huggingface/transformers');
187
+ process.exitCode = 1;
182
188
  }
183
189
  else {
184
190
  printError(`Embedding provider '${embedProvider.kind}' is configured but ${embedProvider.keyEnv} is not set.`);
185
191
  printError(`Export ${embedProvider.keyEnv}, or set config.embeddings.provider back to 'local'.`);
186
192
  process.exitCode = 1;
187
193
  }
188
- return false;
194
+ return null;
189
195
  }
190
196
  //# sourceMappingURL=maintenance.js.map
@@ -1,5 +1,5 @@
1
1
  import { type CliFlags, type CommandContext } from './shared.js';
2
- /** Test-only: reset the module-level recall-history Map. Call from beforeEach. */
2
+ /** Test-only: reset the CLI recall rings. Call from beforeEach. */
3
3
  export declare function __resetSessionRecallHistoryCli(): void;
4
4
  /** Runs `hippo recall`: parse, rank, load continuity, fit the printed block to the budget, audit, then write and print. */
5
5
  export declare function cmdRecall(hippoRoot: string, query: string, flags: CliFlags): Promise<void>;
@@ -20,10 +20,10 @@ import { dropHeldCopies } from '../same-text.js';
20
20
  import { isGlobalStoreRoot } from '../project-identity.js';
21
21
  import { detectScope } from '../scope.js';
22
22
  import { getGlobalRoot } from '../shared.js';
23
- import { auditQueryFields } from '../audit.js';
24
23
  import * as api from '../api.js';
25
24
  import { computePlanningFallacyOutput } from '../predictions/planning-fallacy.js';
26
- import { detectAnchoring, hashQueryText, biasHintEnabled, buildSessionKey, getOrCreateRing, appendRecall, snapshotRing, } from '../recall-history.js';
25
+ import { detectAnchoring, hashQueryText, biasHintEnabled, snapshotRing } from '../recall-history.js';
26
+ import { appendRecallAudit, recallAuditMetadata, recordShownRecall, resetSessionRings, sessionRing } from '../api/recall-record.js';
27
27
  import { detectAvailabilityBias } from '../availability.js';
28
28
  import { resolveTenantId } from '../tenant.js';
29
29
  import { MAX_HOPS, DEFAULT_MAX_NEIGHBORS } from '../graph-recall.js';
@@ -34,13 +34,12 @@ import { isClefModel } from '../rerankers/clef.js';
34
34
  import { handoffText, printedTokens, sessionTrailText, settleTokens, snapshotText } from '../context-render.js';
35
35
  import { withLedgerDb } from '../ledger-db.js';
36
36
  import { printError } from './output.js';
37
- import { parseLimitFlag, parseBudgetFlag, emitCliAudit, requireInit, recallEntryText, recallHeading, parseAsOfFlag, engineFlags, printActiveTaskSnapshot, printSessionEvents, printHandoff, hostSessionId, captureConsole, hookStoreRoot, } from './shared.js';
37
+ import { parseLimitFlag, parseBudgetFlag, requireInit, recallEntryText, recallHeading, parseAsOfFlag, engineFlags, printActiveTaskSnapshot, printSessionEvents, printHandoff, hostSessionId, captureConsole, hookStoreRoot, } from './shared.js';
38
38
  // Per-process rings: a single-shot `hippo recall` starts empty, so anchoring only accumulates in long-lived
39
39
  // hosts (in-process loops, `hippo serve`, the MCP server).
40
- const sessionRecallHistoryCli = new Map();
41
- /** Test-only: reset the module-level recall-history Map. Call from beforeEach. */
40
+ /** Test-only: reset the CLI recall rings. Call from beforeEach. */
42
41
  export function __resetSessionRecallHistoryCli() {
43
- sessionRecallHistoryCli.clear();
42
+ resetSessionRings('cli');
44
43
  }
45
44
  // JSON.stringify keeps quotes or parens in the matched phrase from blurring the line.
46
45
  function planningLine(p) {
@@ -185,7 +184,7 @@ export async function cmdRecall(hippoRoot, query, flags) {
185
184
  const o = parseRecallOptions(hippoRoot, flags);
186
185
  const ranked = await rankForRecall(hippoRoot, query, flags, o);
187
186
  const fit = fitRecallBlock(hippoRoot, query, o, ranked, loadRecallContinuity(hippoRoot, o));
188
- auditRecall(hippoRoot, o.globalRoot, query, fit);
187
+ auditRecall(hippoRoot, o, query, fit);
189
188
  writeRecallResult(hippoRoot, query, o, fit, ranked.localIndex);
190
189
  }
191
190
  /** Every flag recall reads, parsed in the order the single-body command checked them. */
@@ -281,9 +280,7 @@ function loadRecallContinuity(hippoRoot, o) {
281
280
  function recallHinter(query, o, rank) {
282
281
  const { tenantId, sessionId } = o;
283
282
  // HIPPO_ANCHORING=off and HIPPO_AVAILABILITY=off skip the work entirely.
284
- const anchorRing = biasHintEnabled('anchoring') && sessionId
285
- ? getOrCreateRing(sessionRecallHistoryCli, buildSessionKey(tenantId, sessionId))
286
- : null;
283
+ const anchorRing = sessionRing('cli', tenantId, sessionId);
287
284
  const queryHash = hashQueryText(query);
288
285
  const availabilityPool = biasHintEnabled('availability')
289
286
  ? [...rank.localEntries, ...rank.globalEntries].map((e) => ({ id: e.id, created: e.created }))
@@ -304,7 +301,7 @@ function recallHinter(query, o, rank) {
304
301
  });
305
302
  return { anchoring, availability, summary };
306
303
  };
307
- return { anchorRing, queryHash, hintsFor };
304
+ return { anchorRing, hintsFor };
308
305
  }
309
306
  function renderRecallBlock(list, h, v) {
310
307
  const { query, showWhy, showPlan, planText, hasContinuity, entryText } = v;
@@ -388,7 +385,7 @@ function fitRecallBlock(hippoRoot, query, o, ranked, loaded) {
388
385
  const shown = (n) => dropHeldCopies(fitted.slice(0, n), (r) => r.entry);
389
386
  let kept = fitted.length;
390
387
  let results = shown(kept);
391
- const { anchorRing, queryHash, hintsFor } = recallHinter(query, o, ranked.rank);
388
+ const { anchorRing, hintsFor } = recallHinter(query, o, ranked.rank);
392
389
  const view = { query, showWhy: o.showWhy, showPlan, planText, hasContinuity, continuity, entryText: ranked.entryText };
393
390
  let hints = hintsFor(results, kept - results.length);
394
391
  let recallText = renderRecallBlock(results, hints, view);
@@ -399,45 +396,17 @@ function fitRecallBlock(hippoRoot, query, o, ranked, loaded) {
399
396
  hints = hintsFor(results, kept - results.length);
400
397
  recallText = renderRecallBlock(results, hints, view);
401
398
  }
402
- return { results, hints, recallText, continuity, continuityTokens, cmdPlanningFallacyHint, cmdPlanningFallacyWatching, anchorRing, queryHash };
399
+ return { results, hints, recallText, continuity, continuityTokens, cmdPlanningFallacyHint, cmdPlanningFallacyWatching, anchorRing };
403
400
  }
404
- function auditRecall(hippoRoot, globalRoot, query, fit) {
405
- const { results, anchorRing, queryHash } = fit;
406
- const { anchoring: cmdAnchoringHint, availability: cmdAvailabilityHint } = fit.hints;
407
- if (anchorRing) {
408
- // Appended after every detect: anchoredOn feeds the cooldown for the next recall on this session.
409
- appendRecall(anchorRing, queryHash, results[0]?.entry.id ?? null, cmdAnchoringHint?.memoryId);
410
- }
411
- else if (biasHintEnabled('anchoring')) {
412
- // SHA-256/16 per the recall-audit convention; hashQueryText is FNV-1a and brute-forceable on short queries.
413
- emitCliAudit(hippoRoot, 'recall_anchor_skipped_no_session', undefined, auditQueryFields(query));
414
- }
415
- if (cmdAnchoringHint?.reason === 'memory_dominance') {
416
- emitCliAudit(hippoRoot, 'recall_anchor_detected_memory_dominance', cmdAnchoringHint.memoryId, {
417
- memory_id: cmdAnchoringHint.memoryId,
418
- query_count: cmdAnchoringHint.queryCount ?? null,
419
- });
420
- }
421
- else if (cmdAnchoringHint?.reason === 'query_repeat') {
422
- emitCliAudit(hippoRoot, 'recall_anchor_detected_query_repeat', cmdAnchoringHint.memoryId, {
423
- memory_id: cmdAnchoringHint.memoryId,
424
- });
425
- }
426
- if (cmdAvailabilityHint) {
427
- emitCliAudit(hippoRoot, 'recall_availability_detected', undefined, {
428
- recent_fraction: cmdAvailabilityHint.recentFraction,
429
- older_passed_over: cmdAvailabilityHint.olderCandidatesPassedOver,
430
- returned_count: cmdAvailabilityHint.returnedCount,
431
- });
432
- }
401
+ function auditRecall(hippoRoot, o, query, fit) {
402
+ const { results, hints } = fit;
403
+ const who = { hippoRoot, tenantId: o.tenantId, actor: 'cli', bestEffort: true };
404
+ recordShownRecall(who, { query, ring: fit.anchorRing, topId: results[0]?.entry.id ?? null, anchoring: hints.anchoring, availability: hints.availability });
433
405
  // One 'recall' event per query, before the early-empty return, in every participating store.
434
- const recallMetadata = {
435
- ...auditQueryFields(query),
436
- results: results.length,
437
- };
438
- emitCliAudit(hippoRoot, 'recall', undefined, recallMetadata);
439
- if (isInitialized(globalRoot) && globalRoot !== hippoRoot) {
440
- emitCliAudit(globalRoot, 'recall', undefined, recallMetadata);
406
+ const metadata = recallAuditMetadata(query, results.length);
407
+ appendRecallAudit(who, 'recall', undefined, metadata);
408
+ if (isInitialized(o.globalRoot) && o.globalRoot !== hippoRoot) {
409
+ appendRecallAudit({ ...who, hippoRoot: o.globalRoot }, 'recall', undefined, metadata);
441
410
  }
442
411
  }
443
412
  /** Traces the recall, books retrieval for a non-empty list, then prints JSON or the fitted block. */
@@ -10,7 +10,7 @@ import { confidenceLabel } from '../memory.js';
10
10
  import { isInitialized } from '../store/open.js';
11
11
  import { explainMatch } from '../search/explain.js';
12
12
  import { isSharedStore } from '../config.js';
13
- import { openHippoDb, closeHippoDb, isSqliteBusy, noteStoreBusy, withSharedStoreHandles, HOOK_DB_WAIT_MS } from '../db.js';
13
+ import { openHippoDb, closeHippoDb, isSqliteBusy, noteStoreBusy, runWithRequestStores, HOOK_DB_WAIT_MS } from '../db.js';
14
14
  import { withLedgerDb } from '../ledger-db.js';
15
15
  import { sessionPilotArm } from '../pilot-arm.js';
16
16
  import { hookPayloadSessionId, isSubagentPayload, recordTokenUse } from '../token-ledger.js';
@@ -474,7 +474,7 @@ export function resolveAuthRoot(hippoRoot, flags) {
474
474
  /** Hook commands share one handle per store and wait at most HOOK_DB_WAIT_MS for a lock; a store still busy after that skips the hook's work with one warning, exit 0. */
475
475
  export async function runHookWithStores(fn) {
476
476
  try {
477
- return await withSharedStoreHandles(fn, { busyWaitMs: HOOK_DB_WAIT_MS });
477
+ return await runWithRequestStores(fn, { busyWaitMs: HOOK_DB_WAIT_MS, failFastWhenBusy: true });
478
478
  }
479
479
  catch (error) {
480
480
  if (!isSqliteBusy(error))
package/dist/cli.d.ts CHANGED
@@ -54,14 +54,17 @@ export declare const COMMANDS: {
54
54
  };
55
55
  recall: {
56
56
  run: (c: CommandContext) => Promise<void>;
57
+ scoped: true;
57
58
  usage: string[];
58
59
  };
59
60
  drill: {
60
61
  run: (c: CommandContext) => Promise<void>;
62
+ scoped: true;
61
63
  usage: string[];
62
64
  };
63
65
  assemble: {
64
66
  run: (c: CommandContext) => Promise<void>;
67
+ scoped: true;
65
68
  usage: string[];
66
69
  };
67
70
  supersede: {
@@ -70,6 +73,7 @@ export declare const COMMANDS: {
70
73
  };
71
74
  explain: {
72
75
  run: (c: CommandContext) => Promise<void>;
76
+ scoped: true;
73
77
  usage: string[];
74
78
  };
75
79
  eval: {
@@ -86,6 +90,7 @@ export declare const COMMANDS: {
86
90
  };
87
91
  sleep: {
88
92
  run: ({ hippoRoot, flags }: CommandContext) => Promise<void>;
93
+ scoped: true;
89
94
  usage: string[];
90
95
  };
91
96
  'last-sleep': {
@@ -134,10 +139,12 @@ export declare const COMMANDS: {
134
139
  };
135
140
  auth: {
136
141
  run: ({ hippoRoot, args, flags }: CommandContext) => Promise<void>;
142
+ scoped: true;
137
143
  usage: string[];
138
144
  };
139
145
  goal: {
140
146
  run: ({ hippoRoot, args, flags }: CommandContext) => Promise<void>;
147
+ scoped: true;
141
148
  usage: string[];
142
149
  };
143
150
  slack: {
@@ -150,6 +157,7 @@ export declare const COMMANDS: {
150
157
  };
151
158
  audit: {
152
159
  run: (c: CommandContext) => Promise<void>;
160
+ scoped: true;
153
161
  usage: string[];
154
162
  };
155
163
  'correction-latency': {
@@ -166,6 +174,7 @@ export declare const COMMANDS: {
166
174
  };
167
175
  outcome: {
168
176
  run: ({ hippoRoot, flags }: CommandContext) => Promise<void>;
177
+ scoped: true;
169
178
  usage: string[];
170
179
  };
171
180
  conflicts: {
@@ -190,6 +199,7 @@ export declare const COMMANDS: {
190
199
  };
191
200
  dormant: {
192
201
  run: ({ hippoRoot, args, flags }: CommandContext) => Promise<void>;
202
+ scoped: true;
193
203
  usage: string[];
194
204
  };
195
205
  projects: {
@@ -198,14 +208,17 @@ export declare const COMMANDS: {
198
208
  };
199
209
  quarantine: {
200
210
  run: ({ hippoRoot, args, flags }: CommandContext) => Promise<void>;
211
+ scoped: true;
201
212
  usage: string[];
202
213
  };
203
214
  tokens: {
204
215
  run: ({ hippoRoot, flags }: CommandContext) => Promise<void>;
216
+ scoped: true;
205
217
  usage: string[];
206
218
  };
207
219
  failures: {
208
220
  run: ({ hippoRoot, flags }: CommandContext) => Promise<void>;
221
+ scoped: true;
209
222
  usage: string[];
210
223
  };
211
224
  doctor: {
@@ -242,6 +255,7 @@ export declare const COMMANDS: {
242
255
  };
243
256
  forget: {
244
257
  run: (c: CommandContext) => Promise<void>;
258
+ scoped: true;
245
259
  usage: string[];
246
260
  };
247
261
  inspect: {
@@ -274,10 +288,12 @@ export declare const COMMANDS: {
274
288
  };
275
289
  learn: {
276
290
  run: ({ hippoRoot, flags }: CommandContext) => Promise<void>;
291
+ scoped: true;
277
292
  usage: string[];
278
293
  };
279
294
  promote: {
280
295
  run: (c: CommandContext) => Promise<void>;
296
+ scoped: true;
281
297
  usage: string[];
282
298
  };
283
299
  sync: {
package/dist/cli.js CHANGED
@@ -209,14 +209,17 @@ export const COMMANDS = {
209
209
  },
210
210
  recall: {
211
211
  run: async (c) => { await (await import('./cli/recall.js')).handleRecall(c); },
212
+ scoped: true,
212
213
  usage: VERB_USAGE.recall,
213
214
  },
214
215
  drill: {
215
216
  run: async (c) => { await (await import('./cli/dag.js')).handleDrill(c); },
217
+ scoped: true,
216
218
  usage: VERB_USAGE.drill,
217
219
  },
218
220
  assemble: {
219
221
  run: async (c) => { await (await import('./cli/dag.js')).handleAssemble(c); },
222
+ scoped: true,
220
223
  usage: VERB_USAGE.assemble,
221
224
  },
222
225
  supersede: {
@@ -225,6 +228,7 @@ export const COMMANDS = {
225
228
  },
226
229
  explain: {
227
230
  run: async (c) => { await (await import('./cli/explain.js')).handleExplain(c); },
231
+ scoped: true,
228
232
  usage: VERB_USAGE.explain,
229
233
  },
230
234
  eval: {
@@ -241,6 +245,7 @@ export const COMMANDS = {
241
245
  },
242
246
  sleep: {
243
247
  run: async ({ hippoRoot, flags }) => { await (await import('./cli/sleep.js')).cmdSleep(hippoRoot, flags); },
248
+ scoped: true,
244
249
  usage: VERB_USAGE.sleep,
245
250
  },
246
251
  'last-sleep': {
@@ -289,10 +294,12 @@ export const COMMANDS = {
289
294
  },
290
295
  auth: {
291
296
  run: async ({ hippoRoot, args, flags }) => { (await import('./cli/auth.js')).cmdAuth(hippoRoot, args, flags); },
297
+ scoped: true,
292
298
  usage: VERB_USAGE.auth,
293
299
  },
294
300
  goal: {
295
301
  run: async ({ hippoRoot, args, flags }) => { (await import('./cli/goals.js')).cmdGoal(hippoRoot, args, flags); },
302
+ scoped: true,
296
303
  usage: VERB_USAGE.goal,
297
304
  },
298
305
  slack: {
@@ -305,6 +312,7 @@ export const COMMANDS = {
305
312
  },
306
313
  audit: {
307
314
  run: async (c) => { await (await import('./cli/audit.js')).handleAudit(c); },
315
+ scoped: true,
308
316
  usage: VERB_USAGE.audit,
309
317
  },
310
318
  'correction-latency': {
@@ -321,6 +329,7 @@ export const COMMANDS = {
321
329
  },
322
330
  outcome: {
323
331
  run: async ({ hippoRoot, flags }) => { (await import('./cli/curate.js')).cmdOutcome(hippoRoot, flags); },
332
+ scoped: true,
324
333
  usage: VERB_USAGE.outcome,
325
334
  },
326
335
  conflicts: {
@@ -345,6 +354,7 @@ export const COMMANDS = {
345
354
  },
346
355
  dormant: {
347
356
  run: async ({ hippoRoot, args, flags }) => { (await import('./cli/curate.js')).cmdDormant(hippoRoot, args, flags); },
357
+ scoped: true,
348
358
  usage: VERB_USAGE.dormant,
349
359
  },
350
360
  projects: {
@@ -353,14 +363,17 @@ export const COMMANDS = {
353
363
  },
354
364
  quarantine: {
355
365
  run: async ({ hippoRoot, args, flags }) => { (await import('./cli/curate.js')).cmdQuarantine(hippoRoot, args, flags); },
366
+ scoped: true,
356
367
  usage: VERB_USAGE.quarantine,
357
368
  },
358
369
  tokens: {
359
370
  run: async ({ hippoRoot, flags }) => { (await import('./cli/status.js')).cmdTokens(hippoRoot, flags); },
371
+ scoped: true,
360
372
  usage: VERB_USAGE.tokens,
361
373
  },
362
374
  failures: {
363
375
  run: async ({ hippoRoot, flags }) => { (await import('./cli/status.js')).cmdFailures(hippoRoot, flags); },
376
+ scoped: true,
364
377
  usage: VERB_USAGE.failures,
365
378
  },
366
379
  doctor: {
@@ -397,6 +410,7 @@ export const COMMANDS = {
397
410
  },
398
411
  forget: {
399
412
  run: async (c) => { await (await import('./cli/curate.js')).handleForget(c); },
413
+ scoped: true,
400
414
  usage: VERB_USAGE.forget,
401
415
  },
402
416
  inspect: {
@@ -429,10 +443,12 @@ export const COMMANDS = {
429
443
  },
430
444
  learn: {
431
445
  run: async ({ hippoRoot, flags }) => { (await import('./cli/transfer.js')).cmdLearn(hippoRoot, flags); },
446
+ scoped: true,
432
447
  usage: VERB_USAGE.learn,
433
448
  },
434
449
  promote: {
435
450
  run: async (c) => { await (await import('./cli/transfer.js')).handlePromote(c); },
451
+ scoped: true,
436
452
  usage: VERB_USAGE.promote,
437
453
  },
438
454
  sync: {
@@ -630,7 +646,8 @@ async function main(command, args, flags, hippoRoot) {
630
646
  printUsage();
631
647
  process.exit(1);
632
648
  }
633
- await spec.run({ hippoRoot, args, flags });
649
+ const run = () => spec.run({ hippoRoot, args, flags });
650
+ await (spec.scoped ? (await import('./db/request-stores.js')).runWithRequestStores(run) : run());
634
651
  }
635
652
  export async function runCli(argv = process.argv) {
636
653
  const { command, args, flags } = parseArgs(argv);
package/dist/config.d.ts CHANGED
@@ -159,6 +159,8 @@ export declare function loadConfig(hippoRoot: string): HippoConfig;
159
159
  /** True when the store's config.json sets `"sharedStore": true`. Once true, a root stays true for this process,
160
160
  * so a broken edit while a server runs cannot turn it off. Reads only this key: loadConfig has no cache. */
161
161
  export declare function isSharedStore(hippoRoot: string): boolean;
162
+ /** Reads the root as shared for the rest of this process, whatever its config.json says. */
163
+ export declare function markSharedStore(hippoRoot: string): void;
162
164
  /** Test seam: forget every root seen as shared. */
163
165
  export declare function _resetSharedStoreCacheForTests(): void;
164
166
  //# sourceMappingURL=config.d.ts.map
package/dist/config.js CHANGED
@@ -321,6 +321,10 @@ export function isSharedStore(hippoRoot) {
321
321
  sharedStoreRoots.add(key);
322
322
  return true;
323
323
  }
324
+ /** Reads the root as shared for the rest of this process, whatever its config.json says. */
325
+ export function markSharedStore(hippoRoot) {
326
+ sharedStoreRoots.add(sharedStoreKey(hippoRoot));
327
+ }
324
328
  /** Test seam: forget every root seen as shared. */
325
329
  export function _resetSharedStoreCacheForTests() {
326
330
  sharedStoreRoots.clear();
@@ -0,0 +1,5 @@
1
+ import { type DatabaseSyncLike } from './sqlite.js';
2
+ export declare function getHippoDbPath(hippoRoot: string): string;
3
+ /** A new connection with the store's pragmas, migrations and mirror cleanup applied; the caller owns and closes it. */
4
+ export declare function connectHippoDb(hippoRoot: string, busyWaitMs?: number): DatabaseSyncLike;
5
+ //# sourceMappingURL=connect.d.ts.map
@@ -0,0 +1,52 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { cleanupArchivedMirrors } from '../raw-archive-mirror-cleanup.js';
4
+ import { log } from '../log.js';
5
+ import { DatabaseSync } from './sqlite.js';
6
+ import { execWithBusyRetry } from './busy.js';
7
+ import { runMigrations } from './migrate.js';
8
+ export function getHippoDbPath(hippoRoot) {
9
+ return path.join(hippoRoot, 'hippo.db');
10
+ }
11
+ // Owner-only on create; SQLite gives the WAL and SHM files the db's mode. Existing paths keep theirs.
12
+ function createStoreFilesOwnerOnly(hippoRoot) {
13
+ fs.mkdirSync(hippoRoot, { recursive: true, mode: 0o700 });
14
+ try {
15
+ fs.closeSync(fs.openSync(getHippoDbPath(hippoRoot), 'wx', 0o600));
16
+ }
17
+ catch (err) {
18
+ if (!(err instanceof Error && 'code' in err && err.code === 'EEXIST'))
19
+ throw err;
20
+ }
21
+ }
22
+ /** A new connection with the store's pragmas, migrations and mirror cleanup applied; the caller owns and closes it. */
23
+ export function connectHippoDb(hippoRoot, busyWaitMs) {
24
+ createStoreFilesOwnerOnly(hippoRoot);
25
+ const db = new DatabaseSync(getHippoDbPath(hippoRoot));
26
+ try {
27
+ db.exec(`PRAGMA busy_timeout = ${busyWaitMs ?? 5000}`);
28
+ execWithBusyRetry(db, 'PRAGMA journal_mode = WAL', busyWaitMs);
29
+ db.exec('PRAGMA synchronous = NORMAL');
30
+ db.exec('PRAGMA wal_autocheckpoint = 100');
31
+ db.exec('PRAGMA foreign_keys = ON');
32
+ runMigrations(db, hippoRoot, busyWaitMs);
33
+ // Orphan mirrors of archived raw rows go here; a filesystem failure must not block the open.
34
+ try {
35
+ cleanupArchivedMirrors(hippoRoot, db);
36
+ }
37
+ catch (cleanupErr) {
38
+ log.error(`openHippoDb: cleanupArchivedMirrors failed (non-fatal): ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`);
39
+ }
40
+ return db;
41
+ }
42
+ catch (error) {
43
+ try {
44
+ db.close();
45
+ }
46
+ catch {
47
+ // Best effort only.
48
+ }
49
+ throw error;
50
+ }
51
+ }
52
+ //# sourceMappingURL=connect.js.map
package/dist/db/open.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import { type DatabaseSyncLike } from './sqlite.js';
2
- export declare function getHippoDbPath(hippoRoot: string): string;
2
+ import { getHippoDbPath } from './connect.js';
3
+ export { getHippoDbPath };
3
4
  /** Lock wait for hook commands: under the 5 s prompt-hook budget even after a few skipped writes, and far above a normal write's hold. */
4
5
  export declare const HOOK_DB_WAIT_MS = 1000;
6
+ /** Lock wait inside an HTTP request: SQLite waits synchronously, so a long wait would stall every other request on the event loop. */
7
+ export declare const SERVER_DB_WAIT_MS = 250;
5
8
  /** A busy store made a command skip work: warn once per process (the holder is usually `hippo sleep`). */
6
9
  export declare function noteStoreBusy(skipped: string): void;
7
10
  /** Runs `fn` with one handle per store: openHippoDb reuses it and closeHippoDb leaves it open until `fn` settles or the process exits.
@@ -9,21 +12,16 @@ export declare function noteStoreBusy(skipped: string): void;
9
12
  export declare function withSharedStoreHandles<T>(fn: () => T | Promise<T>, opts?: {
10
13
  busyWaitMs?: number;
11
14
  }): Promise<T>;
12
- /** Lock wait inside an HTTP request: SQLite waits synchronously, so a long wait would stall every other request on the event loop. */
13
- export declare const SERVER_DB_WAIT_MS = 250;
14
15
  /** Lock wait for each of sleep's short write transactions, even inside a server request: a run stops only on a writer that holds the lock longer. */
15
16
  export declare const SLEEP_DB_WAIT_MS = 5000;
16
- /** Runs `fn` so that every store opened inside it, across awaits, waits at most `busyWaitMs` for a lock unless the open passes its own. */
17
- export declare function withBusyWait<T>(busyWaitMs: number, fn: () => T): T;
18
- /** The lock wait an open here would get without its own `busyWaitMs`, in openHippoDb's order; undefined outside both scopes. */
17
+ /** The lock wait an open here would get without its own `busyWaitMs`: the request scope's; undefined outside every scope. */
19
18
  export declare function scopedBusyWait(): number | undefined;
20
- /** Thrown by a hippo.db open inside a request served from another store: the code path is not ported to the store port yet. */
21
- export declare class SqliteBlockedError extends Error {
22
- readonly storeKind: string;
23
- constructor(storeKind: string);
24
- }
25
19
  /** Runs `fn` so that every hippo.db open inside it, across awaits, throws; otherwise a missed port would create and write a hippo.db nobody reads. */
26
20
  export declare function withSqliteBlocked<T>(storeKind: string, fn: () => T): T;
21
+ /** Runs `fn` with hippo.db opens allowed again, for a store whose own methods are backed by hippo.db. */
22
+ export declare function withSqliteAllowed<T>(fn: () => T): T;
23
+ /** First line of a best-effort catch around a hippo.db open: an unported path must fail closed, not fall back silently. */
24
+ export declare function rethrowIfSqliteBlocked<E>(err: E): void;
27
25
  /** `busyWaitMs` shortens every lock wait of this open, for a hook that must finish inside its own timeout. */
28
26
  export declare function openHippoDb(hippoRoot: string, opts?: {
29
27
  busyWaitMs?: number;