hippo-memory 1.64.1 → 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.
- package/dist/agent-memories/sync.js +3 -2
- package/dist/api/recall-record.d.ts +46 -0
- package/dist/api/recall-record.js +98 -0
- package/dist/api/recall-request.d.ts +32 -0
- package/dist/api/recall-request.js +121 -0
- package/dist/api/recall-types.d.ts +7 -1
- package/dist/api/recall.d.ts +3 -2
- package/dist/api/recall.js +174 -181
- package/dist/api/tokens.d.ts +1 -1
- package/dist/api/tokens.js +13 -17
- package/dist/cli/recall.d.ts +1 -1
- package/dist/cli/recall.js +18 -49
- package/dist/cli/shared.js +2 -2
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +18 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +4 -0
- package/dist/db/connect.d.ts +5 -0
- package/dist/db/connect.js +52 -0
- package/dist/db/open.d.ts +9 -11
- package/dist/db/open.js +23 -116
- package/dist/db/request-stores.d.ts +30 -0
- package/dist/db/request-stores.js +100 -0
- package/dist/db/sqlite-blocked.d.ts +6 -0
- package/dist/db/sqlite-blocked.js +11 -0
- package/dist/db.d.ts +3 -1
- package/dist/db.js +3 -1
- package/dist/embeddings.js +2 -1
- package/dist/goals.d.ts +18 -11
- package/dist/goals.js +38 -36
- package/dist/http-util.d.ts +2 -0
- package/dist/http-util.js +5 -0
- package/dist/mcp/memory-tools.js +4 -1
- package/dist/mcp/recall-tools.js +28 -133
- package/dist/mcp/request.d.ts +7 -0
- package/dist/mcp/request.js +63 -41
- package/dist/mcp/session-state.d.ts +1 -3
- package/dist/mcp/session-state.js +3 -5
- package/dist/predictions/planning-fallacy.d.ts +52 -0
- package/dist/predictions/planning-fallacy.js +70 -55
- package/dist/recall-trace.d.ts +1 -1
- package/dist/recall-trace.js +5 -4
- package/dist/search/physics-search.js +4 -1
- package/dist/search/vector.js +2 -0
- package/dist/server/routes/recall.d.ts +1 -1
- package/dist/server/routes/recall.js +30 -218
- package/dist/server.d.ts +12 -1
- package/dist/server.js +33 -22
- package/dist/store/entry-writes.d.ts +2 -0
- package/dist/store/entry-writes.js +24 -16
- package/dist/store-port.d.ts +70 -1
- package/dist/store-port.js +87 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/cli/recall.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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,
|
|
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,
|
|
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
|
|
399
|
+
return { results, hints, recallText, continuity, continuityTokens, cmdPlanningFallacyHint, cmdPlanningFallacyWatching, anchorRing };
|
|
403
400
|
}
|
|
404
|
-
function auditRecall(hippoRoot,
|
|
405
|
-
const { results,
|
|
406
|
-
const {
|
|
407
|
-
|
|
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
|
|
435
|
-
|
|
436
|
-
|
|
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. */
|
package/dist/cli/shared.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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;
|
package/dist/db/open.js
CHANGED
|
@@ -1,84 +1,46 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
4
|
-
import { cleanupArchivedMirrors } from '../raw-archive-mirror-cleanup.js';
|
|
5
2
|
import { log } from '../log.js';
|
|
6
3
|
import { DatabaseSync } from './sqlite.js';
|
|
7
|
-
import { execWithBusyRetry } from './busy.js';
|
|
8
4
|
import { tableExists } from './tables.js';
|
|
9
|
-
import { assertBinaryCompatible
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const sharedHandles = new Map();
|
|
15
|
-
const sharedSet = new WeakSet();
|
|
16
|
-
let shareDepth = 0;
|
|
17
|
-
let shareBusyWaitMs;
|
|
5
|
+
import { assertBinaryCompatible } from './migrate.js';
|
|
6
|
+
import { connectHippoDb, getHippoDbPath } from './connect.js';
|
|
7
|
+
import { currentRequestStores, isScopedHandle, runWithRequestStores } from './request-stores.js';
|
|
8
|
+
import { SqliteBlockedError } from './sqlite-blocked.js';
|
|
9
|
+
export { getHippoDbPath };
|
|
18
10
|
/** 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. */
|
|
19
11
|
export const HOOK_DB_WAIT_MS = 1000;
|
|
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 const SERVER_DB_WAIT_MS = 250;
|
|
20
14
|
/** A busy store made a command skip work: warn once per process (the holder is usually `hippo sleep`). */
|
|
21
15
|
export function noteStoreBusy(skipped) {
|
|
22
16
|
log.once('store-busy', 'warn', `store busy (another hippo process holds the write lock); ${skipped}`);
|
|
23
|
-
|
|
24
|
-
for (const db of sharedHandles.values()) {
|
|
25
|
-
if (db.isOpen !== false)
|
|
26
|
-
db.exec('PRAGMA busy_timeout = 0');
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
function closeSharedStoreHandles() {
|
|
30
|
-
for (const db of sharedHandles.values()) {
|
|
31
|
-
sharedSet.delete(db);
|
|
32
|
-
if (db.isOpen !== false)
|
|
33
|
-
db.close();
|
|
34
|
-
}
|
|
35
|
-
sharedHandles.clear();
|
|
17
|
+
currentRequestStores()?.noteBusy();
|
|
36
18
|
}
|
|
37
19
|
/** Runs `fn` with one handle per store: openHippoDb reuses it and closeHippoDb leaves it open until `fn` settles or the process exits.
|
|
38
20
|
* `busyWaitMs` is the lock wait of every open inside `fn` that does not pass its own. */
|
|
39
21
|
export async function withSharedStoreHandles(fn, opts) {
|
|
40
|
-
|
|
41
|
-
process.once('exit', closeSharedStoreHandles);
|
|
42
|
-
shareBusyWaitMs = opts?.busyWaitMs;
|
|
43
|
-
}
|
|
44
|
-
try {
|
|
45
|
-
return await fn();
|
|
46
|
-
}
|
|
47
|
-
finally {
|
|
48
|
-
if (--shareDepth === 0) {
|
|
49
|
-
process.off('exit', closeSharedStoreHandles);
|
|
50
|
-
closeSharedStoreHandles();
|
|
51
|
-
shareBusyWaitMs = undefined;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
22
|
+
return runWithRequestStores(fn, { busyWaitMs: opts?.busyWaitMs });
|
|
54
23
|
}
|
|
55
|
-
/** Lock wait inside an HTTP request: SQLite waits synchronously, so a long wait would stall every other request on the event loop. */
|
|
56
|
-
export const SERVER_DB_WAIT_MS = 250;
|
|
57
24
|
/** 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. */
|
|
58
25
|
export const SLEEP_DB_WAIT_MS = 5000;
|
|
59
|
-
|
|
60
|
-
/** Runs `fn` so that every store opened inside it, across awaits, waits at most `busyWaitMs` for a lock unless the open passes its own. */
|
|
61
|
-
export function withBusyWait(busyWaitMs, fn) {
|
|
62
|
-
return scopedBusyWaitMs.run(busyWaitMs, fn);
|
|
63
|
-
}
|
|
64
|
-
/** The lock wait an open here would get without its own `busyWaitMs`, in openHippoDb's order; undefined outside both scopes. */
|
|
26
|
+
/** The lock wait an open here would get without its own `busyWaitMs`: the request scope's; undefined outside every scope. */
|
|
65
27
|
export function scopedBusyWait() {
|
|
66
|
-
return
|
|
67
|
-
}
|
|
68
|
-
/** Thrown by a hippo.db open inside a request served from another store: the code path is not ported to the store port yet. */
|
|
69
|
-
export class SqliteBlockedError extends Error {
|
|
70
|
-
storeKind;
|
|
71
|
-
constructor(storeKind) {
|
|
72
|
-
super(`hippo.db is not opened while the '${storeKind}' store serves this request; this code path is not ported to the store yet`);
|
|
73
|
-
this.storeKind = storeKind;
|
|
74
|
-
this.name = 'SqliteBlockedError';
|
|
75
|
-
}
|
|
28
|
+
return currentRequestStores()?.busyWaitMs;
|
|
76
29
|
}
|
|
77
30
|
const sqliteBlockedBy = new AsyncLocalStorage();
|
|
78
31
|
/** 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. */
|
|
79
32
|
export function withSqliteBlocked(storeKind, fn) {
|
|
80
33
|
return sqliteBlockedBy.run(storeKind, fn);
|
|
81
34
|
}
|
|
35
|
+
/** Runs `fn` with hippo.db opens allowed again, for a store whose own methods are backed by hippo.db. */
|
|
36
|
+
export function withSqliteAllowed(fn) {
|
|
37
|
+
return sqliteBlockedBy.exit(fn);
|
|
38
|
+
}
|
|
39
|
+
/** First line of a best-effort catch around a hippo.db open: an unported path must fail closed, not fall back silently. */
|
|
40
|
+
export function rethrowIfSqliteBlocked(err) {
|
|
41
|
+
if (err instanceof SqliteBlockedError)
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
82
44
|
function assertSqliteAllowed() {
|
|
83
45
|
const storeKind = sqliteBlockedBy.getStore();
|
|
84
46
|
if (storeKind !== undefined)
|
|
@@ -87,63 +49,8 @@ function assertSqliteAllowed() {
|
|
|
87
49
|
/** `busyWaitMs` shortens every lock wait of this open, for a hook that must finish inside its own timeout. */
|
|
88
50
|
export function openHippoDb(hippoRoot, opts) {
|
|
89
51
|
assertSqliteAllowed();
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const busyWaitMs = opts?.busyWaitMs ?? shareBusyWaitMs ?? scopedBusyWaitMs.getStore();
|
|
93
|
-
const key = `${path.resolve(getHippoDbPath(hippoRoot))}\0${busyWaitMs ?? ''}`;
|
|
94
|
-
const shared = sharedHandles.get(key);
|
|
95
|
-
if (shared?.isOpen && !shared.isTransaction)
|
|
96
|
-
return shared;
|
|
97
|
-
const db = openOwnHippoDb(hippoRoot, { busyWaitMs });
|
|
98
|
-
// An open nested inside a transaction gets its own connection, as it did before sharing.
|
|
99
|
-
if (!shared?.isOpen) {
|
|
100
|
-
sharedHandles.set(key, db);
|
|
101
|
-
sharedSet.add(db);
|
|
102
|
-
}
|
|
103
|
-
return db;
|
|
104
|
-
}
|
|
105
|
-
// Owner-only on create; SQLite gives the WAL and SHM files the db's mode. Existing paths keep theirs.
|
|
106
|
-
function createStoreFilesOwnerOnly(hippoRoot) {
|
|
107
|
-
fs.mkdirSync(hippoRoot, { recursive: true, mode: 0o700 });
|
|
108
|
-
try {
|
|
109
|
-
fs.closeSync(fs.openSync(getHippoDbPath(hippoRoot), 'wx', 0o600));
|
|
110
|
-
}
|
|
111
|
-
catch (err) {
|
|
112
|
-
if (!(err instanceof Error && 'code' in err && err.code === 'EEXIST'))
|
|
113
|
-
throw err;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
function openOwnHippoDb(hippoRoot, opts) {
|
|
117
|
-
createStoreFilesOwnerOnly(hippoRoot);
|
|
118
|
-
const db = new DatabaseSync(getHippoDbPath(hippoRoot));
|
|
119
|
-
const busyWaitMs = opts?.busyWaitMs;
|
|
120
|
-
try {
|
|
121
|
-
db.exec(`PRAGMA busy_timeout = ${busyWaitMs ?? 5000}`);
|
|
122
|
-
execWithBusyRetry(db, 'PRAGMA journal_mode = WAL', busyWaitMs);
|
|
123
|
-
db.exec('PRAGMA synchronous = NORMAL');
|
|
124
|
-
db.exec('PRAGMA wal_autocheckpoint = 100');
|
|
125
|
-
db.exec('PRAGMA foreign_keys = ON');
|
|
126
|
-
runMigrations(db, hippoRoot, busyWaitMs);
|
|
127
|
-
// Path A backfill: delete any orphan markdown mirrors for already-archived
|
|
128
|
-
// raw_archive rows. Idempotent via per-row raw_archive.mirror_cleaned_at
|
|
129
|
-
// (v21). Wrapped in try/catch — a filesystem failure must not prevent DB open.
|
|
130
|
-
try {
|
|
131
|
-
cleanupArchivedMirrors(hippoRoot, db);
|
|
132
|
-
}
|
|
133
|
-
catch (cleanupErr) {
|
|
134
|
-
log.error(`openHippoDb: cleanupArchivedMirrors failed (non-fatal): ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`);
|
|
135
|
-
}
|
|
136
|
-
return db;
|
|
137
|
-
}
|
|
138
|
-
catch (error) {
|
|
139
|
-
try {
|
|
140
|
-
db.close();
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
// Best effort only.
|
|
144
|
-
}
|
|
145
|
-
throw error;
|
|
146
|
-
}
|
|
52
|
+
const stores = currentRequestStores();
|
|
53
|
+
return stores ? stores.get(hippoRoot, opts) : connectHippoDb(hippoRoot, opts?.busyWaitMs);
|
|
147
54
|
}
|
|
148
55
|
/** Open an existing store without changing it: no mkdir, WAL switch, migration or mirror cleanup. Throws when hippo.db is missing. */
|
|
149
56
|
export function openHippoDbReadOnly(hippoRoot) {
|
|
@@ -166,7 +73,7 @@ export function openHippoDbReadOnly(hippoRoot) {
|
|
|
166
73
|
}
|
|
167
74
|
}
|
|
168
75
|
export function closeHippoDb(db) {
|
|
169
|
-
if (
|
|
76
|
+
if (isScopedHandle(db))
|
|
170
77
|
return;
|
|
171
78
|
db.close();
|
|
172
79
|
}
|