hippo-memory 1.64.1 → 1.66.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 (66) hide show
  1. package/dist/agent-memories/sync.js +3 -2
  2. package/dist/api/recall-record.d.ts +46 -0
  3. package/dist/api/recall-record.js +98 -0
  4. package/dist/api/recall-request.d.ts +32 -0
  5. package/dist/api/recall-request.js +121 -0
  6. package/dist/api/recall-types.d.ts +8 -1
  7. package/dist/api/recall.d.ts +3 -2
  8. package/dist/api/recall.js +176 -183
  9. package/dist/api/tokens.d.ts +1 -1
  10. package/dist/api/tokens.js +13 -17
  11. package/dist/cli/recall.d.ts +1 -1
  12. package/dist/cli/recall.js +18 -49
  13. package/dist/cli/shared.js +2 -2
  14. package/dist/cli.d.ts +16 -0
  15. package/dist/cli.js +18 -1
  16. package/dist/config.d.ts +2 -0
  17. package/dist/config.js +4 -0
  18. package/dist/db/connect.d.ts +5 -0
  19. package/dist/db/connect.js +52 -0
  20. package/dist/db/open.d.ts +13 -11
  21. package/dist/db/open.js +47 -119
  22. package/dist/db/request-stores.d.ts +30 -0
  23. package/dist/db/request-stores.js +100 -0
  24. package/dist/db/sqlite-blocked.d.ts +16 -0
  25. package/dist/db/sqlite-blocked.js +29 -0
  26. package/dist/db.d.ts +3 -1
  27. package/dist/db.js +3 -1
  28. package/dist/embeddings.d.ts +11 -2
  29. package/dist/embeddings.js +22 -12
  30. package/dist/goals.d.ts +18 -11
  31. package/dist/goals.js +38 -36
  32. package/dist/http-util.d.ts +2 -0
  33. package/dist/http-util.js +5 -0
  34. package/dist/mcp/memory-tools.js +4 -1
  35. package/dist/mcp/recall-tools.js +28 -133
  36. package/dist/mcp/request.d.ts +7 -0
  37. package/dist/mcp/request.js +63 -41
  38. package/dist/mcp/session-state.d.ts +1 -3
  39. package/dist/mcp/session-state.js +3 -5
  40. package/dist/predictions/planning-fallacy.d.ts +52 -0
  41. package/dist/predictions/planning-fallacy.js +70 -55
  42. package/dist/quality-repair.js +3 -0
  43. package/dist/recall-trace.d.ts +1 -1
  44. package/dist/recall-trace.js +5 -4
  45. package/dist/search/hybrid.d.ts +3 -0
  46. package/dist/search/physics-search.d.ts +3 -0
  47. package/dist/search/physics-search.js +21 -19
  48. package/dist/search/vector.d.ts +7 -2
  49. package/dist/search/vector.js +29 -10
  50. package/dist/server/routes/recall.d.ts +1 -1
  51. package/dist/server/routes/recall.js +27 -218
  52. package/dist/server.d.ts +17 -1
  53. package/dist/server.js +38 -22
  54. package/dist/store/entry-writes.d.ts +2 -0
  55. package/dist/store/entry-writes.js +24 -16
  56. package/dist/store/open.js +2 -1
  57. package/dist/store-port.d.ts +88 -2
  58. package/dist/store-port.js +104 -9
  59. package/dist/vector-store.d.ts +8 -0
  60. package/dist/vector-store.js +31 -19
  61. package/dist/version.d.ts +1 -1
  62. package/dist/version.js +1 -1
  63. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  64. package/extensions/openclaw-plugin/package.json +1 -1
  65. package/openclaw.plugin.json +1 -1
  66. package/package.json +1 -1
package/dist/db/open.js CHANGED
@@ -1,153 +1,81 @@
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
- import { log } from '../log.js';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { errorMessage, log } from '../log.js';
6
5
  import { DatabaseSync } from './sqlite.js';
7
- import { execWithBusyRetry } from './busy.js';
8
6
  import { tableExists } from './tables.js';
9
- import { assertBinaryCompatible, runMigrations } from './migrate.js';
10
- export function getHippoDbPath(hippoRoot) {
11
- return path.join(hippoRoot, 'hippo.db');
12
- }
13
- // Hook commands run on every prompt, so inside withSharedStoreHandles each store pays its pragmas, migration check and mirror cleanup once.
14
- const sharedHandles = new Map();
15
- const sharedSet = new WeakSet();
16
- let shareDepth = 0;
17
- let shareBusyWaitMs;
7
+ import { assertBinaryCompatible } from './migrate.js';
8
+ import { connectHippoDb, getHippoDbPath } from './connect.js';
9
+ import { currentRequestStores, isScopedHandle, runWithRequestStores } from './request-stores.js';
10
+ import { OtherStoreFolderError, SqliteBlockedError } from './sqlite-blocked.js';
11
+ export { getHippoDbPath };
18
12
  /** 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
13
  export const HOOK_DB_WAIT_MS = 1000;
14
+ /** Lock wait inside an HTTP request: SQLite waits synchronously, so a long wait would stall every other request on the event loop. */
15
+ export const SERVER_DB_WAIT_MS = 250;
20
16
  /** A busy store made a command skip work: warn once per process (the holder is usually `hippo sleep`). */
21
17
  export function noteStoreBusy(skipped) {
22
18
  log.once('store-busy', 'warn', `store busy (another hippo process holds the write lock); ${skipped}`);
23
- // A lock held past one full wait belongs to a long transaction, so the hook's later writes skip at once.
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();
19
+ currentRequestStores()?.noteBusy();
36
20
  }
37
21
  /** Runs `fn` with one handle per store: openHippoDb reuses it and closeHippoDb leaves it open until `fn` settles or the process exits.
38
22
  * `busyWaitMs` is the lock wait of every open inside `fn` that does not pass its own. */
39
23
  export async function withSharedStoreHandles(fn, opts) {
40
- if (shareDepth++ === 0) {
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
- }
24
+ return runWithRequestStores(fn, { busyWaitMs: opts?.busyWaitMs });
54
25
  }
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
26
  /** 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
27
  export const SLEEP_DB_WAIT_MS = 5000;
59
- const scopedBusyWaitMs = new AsyncLocalStorage();
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. */
28
+ /** The lock wait an open here would get without its own `busyWaitMs`: the request scope's; undefined outside every scope. */
65
29
  export function scopedBusyWait() {
66
- return shareBusyWaitMs ?? scopedBusyWaitMs.getStore();
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
- }
30
+ return currentRequestStores()?.busyWaitMs;
76
31
  }
77
32
  const sqliteBlockedBy = new AsyncLocalStorage();
78
33
  /** 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
34
  export function withSqliteBlocked(storeKind, fn) {
80
35
  return sqliteBlockedBy.run(storeKind, fn);
81
36
  }
82
- function assertSqliteAllowed() {
83
- const storeKind = sqliteBlockedBy.getStore();
84
- if (storeKind !== undefined)
85
- throw new SqliteBlockedError(storeKind);
37
+ /** Runs `fn` with hippo.db opens allowed again, for a store whose own methods are backed by hippo.db, or a copy that reads the old hippo.db. */
38
+ export function withSqliteAllowed(fn) {
39
+ return sqliteBlockedBy.exit(() => markerWaived.run(true, fn));
86
40
  }
87
- /** `busyWaitMs` shortens every lock wait of this open, for a hook that must finish inside its own timeout. */
88
- export function openHippoDb(hippoRoot, opts) {
89
- assertSqliteAllowed();
90
- if (shareDepth === 0)
91
- return openOwnHippoDb(hippoRoot, { busyWaitMs: opts?.busyWaitMs ?? scopedBusyWaitMs.getStore() });
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;
41
+ /** First line of a best-effort catch around a hippo.db open: an unported path must fail closed, not fall back silently. */
42
+ export function rethrowIfSqliteBlocked(err) {
43
+ if (err instanceof SqliteBlockedError)
44
+ throw err;
104
45
  }
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 });
46
+ /** store init --db, store copy --db and serve --db write this file in the hippo root; its text is the store kind. */
47
+ export const OTHER_STORE_MARKER = 'other-store';
48
+ const markerWaived = new AsyncLocalStorage();
49
+ // An empty or unreadable marker still refuses: falling through would write the hippo.db the marker forbids.
50
+ function markerKind(marker) {
108
51
  try {
109
- fs.closeSync(fs.openSync(getHippoDbPath(hippoRoot), 'wx', 0o600));
52
+ return readFileSync(marker, 'utf8').trim() || 'unknown';
110
53
  }
111
54
  catch (err) {
112
- if (!(err instanceof Error && 'code' in err && err.code === 'EEXIST'))
113
- throw err;
55
+ log.debug(`${marker} is unreadable, so its store kind reads as unknown: ${errorMessage(err)}`);
56
+ return 'unknown';
114
57
  }
115
58
  }
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
- }
59
+ /** Throws where hippo.db must not open; checked on every open, not cached, since the marker can appear while a process runs. */
60
+ export function assertSqliteAllowed(hippoRoot) {
61
+ const storeKind = sqliteBlockedBy.getStore();
62
+ if (storeKind !== undefined)
63
+ throw new SqliteBlockedError(storeKind);
64
+ if (markerWaived.getStore())
65
+ return;
66
+ const marker = join(hippoRoot, OTHER_STORE_MARKER);
67
+ if (existsSync(marker))
68
+ throw new OtherStoreFolderError(markerKind(marker), marker);
69
+ }
70
+ /** `busyWaitMs` shortens every lock wait of this open, for a hook that must finish inside its own timeout. */
71
+ export function openHippoDb(hippoRoot, opts) {
72
+ assertSqliteAllowed(hippoRoot);
73
+ const stores = currentRequestStores();
74
+ return stores ? stores.get(hippoRoot, opts) : connectHippoDb(hippoRoot, opts?.busyWaitMs);
147
75
  }
148
76
  /** Open an existing store without changing it: no mkdir, WAL switch, migration or mirror cleanup. Throws when hippo.db is missing. */
149
77
  export function openHippoDbReadOnly(hippoRoot) {
150
- assertSqliteAllowed();
78
+ assertSqliteAllowed(hippoRoot);
151
79
  const db = new DatabaseSync(getHippoDbPath(hippoRoot), { readOnly: true });
152
80
  try {
153
81
  db.exec('PRAGMA busy_timeout = 5000');
@@ -166,7 +94,7 @@ export function openHippoDbReadOnly(hippoRoot) {
166
94
  }
167
95
  }
168
96
  export function closeHippoDb(db) {
169
- if (sharedSet.has(db))
97
+ if (isScopedHandle(db))
170
98
  return;
171
99
  db.close();
172
100
  }
@@ -0,0 +1,30 @@
1
+ import type { DatabaseSyncLike } from './sqlite.js';
2
+ export interface RequestStoresOptions {
3
+ /** Lock wait of every open in the scope that does not pass its own. */
4
+ readonly busyWaitMs?: number;
5
+ /** Set for hook scopes: after one lock wait runs out, {@link RequestStores.noteBusy} makes the scope's later writes skip at once. */
6
+ readonly failFastWhenBusy?: boolean;
7
+ }
8
+ /** The handles one request opened, keyed by resolved store path and lock wait. */
9
+ export declare class RequestStores {
10
+ #private;
11
+ readonly busyWaitMs: number | undefined;
12
+ constructor(opts?: RequestStoresOptions);
13
+ get closed(): boolean;
14
+ /** The scope's handle on `hippoRoot`, opened on first use. `opts.busyWaitMs` overrides the scope's lock wait. */
15
+ get(hippoRoot: string, opts?: {
16
+ busyWaitMs?: number;
17
+ }): DatabaseSyncLike;
18
+ /** A lock held past one full wait belongs to a long transaction, so a hook scope's later writes skip at once. */
19
+ noteBusy(): void;
20
+ close(): void;
21
+ }
22
+ /** Runs `fn` in a request scope whose handles close when `fn` settles; inside an open scope `fn` joins it and `opts` is ignored. */
23
+ export declare function runWithRequestStores<T>(fn: () => T | Promise<T>, opts?: RequestStoresOptions): Promise<T>;
24
+ /** The scope this code runs in, if any. A closed scope still answers: its `get` falls through to a plain open. */
25
+ export declare function currentRequestStores(): RequestStores | undefined;
26
+ /** Runs `fn` outside every scope, for a connection that must outlive the request that happens to open it. */
27
+ export declare function outsideRequestStores<T>(fn: () => T): T;
28
+ /** Whether a scope owns `db` and will close it itself. */
29
+ export declare function isScopedHandle(db: DatabaseSyncLike): boolean;
30
+ //# sourceMappingURL=request-stores.d.ts.map
@@ -0,0 +1,100 @@
1
+ // One store handle per store for each request (HTTP call, MCP tool call, scoped CLI command), opened lazily and closed with it.
2
+ import * as path from 'node:path';
3
+ import { AsyncLocalStorage } from 'node:async_hooks';
4
+ import { connectHippoDb, getHippoDbPath } from './connect.js';
5
+ // Handles a scope owns, so closeHippoDb leaves them open for the rest of the request.
6
+ const scopedHandles = new WeakSet();
7
+ // process.exit skips every finally, so one exit listener closes the handles of scopes still open.
8
+ const liveScopes = new Set();
9
+ let exitListenerInstalled = false;
10
+ function closeLiveScopes() {
11
+ for (const scope of liveScopes)
12
+ scope.close();
13
+ }
14
+ function trackLive(scope) {
15
+ liveScopes.add(scope);
16
+ if (exitListenerInstalled)
17
+ return;
18
+ process.on('exit', closeLiveScopes);
19
+ exitListenerInstalled = true;
20
+ }
21
+ /** The handles one request opened, keyed by resolved store path and lock wait. */
22
+ export class RequestStores {
23
+ busyWaitMs;
24
+ #failFastWhenBusy;
25
+ #handles = new Map();
26
+ #closed = false;
27
+ constructor(opts = {}) {
28
+ this.busyWaitMs = opts.busyWaitMs;
29
+ this.#failFastWhenBusy = opts.failFastWhenBusy === true;
30
+ }
31
+ get closed() {
32
+ return this.#closed;
33
+ }
34
+ /** The scope's handle on `hippoRoot`, opened on first use. `opts.busyWaitMs` overrides the scope's lock wait. */
35
+ get(hippoRoot, opts) {
36
+ const busyWaitMs = opts?.busyWaitMs ?? this.busyWaitMs;
37
+ // A listener or timer that outlives its request still sees this scope, so it gets a connection of its own.
38
+ if (this.#closed)
39
+ return connectHippoDb(hippoRoot, busyWaitMs);
40
+ const key = `${path.resolve(getHippoDbPath(hippoRoot))}\0${busyWaitMs ?? ''}`;
41
+ const cached = this.#handles.get(key);
42
+ if (cached?.isOpen && !cached.isTransaction)
43
+ return cached;
44
+ const db = connectHippoDb(hippoRoot, busyWaitMs);
45
+ // An open nested inside a transaction gets its own connection, which its caller closes.
46
+ if (cached?.isOpen)
47
+ return db;
48
+ this.#handles.set(key, db);
49
+ scopedHandles.add(db);
50
+ trackLive(this);
51
+ return db;
52
+ }
53
+ /** A lock held past one full wait belongs to a long transaction, so a hook scope's later writes skip at once. */
54
+ noteBusy() {
55
+ if (!this.#failFastWhenBusy)
56
+ return;
57
+ for (const db of this.#handles.values()) {
58
+ if (db.isOpen !== false)
59
+ db.exec('PRAGMA busy_timeout = 0');
60
+ }
61
+ }
62
+ close() {
63
+ this.#closed = true;
64
+ liveScopes.delete(this);
65
+ for (const db of this.#handles.values()) {
66
+ if (db.isOpen !== false)
67
+ db.close();
68
+ }
69
+ this.#handles.clear();
70
+ }
71
+ }
72
+ const scopes = new AsyncLocalStorage();
73
+ /** Runs `fn` in a request scope whose handles close when `fn` settles; inside an open scope `fn` joins it and `opts` is ignored. */
74
+ export async function runWithRequestStores(fn, opts) {
75
+ const outer = scopes.getStore();
76
+ if (outer && !outer.closed)
77
+ return fn();
78
+ const stores = new RequestStores(opts);
79
+ return scopes.run(stores, async () => {
80
+ try {
81
+ return await fn();
82
+ }
83
+ finally {
84
+ stores.close();
85
+ }
86
+ });
87
+ }
88
+ /** The scope this code runs in, if any. A closed scope still answers: its `get` falls through to a plain open. */
89
+ export function currentRequestStores() {
90
+ return scopes.getStore();
91
+ }
92
+ /** Runs `fn` outside every scope, for a connection that must outlive the request that happens to open it. */
93
+ export function outsideRequestStores(fn) {
94
+ return scopes.exit(fn);
95
+ }
96
+ /** Whether a scope owns `db` and will close it itself. */
97
+ export function isScopedHandle(db) {
98
+ return scopedHandles.has(db);
99
+ }
100
+ //# sourceMappingURL=request-stores.js.map
@@ -0,0 +1,16 @@
1
+ /** Thrown by a hippo.db open inside a request served from another store: the code path is not ported to the store port yet. */
2
+ export declare class SqliteBlockedError extends Error {
3
+ readonly storeKind: string;
4
+ constructor(storeKind: string, message?: string);
5
+ }
6
+ /** A store that lacks a whole group of port methods; it maps to the same 501, and the log names the group instead of hippo.db. */
7
+ export declare class StoreNotPortedError extends SqliteBlockedError {
8
+ readonly group: string;
9
+ constructor(storeKind: string, group: string);
10
+ }
11
+ /** Thrown by a hippo.db open in a folder whose marker file names the store that holds its memories, from the CLI as from a request. */
12
+ export declare class OtherStoreFolderError extends SqliteBlockedError {
13
+ readonly markerPath: string;
14
+ constructor(storeKind: string, markerPath: string);
15
+ }
16
+ //# sourceMappingURL=sqlite-blocked.d.ts.map
@@ -0,0 +1,29 @@
1
+ // A leaf module, so http-util can map this error to a 501 without importing db/open, whose migrations reach back to http-util.
2
+ /** Thrown by a hippo.db open inside a request served from another store: the code path is not ported to the store port yet. */
3
+ export class SqliteBlockedError extends Error {
4
+ storeKind;
5
+ constructor(storeKind, message = `hippo.db is not opened while the '${storeKind}' store serves this request; this code path is not ported to the store yet`) {
6
+ super(message);
7
+ this.storeKind = storeKind;
8
+ this.name = 'SqliteBlockedError';
9
+ }
10
+ }
11
+ /** A store that lacks a whole group of port methods; it maps to the same 501, and the log names the group instead of hippo.db. */
12
+ export class StoreNotPortedError extends SqliteBlockedError {
13
+ group;
14
+ constructor(storeKind, group) {
15
+ super(storeKind, `the '${storeKind}' store has no '${group}' reads; this code path is not ported to the store yet`);
16
+ this.group = group;
17
+ this.name = 'StoreNotPortedError';
18
+ }
19
+ }
20
+ /** Thrown by a hippo.db open in a folder whose marker file names the store that holds its memories, from the CLI as from a request. */
21
+ export class OtherStoreFolderError extends SqliteBlockedError {
22
+ markerPath;
23
+ constructor(storeKind, markerPath) {
24
+ super(storeKind, `This folder's memories live in the '${storeKind}' store, so hippo.db is not used here (marker: ${markerPath})`);
25
+ this.markerPath = markerPath;
26
+ this.name = 'OtherStoreFolderError';
27
+ }
28
+ }
29
+ //# sourceMappingURL=sqlite-blocked.js.map
package/dist/db.d.ts CHANGED
@@ -3,5 +3,7 @@ export { isSqliteBusy, isStoreBusy, StoreBusyError, STORE_BUSY_MESSAGE, execWith
3
3
  export { getSchemaVersion, getMeta, setMeta, isFtsAvailable } from './db/meta.js';
4
4
  export { countTableRows, pruneConsolidationRuns } from './db/tables.js';
5
5
  export { getCurrentSchemaVersion, IncompatibleBinaryError, ftsRowCounts, repairFtsDrift } from './db/migrate.js';
6
- export { getHippoDbPath, HOOK_DB_WAIT_MS, noteStoreBusy, withSharedStoreHandles, SERVER_DB_WAIT_MS, SLEEP_DB_WAIT_MS, withBusyWait, scopedBusyWait, SqliteBlockedError, withSqliteBlocked, openHippoDb, openHippoDbReadOnly, closeHippoDb, } from './db/open.js';
6
+ export { getHippoDbPath, HOOK_DB_WAIT_MS, noteStoreBusy, withSharedStoreHandles, SERVER_DB_WAIT_MS, SLEEP_DB_WAIT_MS, scopedBusyWait, withSqliteBlocked, withSqliteAllowed, rethrowIfSqliteBlocked, OTHER_STORE_MARKER, openHippoDb, openHippoDbReadOnly, closeHippoDb, } from './db/open.js';
7
+ export { RequestStores, runWithRequestStores, currentRequestStores, outsideRequestStores } from './db/request-stores.js';
8
+ export { OtherStoreFolderError, SqliteBlockedError } from './db/sqlite-blocked.js';
7
9
  //# sourceMappingURL=db.d.ts.map
package/dist/db.js CHANGED
@@ -2,5 +2,7 @@ export { isSqliteBusy, isStoreBusy, StoreBusyError, STORE_BUSY_MESSAGE, execWith
2
2
  export { getSchemaVersion, getMeta, setMeta, isFtsAvailable } from './db/meta.js';
3
3
  export { countTableRows, pruneConsolidationRuns } from './db/tables.js';
4
4
  export { getCurrentSchemaVersion, IncompatibleBinaryError, ftsRowCounts, repairFtsDrift } from './db/migrate.js';
5
- export { getHippoDbPath, HOOK_DB_WAIT_MS, noteStoreBusy, withSharedStoreHandles, SERVER_DB_WAIT_MS, SLEEP_DB_WAIT_MS, withBusyWait, scopedBusyWait, SqliteBlockedError, withSqliteBlocked, openHippoDb, openHippoDbReadOnly, closeHippoDb, } from './db/open.js';
5
+ export { getHippoDbPath, HOOK_DB_WAIT_MS, noteStoreBusy, withSharedStoreHandles, SERVER_DB_WAIT_MS, SLEEP_DB_WAIT_MS, scopedBusyWait, withSqliteBlocked, withSqliteAllowed, rethrowIfSqliteBlocked, OTHER_STORE_MARKER, openHippoDb, openHippoDbReadOnly, closeHippoDb, } from './db/open.js';
6
+ export { RequestStores, runWithRequestStores, currentRequestStores, outsideRequestStores } from './db/request-stores.js';
7
+ export { OtherStoreFolderError, SqliteBlockedError } from './db/sqlite-blocked.js';
6
8
  //# sourceMappingURL=db.js.map
@@ -64,8 +64,19 @@ export declare function embeddingInputText(entry: {
64
64
  * consistent with the compare side in `embeddingModelRequiresReindex`.
65
65
  */
66
66
  export declare function saveStoredEmbeddingModel(hippoRoot: string, model: string): void;
67
+ /** What a store's vector index was built by: the stored identity, and whether any vector exists. */
68
+ export interface EmbeddingIndexState {
69
+ readonly storedModel: string | null;
70
+ readonly hasVectors: boolean;
71
+ }
72
+ /** The model the index was built by: the stored identity, else the default model when vectors exist. */
73
+ export declare function indexedModel(state: EmbeddingIndexState): string | null;
74
+ /** The one rebuild rule every store applies to its indexed model. */
75
+ export declare function indexNeedsRebuild(indexed: string | null, providerId: string): boolean;
67
76
  export declare function resolveIndexedEmbeddingModel(hippoRoot: string, index?: Record<string, number[]>): string | null;
68
77
  export declare function embeddingModelRequiresReindex(hippoRoot: string, model: string, index?: Record<string, number[]>): boolean;
78
+ /** hippo.db's index state, the meta row and the EXISTS on one handle. */
79
+ export declare function embeddingIndexStateAt(hippoRoot: string): EmbeddingIndexState;
69
80
  /**
70
81
  * Cosine similarity between two vectors. Handles unnormalized vectors.
71
82
  * Returns 0 for empty or mismatched vectors.
@@ -75,8 +86,6 @@ export declare function cosineSimilarity(a: number[], b: number[]): number;
75
86
  export declare function loadEmbeddingIndex(hippoRoot: string): Record<string, number[]>;
76
87
  /** Stored vectors for `ids` only. */
77
88
  export declare function loadStoredVectors(hippoRoot: string, ids: readonly string[]): Map<string, number[]>;
78
- /** Whether the store holds any vector at all. */
79
- export declare function hasEmbeddings(hippoRoot: string): boolean;
80
89
  /** Replace every stored vector with `index`; `model` defaults to the stored index identity. */
81
90
  export declare function saveEmbeddingIndex(hippoRoot: string, index: Record<string, number[]>, model?: string): void;
82
91
  /**
@@ -8,7 +8,7 @@ import * as fs from 'fs';
8
8
  import * as path from 'path';
9
9
  import { randomUUID } from 'crypto';
10
10
  import { loadAllEntries } from './store/entry-reads.js';
11
- import { openHippoDb, closeHippoDb, getMeta, setMeta } from './db.js';
11
+ import { openHippoDb, closeHippoDb, getMeta, rethrowIfSqliteBlocked, setMeta } from './db.js';
12
12
  import { EMBEDDING_MODEL_META_KEY, deleteOrphanVectors, hasStoredVectors, loadVectors, replaceAllVectors, storedVectorIds, upsertVectors, } from './vector-store.js';
13
13
  import { initializeParticle, savePhysicsState, loadPhysicsState, resetAllPhysicsState } from './physics-state.js';
14
14
  import { loadConfig } from './config.js';
@@ -81,6 +81,7 @@ function loadStoredEmbeddingModel(hippoRoot) {
81
81
  }
82
82
  }
83
83
  catch (err) {
84
+ rethrowIfSqliteBlocked(err);
84
85
  log.debug(`stored embedding model unreadable: ${err instanceof Error ? err.message : String(err)}`);
85
86
  return null;
86
87
  }
@@ -100,16 +101,29 @@ export function saveStoredEmbeddingModel(hippoRoot, model) {
100
101
  closeHippoDb(db);
101
102
  }
102
103
  }
104
+ /** The model the index was built by: the stored identity, else the default model when vectors exist. */
105
+ export function indexedModel(state) {
106
+ return state.storedModel ?? (state.hasVectors ? DEFAULT_EMBEDDING_MODEL : null);
107
+ }
108
+ /** The one rebuild rule every store applies to its indexed model. */
109
+ export function indexNeedsRebuild(indexed, providerId) {
110
+ return indexed !== null && indexed !== embeddingIndexIdentity(providerId);
111
+ }
103
112
  export function resolveIndexedEmbeddingModel(hippoRoot, index) {
104
- const stored = loadStoredEmbeddingModel(hippoRoot);
105
- if (stored)
106
- return stored;
107
- const hasVectors = index ? Object.keys(index).length > 0 : withVectorDb(hippoRoot, hasStoredVectors);
108
- return hasVectors ? DEFAULT_EMBEDDING_MODEL : null;
113
+ const storedModel = loadStoredEmbeddingModel(hippoRoot);
114
+ if (storedModel)
115
+ return storedModel;
116
+ return indexedModel({ storedModel, hasVectors: index ? Object.keys(index).length > 0 : withVectorDb(hippoRoot, hasStoredVectors) });
109
117
  }
110
118
  export function embeddingModelRequiresReindex(hippoRoot, model, index) {
111
- const stored = resolveIndexedEmbeddingModel(hippoRoot, index);
112
- return stored !== null && stored !== embeddingIndexIdentity(model);
119
+ return indexNeedsRebuild(resolveIndexedEmbeddingModel(hippoRoot, index), model);
120
+ }
121
+ /** hippo.db's index state, the meta row and the EXISTS on one handle. */
122
+ export function embeddingIndexStateAt(hippoRoot) {
123
+ return withVectorDb(hippoRoot, (db) => ({
124
+ storedModel: getMeta(db, EMBEDDING_MODEL_META_KEY, '').trim() || null,
125
+ hasVectors: hasStoredVectors(db),
126
+ }));
113
127
  }
114
128
  async function rebuildEmbeddingIndex(entries, provider) {
115
129
  const rebuilt = {};
@@ -188,10 +202,6 @@ export function loadEmbeddingIndex(hippoRoot) {
188
202
  export function loadStoredVectors(hippoRoot, ids) {
189
203
  return ids.length === 0 ? new Map() : withVectorDb(hippoRoot, (db) => loadVectors(db, ids));
190
204
  }
191
- /** Whether the store holds any vector at all. */
192
- export function hasEmbeddings(hippoRoot) {
193
- return withVectorDb(hippoRoot, hasStoredVectors);
194
- }
195
205
  /** Replace every stored vector with `index`; `model` defaults to the stored index identity. */
196
206
  export function saveEmbeddingIndex(hippoRoot, index, model) {
197
207
  withVectorDb(hippoRoot, (db) => replaceAllVectors(db, index, model ?? getMeta(db, EMBEDDING_MODEL_META_KEY, '')));
package/dist/goals.d.ts CHANGED
@@ -78,6 +78,13 @@ export interface GetActiveGoalsOpts {
78
78
  tenantId: string;
79
79
  }
80
80
  export declare function getActiveGoals(hippoRoot: string, opts: GetActiveGoalsOpts): Goal[];
81
+ /** A session's active goals, oldest first, and the retrieval policy of each goal that names one. */
82
+ export interface ActiveGoals {
83
+ readonly goals: Goal[];
84
+ readonly policies: ReadonlyMap<string, RetrievalPolicy>;
85
+ }
86
+ /** The active goals and their policies on one handle, so a goal and its policy never disagree. */
87
+ export declare function activeGoalsWithPolicies(hippoRoot: string, opts: GetActiveGoalsOpts): ActiveGoals;
81
88
  /** Every goal of a (tenant, session), whatever its status, oldest first. */
82
89
  export declare function getSessionGoals(hippoRoot: string, opts: GetActiveGoalsOpts): Goal[];
83
90
  export declare function getActiveGoalsWithDb(db: DatabaseSyncLike, opts: GetActiveGoalsOpts): Goal[];
@@ -90,7 +97,7 @@ export interface GoalRecallLogRow {
90
97
  recalledAt: string;
91
98
  score: number;
92
99
  }
93
- /** Options shared by {@link computeGoalStackBoost} and {@link applyGoalStackBoost}. */
100
+ /** Options shared by {@link computeGoalStackBoost} and {@link boostByGoals}. */
94
101
  export interface GoalStackBoostOpts {
95
102
  sessionId: string;
96
103
  tenantId: string;
@@ -107,6 +114,10 @@ export interface GoalStackBoost<R> {
107
114
  results: R[];
108
115
  log: GoalRecallLogRow[];
109
116
  }
117
+ export declare function loadGoalPolicies(db: DatabaseSyncLike, active: readonly Goal[]): Map<string, RetrievalPolicy>;
118
+ /** The rows whose memory lives in this store: goal_recall_log.memory_id references memories, so a global row would fail
119
+ * the insert. A global result keeps its boost; it only earns no log row, so no outcome propagation. */
120
+ export declare function localGoalRecallRows(db: DatabaseSyncLike, rows: readonly GoalRecallLogRow[]): GoalRecallLogRow[];
110
121
  /**
111
122
  * dlPFC goal-stack boost. Applies the multi-goal boost to entry-backed scored
112
123
  * rows when (tenant, session) has active goals and returns the log rows to
@@ -130,21 +141,17 @@ export declare function computeGoalStackBoost<R extends {
130
141
  entry: MemoryEntry;
131
142
  score: number;
132
143
  }>(db: DatabaseSyncLike, results: R[], opts: GoalStackBoostOpts): GoalStackBoost<R>;
144
+ /** {@link computeGoalStackBoost} over goals already read, touching no store. Its log keeps rows whose memory is global,
145
+ * since only the store can tell them apart; {@link localGoalRecallRows} drops those before the write. */
146
+ export declare function boostByGoals<R extends {
147
+ entry: MemoryEntry;
148
+ score: number;
149
+ }>(results: R[], active: ActiveGoals, opts: GoalStackBoostOpts): GoalStackBoost<R>;
133
150
  /**
134
151
  * Writes goal-boost log rows. INSERT OR IGNORE because UNIQUE(memory_id, goal_id)
135
152
  * makes a re-recall during the same goal life a no-op for outcome attribution.
136
153
  */
137
154
  export declare function writeGoalRecallLog(db: DatabaseSyncLike, rows: readonly GoalRecallLogRow[]): void;
138
- /**
139
- * {@link computeGoalStackBoost} plus {@link writeGoalRecallLog} in one call, for
140
- * pipelines that boost and log on the same handle.
141
- *
142
- * @internal Recall ranking helper. Subject to change.
143
- */
144
- export declare function applyGoalStackBoost<R extends {
145
- entry: MemoryEntry;
146
- score: number;
147
- }>(db: DatabaseSyncLike, results: R[], opts: GoalStackBoostOpts): R[];
148
155
  export interface CompleteGoalOpts {
149
156
  outcomeScore?: number;
150
157
  /**