hippo-memory 1.66.0 → 1.68.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 +16 -11
- package/dist/api/auth.js +117 -61
- package/dist/api/types.d.ts +9 -0
- package/dist/auth.d.ts +22 -1
- package/dist/auth.js +20 -8
- 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/sqlite-blocked.js +1 -1
- package/dist/delivery-recorder.d.ts +3 -1
- package/dist/delivery-recorder.js +1 -1
- package/dist/embeddings.d.ts +10 -10
- package/dist/embeddings.js +103 -18
- 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/search/vector.js +2 -5
- package/dist/server/routes/admin.js +3 -3
- package/dist/server/types.d.ts +5 -4
- package/dist/server.d.ts +11 -8
- package/dist/server.js +20 -19
- 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/key-writes.d.ts +9 -0
- package/dist/store/key-writes.js +30 -0
- package/dist/store/vector-writes.d.ts +6 -0
- package/dist/store/vector-writes.js +59 -0
- package/dist/store-port.d.ts +101 -9
- package/dist/store-port.js +53 -2
- 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/mcp/request.js
CHANGED
|
@@ -5,7 +5,7 @@ import { getGlobalRoot, initGlobal } from '../shared.js';
|
|
|
5
5
|
import { loadConfig } from '../config.js';
|
|
6
6
|
import { resolveTenantId } from '../tenant.js';
|
|
7
7
|
import { rethrowIfSqliteBlocked, runWithRequestStores } from '../db.js';
|
|
8
|
-
import { storeFor } from '../store-port.js';
|
|
8
|
+
import { hasGroup, storeFor } from '../store-port.js';
|
|
9
9
|
import { estimateTokens } from '../token-ledger.js';
|
|
10
10
|
import { PACKAGE_VERSION } from '../version.js';
|
|
11
11
|
import { validateToolArgs } from './tool-args.js';
|
|
@@ -60,7 +60,7 @@ export async function recordMcpTokens(toolName, output, ctx) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
const TOOL_HANDLERS = new Map([
|
|
63
|
-
['hippo_recall', { handler: runRecallTool, storeReady:
|
|
63
|
+
['hippo_recall', { handler: runRecallTool, storeReady: 'base' }],
|
|
64
64
|
['hippo_assemble', { handler: runAssembleTool }],
|
|
65
65
|
['hippo_drill', { handler: runDrillTool }],
|
|
66
66
|
['hippo_predict_baserate', { handler: runPredictBaserateTool }],
|
|
@@ -74,12 +74,14 @@ const TOOL_HANDLERS = new Map([
|
|
|
74
74
|
['hippo_share', { handler: runShareTool }],
|
|
75
75
|
['hippo_peers', { handler: runPeersTool }],
|
|
76
76
|
]);
|
|
77
|
-
/** The
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
/** The served store when it is not hippo.db, else null. */
|
|
78
|
+
function otherStore(ctx) {
|
|
79
|
+
const store = ctx?.store;
|
|
80
|
+
return store !== undefined && store.kind !== 'sqlite' ? store : null;
|
|
81
|
+
}
|
|
82
|
+
function runsOn(store, toolName) {
|
|
83
|
+
const group = TOOL_HANDLERS.get(toolName)?.storeReady;
|
|
84
|
+
return group !== undefined && hasGroup(store, group);
|
|
83
85
|
}
|
|
84
86
|
async function executeTool(name, args, ctx) {
|
|
85
87
|
// When a transport hands us a context (HTTP path), trust it: the HTTP
|
|
@@ -130,8 +132,10 @@ export async function handleMcpRequest(req, ctx) {
|
|
|
130
132
|
};
|
|
131
133
|
case 'notifications/initialized':
|
|
132
134
|
return null;
|
|
133
|
-
case 'tools/list':
|
|
134
|
-
|
|
135
|
+
case 'tools/list': {
|
|
136
|
+
const other = otherStore(ctx);
|
|
137
|
+
return { jsonrpc: '2.0', id, result: { tools: other ? TOOLS.filter((t) => runsOn(other, t.name)) : TOOLS } };
|
|
138
|
+
}
|
|
135
139
|
case 'tools/call': {
|
|
136
140
|
const nameValue = params?.name;
|
|
137
141
|
const toolName = isJsonString(nameValue) ? nameValue : '';
|
|
@@ -140,7 +144,8 @@ export async function handleMcpRequest(req, ctx) {
|
|
|
140
144
|
return { jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown tool: ${toolName.slice(0, 128)}` } };
|
|
141
145
|
}
|
|
142
146
|
// The same refusal a ported tool gives when it reaches hippo.db, so a client handles one shape.
|
|
143
|
-
|
|
147
|
+
const other = otherStore(ctx);
|
|
148
|
+
if (other && !runsOn(other, toolName)) {
|
|
144
149
|
return { jsonrpc: '2.0', id, error: { code: -32603, message: STORE_NOT_PORTED_MESSAGE } };
|
|
145
150
|
}
|
|
146
151
|
const refusal = sharedStoreRefusal(toolName, ctx);
|
package/dist/prompt-hook.d.ts
CHANGED
|
@@ -21,7 +21,13 @@ export interface ContextView {
|
|
|
21
21
|
/** The static block's hash the caller says it printed last. Undefined keeps the ledger-only skip rule;
|
|
22
22
|
* a string or null also needs that hash to match, because the ledger counts blocks a caller that prints elsewhere may never have shown. */
|
|
23
23
|
readonly printedHash?: string | null;
|
|
24
|
+
/** The reply's JSON wrapper; absent means Claude Code's UserPromptSubmit. */
|
|
25
|
+
readonly envelope?: HookEnvelope;
|
|
24
26
|
}
|
|
27
|
+
/** `copilot-session-start` is the Copilot harness's top-level reply, `vscode-session-start` the nested one VS Code Local reads. */
|
|
28
|
+
export type HookEnvelope = 'user-prompt-submit' | 'copilot-session-start' | 'vscode-session-start';
|
|
29
|
+
/** A Copilot sessionStart reply's wrapper, picked by the sender's casing: stdin.ts keeps a camelCase `sessionId`, which only the Copilot harness sends. */
|
|
30
|
+
export declare function sessionStartEnvelope(stdinText: string | undefined): HookEnvelope;
|
|
25
31
|
export declare function hasContextData(result: ContextResult): boolean;
|
|
26
32
|
export type RenderItem = {
|
|
27
33
|
entry: MemoryEntry;
|
package/dist/prompt-hook.js
CHANGED
|
@@ -11,7 +11,7 @@ import { withLedgerDb } from './ledger-db.js';
|
|
|
11
11
|
import { sessionPilotArm } from './pilot-arm.js';
|
|
12
12
|
import { assertCallerProject, MAX_PROJECT_ALIASES } from './project-identity.js';
|
|
13
13
|
import { writeDeliveryEventAtRoot, writeDeliveryEventOnHandle } from './recall-trace.js';
|
|
14
|
-
import { blockHash, estimateTokens, isSubagentPayload, lastSentState, recordTokenUse, shouldSkipUnchanged } from './token-ledger.js';
|
|
14
|
+
import { blockHash, estimateTokens, hookPayloadString, isSubagentPayload, lastSentState, recordTokenUse, shouldSkipUnchanged, } from './token-ledger.js';
|
|
15
15
|
/** With `db`, writes on the token ledger's handle (same store); without it, opens its own. A second flush is a no-op. */
|
|
16
16
|
export function flushDeliveryRecorder(rec, db) {
|
|
17
17
|
if (rec === null)
|
|
@@ -24,6 +24,16 @@ export function flushDeliveryRecorder(rec, db) {
|
|
|
24
24
|
console.error(`[hippo] delivery ledger write failed:${error instanceof Error ? error.message : String(error)}`);
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
/** A Copilot sessionStart reply's wrapper, picked by the sender's casing: stdin.ts keeps a camelCase `sessionId`, which only the Copilot harness sends. */
|
|
28
|
+
export function sessionStartEnvelope(stdinText) {
|
|
29
|
+
return hookPayloadString(stdinText, 'sessionId') !== null ? 'copilot-session-start' : 'vscode-session-start';
|
|
30
|
+
}
|
|
31
|
+
function hookReply(envelope, additionalContext) {
|
|
32
|
+
if (envelope === 'copilot-session-start')
|
|
33
|
+
return JSON.stringify({ additionalContext });
|
|
34
|
+
const hookEventName = envelope === 'vscode-session-start' ? 'SessionStart' : 'UserPromptSubmit';
|
|
35
|
+
return JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } });
|
|
36
|
+
}
|
|
27
37
|
const NOTHING_RENDERED = Object.freeze({ stdout: '', staticHash: null });
|
|
28
38
|
export function hasContextData(result) {
|
|
29
39
|
return Boolean(result.entries.length > 0 ||
|
|
@@ -70,12 +80,7 @@ function renderAdditionalContext(view) {
|
|
|
70
80
|
rec?.delivered({ state: 'reused', staticHash: blockHash(staticBlock), staticReused });
|
|
71
81
|
return NOTHING_RENDERED;
|
|
72
82
|
}
|
|
73
|
-
const stdout =
|
|
74
|
-
hookSpecificOutput: {
|
|
75
|
-
hookEventName: 'UserPromptSubmit',
|
|
76
|
-
additionalContext,
|
|
77
|
-
},
|
|
78
|
-
});
|
|
83
|
+
const stdout = hookReply(view.envelope ?? 'user-prompt-submit', additionalContext);
|
|
79
84
|
rec?.delivered({
|
|
80
85
|
state: staticReused ? 'reused-recall-sent' : 'sent',
|
|
81
86
|
staticHash: staticBlock.trim() ? blockHash(staticBlock) : null,
|
|
@@ -94,6 +99,9 @@ function skipUnchangedStatic(view, surface, staticBlock, recallBlock, staticCoun
|
|
|
94
99
|
const ledgerOpts = { sharedStore: view.sharedStore };
|
|
95
100
|
if (!view.pinnedOnly || payloadSessionId === undefined)
|
|
96
101
|
return false;
|
|
102
|
+
// Session start is the only injection Copilot gets, so a resumed session needs the whole block again.
|
|
103
|
+
if (view.envelope === 'copilot-session-start' || view.envelope === 'vscode-session-start')
|
|
104
|
+
return false;
|
|
97
105
|
const injectCfg = loadConfig(hippoRoot).pinnedInject;
|
|
98
106
|
if (injectCfg.skipUnchanged === false)
|
|
99
107
|
return false;
|
package/dist/search/vector.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { cosineSimilarity, indexedModel, indexNeedsRebuild } from '../embeddings.js';
|
|
2
2
|
import { resolveEmbeddingProvider } from '../embedding-provider.js';
|
|
3
3
|
import { rethrowIfSqliteBlocked } from '../db.js';
|
|
4
|
-
import { StoreNotPortedError } from '../db/sqlite-blocked.js';
|
|
5
4
|
import { log } from '../log.js';
|
|
6
|
-
import { sqliteStore } from '../store-port.js';
|
|
5
|
+
import { requireGroup, sqliteStore } from '../store-port.js';
|
|
7
6
|
import { redactSecretsStrict } from '../secret-detect.js';
|
|
8
7
|
import { currentEntries } from './as-of.js';
|
|
9
8
|
// Search runs on every hook prompt, so one line per reason per process says why vectors went unused without flooding stderr.
|
|
@@ -19,9 +18,7 @@ function reindexHint(storeKind) {
|
|
|
19
18
|
}
|
|
20
19
|
/** The store's vector reads; a store without them answers 501, as any unported path does. */
|
|
21
20
|
export function requireVectorReads(store) {
|
|
22
|
-
|
|
23
|
-
throw new StoreNotPortedError(store.kind, 'vectors');
|
|
24
|
-
return store.vectors;
|
|
21
|
+
return requireGroup(store, 'vectors');
|
|
25
22
|
}
|
|
26
23
|
/** The nearest admitted rows not already in `entries`. */
|
|
27
24
|
export async function vectorCandidatesOutside(reads, entries, queryVector, spec) {
|
|
@@ -44,7 +44,7 @@ export async function handleCreateAuthKey({ req, res, opts }) {
|
|
|
44
44
|
// bound to the caller's authenticated tenant (ctx.tenantId, resolved
|
|
45
45
|
// from the Bearer token). Forwarding body.tenantId here would let
|
|
46
46
|
// tenant A mint a key for tenant B — see authCreate doc comment.
|
|
47
|
-
const result = authCreate(ctx, {
|
|
47
|
+
const result = await authCreate(ctx, {
|
|
48
48
|
label: labelRaw,
|
|
49
49
|
role,
|
|
50
50
|
});
|
|
@@ -68,7 +68,7 @@ export async function handleListAuthKeys({ req, res, opts, query }) {
|
|
|
68
68
|
const limit = parseListLimit(query.get('limit'), MAX_AUTH_KEYS_PAGE, MAX_AUTH_KEYS_PAGE);
|
|
69
69
|
const after = parseCursor(query.get('cursor'), 'integer', 'integer');
|
|
70
70
|
const ctx = await buildContextWithAuth(req, opts);
|
|
71
|
-
const page = pageOf(authListRows(ctx, { active, limit: limit + 1, after }), limit, (r) => ({ key: r.rowId, id: r.rowId }));
|
|
71
|
+
const page = pageOf(await authListRows(ctx, { active, limit: limit + 1, after }), limit, (r) => ({ key: r.rowId, id: r.rowId }));
|
|
72
72
|
setNextCursorHeader(res, page.nextCursor);
|
|
73
73
|
sendJson(res, 200, page.items.map((r) => r.key));
|
|
74
74
|
return;
|
|
@@ -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
|
}
|
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,11 @@ 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, type VectorReads } from './store-port.js';
|
|
15
|
-
export type {
|
|
16
|
-
export type {
|
|
14
|
+
export { hasGroup, sqliteStore, type HippoStore, type KeyAudit, type KeyListQuery, type KeyMint, type KeyRevoke, type KeyWrites, type RecallSearchArgs, type RecallWrites, type SelfKeyMint, type StoreGroup, type StoreGroups, type VectorReads, type VectorBackfillQuery, type VectorRowWrite, type VectorWrite, type VectorWriteResult, type VectorWrites, } from './store-port.js';
|
|
15
|
+
export type { HippoDbContext, StoreReply } from './api/types.js';
|
|
16
|
+
export type { ApiKeyListItem, ApiKeyListRow, ApiKeyRecord, ListApiKeysOpts, NewApiKey } from './auth.js';
|
|
17
|
+
export type { KeysetPosition } from './keyset.js';
|
|
18
|
+
export type { AppendAuditOpts, AuditEvent, ListAuditAfterOpts } from './audit.js';
|
|
17
19
|
export type { ContinuityBlock } from './api/recall-types.js';
|
|
18
20
|
export type { ActiveGoals, GetActiveGoalsOpts, Goal, GoalRecallLogRow, RetrievalPolicy } from './goals.js';
|
|
19
21
|
export type { MemoryEntry } from './memory.js';
|
|
@@ -27,8 +29,9 @@ export type { TokenUse } from './token-ledger.js';
|
|
|
27
29
|
export type { EmbeddingIndexState } from './embeddings.js';
|
|
28
30
|
export type { PhysicsParticle } from './physics.js';
|
|
29
31
|
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 { decodeVector, EMBEDDING_MODEL_META_KEY, encodeVector, rankVectorRows, type VectorMatch, type VectorRow } from './vector-store.js';
|
|
33
|
+
export { bufferToFloat32, float32ToBuffer } from './physics-state.js';
|
|
34
|
+
export { replacesIndex } from './embeddings.js';
|
|
32
35
|
export { OTHER_STORE_MARKER, OtherStoreFolderError, withSqliteAllowed } from './db.js';
|
|
33
36
|
export { authCreate, type AuthCreateOpts, type AuthCreateResult } from './api.js';
|
|
34
37
|
export { isSharedStore } from './config.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,11 +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,
|
|
52
|
-
|
|
53
|
-
export {
|
|
51
|
+
// An add-on store encodes, decodes and ranks vectors and particles with hippo.db's own code, and drops the index by its rule,
|
|
52
|
+
// so both stores keep the same bytes and return the same ids in the same order.
|
|
53
|
+
export { decodeVector, EMBEDDING_MODEL_META_KEY, encodeVector, rankVectorRows } from './vector-store.js';
|
|
54
|
+
export { bufferToFloat32, float32ToBuffer } from './physics-state.js';
|
|
55
|
+
export { replacesIndex } from './embeddings.js';
|
|
54
56
|
// store copy --db writes the marker and reads the old hippo.db under the waiver.
|
|
55
57
|
export { OTHER_STORE_MARKER, OtherStoreFolderError, withSqliteAllowed } from './db.js';
|
|
56
58
|
// An add-on's install step mints the first admin key into a store folder it names, which `hippo auth create` cannot reach.
|
|
@@ -98,7 +100,7 @@ const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);
|
|
|
98
100
|
const V1_ROUTES = [
|
|
99
101
|
{ method: 'POST', path: '/v1/memories', handler: handleCreateMemory },
|
|
100
102
|
{ method: 'GET', path: '/v1/graph', handler: handleGetGraph },
|
|
101
|
-
{ method: 'GET', path: '/v1/memories', storeReady:
|
|
103
|
+
{ method: 'GET', path: '/v1/memories', storeReady: 'base', handler: handleRecallMemories },
|
|
102
104
|
{ method: 'GET', pattern: '/v1/sessions/:id/assemble', handler: handleAssembleSession },
|
|
103
105
|
{ method: 'GET', pattern: '/v1/recall/drill/:id', handler: handleDrillRecall },
|
|
104
106
|
{ method: 'POST', pattern: '/v1/memories/:id/archive', handler: handleArchiveMemory },
|
|
@@ -108,9 +110,9 @@ const V1_ROUTES = [
|
|
|
108
110
|
{ method: 'POST', path: '/v1/outcome', handler: handleApplyOutcome },
|
|
109
111
|
{ method: 'GET', path: '/v1/context', handler: handleGetContext },
|
|
110
112
|
{ method: 'POST', path: '/v1/sleep', handler: handleSleep },
|
|
111
|
-
{ method: 'POST', path: '/v1/auth/keys', handler: handleCreateAuthKey },
|
|
112
|
-
{ method: 'GET', path: '/v1/auth/keys', handler: handleListAuthKeys },
|
|
113
|
-
{ method: 'DELETE', pattern: '/v1/auth/keys/:keyId', handler: handleRevokeAuthKey },
|
|
113
|
+
{ method: 'POST', path: '/v1/auth/keys', storeReady: 'keyWrites', handler: handleCreateAuthKey },
|
|
114
|
+
{ method: 'GET', path: '/v1/auth/keys', storeReady: 'keyWrites', handler: handleListAuthKeys },
|
|
115
|
+
{ method: 'DELETE', pattern: '/v1/auth/keys/:keyId', storeReady: 'keyAudit', handler: handleRevokeAuthKey },
|
|
114
116
|
{ method: 'GET', path: '/v1/quarantine', handler: handleListQuarantine },
|
|
115
117
|
{ method: 'POST', pattern: '/v1/quarantine/:id/approve', handler: handleApproveQuarantine },
|
|
116
118
|
{ method: 'POST', pattern: '/v1/quarantine/:id/reject', handler: handleRejectQuarantine },
|
|
@@ -177,8 +179,7 @@ async function dispatchV1Route(r, method, path) {
|
|
|
177
179
|
const run = routeMatches(route, method, path);
|
|
178
180
|
if (run === null)
|
|
179
181
|
continue;
|
|
180
|
-
|
|
181
|
-
await refuseUnportedRoute(r.req, r.opts);
|
|
182
|
+
await refuseUnportedRoute(r.req, r.opts, route.storeReady);
|
|
182
183
|
await run(r);
|
|
183
184
|
return true;
|
|
184
185
|
}
|
|
@@ -229,7 +230,7 @@ async function dispatchAddonRoute({ req, res, opts }, method, path) {
|
|
|
229
230
|
const route = method === 'POST' ? opts.routes?.find((r) => r.path === path) : undefined;
|
|
230
231
|
if (!route)
|
|
231
232
|
return false;
|
|
232
|
-
await refuseUnportedRoute(req, opts);
|
|
233
|
+
await refuseUnportedRoute(req, opts, route.storeReady);
|
|
233
234
|
const ctx = await buildContextWithAuth(req, opts);
|
|
234
235
|
const body = await parseJsonBody(req, ctx);
|
|
235
236
|
sendJson(res, 200, await route.handler({ ctx, body }));
|
|
@@ -248,9 +249,9 @@ function assertSqliteStore(opts) {
|
|
|
248
249
|
if (opts.store.kind !== 'sqlite')
|
|
249
250
|
throw new HttpError(501, STORE_NOT_PORTED_MESSAGE);
|
|
250
251
|
}
|
|
251
|
-
/** Under another store, a route
|
|
252
|
-
async function refuseUnportedRoute(req, opts) {
|
|
253
|
-
if (opts.store.kind === 'sqlite')
|
|
252
|
+
/** 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. */
|
|
253
|
+
async function refuseUnportedRoute(req, opts, group) {
|
|
254
|
+
if (opts.store.kind === 'sqlite' || (group !== undefined && hasGroup(opts.store, group)))
|
|
254
255
|
return;
|
|
255
256
|
await requireAuth(req, opts);
|
|
256
257
|
assertSqliteStore(opts);
|
|
@@ -519,7 +520,7 @@ export async function serve(opts) {
|
|
|
519
520
|
const host = opts.host ?? '127.0.0.1';
|
|
520
521
|
const requestedPort = opts.port ?? Number(envPort() ?? 6789);
|
|
521
522
|
// A frozen copy, so a route the caller adds or renames after boot never skips the check below.
|
|
522
|
-
const routes = Object.freeze((opts.routes ?? []).map(({ path, handler }) => Object.freeze({ path, handler })));
|
|
523
|
+
const routes = Object.freeze((opts.routes ?? []).map(({ path, handler, storeReady }) => Object.freeze(storeReady === undefined ? { path, handler } : { path, handler, storeReady })));
|
|
523
524
|
assertAddonRoutes(routes);
|
|
524
525
|
const publicJsonBodies = assertPublicJson(opts.publicJson ?? {});
|
|
525
526
|
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
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type AppendAuditOpts } from '../audit.js';
|
|
2
|
+
import { type DatabaseSyncLike } from '../db.js';
|
|
3
|
+
import type { KeyMint, SelfKeyMint } from '../store-port.js';
|
|
4
|
+
/** The auth_create row a mint writes; the plaintext never reaches it. */
|
|
5
|
+
export declare function createAuditOf({ key, actor, metadata }: KeyMint): AppendAuditOpts;
|
|
6
|
+
export declare function createKeyAt(db: DatabaseSyncLike, mint: KeyMint): void;
|
|
7
|
+
/** IMMEDIATE takes the write lock before the count, so two mints for one owner cannot both see room under the cap. */
|
|
8
|
+
export declare function createSelfKeyAt(db: DatabaseSyncLike, mint: SelfKeyMint): string[];
|
|
9
|
+
//# sourceMappingURL=key-writes.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// hippo.db's half of the KeyWrites store group.
|
|
2
|
+
import { appendAuditEvent } from '../audit.js';
|
|
3
|
+
import { insertApiKey, listLiveOwnedKeyIds, revokeApiKey } from '../auth.js';
|
|
4
|
+
import { withWriteScope } from '../db.js';
|
|
5
|
+
/** The auth_create row a mint writes; the plaintext never reaches it. */
|
|
6
|
+
export function createAuditOf({ key, actor, metadata }) {
|
|
7
|
+
return { tenantId: key.tenantId, actor, op: 'auth_create', targetId: key.keyId, metadata };
|
|
8
|
+
}
|
|
9
|
+
export function createKeyAt(db, mint) {
|
|
10
|
+
withWriteScope(db, 'create_api_key', () => {
|
|
11
|
+
insertApiKey(db, mint.key);
|
|
12
|
+
appendAuditEvent(db, createAuditOf(mint));
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/** IMMEDIATE takes the write lock before the count, so two mints for one owner cannot both see room under the cap. */
|
|
16
|
+
export function createSelfKeyAt(db, mint) {
|
|
17
|
+
const { key, actor, perSubject } = mint;
|
|
18
|
+
return withWriteScope(db, 'create_self_api_key', () => {
|
|
19
|
+
const live = listLiveOwnedKeyIds(db, key.tenantId, key.ownerSubject, Date.parse(key.createdAt));
|
|
20
|
+
const replaced = live.slice(0, Math.max(0, live.length - perSubject + 1));
|
|
21
|
+
insertApiKey(db, key);
|
|
22
|
+
for (const keyId of replaced) {
|
|
23
|
+
revokeApiKey(db, keyId, key.createdAt);
|
|
24
|
+
appendAuditEvent(db, { tenantId: key.tenantId, actor, op: 'auth_revoke', targetId: keyId, metadata: { replacedBy: key.keyId } });
|
|
25
|
+
}
|
|
26
|
+
appendAuditEvent(db, createAuditOf(mint));
|
|
27
|
+
return replaced;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=key-writes.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type DatabaseSyncLike } from '../db.js';
|
|
2
|
+
import type { MemoryEntry } from '../memory.js';
|
|
3
|
+
import type { VectorBackfillQuery, VectorWrite, VectorWriteResult } from '../store-port.js';
|
|
4
|
+
export declare function entriesWithoutVectorAt(db: DatabaseSyncLike, query: VectorBackfillQuery): MemoryEntry[];
|
|
5
|
+
export declare function writeVectorsAt(db: DatabaseSyncLike, write: VectorWrite): VectorWriteResult;
|
|
6
|
+
//# sourceMappingURL=vector-writes.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// hippo.db's half of the VectorWrites store group.
|
|
2
|
+
import { setMeta, withWriteScope } from '../db.js';
|
|
3
|
+
import { embeddingIndexStateOn, replacesIndex } from '../embeddings.js';
|
|
4
|
+
import { float32ToBuffer } from '../physics-state.js';
|
|
5
|
+
import { EMBEDDING_MODEL_META_KEY, upsertVectors } from '../vector-store.js';
|
|
6
|
+
import { chunked } from './entry-reads.js';
|
|
7
|
+
import { MEMORY_SELECT_COLUMNS, rowToEntry } from './rows.js';
|
|
8
|
+
const MAX_BACKFILL_PAGE = 500;
|
|
9
|
+
export function entriesWithoutVectorAt(db, query) {
|
|
10
|
+
if (!Number.isInteger(query.limit))
|
|
11
|
+
throw new RangeError('limit must be an integer');
|
|
12
|
+
const limit = Math.max(1, Math.min(query.limit, MAX_BACKFILL_PAGE));
|
|
13
|
+
const tenantClause = query.tenantId === undefined ? '' : ' AND tenant_id = ?';
|
|
14
|
+
const tenantArgs = query.tenantId === undefined ? [] : [query.tenantId];
|
|
15
|
+
// SAFETY: selects exactly MEMORY_SELECT_COLUMNS, matching MemoryRow's field set.
|
|
16
|
+
const rows = db.prepare(`SELECT ${MEMORY_SELECT_COLUMNS} FROM memories WHERE id > ?${tenantClause}
|
|
17
|
+
AND NOT EXISTS (SELECT 1 FROM memory_vectors v WHERE v.memory_id = memories.id AND v.model = ?)
|
|
18
|
+
ORDER BY id LIMIT ?`).all(query.afterId ?? '', ...tenantArgs, query.model, limit);
|
|
19
|
+
return rows.map(rowToEntry);
|
|
20
|
+
}
|
|
21
|
+
function ownedIds(db, ids, tenantId) {
|
|
22
|
+
const owned = new Set();
|
|
23
|
+
for (const chunk of chunked([...new Set(ids)])) {
|
|
24
|
+
// SAFETY: the SELECT names one column, id.
|
|
25
|
+
const rows = db.prepare(`SELECT id FROM memories WHERE tenant_id = ? AND id IN (${chunk.map(() => '?').join(', ')})`).all(tenantId, ...chunk);
|
|
26
|
+
for (const row of rows)
|
|
27
|
+
owned.add(row.id);
|
|
28
|
+
}
|
|
29
|
+
return owned;
|
|
30
|
+
}
|
|
31
|
+
// savePhysicsState opens its own transaction and overwrites, so the first particle for a memory is inserted here instead.
|
|
32
|
+
function insertNewParticles(db, rows) {
|
|
33
|
+
const stmt = db.prepare(`
|
|
34
|
+
INSERT INTO memory_physics (memory_id, position_blob, velocity_blob, mass, charge, temperature, last_simulation)
|
|
35
|
+
VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(memory_id) DO NOTHING
|
|
36
|
+
`);
|
|
37
|
+
for (const { memoryId, particle: p } of rows) {
|
|
38
|
+
if (p)
|
|
39
|
+
stmt.run(memoryId, float32ToBuffer(p.position), float32ToBuffer(p.velocity), p.mass, p.charge, p.temperature, p.lastSimulation);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function writeVectorsAt(db, write) {
|
|
43
|
+
return withWriteScope(db, 'write_vectors', () => {
|
|
44
|
+
const replaces = replacesIndex(embeddingIndexStateOn(db), write.model);
|
|
45
|
+
if (replaces && !write.replaceIndex)
|
|
46
|
+
return { written: 0, modelMismatch: true };
|
|
47
|
+
const owned = ownedIds(db, write.rows.map((r) => r.memoryId), write.tenantId);
|
|
48
|
+
const rows = write.rows.filter((r) => owned.has(r.memoryId) && r.vector.length > 0 && r.vector.every(Number.isFinite));
|
|
49
|
+
if (rows.length === 0)
|
|
50
|
+
return { written: 0, modelMismatch: false };
|
|
51
|
+
if (replaces)
|
|
52
|
+
db.exec('DELETE FROM memory_vectors; DELETE FROM memory_physics;');
|
|
53
|
+
const written = upsertVectors(db, rows.map((r) => [r.memoryId, r.vector]), write.model);
|
|
54
|
+
insertNewParticles(db, rows);
|
|
55
|
+
setMeta(db, EMBEDDING_MODEL_META_KEY, write.model);
|
|
56
|
+
return { written, modelMismatch: false };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=vector-writes.js.map
|