hippo-memory 1.65.0 → 1.67.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 +2 -0
- package/dist/api/auth.d.ts +11 -2
- package/dist/api/auth.js +25 -2
- package/dist/api/recall-types.d.ts +1 -0
- package/dist/api/recall.js +5 -5
- package/dist/auth.d.ts +3 -1
- package/dist/auth.js +6 -2
- package/dist/capture/compact.d.ts +2 -0
- package/dist/capture/compact.js +12 -6
- package/dist/capture/copilot-transcript.d.ts +15 -0
- package/dist/capture/copilot-transcript.js +40 -0
- package/dist/capture/failure-reading.js +5 -2
- package/dist/capture/transcript.js +7 -1
- package/dist/capture/working-state.js +14 -3
- package/dist/capture-contract.d.ts +4 -2
- package/dist/capture-contract.js +3 -3
- package/dist/cli/context.js +11 -8
- package/dist/cli/hook-blocks.d.ts +2 -0
- package/dist/cli/hook-blocks.js +6 -0
- package/dist/cli/session-hooks.d.ts +1 -1
- package/dist/cli/session-hooks.js +28 -16
- package/dist/cli/setup.js +66 -7
- package/dist/cli/shared.d.ts +5 -0
- package/dist/cli/shared.js +22 -3
- package/dist/cli/usage.js +20 -9
- package/dist/db/open.d.ts +5 -1
- package/dist/db/open.js +28 -7
- package/dist/db/sqlite-blocked.d.ts +11 -1
- package/dist/db/sqlite-blocked.js +20 -2
- package/dist/db.d.ts +2 -2
- package/dist/db.js +2 -2
- package/dist/delivery-recorder.d.ts +3 -1
- package/dist/delivery-recorder.js +1 -1
- package/dist/embeddings.d.ts +11 -2
- package/dist/embeddings.js +20 -11
- package/dist/hooks/copilot.d.ts +45 -0
- package/dist/hooks/copilot.js +187 -0
- package/dist/hooks/json-hooks.d.ts +1 -1
- package/dist/hooks/json-hooks.js +41 -2
- package/dist/hooks/shared.d.ts +5 -1
- package/dist/hooks/shared.js +9 -0
- package/dist/mcp/request.d.ts +0 -2
- package/dist/mcp/request.js +16 -11
- package/dist/prompt-hook.d.ts +6 -0
- package/dist/prompt-hook.js +15 -7
- package/dist/quality-repair.js +3 -0
- package/dist/search/hybrid.d.ts +3 -0
- package/dist/search/physics-search.d.ts +3 -0
- package/dist/search/physics-search.js +18 -19
- package/dist/search/vector.d.ts +7 -2
- package/dist/search/vector.js +24 -10
- package/dist/server/routes/admin.js +1 -1
- package/dist/server/routes/recall.js +6 -9
- package/dist/server/types.d.ts +5 -4
- package/dist/server.d.ts +11 -6
- package/dist/server.js +18 -14
- package/dist/stdin.d.ts +5 -0
- package/dist/stdin.js +62 -0
- package/dist/store/key-audit.d.ts +6 -0
- package/dist/store/key-audit.js +25 -0
- package/dist/store/open.js +2 -1
- package/dist/store-port.d.ts +42 -4
- package/dist/store-port.js +46 -2
- package/dist/vector-store.d.ts +8 -0
- package/dist/vector-store.js +31 -19
- 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
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { calculateStrength } from '../memory.js';
|
|
2
2
|
import { estimateTokens } from '../token-ledger.js';
|
|
3
3
|
import { evalNow, isRecallBoostAblated } from '../ablation.js';
|
|
4
|
-
import {
|
|
4
|
+
import { indexedModel, indexNeedsRebuild } from '../embeddings.js';
|
|
5
5
|
import { resolveEmbeddingProvider } from '../embedding-provider.js';
|
|
6
6
|
import { physicsScore as computePhysicsScores, computeMass } from '../physics.js';
|
|
7
7
|
import { DEFAULT_PHYSICS_CONFIG } from '../physics-config.js';
|
|
8
|
-
import {
|
|
9
|
-
import { openHippoDb, closeHippoDb, rethrowIfSqliteBlocked } from '../db.js';
|
|
8
|
+
import { rethrowIfSqliteBlocked } from '../db.js';
|
|
10
9
|
import { compareScoredResults } from '../compare.js';
|
|
11
10
|
import { log } from '../log.js';
|
|
11
|
+
import { sqliteStore } from '../store-port.js';
|
|
12
12
|
import { churnStaleFactor, summaryMultipliers, summaryScoring } from './boosts.js';
|
|
13
13
|
import { addDagFields, ageInDays } from './breakdown.js';
|
|
14
14
|
import { fitBudget } from './finalize.js';
|
|
15
15
|
import { hybridSearch } from './hybrid.js';
|
|
16
16
|
import { currentEntries } from './as-of.js';
|
|
17
|
-
import { vectorCandidatesOutside } from './vector.js';
|
|
17
|
+
import { requireVectorReads, vectorCandidatesOutside } from './vector.js';
|
|
18
18
|
import { DEFAULT_RECALL_BUDGET } from './types.js';
|
|
19
19
|
/** Scores memories by gravity, momentum and cluster amplification; rows without physics state rank through hybridSearch. */
|
|
20
20
|
export async function physicsSearch(query, entries, options = {}) {
|
|
@@ -27,11 +27,14 @@ export async function physicsSearch(query, entries, options = {}) {
|
|
|
27
27
|
if (options.asOf)
|
|
28
28
|
return hybridSearch(query, entries, options);
|
|
29
29
|
const root = options.hippoRoot;
|
|
30
|
-
const
|
|
30
|
+
const store = options.store ?? sqliteStore(root);
|
|
31
|
+
const queryVector = await physicsQueryVector(query, root, store, options.queryEmbedding);
|
|
31
32
|
if (!queryVector)
|
|
32
33
|
return hybridSearch(query, entries, options);
|
|
33
|
-
|
|
34
|
-
const
|
|
34
|
+
// Checked here too, since a caller's own query vector skips the check inside physicsQueryVector.
|
|
35
|
+
const reads = requireVectorReads(store);
|
|
36
|
+
const pool = currentEntries(await withVectorCandidates(reads, entries, queryVector, options.vectorCandidates), options);
|
|
37
|
+
const physicsMap = await loadCandidateParticles(reads, pool);
|
|
35
38
|
if (!physicsMap)
|
|
36
39
|
return hybridSearch(query, pool, options);
|
|
37
40
|
const pools = splitByParticle(pool, physicsMap, queryVector, now);
|
|
@@ -45,13 +48,15 @@ export async function physicsSearch(query, entries, options = {}) {
|
|
|
45
48
|
return fitBudget(merged, options.budget ?? DEFAULT_RECALL_BUDGET, options.minResults ?? 1, options.cost);
|
|
46
49
|
}
|
|
47
50
|
/** The caller's vector, else the provider's; null sends the caller to hybridSearch. */
|
|
48
|
-
async function physicsQueryVector(query, root, given) {
|
|
51
|
+
async function physicsQueryVector(query, root, store, given) {
|
|
49
52
|
if (given && given.length > 0)
|
|
50
53
|
return given;
|
|
51
54
|
// Physics scores against particle positions, not the stored vector index, so a pruned index must not block it.
|
|
52
55
|
try {
|
|
53
56
|
const provider = resolveEmbeddingProvider(root);
|
|
54
|
-
if (!provider.isAvailable()
|
|
57
|
+
if (!provider.isAvailable())
|
|
58
|
+
return null;
|
|
59
|
+
if (indexNeedsRebuild(indexedModel(await requireVectorReads(store).embeddingIndexState()), provider.id))
|
|
55
60
|
return null;
|
|
56
61
|
const [vec] = await provider.embed([query], 'query');
|
|
57
62
|
return vec && vec.length > 0 ? vec : null;
|
|
@@ -62,11 +67,11 @@ async function physicsQueryVector(query, root, given) {
|
|
|
62
67
|
return null;
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
|
-
function withVectorCandidates(
|
|
70
|
+
async function withVectorCandidates(reads, entries, queryVector, spec) {
|
|
66
71
|
if (!spec)
|
|
67
72
|
return entries;
|
|
68
73
|
try {
|
|
69
|
-
return [...entries, ...vectorCandidatesOutside(
|
|
74
|
+
return [...entries, ...await vectorCandidatesOutside(reads, entries, queryVector, spec)];
|
|
70
75
|
}
|
|
71
76
|
catch (err) {
|
|
72
77
|
rethrowIfSqliteBlocked(err);
|
|
@@ -75,15 +80,9 @@ function withVectorCandidates(root, entries, queryVector, spec) {
|
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
/** Particles for the candidate rows only, so one tenant's search never reads every tenant's physics state. */
|
|
78
|
-
function loadCandidateParticles(
|
|
83
|
+
async function loadCandidateParticles(reads, pool) {
|
|
79
84
|
try {
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
return loadPhysicsState(db, pool.map((e) => e.id));
|
|
83
|
-
}
|
|
84
|
-
finally {
|
|
85
|
-
closeHippoDb(db);
|
|
86
|
-
}
|
|
85
|
+
return await reads.physicsParticles(pool.map((e) => e.id));
|
|
87
86
|
}
|
|
88
87
|
catch (err) {
|
|
89
88
|
rethrowIfSqliteBlocked(err);
|
package/dist/search/vector.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MemoryEntry } from '../memory.js';
|
|
2
|
-
import {
|
|
2
|
+
import type { VectorCandidateSpec } from '../store/search-rows.js';
|
|
3
|
+
import { type HippoStore, type VectorReads } from '../store-port.js';
|
|
3
4
|
import { type CurrentnessOptions } from './as-of.js';
|
|
4
5
|
/** hybridSearch's vector arm: which rows it may add, plus the caller's JS admission rules (exact private regex, entry filters). */
|
|
5
6
|
export type HybridVectorCandidates = VectorCandidateSpec & {
|
|
@@ -16,9 +17,13 @@ export interface VectorArm {
|
|
|
16
17
|
export interface VectorArmOptions extends CurrentnessOptions {
|
|
17
18
|
hippoRoot?: string;
|
|
18
19
|
vectorCandidates?: HybridVectorCandidates;
|
|
20
|
+
/** Where vectors are read; hippo.db under `hippoRoot` when unset. */
|
|
21
|
+
store?: HippoStore;
|
|
19
22
|
}
|
|
23
|
+
/** The store's vector reads; a store without them answers 501, as any unported path does. */
|
|
24
|
+
export declare function requireVectorReads(store: HippoStore): VectorReads;
|
|
20
25
|
/** The nearest admitted rows not already in `entries`. */
|
|
21
|
-
export declare function vectorCandidatesOutside(
|
|
26
|
+
export declare function vectorCandidatesOutside(reads: VectorReads, entries: readonly MemoryEntry[], queryVector: readonly number[], spec: HybridVectorCandidates): Promise<MemoryEntry[]>;
|
|
22
27
|
/** Embeds the query and loads stored vectors; any failure leaves BM25 to rank alone. */
|
|
23
28
|
export declare function resolveVectorArm(query: string, entries: MemoryEntry[], options: VectorArmOptions): Promise<VectorArm>;
|
|
24
29
|
export interface DenseScores {
|
package/dist/search/vector.js
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
|
-
import { cosineSimilarity,
|
|
2
|
-
import { loadVectorCandidateEntries } from '../store/search-rows.js';
|
|
1
|
+
import { cosineSimilarity, indexedModel, indexNeedsRebuild } from '../embeddings.js';
|
|
3
2
|
import { resolveEmbeddingProvider } from '../embedding-provider.js';
|
|
4
3
|
import { rethrowIfSqliteBlocked } from '../db.js';
|
|
5
4
|
import { log } from '../log.js';
|
|
5
|
+
import { requireGroup, sqliteStore } from '../store-port.js';
|
|
6
6
|
import { redactSecretsStrict } from '../secret-detect.js';
|
|
7
7
|
import { currentEntries } from './as-of.js';
|
|
8
8
|
// Search runs on every hook prompt, so one line per reason per process says why vectors went unused without flooding stderr.
|
|
9
9
|
function warnBm25Fallback(reason, detail) {
|
|
10
10
|
log.once(`search.bm25-fallback.${reason}`, 'warn', `hybrid search fell back to BM25 only: ${redactSecretsStrict(detail)}`);
|
|
11
11
|
}
|
|
12
|
+
function reindexHint(storeKind) {
|
|
13
|
+
const why = 'the embedding index was built by another model or is being rebuilt';
|
|
14
|
+
// `hippo embed` writes only hippo.db, so another store gets its vectors by a rebuild from it.
|
|
15
|
+
return storeKind === 'sqlite'
|
|
16
|
+
? `${why}; run 'hippo embed'`
|
|
17
|
+
: `${why}; run 'hippo embed' on the SQLite store, then rebuild the '${storeKind}' database from it`;
|
|
18
|
+
}
|
|
19
|
+
/** The store's vector reads; a store without them answers 501, as any unported path does. */
|
|
20
|
+
export function requireVectorReads(store) {
|
|
21
|
+
return requireGroup(store, 'vectors');
|
|
22
|
+
}
|
|
12
23
|
/** The nearest admitted rows not already in `entries`. */
|
|
13
|
-
export function vectorCandidatesOutside(
|
|
24
|
+
export async function vectorCandidatesOutside(reads, entries, queryVector, spec) {
|
|
14
25
|
const inPool = new Set(entries.map((e) => e.id));
|
|
15
|
-
return
|
|
26
|
+
return (await reads.nearestEntries(queryVector, spec)).filter((e) => !inPool.has(e.id) && (spec.admit?.(e) ?? true));
|
|
16
27
|
}
|
|
17
28
|
/** Embeds the query and loads stored vectors; any failure leaves BM25 to rank alone. */
|
|
18
29
|
export async function resolveVectorArm(query, entries, options) {
|
|
@@ -32,14 +43,17 @@ async function fillVectorArm(arm, query, root, options) {
|
|
|
32
43
|
const provider = resolveEmbeddingProvider(root);
|
|
33
44
|
if (!provider.isAvailable())
|
|
34
45
|
return;
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
const store = options.store ?? sqliteStore(root);
|
|
47
|
+
const reads = requireVectorReads(store);
|
|
48
|
+
const index = await reads.embeddingIndexState();
|
|
49
|
+
if (indexNeedsRebuild(indexedModel(index), provider.id)) {
|
|
50
|
+
warnBm25Fallback('reindex', reindexHint(store.kind));
|
|
37
51
|
return;
|
|
38
52
|
}
|
|
39
53
|
const spec = options.vectorCandidates;
|
|
40
|
-
const vectors =
|
|
54
|
+
const vectors = await reads.storedVectors(arm.entries.map((e) => e.id));
|
|
41
55
|
// Only spend a (possibly paid, off-box) query embedding when there is a stored vector this search can use.
|
|
42
|
-
if (vectors.size === 0 && !(spec !== undefined &&
|
|
56
|
+
if (vectors.size === 0 && !(spec !== undefined && index.hasVectors))
|
|
43
57
|
return;
|
|
44
58
|
const [vec] = await provider.embed([query], 'query');
|
|
45
59
|
arm.queryVector = vec ?? [];
|
|
@@ -47,11 +61,11 @@ async function fillVectorArm(arm, query, root, options) {
|
|
|
47
61
|
warnBm25Fallback('empty-query-vector', 'the embedding provider returned no vector for the query');
|
|
48
62
|
return;
|
|
49
63
|
}
|
|
50
|
-
const added = spec ? vectorCandidatesOutside(
|
|
64
|
+
const added = spec ? await vectorCandidatesOutside(reads, arm.entries, arm.queryVector, spec) : [];
|
|
51
65
|
if (added.length > 0) {
|
|
52
66
|
arm.addedRows = true;
|
|
53
67
|
arm.entries = currentEntries([...arm.entries, ...added], options);
|
|
54
|
-
for (const [id, v] of
|
|
68
|
+
for (const [id, v] of await reads.storedVectors(added.map((e) => e.id)))
|
|
55
69
|
vectors.set(id, v);
|
|
56
70
|
}
|
|
57
71
|
arm.embeddingIndex = Object.fromEntries(vectors);
|
|
@@ -79,7 +79,7 @@ export async function handleListAuthKeys({ req, res, opts, query }) {
|
|
|
79
79
|
export async function handleRevokeAuthKey({ req, res, opts }, keyMatch) {
|
|
80
80
|
validateIdSegment(keyMatch.keyId, 'key id');
|
|
81
81
|
const ctx = await buildContextWithAuth(req, opts);
|
|
82
|
-
const result = authRevoke(ctx, keyMatch.keyId);
|
|
82
|
+
const result = await authRevoke(ctx, keyMatch.keyId);
|
|
83
83
|
sendJson(res, 200, result);
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
@@ -16,21 +16,18 @@ export function __resetSessionRecallHistoryHttp() {
|
|
|
16
16
|
resetSessionRings('http');
|
|
17
17
|
}
|
|
18
18
|
// HTTP threads the ring through opts.recallHistory, so the hint retrieve() returns is the one the caller sees.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return undefined;
|
|
22
|
-
if (sessionId)
|
|
23
|
-
return peekSessionRing('http', ctx.tenantId, sessionId);
|
|
24
|
-
await storeFor(ctx).appendAuditEvents(anchorSkippedRows({ tenantId: ctx.tenantId, actor: ctx.actor.subject }, q));
|
|
25
|
-
return undefined;
|
|
19
|
+
function recallHistoryFor(ctx, sessionId) {
|
|
20
|
+
return sessionId && biasHintEnabled('anchoring') ? peekSessionRing('http', ctx.tenantId, sessionId) : undefined;
|
|
26
21
|
}
|
|
27
22
|
// GET /v1/memories?q=...&limit=...&mode=...&scope=...&include_continuity=1
|
|
28
23
|
export async function handleRecallMemories({ req, res, opts, query }) {
|
|
29
24
|
const { opts: recallOpts, limit, mode, explain } = parseRecallRequest(httpParams(query));
|
|
30
25
|
const { query: q, includeContinuity, sessionId } = recallOpts;
|
|
31
26
|
const ctx = await buildContextWithAuth(req, opts);
|
|
32
|
-
const recallHistory =
|
|
33
|
-
|
|
27
|
+
const recallHistory = recallHistoryFor(ctx, sessionId);
|
|
28
|
+
// Written first in the recall's own write, so a recall that fails leaves no row.
|
|
29
|
+
const leadingAudit = sessionId ? [] : anchorSkippedRows({ tenantId: ctx.tenantId, actor: ctx.actor.subject }, q);
|
|
30
|
+
const result = await retrieve(ctx, { ...recallOpts, limit, mode, explain, recallHistory, leadingAudit });
|
|
34
31
|
// The ring is created only after recall succeeds, so a 400 cannot LRU-evict a live session.
|
|
35
32
|
const ring = sessionRing('http', ctx.tenantId, sessionId);
|
|
36
33
|
if (ring)
|
package/dist/server/types.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
2
2
|
import type { Context } from '../api.js';
|
|
3
3
|
import type { JsonValue } from '../json.js';
|
|
4
4
|
import type { RateLimiter } from '../rate-limit.js';
|
|
5
|
-
import type { HippoStore } from '../store-port.js';
|
|
5
|
+
import type { HippoStore, StoreGroup } from '../store-port.js';
|
|
6
6
|
export interface ServerHandle {
|
|
7
7
|
port: number;
|
|
8
8
|
url: string;
|
|
@@ -28,10 +28,11 @@ export interface AddonCall {
|
|
|
28
28
|
readonly ctx: Context;
|
|
29
29
|
readonly body: Readonly<Record<string, JsonValue>>;
|
|
30
30
|
}
|
|
31
|
-
/** A POST /v1 route an add-on mounts through serve(): core authenticates and parses first, then sends the returned value as 200 JSON. */
|
|
31
|
+
/** A POST /v1 route an add-on mounts through serve(): core authenticates and parses first, then sends the returned value as 200 JSON. Under another store it runs only when that store has its `storeReady` group. */
|
|
32
32
|
export interface AddonRoute {
|
|
33
33
|
readonly path: string;
|
|
34
34
|
readonly handler: (call: AddonCall) => Promise<JsonValue>;
|
|
35
|
+
readonly storeReady?: StoreGroup;
|
|
35
36
|
}
|
|
36
37
|
export interface RateLimitSpec {
|
|
37
38
|
ratePerSec: number;
|
|
@@ -76,10 +77,10 @@ export interface RouteRequest {
|
|
|
76
77
|
opts: ResolvedServeOpts;
|
|
77
78
|
query: URLSearchParams;
|
|
78
79
|
}
|
|
79
|
-
/** One /v1 route: an exact path, a matchPath pattern, or a regex, each paired with the handler for one method.
|
|
80
|
+
/** One /v1 route: an exact path, a matchPath pattern, or a regex, each paired with the handler for one method. Under another store it runs only when that store has its `storeReady` group. */
|
|
80
81
|
export type Route = {
|
|
81
82
|
method: string;
|
|
82
|
-
storeReady?:
|
|
83
|
+
storeReady?: StoreGroup;
|
|
83
84
|
} & ({
|
|
84
85
|
path: string;
|
|
85
86
|
handler: (r: RouteRequest) => Promise<void>;
|
package/dist/server.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type Actor, authCreateSelf, type AuthCreateSelfOpts, type AuthCreateSelfResult, authRevoke, type Context } from './api.js';
|
|
2
|
-
import { ForbiddenError } from './api-errors.js';
|
|
1
|
+
import { type Actor, authCreateSelf, type AuthCreateSelfOpts, type AuthCreateSelfResult, authRevoke, type AuthRevokeReply, type AuthRevokeResult, type Context } from './api.js';
|
|
2
|
+
import { ForbiddenError, NotFoundError } from './api-errors.js';
|
|
3
3
|
import type { ServeOpts, ServerHandle } from './server/types.js';
|
|
4
|
-
export { authCreateSelf, authRevoke, ForbiddenError, type AuthCreateSelfOpts, type AuthCreateSelfResult, type Context, type Actor };
|
|
4
|
+
export { authCreateSelf, authRevoke, ForbiddenError, NotFoundError, type AuthCreateSelfOpts, type AuthCreateSelfResult, type AuthRevokeReply, type AuthRevokeResult, type Context, type Actor, };
|
|
5
5
|
export { isCrossSite, LOOPBACK_HOST_HEADER } from './http-util.js';
|
|
6
6
|
export { __resetSessionRecallHistoryHttp } from './server/routes/recall.js';
|
|
7
7
|
export { clientIpForRateLimit } from './server/client-ip.js';
|
|
@@ -11,9 +11,9 @@ export { HttpError } from './http-util.js';
|
|
|
11
11
|
export { promptHookContext, type CallerProject } from './prompt-hook.js';
|
|
12
12
|
export type { JsonValue } from './json.js';
|
|
13
13
|
export { captureSessionTexts, type SessionCaptureRequest, type SessionCaptureResult } from './capture/session-texts.js';
|
|
14
|
-
export { sqliteStore, type HippoStore, type RecallSearchArgs, type RecallWrites } from './store-port.js';
|
|
14
|
+
export { hasGroup, sqliteStore, type HippoStore, type KeyAudit, type KeyRevoke, type RecallSearchArgs, type RecallWrites, type StoreGroup, type StoreGroups, type VectorReads, } from './store-port.js';
|
|
15
15
|
export type { ApiKeyRecord } from './auth.js';
|
|
16
|
-
export type { AppendAuditOpts } from './audit.js';
|
|
16
|
+
export type { AppendAuditOpts, AuditEvent, ListAuditAfterOpts } from './audit.js';
|
|
17
17
|
export type { ContinuityBlock } from './api/recall-types.js';
|
|
18
18
|
export type { ActiveGoals, GetActiveGoalsOpts, Goal, GoalRecallLogRow, RetrievalPolicy } from './goals.js';
|
|
19
19
|
export type { MemoryEntry } from './memory.js';
|
|
@@ -21,10 +21,15 @@ export type { ClassResolution, PlanningFallacyEvidence } from './predictions/pla
|
|
|
21
21
|
export type { PredictionBaserate } from './predictions/store.js';
|
|
22
22
|
export type { RecallTraceInput } from './recall-trace.js';
|
|
23
23
|
export type { StrengthenOptions } from './store/entry-writes.js';
|
|
24
|
-
export type { OriginFilter } from './store/search-rows.js';
|
|
24
|
+
export type { OriginFilter, RecallScopeFilter, VectorCandidateSpec } from './store/search-rows.js';
|
|
25
25
|
export type { ContinuityKey } from './store/sessions.js';
|
|
26
26
|
export type { TokenUse } from './token-ledger.js';
|
|
27
|
+
export type { EmbeddingIndexState } from './embeddings.js';
|
|
28
|
+
export type { PhysicsParticle } from './physics.js';
|
|
27
29
|
export { StoreBusyError } from './db.js';
|
|
30
|
+
export { decodeVector, EMBEDDING_MODEL_META_KEY, rankVectorRows, type VectorMatch, type VectorRow } from './vector-store.js';
|
|
31
|
+
export { bufferToFloat32 } from './physics-state.js';
|
|
32
|
+
export { OTHER_STORE_MARKER, OtherStoreFolderError, withSqliteAllowed } from './db.js';
|
|
28
33
|
export { authCreate, type AuthCreateOpts, type AuthCreateResult } from './api.js';
|
|
29
34
|
export { isSharedStore } from './config.js';
|
|
30
35
|
export { ownerOrSubject } from './api.js';
|
package/dist/server.js
CHANGED
|
@@ -3,17 +3,17 @@ import { createServer } from 'node:http';
|
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { detectServer, removePidfileIfOwned, writePidfile } from './server-detect.js';
|
|
5
5
|
import { closeHippoDb, getHippoDbPath, isStoreBusy, openHippoDb, outsideRequestStores, runWithRequestStores, SERVER_DB_WAIT_MS, withSqliteBlocked } from './db.js';
|
|
6
|
-
import { sqliteStore } from './store-port.js';
|
|
6
|
+
import { hasGroup, sqliteStore } from './store-port.js';
|
|
7
7
|
import { markSharedStore } from './config.js';
|
|
8
8
|
import { auditWriteFailureCount } from './audit.js';
|
|
9
9
|
import { PACKAGE_VERSION } from './version.js';
|
|
10
10
|
import { errorFields, log } from './log.js';
|
|
11
11
|
import { createRateLimiter } from './rate-limit.js';
|
|
12
|
-
import { authCreateSelf, authRevoke, RecallContractError } from './api.js';
|
|
12
|
+
import { authCreateSelf, authRevoke, RecallContractError, } from './api.js';
|
|
13
13
|
import { handleSlackEventsWebhook } from './connectors/slack/webhook.js';
|
|
14
14
|
import { handleGitHubEventsWebhook } from './connectors/github/webhook.js';
|
|
15
15
|
import { BodyTimeoutError, BodyTooLargeError, closeAfterReply, HttpError, JSON_HEADERS, sendJson, STORE_NOT_PORTED_MESSAGE } from './http-util.js';
|
|
16
|
-
import { ForbiddenError } from './api-errors.js';
|
|
16
|
+
import { ForbiddenError, NotFoundError } from './api-errors.js';
|
|
17
17
|
import { buildContextWithAuth, isLoopback, LIMITER_MAX_KEYS, requireAuth } from './server/auth.js';
|
|
18
18
|
import { enforceRateLimit } from './server/client-ip.js';
|
|
19
19
|
import { drainAndClose } from './server/lifecycle.js';
|
|
@@ -33,7 +33,7 @@ import { handleAssembleSession, handleDrillRecall, handleGetContext, handleRecal
|
|
|
33
33
|
import { handleCloseSkill, handleCreateSkill, handleExportSkills, handleGetSkill, handleListSkills, handleSupersedeSkill } from './server/routes/skills.js';
|
|
34
34
|
import { parseJsonBody } from './server/validation.js';
|
|
35
35
|
// Add-on packages mint and revoke keys through these without importing the whole api surface.
|
|
36
|
-
export { authCreateSelf, authRevoke, ForbiddenError };
|
|
36
|
+
export { authCreateSelf, authRevoke, ForbiddenError, NotFoundError, };
|
|
37
37
|
// Published on the hippo-memory/server subpath before they moved to http-util.ts, so they stay exported here.
|
|
38
38
|
export { isCrossSite, LOOPBACK_HOST_HEADER } from './http-util.js';
|
|
39
39
|
// The code behind these lives in src/server/; this subpath keeps exporting them.
|
|
@@ -46,8 +46,13 @@ export { promptHookContext } from './prompt-hook.js';
|
|
|
46
46
|
// A session-end route stores the turns its caller read from a transcript on the caller's own machine.
|
|
47
47
|
export { captureSessionTexts } from './capture/session-texts.js';
|
|
48
48
|
// An add-on serves from another database by passing serve() its own HippoStore.
|
|
49
|
-
export { sqliteStore } from './store-port.js';
|
|
49
|
+
export { hasGroup, sqliteStore, } from './store-port.js';
|
|
50
50
|
export { StoreBusyError } from './db.js';
|
|
51
|
+
// An add-on store decodes and ranks with hippo.db's own code, so both return the same ids in the same order.
|
|
52
|
+
export { decodeVector, EMBEDDING_MODEL_META_KEY, rankVectorRows } from './vector-store.js';
|
|
53
|
+
export { bufferToFloat32 } from './physics-state.js';
|
|
54
|
+
// store copy --db writes the marker and reads the old hippo.db under the waiver.
|
|
55
|
+
export { OTHER_STORE_MARKER, OtherStoreFolderError, withSqliteAllowed } from './db.js';
|
|
51
56
|
// An add-on's install step mints the first admin key into a store folder it names, which `hippo auth create` cannot reach.
|
|
52
57
|
export { authCreate } from './api.js';
|
|
53
58
|
// An add-on that serves a team store checks the flag before it starts.
|
|
@@ -93,7 +98,7 @@ const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);
|
|
|
93
98
|
const V1_ROUTES = [
|
|
94
99
|
{ method: 'POST', path: '/v1/memories', handler: handleCreateMemory },
|
|
95
100
|
{ method: 'GET', path: '/v1/graph', handler: handleGetGraph },
|
|
96
|
-
{ method: 'GET', path: '/v1/memories', storeReady:
|
|
101
|
+
{ method: 'GET', path: '/v1/memories', storeReady: 'base', handler: handleRecallMemories },
|
|
97
102
|
{ method: 'GET', pattern: '/v1/sessions/:id/assemble', handler: handleAssembleSession },
|
|
98
103
|
{ method: 'GET', pattern: '/v1/recall/drill/:id', handler: handleDrillRecall },
|
|
99
104
|
{ method: 'POST', pattern: '/v1/memories/:id/archive', handler: handleArchiveMemory },
|
|
@@ -105,7 +110,7 @@ const V1_ROUTES = [
|
|
|
105
110
|
{ method: 'POST', path: '/v1/sleep', handler: handleSleep },
|
|
106
111
|
{ method: 'POST', path: '/v1/auth/keys', handler: handleCreateAuthKey },
|
|
107
112
|
{ method: 'GET', path: '/v1/auth/keys', handler: handleListAuthKeys },
|
|
108
|
-
{ method: 'DELETE', pattern: '/v1/auth/keys/:keyId', handler: handleRevokeAuthKey },
|
|
113
|
+
{ method: 'DELETE', pattern: '/v1/auth/keys/:keyId', storeReady: 'keyAudit', handler: handleRevokeAuthKey },
|
|
109
114
|
{ method: 'GET', path: '/v1/quarantine', handler: handleListQuarantine },
|
|
110
115
|
{ method: 'POST', pattern: '/v1/quarantine/:id/approve', handler: handleApproveQuarantine },
|
|
111
116
|
{ method: 'POST', pattern: '/v1/quarantine/:id/reject', handler: handleRejectQuarantine },
|
|
@@ -172,8 +177,7 @@ async function dispatchV1Route(r, method, path) {
|
|
|
172
177
|
const run = routeMatches(route, method, path);
|
|
173
178
|
if (run === null)
|
|
174
179
|
continue;
|
|
175
|
-
|
|
176
|
-
await refuseUnportedRoute(r.req, r.opts);
|
|
180
|
+
await refuseUnportedRoute(r.req, r.opts, route.storeReady);
|
|
177
181
|
await run(r);
|
|
178
182
|
return true;
|
|
179
183
|
}
|
|
@@ -224,7 +228,7 @@ async function dispatchAddonRoute({ req, res, opts }, method, path) {
|
|
|
224
228
|
const route = method === 'POST' ? opts.routes?.find((r) => r.path === path) : undefined;
|
|
225
229
|
if (!route)
|
|
226
230
|
return false;
|
|
227
|
-
await refuseUnportedRoute(req, opts);
|
|
231
|
+
await refuseUnportedRoute(req, opts, route.storeReady);
|
|
228
232
|
const ctx = await buildContextWithAuth(req, opts);
|
|
229
233
|
const body = await parseJsonBody(req, ctx);
|
|
230
234
|
sendJson(res, 200, await route.handler({ ctx, body }));
|
|
@@ -243,9 +247,9 @@ function assertSqliteStore(opts) {
|
|
|
243
247
|
if (opts.store.kind !== 'sqlite')
|
|
244
248
|
throw new HttpError(501, STORE_NOT_PORTED_MESSAGE);
|
|
245
249
|
}
|
|
246
|
-
/** Under another store, a route
|
|
247
|
-
async function refuseUnportedRoute(req, opts) {
|
|
248
|
-
if (opts.store.kind === 'sqlite')
|
|
250
|
+
/** Under another store, a route that names no group, or one the store lacks, answers 501 without running; the caller is checked first, so a bad key is still a 401. */
|
|
251
|
+
async function refuseUnportedRoute(req, opts, group) {
|
|
252
|
+
if (opts.store.kind === 'sqlite' || (group !== undefined && hasGroup(opts.store, group)))
|
|
249
253
|
return;
|
|
250
254
|
await requireAuth(req, opts);
|
|
251
255
|
assertSqliteStore(opts);
|
|
@@ -514,7 +518,7 @@ export async function serve(opts) {
|
|
|
514
518
|
const host = opts.host ?? '127.0.0.1';
|
|
515
519
|
const requestedPort = opts.port ?? Number(envPort() ?? 6789);
|
|
516
520
|
// A frozen copy, so a route the caller adds or renames after boot never skips the check below.
|
|
517
|
-
const routes = Object.freeze((opts.routes ?? []).map(({ path, handler }) => Object.freeze({ path, handler })));
|
|
521
|
+
const routes = Object.freeze((opts.routes ?? []).map(({ path, handler, storeReady }) => Object.freeze(storeReady === undefined ? { path, handler } : { path, handler, storeReady })));
|
|
518
522
|
assertAddonRoutes(routes);
|
|
519
523
|
const publicJsonBodies = assertPublicJson(opts.publicJson ?? {});
|
|
520
524
|
const { perAddress: limiter, callerLimiter, failedAuthLimiter } = bootLimiters(opts.rateLimits);
|
package/dist/stdin.d.ts
CHANGED
|
@@ -9,4 +9,9 @@ export interface BoundedStdin {
|
|
|
9
9
|
* idle (default 1000ms, or `HIPPO_STDIN_WAIT_MS`), refreshed on each chunk
|
|
10
10
|
* so a slow but real write is not cut off, and capped at `waitMs * 10`. */
|
|
11
11
|
export declare function readStdinBounded(waitMs?: number): Promise<BoundedStdin>;
|
|
12
|
+
/** A hook payload with Copilot's camelCase fields copied to the snake_case keys hippo reads. The camelCase keys stay,
|
|
13
|
+
* so the sessionStart reply can follow the sender's casing; text that needs no change comes back exactly as sent. */
|
|
14
|
+
export declare function normaliseHookPayload(text: string | undefined): string | undefined;
|
|
15
|
+
/** The stdin read every hook verb uses: {@link readStdinBounded}, with the payload passed through {@link normaliseHookPayload}. */
|
|
16
|
+
export declare function readHookStdin(waitMs?: number): Promise<BoundedStdin>;
|
|
12
17
|
//# sourceMappingURL=stdin.d.ts.map
|
package/dist/stdin.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { envStdinWaitMs } from './env.js';
|
|
2
|
+
import { isJsonObject } from './hooks/shared.js';
|
|
3
|
+
import { isJsonString } from './json.js';
|
|
2
4
|
function defaultWaitMs() {
|
|
3
5
|
return envStdinWaitMs() ?? 1000;
|
|
4
6
|
}
|
|
@@ -39,4 +41,64 @@ export function readStdinBounded(waitMs = defaultWaitMs()) {
|
|
|
39
41
|
stdin.once('error', done);
|
|
40
42
|
});
|
|
41
43
|
}
|
|
44
|
+
// The Copilot harness's camelCase hook fields and the snake_case keys every hippo payload reader looks up.
|
|
45
|
+
const CAMEL_TO_SNAKE = [
|
|
46
|
+
['sessionId', 'session_id'],
|
|
47
|
+
['transcriptPath', 'transcript_path'],
|
|
48
|
+
['toolName', 'tool_name'],
|
|
49
|
+
['hookEventName', 'hook_event_name'],
|
|
50
|
+
['customInstructions', 'custom_instructions'],
|
|
51
|
+
];
|
|
52
|
+
/** Copilot sends `toolArgs` as the model's raw JSON string; Claude Code and VS Code send `tool_input` already parsed. */
|
|
53
|
+
function parsedToolArgs(value) {
|
|
54
|
+
if (!isJsonString(value))
|
|
55
|
+
return value;
|
|
56
|
+
try {
|
|
57
|
+
// SAFETY: JSON.parse returns a JSON value by definition.
|
|
58
|
+
return JSON.parse(value);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Arguments that are not JSON stay the string the model wrote.
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** The snake_case keys a payload lacks, filled from its camelCase twins; a snake_case key already present wins. */
|
|
66
|
+
function snakeCaseFields(payload) {
|
|
67
|
+
const added = {};
|
|
68
|
+
for (const [camel, snake] of CAMEL_TO_SNAKE) {
|
|
69
|
+
if (camel in payload && !(snake in payload))
|
|
70
|
+
added[snake] = payload[camel];
|
|
71
|
+
}
|
|
72
|
+
if ('toolArgs' in payload && !('tool_input' in payload))
|
|
73
|
+
added.tool_input = parsedToolArgs(payload.toolArgs);
|
|
74
|
+
const error = payload.error;
|
|
75
|
+
// An error object gives its message, so readers that expect the documented string still get one.
|
|
76
|
+
if (isJsonObject(error) && isJsonString(error.message))
|
|
77
|
+
added.error = error.message;
|
|
78
|
+
return added;
|
|
79
|
+
}
|
|
80
|
+
/** A hook payload with Copilot's camelCase fields copied to the snake_case keys hippo reads. The camelCase keys stay,
|
|
81
|
+
* so the sessionStart reply can follow the sender's casing; text that needs no change comes back exactly as sent. */
|
|
82
|
+
export function normaliseHookPayload(text) {
|
|
83
|
+
if (text === undefined || text.trim() === '')
|
|
84
|
+
return text;
|
|
85
|
+
let payload;
|
|
86
|
+
try {
|
|
87
|
+
// SAFETY: JSON.parse returns a JSON value by definition.
|
|
88
|
+
payload = JSON.parse(text.trim());
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Not JSON: each reader already treats that as a malformed payload, so it passes through untouched.
|
|
92
|
+
return text;
|
|
93
|
+
}
|
|
94
|
+
if (!isJsonObject(payload))
|
|
95
|
+
return text;
|
|
96
|
+
const added = snakeCaseFields(payload);
|
|
97
|
+
return Object.keys(added).length === 0 ? text : JSON.stringify({ ...payload, ...added });
|
|
98
|
+
}
|
|
99
|
+
/** The stdin read every hook verb uses: {@link readStdinBounded}, with the payload passed through {@link normaliseHookPayload}. */
|
|
100
|
+
export async function readHookStdin(waitMs) {
|
|
101
|
+
const read = await readStdinBounded(waitMs);
|
|
102
|
+
return { text: normaliseHookPayload(read.text), timedOut: read.timedOut };
|
|
103
|
+
}
|
|
42
104
|
//# sourceMappingURL=stdin.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type DatabaseSyncLike } from '../db.js';
|
|
2
|
+
import type { KeyRevoke } from '../store-port.js';
|
|
3
|
+
/** The revoke and its audit row commit together, so a failed audit write leaves the key live and the caller sees the error. */
|
|
4
|
+
export declare function revokeKeyAt(db: DatabaseSyncLike, revoke: KeyRevoke): string;
|
|
5
|
+
export declare function auditHighIdAt(db: DatabaseSyncLike): number;
|
|
6
|
+
//# sourceMappingURL=key-audit.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// hippo.db's half of the KeyAudit store group.
|
|
2
|
+
import { NotFoundError } from '../api-errors.js';
|
|
3
|
+
import { appendAuditEvent } from '../audit.js';
|
|
4
|
+
import { revokeApiKey } from '../auth.js';
|
|
5
|
+
import { withWriteScope } from '../db.js';
|
|
6
|
+
import { selectApiKeyOwner } from './tenant-lookup.js';
|
|
7
|
+
/** The revoke and its audit row commit together, so a failed audit write leaves the key live and the caller sees the error. */
|
|
8
|
+
export function revokeKeyAt(db, revoke) {
|
|
9
|
+
return withWriteScope(db, 'revoke_api_key', () => {
|
|
10
|
+
const row = selectApiKeyOwner(db, revoke.keyId);
|
|
11
|
+
if (!row || row.tenantId !== revoke.tenantId)
|
|
12
|
+
throw new NotFoundError(`Unknown key_id: ${revoke.keyId}`);
|
|
13
|
+
if (row.revokedAt)
|
|
14
|
+
return row.revokedAt;
|
|
15
|
+
revokeApiKey(db, revoke.keyId, revoke.at);
|
|
16
|
+
appendAuditEvent(db, { tenantId: row.tenantId, actor: revoke.actor, op: 'auth_revoke', targetId: revoke.keyId });
|
|
17
|
+
return revoke.at;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function auditHighIdAt(db) {
|
|
21
|
+
// SAFETY: the SELECT names one column, seq; `.get` returns undefined before audit_log's first row.
|
|
22
|
+
const row = db.prepare("SELECT seq FROM sqlite_sequence WHERE name = 'audit_log'").get();
|
|
23
|
+
return row?.seq ?? 0;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=key-audit.js.map
|
package/dist/store/open.js
CHANGED
|
@@ -25,9 +25,10 @@ export function initStore(hippoRoot) {
|
|
|
25
25
|
}
|
|
26
26
|
/** One open connection with init done on it, for callers who used to pay for `initStore` + a second `openHippoDb`. */
|
|
27
27
|
export function openStore(hippoRoot, opts) {
|
|
28
|
-
|
|
28
|
+
// Open first: a folder marked for another store must refuse before any mirror folder appears.
|
|
29
29
|
const db = openHippoDb(hippoRoot, opts);
|
|
30
30
|
try {
|
|
31
|
+
ensureMirrorDirectories(hippoRoot);
|
|
31
32
|
const bootstrapped = bootstrapLegacyStore(db, hippoRoot);
|
|
32
33
|
if (bootstrapped) {
|
|
33
34
|
syncMirrorFiles(hippoRoot, db);
|
package/dist/store-port.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { type ApiKeyRecord } from './auth.js';
|
|
2
|
-
import { type AppendAuditOpts } from './audit.js';
|
|
2
|
+
import { type AppendAuditOpts, type AuditEvent, type ListAuditAfterOpts } from './audit.js';
|
|
3
3
|
import type { ContinuityBlock } from './api/recall-types.js';
|
|
4
|
+
import { type EmbeddingIndexState } from './embeddings.js';
|
|
4
5
|
import { type ActiveGoals, type GetActiveGoalsOpts, type GoalRecallLogRow } from './goals.js';
|
|
5
6
|
import type { MemoryEntry } from './memory.js';
|
|
7
|
+
import type { PhysicsParticle } from './physics.js';
|
|
6
8
|
import { type PlanningFallacyEvidence } from './predictions/planning-fallacy.js';
|
|
7
9
|
import { type RecallTraceInput } from './recall-trace.js';
|
|
8
10
|
import { type StrengthenOptions } from './store/entry-writes.js';
|
|
9
|
-
import { type OriginFilter } from './store/search-rows.js';
|
|
11
|
+
import { type OriginFilter, type VectorCandidateSpec } from './store/search-rows.js';
|
|
10
12
|
import { type ContinuityKey } from './store/sessions.js';
|
|
11
13
|
import { type TokenUse } from './token-ledger.js';
|
|
12
14
|
/** The arguments of `loadRecallSearchEntries` after the query, by name. */
|
|
@@ -35,8 +37,44 @@ export interface RecallWrites {
|
|
|
35
37
|
readonly opts: StrengthenOptions;
|
|
36
38
|
};
|
|
37
39
|
}
|
|
40
|
+
/** The reads behind recall's vector arm. */
|
|
41
|
+
export interface VectorReads {
|
|
42
|
+
/** The stored model and whether any vector exists, in one snapshot. */
|
|
43
|
+
embeddingIndexState(): Promise<EmbeddingIndexState>;
|
|
44
|
+
storedVectors(ids: readonly string[]): Promise<Map<string, number[]>>;
|
|
45
|
+
/** The `spec.limit ?? 50` rows nearest `queryVector` by `rankVectorRows` among those where the tenant matches (when set), kind is not 'archived', a superseded row
|
|
46
|
+
* shows only with `includeSuperseded`, scope follows `RecallScopeFilter`, and origin is '' or listed (when `spec.origin` is set). */
|
|
47
|
+
nearestEntries(queryVector: readonly number[], spec: VectorCandidateSpec): Promise<MemoryEntry[]>;
|
|
48
|
+
/** The particles of `ids` only; an empty list reads nothing. */
|
|
49
|
+
physicsParticles(ids: readonly string[]): Promise<Map<string, PhysicsParticle>>;
|
|
50
|
+
}
|
|
51
|
+
export interface KeyRevoke {
|
|
52
|
+
readonly tenantId: string;
|
|
53
|
+
readonly keyId: string;
|
|
54
|
+
readonly actor: string;
|
|
55
|
+
readonly at: string;
|
|
56
|
+
}
|
|
57
|
+
export interface KeyAudit {
|
|
58
|
+
/** Sets revoked_at to `at` and appends one auth_revoke row (the key's tenant, `actor`, the key id) in one transaction, then resolves to `at`. A revoked
|
|
59
|
+
* key resolves to its own revoked_at and writes nothing; a key missing from `tenantId` rejects with NotFoundError `Unknown key_id: <keyId>`. */
|
|
60
|
+
revokeApiKey(revoke: KeyRevoke): Promise<string>;
|
|
61
|
+
/** As `listAuditEventsAfter`: rows above `afterId` in ascending id order, with its RangeErrors, limit clamp and tenant filter. */
|
|
62
|
+
auditEventsAfter(opts: ListAuditAfterOpts): Promise<AuditEvent[]>;
|
|
63
|
+
/** The highest audit id ever assigned, pruned rows included, 0 before the first; a tail that sees it drop knows the log was restored. */
|
|
64
|
+
auditHighId(): Promise<number>;
|
|
65
|
+
}
|
|
66
|
+
/** The optional groups: a store sets each one whole or leaves it unset, and a route or MCP tool names the one it needs. */
|
|
67
|
+
export interface StoreGroups {
|
|
68
|
+
/** Unset on a store built before them, where hybrid and physics recall under an embedding provider answer 501. */
|
|
69
|
+
readonly vectors: VectorReads;
|
|
70
|
+
readonly keyAudit: KeyAudit;
|
|
71
|
+
}
|
|
72
|
+
export type StoreGroup = 'base' | keyof StoreGroups;
|
|
73
|
+
export declare function hasGroup(store: HippoStore, group: StoreGroup): boolean;
|
|
74
|
+
/** The group's methods; a store without them throws StoreNotPortedError, which answers 501 as any unported path does. */
|
|
75
|
+
export declare function requireGroup<G extends keyof StoreGroups>(store: HippoStore, group: G): StoreGroups[G];
|
|
38
76
|
/** 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`. */
|
|
39
|
-
export interface HippoStore {
|
|
77
|
+
export interface HippoStore extends Partial<StoreGroups> {
|
|
40
78
|
/** '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. */
|
|
41
79
|
readonly kind: string;
|
|
42
80
|
/** The api_keys row for `keyId` with its scope grants, revoked or not; null when no row matches. */
|
|
@@ -74,7 +112,7 @@ export declare function storeFor(ctx: {
|
|
|
74
112
|
readonly store?: HippoStore;
|
|
75
113
|
}): HippoStore;
|
|
76
114
|
/** 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. */
|
|
77
|
-
export declare function sqliteStore(hippoRoot: string): HippoStore;
|
|
115
|
+
export declare function sqliteStore(hippoRoot: string): HippoStore & StoreGroups;
|
|
78
116
|
/** sqliteStore's continuity read, for the synchronous recall that cannot await the port. */
|
|
79
117
|
export declare function continuityAt(hippoRoot: string, tenantId: string, eventLimit: number, key: ContinuityKey | null): ContinuityBlock;
|
|
80
118
|
/** sqliteStore's finishRecall, for the synchronous recall that cannot await the port. */
|