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
package/dist/server.js CHANGED
@@ -2,8 +2,9 @@ import { envPort, envRequireAuth, envV1Rps } from './env.js';
2
2
  import { createServer } from 'node:http';
3
3
  import { existsSync } from 'node:fs';
4
4
  import { detectServer, removePidfileIfOwned, writePidfile } from './server-detect.js';
5
- import { closeHippoDb, getHippoDbPath, isStoreBusy, openHippoDb, SERVER_DB_WAIT_MS, withBusyWait, withSqliteBlocked } from './db.js';
5
+ import { closeHippoDb, getHippoDbPath, isStoreBusy, openHippoDb, outsideRequestStores, runWithRequestStores, SERVER_DB_WAIT_MS, withSqliteBlocked } from './db.js';
6
6
  import { sqliteStore } from './store-port.js';
7
+ import { markSharedStore } from './config.js';
7
8
  import { auditWriteFailureCount } from './audit.js';
8
9
  import { PACKAGE_VERSION } from './version.js';
9
10
  import { errorFields, log } from './log.js';
@@ -11,7 +12,7 @@ import { createRateLimiter } from './rate-limit.js';
11
12
  import { authCreateSelf, authRevoke, RecallContractError } from './api.js';
12
13
  import { handleSlackEventsWebhook } from './connectors/slack/webhook.js';
13
14
  import { handleGitHubEventsWebhook } from './connectors/github/webhook.js';
14
- import { BodyTimeoutError, BodyTooLargeError, closeAfterReply, HttpError, JSON_HEADERS, sendJson } from './http-util.js';
15
+ import { BodyTimeoutError, BodyTooLargeError, closeAfterReply, HttpError, JSON_HEADERS, sendJson, STORE_NOT_PORTED_MESSAGE } from './http-util.js';
15
16
  import { ForbiddenError } from './api-errors.js';
16
17
  import { buildContextWithAuth, isLoopback, LIMITER_MAX_KEYS, requireAuth } from './server/auth.js';
17
18
  import { enforceRateLimit } from './server/client-ip.js';
@@ -92,7 +93,7 @@ const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);
92
93
  const V1_ROUTES = [
93
94
  { method: 'POST', path: '/v1/memories', handler: handleCreateMemory },
94
95
  { method: 'GET', path: '/v1/graph', handler: handleGetGraph },
95
- { method: 'GET', path: '/v1/memories', handler: handleRecallMemories },
96
+ { method: 'GET', path: '/v1/memories', storeReady: true, handler: handleRecallMemories },
96
97
  { method: 'GET', pattern: '/v1/sessions/:id/assemble', handler: handleAssembleSession },
97
98
  { method: 'GET', pattern: '/v1/recall/drill/:id', handler: handleDrillRecall },
98
99
  { method: 'POST', pattern: '/v1/memories/:id/archive', handler: handleArchiveMemory },
@@ -238,10 +239,9 @@ function dispatchPublicJson({ res, opts }, method, path) {
238
239
  res.end(text);
239
240
  return true;
240
241
  }
241
- const NOT_ON_STORE_MESSAGE = 'not available on this store';
242
242
  function assertSqliteStore(opts) {
243
243
  if (opts.store.kind !== 'sqlite')
244
- throw new HttpError(501, NOT_ON_STORE_MESSAGE);
244
+ throw new HttpError(501, STORE_NOT_PORTED_MESSAGE);
245
245
  }
246
246
  /** Under another store, a route not yet ported answers 501 without running; the caller is checked first, so a bad key is still a 401. */
247
247
  async function refuseUnportedRoute(req, opts) {
@@ -264,12 +264,25 @@ async function handleRequest(req, res, opts, startedAt, streamSlots, limiter) {
264
264
  }
265
265
  enforceRateLimit(req, path, limiter);
266
266
  const routeRequest = { req, res, opts, query };
267
- if (await dispatchV1Route(routeRequest, method, path))
267
+ if (await runWithRequestStores(() => dispatchScopedRoute(routeRequest, method, path), { busyWaitMs: SERVER_DB_WAIT_MS }))
268
268
  return;
269
- if (dispatchPublicJson(routeRequest, method, path))
270
- return;
271
- if (await dispatchAddonRoute(routeRequest, method, path))
269
+ // Outside the request scope: the heartbeat timer outlives the request. Store-ready: the stream only authenticates, through the port.
270
+ if (method === 'GET' && path === '/mcp/stream') {
271
+ await handleMcpStream(req, res, opts, streamSlots);
272
272
  return;
273
+ }
274
+ res.writeHead(404, JSON_HEADERS);
275
+ res.end(JSON.stringify({ error: 'not found' }));
276
+ }
277
+ /** Every route that runs inside a request scope, so it opens each store once: the /v1 table, public JSON, add-on routes, the webhooks and POST /mcp. */
278
+ async function dispatchScopedRoute(r, method, path) {
279
+ if (await dispatchV1Route(r, method, path))
280
+ return true;
281
+ if (dispatchPublicJson(r, method, path))
282
+ return true;
283
+ if (await dispatchAddonRoute(r, method, path))
284
+ return true;
285
+ const { req, res, opts } = r;
273
286
  if (method === 'POST' && path === '/v1/connectors/slack/events') {
274
287
  // Bearer auth deliberately skipped: this route is in PUBLIC_ROUTES and authenticates via the Slack HMAC signature.
275
288
  if (!isPublicRoute(method, path)) {
@@ -278,7 +291,7 @@ async function handleRequest(req, res, opts, startedAt, streamSlots, limiter) {
278
291
  }
279
292
  assertSqliteStore(opts);
280
293
  await handleSlackEventsWebhook({ req, res, opts });
281
- return;
294
+ return true;
282
295
  }
283
296
  if (method === 'POST' && path === '/v1/connectors/github/events') {
284
297
  if (!isPublicRoute(method, path)) {
@@ -286,20 +299,14 @@ async function handleRequest(req, res, opts, startedAt, streamSlots, limiter) {
286
299
  }
287
300
  assertSqliteStore(opts);
288
301
  await handleGitHubEventsWebhook({ req, res, opts });
289
- return;
302
+ return true;
290
303
  }
304
+ // Store-ready: under another store the MCP layer lists and runs only the tools ported to the port.
291
305
  if (method === 'POST' && path === '/mcp') {
292
- await refuseUnportedRoute(req, opts);
293
306
  await handleMcpPost(req, res, opts);
294
- return;
295
- }
296
- // Store-ready: the stream and its heartbeat only authenticate, and auth goes through the port.
297
- if (method === 'GET' && path === '/mcp/stream') {
298
- await handleMcpStream(req, res, opts, streamSlots);
299
- return;
307
+ return true;
300
308
  }
301
- res.writeHead(404, JSON_HEADERS);
302
- res.end(JSON.stringify({ error: 'not found' }));
309
+ return false;
303
310
  }
304
311
  function sendHealth(req, res, startedAt) {
305
312
  // Loopback callers (detectServer's stale-pidfile probe reads version and
@@ -387,7 +394,8 @@ function createStoreHolder(hippoRoot, store) {
387
394
  if (heldDb || stopHolding || !existsSync(getHippoDbPath(hippoRoot)))
388
395
  return;
389
396
  try {
390
- heldDb = openHippoDb(hippoRoot);
397
+ // The 'finish' listener can fire inside a request scope, which would close this connection with the request.
398
+ heldDb = outsideRequestStores(() => openHippoDb(hippoRoot));
391
399
  }
392
400
  catch (err) {
393
401
  stopHolding = true;
@@ -522,6 +530,9 @@ export async function serve(opts) {
522
530
  ...opts, routes, publicJsonBodies, store: opts.store ?? sqliteStore(opts.hippoRoot), callerLimiter, failedAuthLimiter,
523
531
  };
524
532
  const { kind } = served.store;
533
+ // A store other than hippo.db is a team's central server, so its folder's config.json must not decide shared-ness.
534
+ if (kind !== 'sqlite')
535
+ markSharedStore(opts.hippoRoot);
525
536
  const holder = createStoreHolder(opts.hippoRoot, served.store);
526
537
  const inflight = new Set();
527
538
  const server = createServer((req, res) => {
@@ -531,7 +542,7 @@ export async function serve(opts) {
531
542
  const requestId = resolveRequestId(req.headers['x-request-id']);
532
543
  requestIds.set(req, requestId);
533
544
  res.setHeader('X-Request-Id', requestId);
534
- const run = () => withBusyWait(SERVER_DB_WAIT_MS, () => handleRequest(req, res, served, startedAt, streamSlots, limiter));
545
+ const run = () => handleRequest(req, res, served, startedAt, streamSlots, limiter);
535
546
  // A missed port under another store would otherwise create and write a hippo.db that store never reads.
536
547
  (kind === 'sqlite' ? run() : withSqliteBlocked(kind, run)).catch((err) => replyOrClose(req, res, err, requestId));
537
548
  });
@@ -36,6 +36,8 @@ export interface StrengthenOptions {
36
36
  /** Strengthen what a read returned: update only the four retrieval columns on the live row, never a stale copy.
37
37
  * Best effort: a failure logs and never fails the read. Returns the ids found in this store. */
38
38
  export declare function strengthenRetrieved(hippoRoot: string, ids: readonly string[], opts: StrengthenOptions): Set<string>;
39
+ /** strengthenRetrieved on an open handle that holds no transaction, so a recall's last writes share one handle. */
40
+ export declare function strengthenRetrievedInOwnTx(db: DatabaseSyncLike, ids: readonly string[], opts: StrengthenOptions): Set<string>;
39
41
  /** strengthenRetrieved on the caller's handle, inside the caller's transaction. Throws; the caller decides. */
40
42
  export declare function strengthenRetrievedOn(db: DatabaseSyncLike, ids: readonly string[], opts: StrengthenOptions): Set<string>;
41
43
  /** Rewrites a live row's tags and its full-text row on the caller's transaction, with no audit row. */
@@ -1,5 +1,5 @@
1
1
  import { markRetrieved } from '../memory.js';
2
- import { closeHippoDb, openHippoDb, withWriteScope } from '../db.js';
2
+ import { closeHippoDb, openHippoDb, rethrowIfSqliteBlocked, withWriteScope } from '../db.js';
3
3
  import { RejectedValueError } from '../rejection.js';
4
4
  import { markSummaryDirtyInTx } from '../summary-dirty.js';
5
5
  import { log } from '../log.js';
@@ -73,30 +73,38 @@ export function writeEntryMirrors(hippoRoot, entry) {
73
73
  /** Strengthen what a read returned: update only the four retrieval columns on the live row, never a stale copy.
74
74
  * Best effort: a failure logs and never fails the read. Returns the ids found in this store. */
75
75
  export function strengthenRetrieved(hippoRoot, ids, opts) {
76
- const found = new Set();
77
76
  if (ids.length === 0 || opts.recallBoostAblated)
78
- return found;
77
+ return new Set();
79
78
  let db;
80
79
  try {
81
80
  db = openHippoDb(hippoRoot);
82
- db.exec('BEGIN IMMEDIATE');
83
- for (const id of strengthenRetrievedOn(db, ids, opts))
84
- found.add(id);
85
- db.exec('COMMIT');
86
81
  }
87
82
  catch (error) {
88
- try {
89
- db?.exec('ROLLBACK');
90
- }
91
- catch { /* already rolled back; keep the original error */ }
92
- log.warn(`retrieval stats not saved (${error instanceof Error ? error.message : String(error)})`);
93
- found.clear();
83
+ rethrowIfSqliteBlocked(error);
84
+ warnStrengthenFailed(error);
85
+ return new Set();
86
+ }
87
+ try {
88
+ return strengthenRetrievedInOwnTx(db, ids, opts);
94
89
  }
95
90
  finally {
96
- if (db)
97
- closeHippoDb(db);
91
+ closeHippoDb(db);
98
92
  }
99
- return found;
93
+ }
94
+ /** strengthenRetrieved on an open handle that holds no transaction, so a recall's last writes share one handle. */
95
+ export function strengthenRetrievedInOwnTx(db, ids, opts) {
96
+ if (ids.length === 0 || opts.recallBoostAblated)
97
+ return new Set();
98
+ try {
99
+ return withWriteScope(db, 'strengthen_retrieved', () => strengthenRetrievedOn(db, ids, opts));
100
+ }
101
+ catch (error) {
102
+ warnStrengthenFailed(error);
103
+ return new Set();
104
+ }
105
+ }
106
+ function warnStrengthenFailed(error) {
107
+ log.warn(`retrieval stats not saved (${error instanceof Error ? error.message : String(error)})`);
100
108
  }
101
109
  /** strengthenRetrieved on the caller's handle, inside the caller's transaction. Throws; the caller decides. */
102
110
  export function strengthenRetrievedOn(db, ids, opts) {
@@ -1,13 +1,82 @@
1
1
  import { type ApiKeyRecord } from './auth.js';
2
+ import { type AppendAuditOpts } from './audit.js';
3
+ import type { ContinuityBlock } from './api/recall-types.js';
4
+ import { type ActiveGoals, type GetActiveGoalsOpts, type GoalRecallLogRow } from './goals.js';
5
+ import type { MemoryEntry } from './memory.js';
6
+ import { type PlanningFallacyEvidence } from './predictions/planning-fallacy.js';
7
+ import { type RecallTraceInput } from './recall-trace.js';
8
+ import { type StrengthenOptions } from './store/entry-writes.js';
9
+ import { type OriginFilter } from './store/search-rows.js';
10
+ import { type ContinuityKey } from './store/sessions.js';
11
+ import { type TokenUse } from './token-ledger.js';
12
+ /** The arguments of `loadRecallSearchEntries` after the query, by name. */
13
+ export interface RecallSearchArgs {
14
+ readonly limit: number;
15
+ readonly tenantId?: string;
16
+ readonly requestedScope?: string;
17
+ readonly explicitScopeMode: 'exact' | 'additive';
18
+ readonly includeSuperseded: boolean;
19
+ readonly originProjects?: OriginFilter;
20
+ /** The caller's personal scope, which the default deny admits; undefined admits no personal row. */
21
+ readonly ownScope: string | undefined;
22
+ }
23
+ /** Everything one recall writes once its reply is decided, so a store writes it on one connection. */
24
+ export interface RecallWrites {
25
+ /** Insert or ignore on (memory_id, goal_id), so the first row wins, a duplicate inside this batch included. First drop a row
26
+ * whose memory_id is in no memories row of this store, whatever that row's tenant, as `localGoalRecallRows` does. */
27
+ readonly goalLog: readonly GoalRecallLogRow[];
28
+ /** In the order they happened. */
29
+ readonly audit: readonly AppendAuditOpts[];
30
+ readonly trace?: RecallTraceInput;
31
+ /** Nothing when `recallBoostAblated`. Each id found, in `tenantId` when set, gets the retrieval_count, last_retrieved,
32
+ * half_life_days and strength `markRetrieved` computes from the row read before any update, so a repeated id moves once. */
33
+ readonly strengthen?: {
34
+ readonly ids: readonly string[];
35
+ readonly opts: StrengthenOptions;
36
+ };
37
+ }
2
38
  /** What `serve()` reads and writes through. Each method is atomic and no transaction spans an await, since SQLite's lock wait blocks the event loop; a lock timeout throws `StoreBusyError`. */
3
39
  export interface HippoStore {
4
40
  /** 'sqlite' is hippo.db under the served root. Under any other kind, an unported route answers 501 and a hippo.db open inside a request throws. */
5
41
  readonly kind: string;
6
42
  /** The api_keys row for `keyId` with its scope grants, revoked or not; null when no row matches. */
7
43
  findApiKey(keyId: string): Promise<ApiKeyRecord | null>;
44
+ /** Recall candidates in `loadRecallSearchEntries` order: FTS, then LIKE, then every row in scope. */
45
+ searchRecallEntries(query: string, args: RecallSearchArgs): Promise<MemoryEntry[]>;
46
+ /** The rows among the first 500 ids, a tenant narrowing them, ordered by created, then content, then id, all ascending,
47
+ * as `loadEntriesByIds` does; the input order is ignored. */
48
+ entriesByIds(ids: readonly string[], tenantId?: string): Promise<MemoryEntry[]>;
49
+ /** The session's active goals and their policies, read together so a goal and its policy never disagree. */
50
+ activeGoals(opts: GetActiveGoalsOpts): Promise<ActiveGoals>;
51
+ /** The first `count` (at most 200) unsuperseded raw rows a tenant, a non-empty session id and non-null `origins` (those projects and
52
+ * user-global rows) must narrow, by created descending, then content and id ascending, as `loadFreshRawMemories` does. */
53
+ freshRawEntries(count: number, tenantId: string | undefined, sessionId: string | undefined, origins: readonly string[] | null): Promise<MemoryEntry[]>;
54
+ /** The newest active snapshot a non-null `key` must narrow to one owner and project (null only on an unshared hippo.db), then for its session
55
+ * the newest handoff (same key) and `eventLimit` newest events returned oldest first, each tie broken by the larger id; the caller applies scope. */
56
+ continuity(tenantId: string, eventLimit: number, key: ContinuityKey | null): Promise<ContinuityBlock>;
57
+ /** Resolves a forward claim's tokens to one class and reads its baserate, writing no audit row. */
58
+ planningFallacyEvidence(tenantId: string, classQueryTokens: readonly string[]): Promise<PlanningFallacyEvidence>;
59
+ /** Appends the rows in order, all or none. */
60
+ appendAuditEvents(events: readonly AppendAuditOpts[]): Promise<void>;
61
+ /** Writes the goal log and audit rows in one transaction and rejects with none written if it fails. Then the trace and
62
+ * the strengthen, each on its own: a failure there logs and still resolves, since the reply is already decided. */
63
+ finishRecall(writes: RecallWrites): Promise<void>;
64
+ /** Adds `recalled` to the one store-wide total_recalled counter, no tenant; the SQLite store also rewrites stats.json. */
65
+ bumpRecallStats(recalled: number): Promise<void>;
66
+ /** One token-ledger row. Throws, so the caller decides whether a ledger failure matters. */
67
+ recordTokens(use: TokenUse): Promise<void>;
8
68
  /** Releases the store's connections; `serve()` closes only a store it made itself. */
9
69
  close(): Promise<void>;
10
70
  }
11
- /** The built-in store: today's synchronous hippo.db functions behind the port, one handle per call, so close has nothing to release. */
71
+ /** The store a request runs on: the served one, else hippo.db under its root, as the CLI and SDK callers have it. */
72
+ export declare function storeFor(ctx: {
73
+ readonly hippoRoot: string;
74
+ readonly store?: HippoStore;
75
+ }): HippoStore;
76
+ /** The built-in store: today's synchronous hippo.db functions behind the port, each call on its own short-lived handles, so close has nothing to release. */
12
77
  export declare function sqliteStore(hippoRoot: string): HippoStore;
78
+ /** sqliteStore's continuity read, for the synchronous recall that cannot await the port. */
79
+ export declare function continuityAt(hippoRoot: string, tenantId: string, eventLimit: number, key: ContinuityKey | null): ContinuityBlock;
80
+ /** sqliteStore's finishRecall, for the synchronous recall that cannot await the port. */
81
+ export declare function finishRecallAt(hippoRoot: string, writes: RecallWrites): void;
13
82
  //# sourceMappingURL=store-port.d.ts.map
@@ -1,20 +1,98 @@
1
1
  // The async seam the server reaches its store through, so an add-on can serve from a database other than hippo.db.
2
2
  import { readApiKeyRecord } from './auth.js';
3
- import { closeHippoDb, openHippoDb } from './db.js';
4
- /** The built-in store: today's synchronous hippo.db functions behind the port, one handle per call, so close has nothing to release. */
3
+ import { appendAuditEvent } from './audit.js';
4
+ import { closeHippoDb, openHippoDb, withWriteScope } from './db.js';
5
+ import { activeGoalsWithPolicies, localGoalRecallRows, writeGoalRecallLog, } from './goals.js';
6
+ import { planningFallacyEvidenceAt } from './predictions/planning-fallacy.js';
7
+ import { writeRecallTrace } from './recall-trace.js';
8
+ import { loadEntriesByIds, loadFreshRawMemories } from './store/entry-reads.js';
9
+ import { strengthenRetrievedInOwnTx } from './store/entry-writes.js';
10
+ import { loadLatestHandoff } from './store/handoffs.js';
11
+ import { updateStats } from './store/index-and-stats.js';
12
+ import { loadRecallSearchEntries } from './store/search-rows.js';
13
+ import { listSessionEvents, loadActiveTaskSnapshot } from './store/sessions.js';
14
+ import { recordTokenUse } from './token-ledger.js';
15
+ function onHandle(hippoRoot, fn) {
16
+ const db = openHippoDb(hippoRoot);
17
+ try {
18
+ return fn(db);
19
+ }
20
+ finally {
21
+ closeHippoDb(db);
22
+ }
23
+ }
24
+ /** The store a request runs on: the served one, else hippo.db under its root, as the CLI and SDK callers have it. */
25
+ export function storeFor(ctx) {
26
+ return ctx.store ?? sqliteStore(ctx.hippoRoot);
27
+ }
28
+ /** The built-in store: today's synchronous hippo.db functions behind the port, each call on its own short-lived handles, so close has nothing to release. */
5
29
  export function sqliteStore(hippoRoot) {
6
30
  return {
7
31
  kind: 'sqlite',
8
32
  async findApiKey(keyId) {
9
- const db = openHippoDb(hippoRoot);
10
- try {
11
- return readApiKeyRecord(db, keyId);
12
- }
13
- finally {
14
- closeHippoDb(db);
15
- }
33
+ return onHandle(hippoRoot, (db) => readApiKeyRecord(db, keyId));
34
+ },
35
+ async searchRecallEntries(query, args) {
36
+ return loadRecallSearchEntries(hippoRoot, query, args.limit, args.tenantId, args.requestedScope, args.explicitScopeMode, args.includeSuperseded, args.originProjects, args.ownScope);
37
+ },
38
+ async entriesByIds(ids, tenantId) {
39
+ return loadEntriesByIds(hippoRoot, ids, tenantId);
40
+ },
41
+ async activeGoals(opts) {
42
+ return activeGoalsWithPolicies(hippoRoot, opts);
43
+ },
44
+ async freshRawEntries(count, tenantId, sessionId, origins) {
45
+ return loadFreshRawMemories(hippoRoot, count, tenantId, sessionId, origins ?? undefined);
46
+ },
47
+ async continuity(tenantId, eventLimit, key) {
48
+ return continuityAt(hippoRoot, tenantId, eventLimit, key);
49
+ },
50
+ async planningFallacyEvidence(tenantId, classQueryTokens) {
51
+ return planningFallacyEvidenceAt(hippoRoot, tenantId, classQueryTokens);
52
+ },
53
+ async appendAuditEvents(events) {
54
+ if (events.length === 0)
55
+ return;
56
+ onHandle(hippoRoot, (db) => withWriteScope(db, 'append_audit_events', () => {
57
+ for (const event of events)
58
+ appendAuditEvent(db, event);
59
+ }));
60
+ },
61
+ async finishRecall(writes) {
62
+ finishRecallAt(hippoRoot, writes);
63
+ },
64
+ async bumpRecallStats(recalled) {
65
+ updateStats(hippoRoot, { recalled });
66
+ },
67
+ async recordTokens(use) {
68
+ onHandle(hippoRoot, (db) => recordTokenUse(db, use));
16
69
  },
17
70
  async close() { },
18
71
  };
19
72
  }
73
+ /** sqliteStore's continuity read, for the synchronous recall that cannot await the port. */
74
+ export function continuityAt(hippoRoot, tenantId, eventLimit, key) {
75
+ const activeSnapshot = loadActiveTaskSnapshot(hippoRoot, tenantId, key ?? undefined);
76
+ const sessionId = activeSnapshot?.session_id ?? undefined;
77
+ return {
78
+ activeSnapshot,
79
+ sessionHandoff: sessionId ? loadLatestHandoff(hippoRoot, tenantId, sessionId, {}, key ?? undefined) : null,
80
+ recentSessionEvents: sessionId ? listSessionEvents(hippoRoot, tenantId, { session_id: sessionId, limit: eventLimit }) : [],
81
+ };
82
+ }
83
+ /** sqliteStore's finishRecall, for the synchronous recall that cannot await the port. */
84
+ export function finishRecallAt(hippoRoot, writes) {
85
+ onHandle(hippoRoot, (db) => {
86
+ withWriteScope(db, 'finish_recall', () => {
87
+ writeGoalRecallLog(db, localGoalRecallRows(db, writes.goalLog));
88
+ for (const event of writes.audit)
89
+ appendAuditEvent(db, event);
90
+ });
91
+ // Each opens its own transaction, so neither can share the scope above.
92
+ if (writes.trace)
93
+ writeRecallTrace(db, writes.trace);
94
+ if (writes.strengthen)
95
+ strengthenRetrievedInOwnTx(db, writes.strengthen.ids, writes.strengthen.opts);
96
+ });
97
+ }
20
98
  //# sourceMappingURL=store-port.js.map
package/dist/version.d.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  * an ESM `import` can resolve cleanly, and a hardcoded constant survives
17
17
  * any packager that drops .json files.
18
18
  */
19
- export declare const PACKAGE_VERSION = "1.64.0";
19
+ export declare const PACKAGE_VERSION = "1.65.0";
20
20
  /** The floor a store takes on its first expiring key: the first release with schema v53, set by hand in that release; scripts/check-expiring-keys-floor.mjs gates it. */
21
21
  export declare const EXPIRING_KEYS_MIN_BINARY = "1.64.0";
22
22
  /** The floor a store takes on its first session bind or owner snapshot: first release with v54; set by hand in that release. */
package/dist/version.js CHANGED
@@ -16,7 +16,7 @@
16
16
  * an ESM `import` can resolve cleanly, and a hardcoded constant survives
17
17
  * any packager that drops .json files.
18
18
  */
19
- export const PACKAGE_VERSION = '1.64.0';
19
+ export const PACKAGE_VERSION = '1.65.0';
20
20
  /** The floor a store takes on its first expiring key: the first release with schema v53, set by hand in that release; scripts/check-expiring-keys-floor.mjs gates it. */
21
21
  export const EXPIRING_KEYS_MIN_BINARY = '1.64.0';
22
22
  /** The floor a store takes on its first session bind or owner snapshot: first release with v54; set by hand in that release. */
@@ -2,7 +2,7 @@
2
2
  "id": "hippo-memory",
3
3
  "name": "Hippo Memory",
4
4
  "description": "Make your agent's memory work like a brain. Hippo is long-term memory for coding agents.",
5
- "version": "1.64.0",
5
+ "version": "1.65.0",
6
6
 
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hippo-memory",
3
- "version": "1.64.0",
3
+ "version": "1.65.0",
4
4
  "type": "module",
5
5
  "description": "Make your agent's memory work like a brain. Hippo is long-term memory for coding agents.",
6
6
  "main": "index.ts",
@@ -2,7 +2,7 @@
2
2
  "id": "hippo-memory",
3
3
  "name": "Hippo Memory",
4
4
  "description": "Make your agent's memory work like a brain. Hippo is long-term memory for coding agents.",
5
- "version": "1.64.0",
5
+ "version": "1.65.0",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hippo-memory",
3
- "version": "1.64.0",
3
+ "version": "1.65.0",
4
4
  "description": "Make your agent's memory work like a brain. Hippo is long-term memory for coding agents.",
5
5
  "mcpName": "io.github.kitfunso/hippo-memory",
6
6
  "type": "module",