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.
- package/README.md +1 -1
- 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/maintenance.d.ts +2 -1
- package/dist/cli/maintenance.js +17 -11
- 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/embedding-provider.js +4 -3
- 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/local-embedding.d.ts +2 -0
- package/dist/local-embedding.js +20 -4
- 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/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,
|
|
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,
|
|
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
|
|
267
|
+
if (await runWithRequestStores(() => dispatchScopedRoute(routeRequest, method, path), { busyWaitMs: SERVER_DB_WAIT_MS }))
|
|
268
268
|
return;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = () =>
|
|
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
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
closeHippoDb(db);
|
|
91
|
+
closeHippoDb(db);
|
|
98
92
|
}
|
|
99
|
-
|
|
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) {
|
package/dist/store-port.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/store-port.js
CHANGED
|
@@ -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 {
|
|
4
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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.
|
|
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.
|
|
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. */
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "1.65.0",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
package/package.json
CHANGED