mnemocyte 0.2.0 → 0.4.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 +292 -27
- package/dist/embedders/openai.d.mts +1 -1
- package/dist/embedders/openai.d.mts.map +1 -1
- package/dist/embedders/openai.mjs +24 -9
- package/dist/embedders/openai.mjs.map +1 -1
- package/dist/{errors-BG3-mNur.mjs → errors-B_7ZdjEO.mjs} +4 -3
- package/dist/errors-B_7ZdjEO.mjs.map +1 -0
- package/dist/index.d.mts +11 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1724 -1092
- package/dist/index.mjs.map +1 -1
- package/dist/{types-DXEGcc47.d.mts → types-DH0inEay.d.mts} +166 -52
- package/dist/types-DH0inEay.d.mts.map +1 -0
- package/migrations/0000_initial.sql.template +2 -1
- package/migrations/0002_add_embedding_model.sql +15 -0
- package/migrations/meta/0002_snapshot.json +315 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +4 -3
- package/dist/errors-BG3-mNur.mjs.map +0 -1
- package/dist/types-DXEGcc47.d.mts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { n as isMnemocyteError, t as MnemocyteError } from "./errors-
|
|
1
|
+
import { n as isMnemocyteError, t as MnemocyteError } from "./errors-B_7ZdjEO.mjs";
|
|
2
2
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
3
3
|
import postgres from "postgres";
|
|
4
|
-
import { and, count, countDistinct,
|
|
5
|
-
import { customType, index, integer, jsonb, pgTable, real, text, timestamp } from "drizzle-orm/pg-core";
|
|
4
|
+
import { and, count, countDistinct, eq, inArray, isNotNull, isNull, lt, or, sql } from "drizzle-orm";
|
|
5
|
+
import { PgDialect, customType, index, integer, jsonb, pgTable, real, text, timestamp } from "drizzle-orm/pg-core";
|
|
6
6
|
//#region \0rolldown/runtime.js
|
|
7
7
|
var __defProp = Object.defineProperty;
|
|
8
8
|
var __exportAll = (all, no_symbols) => {
|
|
@@ -15,6 +15,12 @@ var __exportAll = (all, no_symbols) => {
|
|
|
15
15
|
return target;
|
|
16
16
|
};
|
|
17
17
|
//#endregion
|
|
18
|
+
//#region src/db/vector.ts
|
|
19
|
+
function formatVectorComponent(value) {
|
|
20
|
+
if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
|
|
21
|
+
return Object.is(value, -0) ? "0" : value.toString();
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
18
24
|
//#region src/db/schema.ts
|
|
19
25
|
var schema_exports = /* @__PURE__ */ __exportAll({
|
|
20
26
|
eventsTable: () => eventsTable,
|
|
@@ -26,16 +32,12 @@ const vector = customType({
|
|
|
26
32
|
return `vector(${config?.dimensions ?? 1536})`;
|
|
27
33
|
},
|
|
28
34
|
toDriver(value) {
|
|
29
|
-
return `[${value.map(formatVectorComponent
|
|
35
|
+
return `[${value.map(formatVectorComponent).join(",")}]`;
|
|
30
36
|
},
|
|
31
37
|
fromDriver(value) {
|
|
32
38
|
return value.slice(1, -1).split(",").filter(Boolean).map(Number);
|
|
33
39
|
}
|
|
34
40
|
});
|
|
35
|
-
function formatVectorComponent$1(value) {
|
|
36
|
-
if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
|
|
37
|
-
return Object.is(value, -0) ? "0" : value.toFixed(17);
|
|
38
|
-
}
|
|
39
41
|
const memoriesTable = pgTable("mnemocyte_memories", {
|
|
40
42
|
id: text("id").primaryKey(),
|
|
41
43
|
entityId: text("entity_id").notNull(),
|
|
@@ -44,7 +46,7 @@ const memoriesTable = pgTable("mnemocyte_memories", {
|
|
|
44
46
|
importance: text("importance").notNull().default("normal"),
|
|
45
47
|
tags: text("tags").array().notNull().default(sql`'{}'::text[]`),
|
|
46
48
|
source: text("source"),
|
|
47
|
-
metadata: jsonb("metadata").notNull().default({}),
|
|
49
|
+
metadata: jsonb("metadata").$type().notNull().default({}),
|
|
48
50
|
confidence: real("confidence").notNull().default(1),
|
|
49
51
|
embedding: vector("embedding", { dimensions: 1536 }),
|
|
50
52
|
embeddingModel: text("embedding_model").notNull(),
|
|
@@ -65,12 +67,13 @@ const eventsTable = pgTable("mnemocyte_events", {
|
|
|
65
67
|
id: text("id").primaryKey(),
|
|
66
68
|
entityId: text("entity_id").notNull(),
|
|
67
69
|
description: text("description").notNull(),
|
|
68
|
-
metadata: jsonb("metadata").notNull().default({}),
|
|
70
|
+
metadata: jsonb("metadata").$type().notNull().default({}),
|
|
69
71
|
timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow()
|
|
70
72
|
}, (table) => [index("mnemocyte_events_entity_time_idx").on(table.entityId, table.timestamp)]);
|
|
71
73
|
const metaTable = pgTable("mnemocyte_meta", {
|
|
72
74
|
key: text("key").primaryKey(),
|
|
73
|
-
embeddingDimensions: integer("embedding_dimensions").notNull()
|
|
75
|
+
embeddingDimensions: integer("embedding_dimensions").notNull(),
|
|
76
|
+
embeddingModel: text("embedding_model")
|
|
74
77
|
});
|
|
75
78
|
//#endregion
|
|
76
79
|
//#region src/db/index.ts
|
|
@@ -81,7 +84,13 @@ function parseSslFromUrl(url) {
|
|
|
81
84
|
if (mode === "verify-full") return "verify-full";
|
|
82
85
|
}
|
|
83
86
|
function createDatabase(databaseUrl) {
|
|
84
|
-
|
|
87
|
+
let url;
|
|
88
|
+
try {
|
|
89
|
+
url = new URL(databaseUrl);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw new MnemocyteError("databaseUrl must be a valid Postgres connection URL.", "CONFIG", error);
|
|
92
|
+
}
|
|
93
|
+
if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") throw new MnemocyteError("databaseUrl must use the postgres: or postgresql: protocol.", "CONFIG");
|
|
85
94
|
const ssl = parseSslFromUrl(url);
|
|
86
95
|
const isPooler = url.port === "6543" || url.searchParams.get("pgbouncer") === "true";
|
|
87
96
|
const client = postgres(databaseUrl, {
|
|
@@ -99,158 +108,6 @@ function createDatabase(databaseUrl) {
|
|
|
99
108
|
};
|
|
100
109
|
}
|
|
101
110
|
//#endregion
|
|
102
|
-
//#region src/context/formatter.ts
|
|
103
|
-
function escapeXml(value) {
|
|
104
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
105
|
-
}
|
|
106
|
-
function formatMetadata(memory) {
|
|
107
|
-
const parts = [
|
|
108
|
-
`type=${memory.type}`,
|
|
109
|
-
`importance=${memory.importance}`,
|
|
110
|
-
`score=${memory.score.toFixed(3)}`
|
|
111
|
-
];
|
|
112
|
-
if (memory.source) parts.push(`source=${memory.source}`);
|
|
113
|
-
if (memory.tags.length > 0) parts.push(`tags=${memory.tags.join(",")}`);
|
|
114
|
-
return parts.join("; ");
|
|
115
|
-
}
|
|
116
|
-
function markdownFence(value) {
|
|
117
|
-
const longestBacktickRun = Math.max(0, ...Array.from(value.matchAll(/`+/g), (match) => match[0].length));
|
|
118
|
-
return "`".repeat(Math.max(3, longestBacktickRun + 1));
|
|
119
|
-
}
|
|
120
|
-
function formatMarkdown(memories, query, omittedCount) {
|
|
121
|
-
const lines = [
|
|
122
|
-
"# Memory Context",
|
|
123
|
-
"",
|
|
124
|
-
`Query: ${query}`,
|
|
125
|
-
"",
|
|
126
|
-
"## Relevant Memories"
|
|
127
|
-
];
|
|
128
|
-
for (const memory of memories) {
|
|
129
|
-
const fence = markdownFence(memory.content);
|
|
130
|
-
lines.push("", `- ${formatMetadata(memory)}`, ` ${fence}text`, ...memory.content.split("\n").map((line) => ` ${line}`), ` ${fence}`);
|
|
131
|
-
}
|
|
132
|
-
if (omittedCount > 0) lines.push("", `[${omittedCount} memories omitted to fit token budget]`);
|
|
133
|
-
return lines.join("\n");
|
|
134
|
-
}
|
|
135
|
-
function formatPlain(memories, query, omittedCount) {
|
|
136
|
-
const lines = [
|
|
137
|
-
"MEMORY CONTEXT",
|
|
138
|
-
"",
|
|
139
|
-
`QUERY: ${query}`,
|
|
140
|
-
"",
|
|
141
|
-
"RELEVANT MEMORIES"
|
|
142
|
-
];
|
|
143
|
-
for (const [index, memory] of memories.entries()) lines.push("", `--- MEMORY ${index + 1} START ---`, formatMetadata(memory), memory.content, `--- MEMORY ${index + 1} END ---`);
|
|
144
|
-
if (omittedCount > 0) lines.push("", `[${omittedCount} memories omitted to fit token budget]`);
|
|
145
|
-
return lines.join("\n");
|
|
146
|
-
}
|
|
147
|
-
function formatXml(memories, query, omittedCount) {
|
|
148
|
-
const lines = [`<memory_context query="${escapeXml(query)}">`];
|
|
149
|
-
for (const memory of memories) lines.push(` <memory id="${escapeXml(memory.id)}" type="${escapeXml(memory.type)}" importance="${escapeXml(memory.importance)}" score="${memory.score.toFixed(3)}">`, ` <content>${escapeXml(memory.content)}</content>`, ` <metadata>${escapeXml(formatMetadata(memory))}</metadata>`, " </memory>");
|
|
150
|
-
if (omittedCount > 0) lines.push(` <omitted count="${omittedCount}" reason="token_budget" />`);
|
|
151
|
-
lines.push("</memory_context>");
|
|
152
|
-
return lines.join("\n");
|
|
153
|
-
}
|
|
154
|
-
function formatContext(memories, query, format, omittedCount = 0) {
|
|
155
|
-
switch (format) {
|
|
156
|
-
case "markdown": return formatMarkdown(memories, query, omittedCount);
|
|
157
|
-
case "plain": return formatPlain(memories, query, omittedCount);
|
|
158
|
-
case "xml": return formatXml(memories, query, omittedCount);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
//#endregion
|
|
162
|
-
//#region src/context/tokens.ts
|
|
163
|
-
const heuristicTokenCounter = { count(text) {
|
|
164
|
-
return Math.ceil(text.length / 4);
|
|
165
|
-
} };
|
|
166
|
-
function trimToTokenBudget(text, maxTokens, tokenCounter) {
|
|
167
|
-
if (maxTokens < 1 || tokenCounter.count(text) <= maxTokens) return text;
|
|
168
|
-
let low = 0;
|
|
169
|
-
let high = text.length;
|
|
170
|
-
let best = "";
|
|
171
|
-
while (low <= high) {
|
|
172
|
-
const mid = Math.floor((low + high) / 2);
|
|
173
|
-
const output = `${text.slice(0, mid).trimEnd()}\n[truncated to fit token budget]`;
|
|
174
|
-
if (tokenCounter.count(output) <= maxTokens) {
|
|
175
|
-
best = output;
|
|
176
|
-
low = mid + 1;
|
|
177
|
-
} else high = mid - 1;
|
|
178
|
-
}
|
|
179
|
-
return best || "[truncated to fit token budget]";
|
|
180
|
-
}
|
|
181
|
-
//#endregion
|
|
182
|
-
//#region src/context/builder.ts
|
|
183
|
-
async function buildContext(options) {
|
|
184
|
-
const format = options.input.format ?? "markdown";
|
|
185
|
-
const maxTokens = options.input.maxTokens ?? 1200;
|
|
186
|
-
const tokenCounter = options.input.tokenCounter ?? heuristicTokenCounter;
|
|
187
|
-
const memories = await options.recall(options.input);
|
|
188
|
-
let low = 0;
|
|
189
|
-
let high = memories.length;
|
|
190
|
-
let bestContext = "";
|
|
191
|
-
while (low <= high) {
|
|
192
|
-
const mid = Math.floor((low + high) / 2);
|
|
193
|
-
const omittedCount = memories.length - mid;
|
|
194
|
-
const context = formatContext(memories.slice(0, mid), options.input.query, format, omittedCount);
|
|
195
|
-
if (tokenCounter.count(context) <= maxTokens) {
|
|
196
|
-
bestContext = context;
|
|
197
|
-
low = mid + 1;
|
|
198
|
-
} else high = mid - 1;
|
|
199
|
-
}
|
|
200
|
-
if (bestContext || memories.length === 0) return bestContext;
|
|
201
|
-
return trimToTokenBudget(formatContext([], options.input.query, format, memories.length), maxTokens, tokenCounter);
|
|
202
|
-
}
|
|
203
|
-
//#endregion
|
|
204
|
-
//#region src/observability.ts
|
|
205
|
-
function withMetadata(event, metadata) {
|
|
206
|
-
return {
|
|
207
|
-
...event,
|
|
208
|
-
...metadata.entityId === void 0 ? {} : { entityId: metadata.entityId },
|
|
209
|
-
...metadata.memoryId === void 0 ? {} : { memoryId: metadata.memoryId },
|
|
210
|
-
...metadata.count === void 0 ? {} : { count: metadata.count }
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
async function emitObservation(config, event) {
|
|
214
|
-
try {
|
|
215
|
-
await config.observability?.onEvent?.(event);
|
|
216
|
-
} catch {
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
async function observe(config, backend, operation, metadata, action, successMetadata) {
|
|
221
|
-
const startedAt = Date.now();
|
|
222
|
-
await emitObservation(config, withMetadata({
|
|
223
|
-
operation,
|
|
224
|
-
phase: "start",
|
|
225
|
-
backend,
|
|
226
|
-
timestamp: new Date(startedAt)
|
|
227
|
-
}, metadata));
|
|
228
|
-
try {
|
|
229
|
-
const result = await action();
|
|
230
|
-
await emitObservation(config, withMetadata({
|
|
231
|
-
operation,
|
|
232
|
-
phase: "success",
|
|
233
|
-
backend,
|
|
234
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
235
|
-
durationMs: Date.now() - startedAt
|
|
236
|
-
}, {
|
|
237
|
-
...metadata,
|
|
238
|
-
...successMetadata?.(result)
|
|
239
|
-
}));
|
|
240
|
-
return result;
|
|
241
|
-
} catch (error) {
|
|
242
|
-
await emitObservation(config, withMetadata({
|
|
243
|
-
operation,
|
|
244
|
-
phase: "error",
|
|
245
|
-
backend,
|
|
246
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
247
|
-
durationMs: Date.now() - startedAt,
|
|
248
|
-
error
|
|
249
|
-
}, metadata));
|
|
250
|
-
throw error;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
//#endregion
|
|
254
111
|
//#region src/resilience.ts
|
|
255
112
|
/**
|
|
256
113
|
* Default per-provider-call timeout. Disabled by default so that callers
|
|
@@ -277,11 +134,11 @@ function throwIfAborted(signal) {
|
|
|
277
134
|
* Conservative by design: only retries on common transient failure
|
|
278
135
|
* indicators (network errors, 5xx-ish messages, `ECONN*` etc.). Never
|
|
279
136
|
* retries on validation, configuration, or {@link MnemocyteError} of
|
|
280
|
-
* code `"VALIDATION"` / `"CONFIG"` / `"ABORTED"`.
|
|
137
|
+
* code `"VALIDATION"` / `"CONFIG"` / `"CONFLICT"` / `"ABORTED"`.
|
|
281
138
|
*/
|
|
282
139
|
function defaultShouldRetry(error) {
|
|
283
140
|
if (error instanceof MnemocyteError) {
|
|
284
|
-
if (error.code === "VALIDATION" || error.code === "CONFIG" || error.code === "ABORTED") return false;
|
|
141
|
+
if (error.code === "VALIDATION" || error.code === "CONFIG" || error.code === "CONFLICT" || error.code === "ABORTED") return false;
|
|
285
142
|
return true;
|
|
286
143
|
}
|
|
287
144
|
if (typeof error === "object" && error !== null && "status" in error && typeof error.status === "number" && [
|
|
@@ -336,37 +193,44 @@ function delay(ms, signal) {
|
|
|
336
193
|
}
|
|
337
194
|
});
|
|
338
195
|
}
|
|
196
|
+
function timeoutError(timeoutMs) {
|
|
197
|
+
return new MnemocyteError(`Provider call timed out after ${timeoutMs}ms.`, "TIMEOUT");
|
|
198
|
+
}
|
|
339
199
|
/**
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
200
|
+
* Run one provider attempt with a signal linked to the caller's signal and
|
|
201
|
+
* Mnemocyte's configured timeout. Timeout aborts are actively propagated to
|
|
202
|
+
* providers that honor `AbortSignal`.
|
|
343
203
|
*/
|
|
344
|
-
function
|
|
345
|
-
if (timeoutMs <= 0 && !signal) return
|
|
204
|
+
function runAttempt(action, timeoutMs, signal) {
|
|
205
|
+
if (timeoutMs <= 0 && !signal) return action(void 0);
|
|
346
206
|
return new Promise((resolve, reject) => {
|
|
347
207
|
let settled = false;
|
|
208
|
+
const controller = new AbortController();
|
|
348
209
|
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
349
210
|
if (settled) return;
|
|
350
211
|
settled = true;
|
|
351
212
|
signal?.removeEventListener("abort", onAbort);
|
|
352
|
-
|
|
213
|
+
controller.abort(timeoutError(timeoutMs));
|
|
214
|
+
reject(timeoutError(timeoutMs));
|
|
353
215
|
}, timeoutMs) : void 0;
|
|
354
216
|
const onAbort = () => {
|
|
355
217
|
if (settled) return;
|
|
356
218
|
settled = true;
|
|
357
219
|
if (timer) clearTimeout(timer);
|
|
220
|
+
controller.abort(signal?.reason);
|
|
358
221
|
reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal?.reason));
|
|
359
222
|
};
|
|
360
223
|
if (signal) {
|
|
361
224
|
if (signal.aborted) {
|
|
362
225
|
if (timer) clearTimeout(timer);
|
|
363
226
|
settled = true;
|
|
227
|
+
controller.abort(signal.reason);
|
|
364
228
|
reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason));
|
|
365
229
|
return;
|
|
366
230
|
}
|
|
367
231
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
368
232
|
}
|
|
369
|
-
|
|
233
|
+
action(controller.signal).then((value) => {
|
|
370
234
|
if (settled) return;
|
|
371
235
|
settled = true;
|
|
372
236
|
if (timer) clearTimeout(timer);
|
|
@@ -389,7 +253,7 @@ function withTimeout(promise, timeoutMs, signal) {
|
|
|
389
253
|
* underlying provider honours signals.
|
|
390
254
|
*
|
|
391
255
|
* Errors from {@link MnemocyteError} with code `"VALIDATION"` /
|
|
392
|
-
* `"CONFIG"` / `"ABORTED"` are never retried.
|
|
256
|
+
* `"CONFIG"` / `"CONFLICT"` / `"ABORTED"` are never retried.
|
|
393
257
|
*/
|
|
394
258
|
async function withResilience(action, options = {}) {
|
|
395
259
|
const config = options.resilience ?? {};
|
|
@@ -405,7 +269,7 @@ async function withResilience(action, options = {}) {
|
|
|
405
269
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
406
270
|
throwIfAborted(externalSignal);
|
|
407
271
|
try {
|
|
408
|
-
return await
|
|
272
|
+
return await runAttempt(action, timeoutMs, externalSignal);
|
|
409
273
|
} catch (error) {
|
|
410
274
|
lastError = error;
|
|
411
275
|
if (error instanceof MnemocyteError && error.code === "ABORTED") throw error;
|
|
@@ -416,31 +280,55 @@ async function withResilience(action, options = {}) {
|
|
|
416
280
|
}
|
|
417
281
|
throw lastError;
|
|
418
282
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/memory/json.ts
|
|
285
|
+
function invalidJsonValue(path, cause) {
|
|
286
|
+
return new MnemocyteError(`${path} must contain only JSON-compatible value data.`, "VALIDATION", cause);
|
|
287
|
+
}
|
|
288
|
+
function cloneJsonValue(value, path, ancestors) {
|
|
289
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
290
|
+
if (typeof value === "number") {
|
|
291
|
+
if (!Number.isFinite(value)) throw invalidJsonValue(path);
|
|
292
|
+
return Object.is(value, -0) ? 0 : value;
|
|
293
|
+
}
|
|
294
|
+
if (typeof value !== "object") throw invalidJsonValue(path);
|
|
295
|
+
if (ancestors.has(value)) throw invalidJsonValue(path);
|
|
296
|
+
ancestors.add(value);
|
|
297
|
+
try {
|
|
298
|
+
if (Array.isArray(value)) {
|
|
299
|
+
if (Object.getOwnPropertySymbols(value).length > 0 || Object.keys(value).some((key) => !/^(0|[1-9]\d*)$/.test(key))) throw invalidJsonValue(path);
|
|
300
|
+
return Array.from({ length: value.length }, (_, index) => {
|
|
301
|
+
if (!Object.hasOwn(value, index)) throw invalidJsonValue(`${path}[${index}]`);
|
|
302
|
+
return cloneJsonValue(value[index], `${path}[${index}]`, ancestors);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
const prototype = Object.getPrototypeOf(value);
|
|
306
|
+
if (prototype !== Object.prototype && prototype !== null) throw invalidJsonValue(path);
|
|
307
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
308
|
+
const entries = [];
|
|
309
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
310
|
+
if (typeof key !== "string") throw invalidJsonValue(path);
|
|
311
|
+
const descriptor = descriptors[key];
|
|
312
|
+
if (descriptor === void 0 || descriptor.enumerable !== true || !("value" in descriptor)) throw invalidJsonValue(`${path}.${key}`);
|
|
313
|
+
entries.push([key, cloneJsonValue(descriptor.value, `${path}.${key}`, ancestors)]);
|
|
314
|
+
}
|
|
315
|
+
return Object.fromEntries(entries);
|
|
316
|
+
} finally {
|
|
317
|
+
ancestors.delete(value);
|
|
318
|
+
}
|
|
438
319
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
320
|
+
/** Validate and deep-clone a JSON-compatible object. */
|
|
321
|
+
function cloneJsonObject(value, path = "metadata") {
|
|
322
|
+
try {
|
|
323
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw invalidJsonValue(path);
|
|
324
|
+
return cloneJsonValue(value, path, /* @__PURE__ */ new Set());
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (error instanceof MnemocyteError) throw error;
|
|
327
|
+
throw invalidJsonValue(path, error);
|
|
328
|
+
}
|
|
443
329
|
}
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region src/memory/records.ts
|
|
444
332
|
function normalizeTags(tags) {
|
|
445
333
|
if (!tags) return [];
|
|
446
334
|
return [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))];
|
|
@@ -452,226 +340,67 @@ function createId() {
|
|
|
452
340
|
function createEventId() {
|
|
453
341
|
return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
454
342
|
}
|
|
455
|
-
function isExpired(memory, now) {
|
|
456
|
-
return memory.expiresAt !== null && memory.expiresAt.getTime() <= now.getTime();
|
|
457
|
-
}
|
|
458
343
|
function cloneMemory(memory) {
|
|
459
344
|
return {
|
|
460
|
-
|
|
345
|
+
id: memory.id,
|
|
346
|
+
entityId: memory.entityId,
|
|
347
|
+
content: memory.content,
|
|
348
|
+
type: memory.type,
|
|
349
|
+
importance: memory.importance,
|
|
461
350
|
tags: [...memory.tags],
|
|
462
|
-
|
|
351
|
+
source: memory.source,
|
|
352
|
+
metadata: cloneJsonObject(memory.metadata),
|
|
353
|
+
confidence: memory.confidence,
|
|
354
|
+
embeddingModel: memory.embeddingModel,
|
|
355
|
+
embeddingDimensions: memory.embeddingDimensions,
|
|
356
|
+
supersededBy: memory.supersededBy,
|
|
463
357
|
supersededAt: memory.supersededAt ? new Date(memory.supersededAt) : null,
|
|
464
358
|
expiresAt: memory.expiresAt ? new Date(memory.expiresAt) : null,
|
|
465
359
|
lastAccessedAt: memory.lastAccessedAt ? new Date(memory.lastAccessedAt) : null,
|
|
360
|
+
accessCount: memory.accessCount,
|
|
466
361
|
createdAt: new Date(memory.createdAt),
|
|
467
362
|
updatedAt: new Date(memory.updatedAt)
|
|
468
363
|
};
|
|
469
364
|
}
|
|
470
|
-
|
|
471
|
-
|
|
365
|
+
//#endregion
|
|
366
|
+
//#region src/retrieval/scorer.ts
|
|
367
|
+
const IMPORTANCE_SCORES = {
|
|
368
|
+
low: 0,
|
|
369
|
+
normal: .5,
|
|
370
|
+
high: .8,
|
|
371
|
+
critical: 1
|
|
372
|
+
};
|
|
373
|
+
const DEFAULT_RETRIEVAL_WEIGHTS = {
|
|
374
|
+
vector: .55,
|
|
375
|
+
lexical: .2,
|
|
376
|
+
recency: .1,
|
|
377
|
+
confidence: .05,
|
|
378
|
+
access: .05,
|
|
379
|
+
importance: .05
|
|
380
|
+
};
|
|
381
|
+
const DEFAULT_RECENCY_HALF_LIFE_DAYS = 90;
|
|
382
|
+
const DEFAULT_ACCESS_SATURATION = 10;
|
|
383
|
+
function cosineSimilarity(a, b) {
|
|
384
|
+
let dot = 0;
|
|
385
|
+
let aMagnitude = 0;
|
|
386
|
+
let bMagnitude = 0;
|
|
387
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
388
|
+
const av = a[i] ?? 0;
|
|
389
|
+
const bv = b[i] ?? 0;
|
|
390
|
+
dot += av * bv;
|
|
391
|
+
aMagnitude += av * av;
|
|
392
|
+
bMagnitude += bv * bv;
|
|
393
|
+
}
|
|
394
|
+
if (aMagnitude === 0 || bMagnitude === 0) return 0;
|
|
395
|
+
return dot / (Math.sqrt(aMagnitude) * Math.sqrt(bMagnitude));
|
|
472
396
|
}
|
|
473
|
-
function
|
|
474
|
-
return
|
|
397
|
+
function tokenizeQuery(query) {
|
|
398
|
+
return query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
475
399
|
}
|
|
476
|
-
function
|
|
477
|
-
return
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
content: row.content,
|
|
481
|
-
type: row.type,
|
|
482
|
-
importance: row.importance,
|
|
483
|
-
tags: row.tags,
|
|
484
|
-
source: row.source,
|
|
485
|
-
metadata: row.metadata,
|
|
486
|
-
confidence: row.confidence,
|
|
487
|
-
embeddingModel: row.embeddingModel,
|
|
488
|
-
embeddingDimensions: row.embeddingDimensions,
|
|
489
|
-
supersededBy: row.supersededBy,
|
|
490
|
-
supersededAt: toNullableDate(row.supersededAt),
|
|
491
|
-
expiresAt: toNullableDate(row.expiresAt),
|
|
492
|
-
lastAccessedAt: toNullableDate(row.lastAccessedAt),
|
|
493
|
-
accessCount: row.accessCount,
|
|
494
|
-
createdAt: toDate(row.createdAt),
|
|
495
|
-
updatedAt: toDate(row.updatedAt)
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
async function embedOne(embedder, text, options = {}) {
|
|
499
|
-
let embeddings;
|
|
500
|
-
try {
|
|
501
|
-
embeddings = await withResilience((signal) => signal === void 0 ? embedder.embed([text]) : embedder.embed([text], { signal }), {
|
|
502
|
-
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
503
|
-
...options.resilience === void 0 ? {} : { resilience: options.resilience }
|
|
504
|
-
});
|
|
505
|
-
} catch (error) {
|
|
506
|
-
if (error instanceof MnemocyteError && (error.code === "TIMEOUT" || error.code === "ABORTED")) throw error;
|
|
507
|
-
throw new MnemocyteError("Failed to embed text.", "EMBEDDING", error);
|
|
508
|
-
}
|
|
509
|
-
const embedding = embeddings[0];
|
|
510
|
-
if (!embedding) throw new MnemocyteError("Embedder returned no embedding.", "EMBEDDING");
|
|
511
|
-
if (embedding.length !== embedder.dimensions) throw new MnemocyteError("Embedder returned an embedding with unexpected dimensions.", "EMBEDDING");
|
|
512
|
-
return embedding;
|
|
513
|
-
}
|
|
514
|
-
async function embedMany(embedder, texts, options = {}) {
|
|
515
|
-
if (texts.length === 0) return [];
|
|
516
|
-
let embeddings;
|
|
517
|
-
try {
|
|
518
|
-
embeddings = await withResilience((signal) => signal === void 0 ? embedder.embed(texts) : embedder.embed(texts, { signal }), {
|
|
519
|
-
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
520
|
-
...options.resilience === void 0 ? {} : { resilience: options.resilience }
|
|
521
|
-
});
|
|
522
|
-
} catch (error) {
|
|
523
|
-
if (error instanceof MnemocyteError && (error.code === "TIMEOUT" || error.code === "ABORTED")) throw error;
|
|
524
|
-
throw new MnemocyteError("Failed to embed texts.", "EMBEDDING", error);
|
|
525
|
-
}
|
|
526
|
-
if (embeddings.length !== texts.length) throw new MnemocyteError(`Embedder returned ${embeddings.length} embeddings for ${texts.length} texts.`, "EMBEDDING");
|
|
527
|
-
for (const embedding of embeddings) if (embedding.length !== embedder.dimensions) throw new MnemocyteError("Embedder returned an embedding with unexpected dimensions.", "EMBEDDING");
|
|
528
|
-
return embeddings;
|
|
529
|
-
}
|
|
530
|
-
function validateRememberInput(input) {
|
|
531
|
-
assertNonEmptyString(input.entityId, "entityId");
|
|
532
|
-
assertNonEmptyString(input.content, "content");
|
|
533
|
-
if (input.confidence !== void 0 && (!Number.isFinite(input.confidence) || input.confidence < 0 || input.confidence > 1)) throw new MnemocyteError("confidence must be a number between 0 and 1.", "VALIDATION");
|
|
534
|
-
}
|
|
535
|
-
function validateRecallInput(input) {
|
|
536
|
-
assertNonEmptyString(input.entityId, "entityId");
|
|
537
|
-
assertNonEmptyString(input.query, "query");
|
|
538
|
-
if (input.limit !== void 0) assertLimit(input.limit);
|
|
539
|
-
if (input.minScore !== void 0) assertMinScore(input.minScore);
|
|
540
|
-
}
|
|
541
|
-
function contextInputToRecallInput(input) {
|
|
542
|
-
return {
|
|
543
|
-
entityId: input.entityId,
|
|
544
|
-
query: input.query,
|
|
545
|
-
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
546
|
-
...input.minScore === void 0 ? {} : { minScore: input.minScore },
|
|
547
|
-
...input.types === void 0 ? {} : { types: input.types },
|
|
548
|
-
...input.tags === void 0 ? {} : { tags: input.tags },
|
|
549
|
-
...input.includeSuperseded === void 0 ? {} : { includeSuperseded: input.includeSuperseded },
|
|
550
|
-
...input.includeExpired === void 0 ? {} : { includeExpired: input.includeExpired }
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
/**
|
|
554
|
-
* Validate a {@link PruneInput} and throw a `"VALIDATION"`
|
|
555
|
-
* {@link MnemocyteError} when no selector is supplied. Mnemocyte
|
|
556
|
-
* deliberately rejects `prune({})` to avoid surprise full-table deletes.
|
|
557
|
-
*/
|
|
558
|
-
function validatePruneInput(input) {
|
|
559
|
-
if (input.entityId !== void 0) assertNonEmptyString(input.entityId, "entityId");
|
|
560
|
-
if (!(input.entityId !== void 0 || input.expired === true || input.superseded === true || input.createdBefore !== void 0 || input.notAccessedSince !== void 0 || input.types !== void 0 && input.types.length > 0 || input.tags !== void 0 && input.tags.length > 0 || input.maxImportance !== void 0)) throw new MnemocyteError("prune requires at least one selector (entityId, expired, superseded, createdBefore, notAccessedSince, types, tags, or maxImportance).", "VALIDATION");
|
|
561
|
-
}
|
|
562
|
-
/**
|
|
563
|
-
* Test whether `memory` matches a {@link PruneInput}. All specified
|
|
564
|
-
* selectors are AND-combined; unspecified selectors do not restrict.
|
|
565
|
-
*
|
|
566
|
-
* `now` is supplied by the caller so callers can share a consistent
|
|
567
|
-
* timestamp across a batch.
|
|
568
|
-
*/
|
|
569
|
-
function matchesPruneFilter(memory, input, now) {
|
|
570
|
-
if (input.entityId !== void 0 && memory.entityId !== input.entityId) return false;
|
|
571
|
-
if (input.expired === true && !isExpired(memory, now)) return false;
|
|
572
|
-
if (input.superseded === true && memory.supersededBy === null) return false;
|
|
573
|
-
if (input.createdBefore !== void 0 && memory.createdAt.getTime() >= input.createdBefore.getTime()) return false;
|
|
574
|
-
if (input.notAccessedSince !== void 0) {
|
|
575
|
-
const last = memory.lastAccessedAt;
|
|
576
|
-
if (last !== null && last.getTime() >= input.notAccessedSince.getTime()) return false;
|
|
577
|
-
}
|
|
578
|
-
if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
|
|
579
|
-
if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
|
|
580
|
-
if (input.maxImportance !== void 0 && IMPORTANCE_RANK[memory.importance] > IMPORTANCE_RANK[input.maxImportance]) return false;
|
|
581
|
-
return true;
|
|
582
|
-
}
|
|
583
|
-
/**
|
|
584
|
-
* Validate a {@link FindDuplicatesInput} and throw a `"VALIDATION"`
|
|
585
|
-
* {@link MnemocyteError} for malformed `threshold` / `limit` values.
|
|
586
|
-
*/
|
|
587
|
-
function validateFindDuplicatesInput(input) {
|
|
588
|
-
assertNonEmptyString(input.entityId, "entityId");
|
|
589
|
-
if (input.threshold !== void 0) {
|
|
590
|
-
if (!Number.isFinite(input.threshold) || input.threshold < 0 || input.threshold > 1) throw new MnemocyteError("threshold must be a number between 0 and 1.", "VALIDATION");
|
|
591
|
-
}
|
|
592
|
-
if (input.limit !== void 0) assertLimit(input.limit);
|
|
593
|
-
}
|
|
594
|
-
/**
|
|
595
|
-
* Return `true` when a memory should participate in a duplicate scan,
|
|
596
|
-
* given the filters on `input` and a shared `now` timestamp.
|
|
597
|
-
*/
|
|
598
|
-
function matchesDuplicateFilter(memory, input, now) {
|
|
599
|
-
if (memory.entityId !== input.entityId) return false;
|
|
600
|
-
if (input.includeSuperseded !== true && memory.supersededBy !== null) return false;
|
|
601
|
-
if (input.includeExpired !== true && isExpired(memory, now)) return false;
|
|
602
|
-
if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
|
|
603
|
-
if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
|
|
604
|
-
return true;
|
|
605
|
-
}
|
|
606
|
-
/**
|
|
607
|
-
* Validate a {@link ConsolidateInput} and throw a `"VALIDATION"`
|
|
608
|
-
* {@link MnemocyteError} for malformed inputs (empty `supersededIds`
|
|
609
|
-
* or a `survivorId` appearing in `supersededIds`).
|
|
610
|
-
*/
|
|
611
|
-
function validateConsolidateInput(input) {
|
|
612
|
-
assertNonEmptyString(input.entityId, "entityId");
|
|
613
|
-
assertNonEmptyString(input.survivorId, "survivorId");
|
|
614
|
-
if (input.supersededIds.length === 0) throw new MnemocyteError("supersededIds must contain at least one memory id.", "VALIDATION");
|
|
615
|
-
if (input.supersededIds.includes(input.survivorId)) throw new MnemocyteError("survivorId must not appear in supersededIds.", "VALIDATION");
|
|
616
|
-
for (const id of input.supersededIds) assertNonEmptyString(id, "supersededIds[*]");
|
|
617
|
-
}
|
|
618
|
-
/**
|
|
619
|
-
* Validate a {@link ListAuditLogInput} and throw a `"VALIDATION"`
|
|
620
|
-
* {@link MnemocyteError} when malformed.
|
|
621
|
-
*/
|
|
622
|
-
function validateListAuditLogInput(input) {
|
|
623
|
-
assertNonEmptyString(input.entityId, "entityId");
|
|
624
|
-
if (input.limit !== void 0) assertLimit(input.limit);
|
|
625
|
-
}
|
|
626
|
-
function matchesRecallFilter(memory, input, now) {
|
|
627
|
-
if (memory.entityId !== input.entityId) return false;
|
|
628
|
-
if (!input.includeSuperseded && memory.supersededBy !== null) return false;
|
|
629
|
-
if (!input.includeExpired && isExpired(memory, now)) return false;
|
|
630
|
-
if (input.types && !input.types.includes(memory.type)) return false;
|
|
631
|
-
if (input.tags && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
|
|
632
|
-
if (input.before && memory.createdAt >= input.before) return false;
|
|
633
|
-
if (input.after && memory.createdAt <= input.after) return false;
|
|
634
|
-
return true;
|
|
635
|
-
}
|
|
636
|
-
//#endregion
|
|
637
|
-
//#region src/retrieval/scorer.ts
|
|
638
|
-
const IMPORTANCE_SCORES = {
|
|
639
|
-
low: 0,
|
|
640
|
-
normal: .5,
|
|
641
|
-
high: .8,
|
|
642
|
-
critical: 1
|
|
643
|
-
};
|
|
644
|
-
const DEFAULT_WEIGHTS = {
|
|
645
|
-
vector: .55,
|
|
646
|
-
lexical: .2,
|
|
647
|
-
recency: .1,
|
|
648
|
-
confidence: .05,
|
|
649
|
-
access: .05,
|
|
650
|
-
importance: .05
|
|
651
|
-
};
|
|
652
|
-
const DEFAULT_RECENCY_HALF_LIFE_DAYS = 90;
|
|
653
|
-
const DEFAULT_ACCESS_SATURATION = 10;
|
|
654
|
-
function cosineSimilarity(a, b) {
|
|
655
|
-
let dot = 0;
|
|
656
|
-
let aMagnitude = 0;
|
|
657
|
-
let bMagnitude = 0;
|
|
658
|
-
for (let i = 0; i < a.length; i += 1) {
|
|
659
|
-
const av = a[i] ?? 0;
|
|
660
|
-
const bv = b[i] ?? 0;
|
|
661
|
-
dot += av * bv;
|
|
662
|
-
aMagnitude += av * av;
|
|
663
|
-
bMagnitude += bv * bv;
|
|
664
|
-
}
|
|
665
|
-
if (aMagnitude === 0 || bMagnitude === 0) return 0;
|
|
666
|
-
return dot / (Math.sqrt(aMagnitude) * Math.sqrt(bMagnitude));
|
|
667
|
-
}
|
|
668
|
-
function tokenizeQuery(query) {
|
|
669
|
-
return query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
670
|
-
}
|
|
671
|
-
function lexicalScoreWithTerms(content, terms) {
|
|
672
|
-
if (terms.length === 0) return 0;
|
|
673
|
-
const lowerContent = content.toLowerCase();
|
|
674
|
-
return terms.filter((term) => lowerContent.includes(term)).length / terms.length;
|
|
400
|
+
function lexicalScoreWithTerms(content, terms) {
|
|
401
|
+
if (terms.length === 0) return 0;
|
|
402
|
+
const lowerContent = content.toLowerCase();
|
|
403
|
+
return terms.filter((term) => lowerContent.includes(term)).length / terms.length;
|
|
675
404
|
}
|
|
676
405
|
function createLexicalScorer(query) {
|
|
677
406
|
const terms = tokenizeQuery(query);
|
|
@@ -681,13 +410,16 @@ function clampScore(value) {
|
|
|
681
410
|
if (!Number.isFinite(value)) return 0;
|
|
682
411
|
return Math.max(0, Math.min(1, value));
|
|
683
412
|
}
|
|
413
|
+
function toVectorScore(cosineSimilarity) {
|
|
414
|
+
return clampScore(cosineSimilarity);
|
|
415
|
+
}
|
|
684
416
|
function normalizeWeights(weights) {
|
|
685
417
|
const merged = {
|
|
686
|
-
...
|
|
418
|
+
...DEFAULT_RETRIEVAL_WEIGHTS,
|
|
687
419
|
...weights
|
|
688
420
|
};
|
|
689
421
|
const total = merged.vector + merged.lexical + merged.recency + merged.confidence + merged.access + merged.importance;
|
|
690
|
-
if (total <= 0) return
|
|
422
|
+
if (total <= 0) return { ...DEFAULT_RETRIEVAL_WEIGHTS };
|
|
691
423
|
return {
|
|
692
424
|
vector: merged.vector / total,
|
|
693
425
|
lexical: merged.lexical / total,
|
|
@@ -745,356 +477,1238 @@ function toScoredMemoryWithConfig(memory, vector, lexical, input, config) {
|
|
|
745
477
|
};
|
|
746
478
|
}
|
|
747
479
|
//#endregion
|
|
748
|
-
//#region src/
|
|
749
|
-
function
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
480
|
+
//#region src/context/formatter.ts
|
|
481
|
+
function escapeXml(value) {
|
|
482
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
483
|
+
}
|
|
484
|
+
function formatMetadata(memory) {
|
|
485
|
+
const parts = [
|
|
486
|
+
`type=${memory.type}`,
|
|
487
|
+
`importance=${memory.importance}`,
|
|
488
|
+
`score=${memory.score.toFixed(3)}`
|
|
489
|
+
];
|
|
490
|
+
if (memory.source) parts.push(`source=${memory.source}`);
|
|
491
|
+
if (memory.tags.length > 0) parts.push(`tags=${memory.tags.join(",")}`);
|
|
492
|
+
return parts.join("; ");
|
|
493
|
+
}
|
|
494
|
+
function markdownFence(value) {
|
|
495
|
+
const longestBacktickRun = Math.max(0, ...Array.from(value.matchAll(/`+/g), (match) => match[0].length));
|
|
496
|
+
return "`".repeat(Math.max(3, longestBacktickRun + 1));
|
|
497
|
+
}
|
|
498
|
+
function plainFence(values) {
|
|
499
|
+
const longestEqualsRun = Math.max(0, ...values.flatMap((value) => Array.from(value.matchAll(/=+/g), (match) => match[0].length)));
|
|
500
|
+
return "=".repeat(Math.max(8, longestEqualsRun + 1));
|
|
501
|
+
}
|
|
502
|
+
function formatMarkdown(memories, query, omittedCount) {
|
|
503
|
+
const lines = [
|
|
504
|
+
"# Memory Context",
|
|
505
|
+
"",
|
|
506
|
+
`Query: ${query}`,
|
|
507
|
+
"",
|
|
508
|
+
"## Relevant Memories"
|
|
509
|
+
];
|
|
510
|
+
for (const memory of memories) {
|
|
511
|
+
const fence = markdownFence(memory.content);
|
|
512
|
+
lines.push("", `- ${formatMetadata(memory)}`, ` ${fence}text`, ...memory.content.split("\n").map((line) => ` ${line}`), ` ${fence}`);
|
|
755
513
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
514
|
+
if (omittedCount > 0) lines.push("", `[${omittedCount} memories omitted to fit token budget]`);
|
|
515
|
+
return lines.join("\n");
|
|
516
|
+
}
|
|
517
|
+
function formatPlain(memories, query, omittedCount) {
|
|
518
|
+
const formattedMemories = memories.map((memory) => ({
|
|
519
|
+
memory,
|
|
520
|
+
metadata: formatMetadata(memory)
|
|
521
|
+
}));
|
|
522
|
+
const fence = plainFence([query, ...formattedMemories.flatMap(({ memory, metadata }) => [metadata, memory.content])]);
|
|
523
|
+
const lines = [
|
|
524
|
+
"MEMORY CONTEXT",
|
|
525
|
+
"",
|
|
526
|
+
`QUERY: ${query}`,
|
|
527
|
+
"",
|
|
528
|
+
"RELEVANT MEMORIES"
|
|
529
|
+
];
|
|
530
|
+
for (const [index, { memory, metadata }] of formattedMemories.entries()) lines.push("", `${fence} MEMORY ${index + 1} START ${fence}`, metadata, memory.content, `${fence} MEMORY ${index + 1} END ${fence}`);
|
|
531
|
+
if (omittedCount > 0) lines.push("", `[${omittedCount} memories omitted to fit token budget]`);
|
|
532
|
+
return lines.join("\n");
|
|
533
|
+
}
|
|
534
|
+
function formatXml(memories, query, omittedCount) {
|
|
535
|
+
const lines = [`<memory_context query="${escapeXml(query)}">`];
|
|
536
|
+
for (const memory of memories) lines.push(` <memory id="${escapeXml(memory.id)}" type="${escapeXml(memory.type)}" importance="${escapeXml(memory.importance)}" score="${memory.score.toFixed(3)}">`, ` <content>${escapeXml(memory.content)}</content>`, ` <metadata>${escapeXml(formatMetadata(memory))}</metadata>`, " </memory>");
|
|
537
|
+
if (omittedCount > 0) lines.push(` <omitted count="${omittedCount}" reason="token_budget" />`);
|
|
538
|
+
lines.push("</memory_context>");
|
|
539
|
+
return lines.join("\n");
|
|
540
|
+
}
|
|
541
|
+
function formatContext(memories, query, format, omittedCount = 0) {
|
|
542
|
+
switch (format) {
|
|
543
|
+
case "markdown": return formatMarkdown(memories, query, omittedCount);
|
|
544
|
+
case "plain": return formatPlain(memories, query, omittedCount);
|
|
545
|
+
case "xml": return formatXml(memories, query, omittedCount);
|
|
765
546
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
547
|
+
}
|
|
548
|
+
//#endregion
|
|
549
|
+
//#region src/context/tokens.ts
|
|
550
|
+
const TRUNCATION_MARKER = "[truncated to fit token budget]";
|
|
551
|
+
const heuristicTokenCounter = { count(text) {
|
|
552
|
+
return Math.ceil(text.length / 4);
|
|
553
|
+
} };
|
|
554
|
+
function trimToTokenBudget(text, maxTokens, tokenCounter) {
|
|
555
|
+
if (maxTokens < 1 || tokenCounter.count(text) <= maxTokens) return text;
|
|
556
|
+
let low = 0;
|
|
557
|
+
let high = text.length;
|
|
558
|
+
let best = "";
|
|
559
|
+
while (low <= high) {
|
|
560
|
+
const mid = Math.floor((low + high) / 2);
|
|
561
|
+
const output = `${text.slice(0, mid).trimEnd()}\n${TRUNCATION_MARKER}`;
|
|
562
|
+
if (tokenCounter.count(output) <= maxTokens) {
|
|
563
|
+
best = output;
|
|
564
|
+
low = mid + 1;
|
|
565
|
+
} else high = mid - 1;
|
|
774
566
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
567
|
+
if (best) return best;
|
|
568
|
+
for (let length = 31; length > 0; length -= 1) {
|
|
569
|
+
const markerFragment = TRUNCATION_MARKER.slice(0, length);
|
|
570
|
+
if (tokenCounter.count(markerFragment) <= maxTokens) return markerFragment;
|
|
571
|
+
}
|
|
572
|
+
return "";
|
|
573
|
+
}
|
|
574
|
+
//#endregion
|
|
575
|
+
//#region src/context/builder.ts
|
|
576
|
+
async function buildContext(options) {
|
|
577
|
+
const format = options.input.format ?? "markdown";
|
|
578
|
+
const maxTokens = options.input.maxTokens ?? 1200;
|
|
579
|
+
const tokenCounter = options.input.tokenCounter ?? heuristicTokenCounter;
|
|
580
|
+
const memories = await options.recall(options.input);
|
|
581
|
+
let low = 0;
|
|
582
|
+
let high = memories.length;
|
|
583
|
+
let bestContext = "";
|
|
584
|
+
while (low <= high) {
|
|
585
|
+
const mid = Math.floor((low + high) / 2);
|
|
586
|
+
const omittedCount = memories.length - mid;
|
|
587
|
+
const context = formatContext(memories.slice(0, mid), options.input.query, format, omittedCount);
|
|
588
|
+
if (tokenCounter.count(context) <= maxTokens) {
|
|
589
|
+
bestContext = context;
|
|
590
|
+
low = mid + 1;
|
|
591
|
+
} else high = mid - 1;
|
|
592
|
+
}
|
|
593
|
+
if (bestContext || memories.length === 0) return bestContext;
|
|
594
|
+
return trimToTokenBudget(formatContext([], options.input.query, format, memories.length), maxTokens, tokenCounter);
|
|
595
|
+
}
|
|
596
|
+
//#endregion
|
|
597
|
+
//#region src/observability.ts
|
|
598
|
+
function withMetadata(event, metadata) {
|
|
599
|
+
return {
|
|
600
|
+
...event,
|
|
601
|
+
...metadata.entityId === void 0 ? {} : { entityId: metadata.entityId },
|
|
602
|
+
...metadata.memoryId === void 0 ? {} : { memoryId: metadata.memoryId },
|
|
603
|
+
...metadata.count === void 0 ? {} : { count: metadata.count }
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
async function emitObservation(config, event) {
|
|
607
|
+
try {
|
|
608
|
+
await config.observability?.onEvent?.(event);
|
|
609
|
+
} catch {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
async function observeFromStartedAt(config, backend, operation, metadata, startedAt, action, successMetadata) {
|
|
614
|
+
await emitObservation(config, withMetadata({
|
|
615
|
+
operation,
|
|
616
|
+
phase: "start",
|
|
617
|
+
backend,
|
|
618
|
+
timestamp: new Date(startedAt)
|
|
619
|
+
}, metadata));
|
|
620
|
+
try {
|
|
621
|
+
const result = await action();
|
|
622
|
+
await emitObservation(config, withMetadata({
|
|
623
|
+
operation,
|
|
624
|
+
phase: "success",
|
|
625
|
+
backend,
|
|
626
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
627
|
+
durationMs: Date.now() - startedAt
|
|
628
|
+
}, {
|
|
629
|
+
...metadata,
|
|
630
|
+
...successMetadata?.(result)
|
|
631
|
+
}));
|
|
632
|
+
return result;
|
|
633
|
+
} catch (error) {
|
|
634
|
+
await emitObservation(config, withMetadata({
|
|
635
|
+
operation,
|
|
636
|
+
phase: "error",
|
|
637
|
+
backend,
|
|
638
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
639
|
+
durationMs: Date.now() - startedAt,
|
|
640
|
+
error
|
|
641
|
+
}, metadata));
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function observe(config, backend, operation, metadata, action, successMetadata) {
|
|
646
|
+
return observeFromStartedAt(config, backend, operation, metadata, Date.now(), action, successMetadata);
|
|
647
|
+
}
|
|
648
|
+
function observePrepared(config, backend, operation, metadata, prepare, action, successMetadata) {
|
|
649
|
+
const startedAt = Date.now();
|
|
650
|
+
try {
|
|
651
|
+
const prepared = prepare();
|
|
652
|
+
return observeFromStartedAt(config, backend, operation, metadata, startedAt, () => action(prepared), successMetadata);
|
|
653
|
+
} catch (error) {
|
|
654
|
+
return observeFromStartedAt(config, backend, operation, metadata, startedAt, async () => {
|
|
655
|
+
throw error;
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Ordering of {@link ImportanceLevel} from least to most important. Used
|
|
661
|
+
* by `prune({ maxImportance })` to compare a memory's importance against
|
|
662
|
+
* the caller-supplied ceiling.
|
|
663
|
+
*/
|
|
664
|
+
const IMPORTANCE_RANK = {
|
|
665
|
+
low: 0,
|
|
666
|
+
normal: 1,
|
|
667
|
+
high: 2,
|
|
668
|
+
critical: 3
|
|
669
|
+
};
|
|
670
|
+
//#endregion
|
|
671
|
+
//#region src/memory/embeddings.ts
|
|
672
|
+
function validateEmbedding(embedding, dimensions) {
|
|
673
|
+
if (embedding.length !== dimensions) throw new MnemocyteError("Embedder returned an embedding with unexpected dimensions.", "EMBEDDING");
|
|
674
|
+
if (embedding.some((component) => !Number.isFinite(component))) throw new MnemocyteError("Embedder returned an embedding with non-finite values.", "EMBEDDING");
|
|
675
|
+
if (embedding.every((component) => component === 0)) throw new MnemocyteError("Embedder returned an embedding with zero norm.", "EMBEDDING");
|
|
676
|
+
}
|
|
677
|
+
async function embedOne(embedder, text, options = {}) {
|
|
678
|
+
let embeddings;
|
|
679
|
+
try {
|
|
680
|
+
embeddings = await withResilience((signal) => signal === void 0 ? embedder.embed([text]) : embedder.embed([text], { signal }), {
|
|
681
|
+
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
682
|
+
...options.resilience === void 0 ? {} : { resilience: options.resilience }
|
|
683
|
+
});
|
|
684
|
+
} catch (error) {
|
|
685
|
+
if (error instanceof MnemocyteError && (error.code === "TIMEOUT" || error.code === "ABORTED")) throw error;
|
|
686
|
+
throw new MnemocyteError("Failed to embed text.", "EMBEDDING", error);
|
|
687
|
+
}
|
|
688
|
+
const embedding = embeddings[0];
|
|
689
|
+
if (!embedding) throw new MnemocyteError("Embedder returned no embedding.", "EMBEDDING");
|
|
690
|
+
validateEmbedding(embedding, embedder.dimensions);
|
|
691
|
+
return embedding;
|
|
692
|
+
}
|
|
693
|
+
async function embedMany(embedder, texts, options = {}) {
|
|
694
|
+
if (texts.length === 0) return [];
|
|
695
|
+
let embeddings;
|
|
696
|
+
try {
|
|
697
|
+
embeddings = await withResilience((signal) => signal === void 0 ? embedder.embed(texts) : embedder.embed(texts, { signal }), {
|
|
698
|
+
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
699
|
+
...options.resilience === void 0 ? {} : { resilience: options.resilience }
|
|
700
|
+
});
|
|
701
|
+
} catch (error) {
|
|
702
|
+
if (error instanceof MnemocyteError && (error.code === "TIMEOUT" || error.code === "ABORTED")) throw error;
|
|
703
|
+
throw new MnemocyteError("Failed to embed texts.", "EMBEDDING", error);
|
|
704
|
+
}
|
|
705
|
+
if (embeddings.length !== texts.length) throw new MnemocyteError(`Embedder returned ${embeddings.length} embeddings for ${texts.length} texts.`, "EMBEDDING");
|
|
706
|
+
for (const embedding of embeddings) validateEmbedding(embedding, embedder.dimensions);
|
|
707
|
+
return embeddings;
|
|
708
|
+
}
|
|
709
|
+
//#endregion
|
|
710
|
+
//#region src/memory/store.ts
|
|
711
|
+
function hasPruneSelector(filter) {
|
|
712
|
+
return filter.entityId !== void 0 || filter.expired === true || filter.superseded === true || filter.createdBefore !== void 0 || filter.notAccessedSince !== void 0 || filter.types !== void 0 && filter.types.length > 0 || filter.tags !== void 0 && filter.tags.length > 0 || filter.maxImportance !== void 0;
|
|
713
|
+
}
|
|
714
|
+
function assertPruneFilterHasSelector(filter) {
|
|
715
|
+
if (!hasPruneSelector(filter)) throw new MnemocyteError("prune requires at least one selector (entityId, expired, superseded, createdBefore, notAccessedSince, types, tags, or maxImportance).", "VALIDATION");
|
|
716
|
+
}
|
|
717
|
+
//#endregion
|
|
718
|
+
//#region src/memory/validation.ts
|
|
719
|
+
const MEMORY_TYPES = [
|
|
720
|
+
"fact",
|
|
721
|
+
"preference",
|
|
722
|
+
"instruction",
|
|
723
|
+
"backstory",
|
|
724
|
+
"episode",
|
|
725
|
+
"session"
|
|
726
|
+
];
|
|
727
|
+
const IMPORTANCE_LEVELS$1 = [
|
|
728
|
+
"low",
|
|
729
|
+
"normal",
|
|
730
|
+
"high",
|
|
731
|
+
"critical"
|
|
732
|
+
];
|
|
733
|
+
function isMemoryType(value) {
|
|
734
|
+
return typeof value === "string" && MEMORY_TYPES.includes(value);
|
|
735
|
+
}
|
|
736
|
+
function isImportanceLevel(value) {
|
|
737
|
+
return typeof value === "string" && IMPORTANCE_LEVELS$1.includes(value);
|
|
738
|
+
}
|
|
739
|
+
function assertMemoryType(value, field) {
|
|
740
|
+
if (!isMemoryType(value)) throw new MnemocyteError(`${field} must be a known memory type.`, "VALIDATION");
|
|
741
|
+
}
|
|
742
|
+
function assertImportanceLevel(value, field) {
|
|
743
|
+
if (!isImportanceLevel(value)) throw new MnemocyteError(`${field} must be a known importance level.`, "VALIDATION");
|
|
744
|
+
}
|
|
745
|
+
function assertMemoryTypeArray(value, field) {
|
|
746
|
+
if (!Array.isArray(value)) throw new MnemocyteError(`${field} must be an array.`, "VALIDATION");
|
|
747
|
+
for (const item of value) if (!isMemoryType(item)) throw new MnemocyteError(`${field} must contain only known memory types.`, "VALIDATION");
|
|
748
|
+
}
|
|
749
|
+
function assertValidDate(value, field) {
|
|
750
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime())) throw new MnemocyteError(`${field} must be a valid Date.`, "VALIDATION");
|
|
751
|
+
}
|
|
752
|
+
function assertNonEmptyString(value, field, code = "VALIDATION") {
|
|
753
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new MnemocyteError(`${field} must be a non-empty string.`, code);
|
|
754
|
+
}
|
|
755
|
+
function assertLimit(value) {
|
|
756
|
+
if (!Number.isInteger(value) || value < 1) throw new MnemocyteError("limit must be a positive integer.", "VALIDATION");
|
|
757
|
+
}
|
|
758
|
+
function assertMinScore(value) {
|
|
759
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) throw new MnemocyteError("minScore must be a number between 0 and 1.", "VALIDATION");
|
|
760
|
+
}
|
|
761
|
+
function assertEmbedder(embedder) {
|
|
762
|
+
if (!embedder || typeof embedder.embed !== "function") throw new MnemocyteError("embedder with an embed function is required.", "CONFIG");
|
|
763
|
+
assertNonEmptyString(embedder.model, "embedder.model", "CONFIG");
|
|
764
|
+
if (!Number.isInteger(embedder.dimensions) || embedder.dimensions < 1) throw new MnemocyteError("embedder.dimensions must be a positive integer.", "CONFIG");
|
|
765
|
+
}
|
|
766
|
+
const RETRIEVAL_WEIGHT_KEYS = [
|
|
767
|
+
"vector",
|
|
768
|
+
"lexical",
|
|
769
|
+
"recency",
|
|
770
|
+
"confidence",
|
|
771
|
+
"access",
|
|
772
|
+
"importance"
|
|
773
|
+
];
|
|
774
|
+
function assertPositiveFiniteConfigValue(value, field) {
|
|
775
|
+
if (!Number.isFinite(value) || value <= 0) throw new MnemocyteError(`${field} must be a positive finite number.`, "CONFIG");
|
|
776
|
+
}
|
|
777
|
+
function validateRetrievalConfig(config) {
|
|
778
|
+
if (config === void 0) return;
|
|
779
|
+
let weightTotal = 0;
|
|
780
|
+
for (const key of RETRIEVAL_WEIGHT_KEYS) {
|
|
781
|
+
const weight = config.weights?.[key] ?? DEFAULT_RETRIEVAL_WEIGHTS[key];
|
|
782
|
+
if (!Number.isFinite(weight) || weight < 0) throw new MnemocyteError(`retrieval.weights.${key} must be a non-negative finite number.`, "CONFIG");
|
|
783
|
+
weightTotal += weight;
|
|
784
|
+
}
|
|
785
|
+
if (weightTotal === 0) throw new MnemocyteError("retrieval.weights must have an effective total greater than zero.", "CONFIG");
|
|
786
|
+
if (config.recencyHalfLifeDays !== void 0) assertPositiveFiniteConfigValue(config.recencyHalfLifeDays, "retrieval.recencyHalfLifeDays");
|
|
787
|
+
if (config.accessSaturation !== void 0) assertPositiveFiniteConfigValue(config.accessSaturation, "retrieval.accessSaturation");
|
|
788
|
+
if (config.candidateMultiplier !== void 0 && (!Number.isInteger(config.candidateMultiplier) || config.candidateMultiplier < 1)) throw new MnemocyteError("retrieval.candidateMultiplier must be an integer greater than or equal to 1.", "CONFIG");
|
|
789
|
+
}
|
|
790
|
+
function validateProviderResilienceConfig(config) {
|
|
791
|
+
if (config === void 0) return;
|
|
792
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) throw new MnemocyteError("provider must be an object.", "CONFIG");
|
|
793
|
+
for (const [field, value] of [
|
|
794
|
+
["provider.timeoutMs", config.timeoutMs],
|
|
795
|
+
["provider.baseDelayMs", config.baseDelayMs],
|
|
796
|
+
["provider.maxDelayMs", config.maxDelayMs]
|
|
797
|
+
]) if (value !== void 0 && (!Number.isFinite(value) || value < 0)) throw new MnemocyteError(`${field} must be a non-negative finite number.`, "CONFIG");
|
|
798
|
+
if (config.retries !== void 0 && (!Number.isInteger(config.retries) || config.retries < 0)) throw new MnemocyteError("provider.retries must be a non-negative integer.", "CONFIG");
|
|
799
|
+
if (config.shouldRetry !== void 0 && typeof config.shouldRetry !== "function") throw new MnemocyteError("provider.shouldRetry must be a function.", "CONFIG");
|
|
800
|
+
}
|
|
801
|
+
function validateBuildContextInput(input) {
|
|
802
|
+
if (input.maxTokens !== void 0 && (!Number.isInteger(input.maxTokens) || input.maxTokens < 1)) throw new MnemocyteError("maxTokens must be a positive integer when supplied.", "VALIDATION");
|
|
803
|
+
}
|
|
804
|
+
function validateRememberInput(input) {
|
|
805
|
+
assertNonEmptyString(input.entityId, "entityId");
|
|
806
|
+
assertNonEmptyString(input.content, "content");
|
|
807
|
+
if (input.type !== void 0) assertMemoryType(input.type, "type");
|
|
808
|
+
if (input.importance !== void 0) assertImportanceLevel(input.importance, "importance");
|
|
809
|
+
if (input.tags !== void 0) {
|
|
810
|
+
if (!Array.isArray(input.tags)) throw new MnemocyteError("tags must be an array.", "VALIDATION");
|
|
811
|
+
for (const tag of input.tags) if (typeof tag !== "string") throw new MnemocyteError("tags must contain only strings.", "VALIDATION");
|
|
812
|
+
}
|
|
813
|
+
if (input.source !== void 0 && typeof input.source !== "string") throw new MnemocyteError("source must be a string.", "VALIDATION");
|
|
814
|
+
if (input.confidence !== void 0 && (!Number.isFinite(input.confidence) || input.confidence < 0 || input.confidence > 1)) throw new MnemocyteError("confidence must be a number between 0 and 1.", "VALIDATION");
|
|
815
|
+
if (input.expiresAt !== void 0) assertValidDate(input.expiresAt, "expiresAt");
|
|
816
|
+
}
|
|
817
|
+
function validateRecallInput(input) {
|
|
818
|
+
assertNonEmptyString(input.entityId, "entityId");
|
|
819
|
+
assertNonEmptyString(input.query, "query");
|
|
820
|
+
if (input.limit !== void 0) assertLimit(input.limit);
|
|
821
|
+
if (input.minScore !== void 0) assertMinScore(input.minScore);
|
|
822
|
+
if (input.types !== void 0) assertMemoryTypeArray(input.types, "types");
|
|
823
|
+
}
|
|
824
|
+
function contextInputToRecallInput(input) {
|
|
825
|
+
return {
|
|
826
|
+
entityId: input.entityId,
|
|
827
|
+
query: input.query,
|
|
828
|
+
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
829
|
+
...input.minScore === void 0 ? {} : { minScore: input.minScore },
|
|
830
|
+
...input.types === void 0 ? {} : { types: input.types },
|
|
831
|
+
...input.tags === void 0 ? {} : { tags: input.tags },
|
|
832
|
+
...input.includeSuperseded === void 0 ? {} : { includeSuperseded: input.includeSuperseded },
|
|
833
|
+
...input.includeExpired === void 0 ? {} : { includeExpired: input.includeExpired }
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Validate and normalize a {@link PruneInput}. Throws a `"VALIDATION"`
|
|
838
|
+
* {@link MnemocyteError} for malformed fields or when normalization leaves no
|
|
839
|
+
* effective selector, preventing surprise full-table deletes.
|
|
840
|
+
*/
|
|
841
|
+
function validatePruneInput(input) {
|
|
842
|
+
if (input.entityId !== void 0) assertNonEmptyString(input.entityId, "entityId");
|
|
843
|
+
for (const [field, value] of [
|
|
844
|
+
["expired", input.expired],
|
|
845
|
+
["superseded", input.superseded],
|
|
846
|
+
["dryRun", input.dryRun]
|
|
847
|
+
]) if (value !== void 0 && typeof value !== "boolean") throw new MnemocyteError(`${field} must be a boolean.`, "VALIDATION");
|
|
848
|
+
for (const [field, value] of [["createdBefore", input.createdBefore], ["notAccessedSince", input.notAccessedSince]]) if (value !== void 0) assertValidDate(value, field);
|
|
849
|
+
let types;
|
|
850
|
+
if (input.types !== void 0) {
|
|
851
|
+
assertMemoryTypeArray(input.types, "types");
|
|
852
|
+
const normalized = new Set(input.types);
|
|
853
|
+
if (normalized.size > 0) types = [...normalized];
|
|
854
|
+
}
|
|
855
|
+
let tags;
|
|
856
|
+
if (input.tags !== void 0) {
|
|
857
|
+
if (!Array.isArray(input.tags)) throw new MnemocyteError("tags must be an array.", "VALIDATION");
|
|
858
|
+
const normalized = /* @__PURE__ */ new Set();
|
|
859
|
+
for (const value of input.tags) {
|
|
860
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new MnemocyteError("tags must contain only non-empty strings.", "VALIDATION");
|
|
861
|
+
normalized.add(value.trim());
|
|
862
|
+
}
|
|
863
|
+
if (normalized.size > 0) tags = [...normalized];
|
|
864
|
+
}
|
|
865
|
+
if (input.maxImportance !== void 0) assertImportanceLevel(input.maxImportance, "maxImportance");
|
|
866
|
+
if (input.signal !== void 0 && (typeof input.signal !== "object" || input.signal === null || typeof input.signal.aborted !== "boolean" || typeof input.signal.addEventListener !== "function" || typeof input.signal.removeEventListener !== "function")) throw new MnemocyteError("signal must be an AbortSignal.", "VALIDATION");
|
|
867
|
+
const filter = {
|
|
868
|
+
...input.entityId === void 0 ? {} : { entityId: input.entityId },
|
|
869
|
+
...input.expired === true ? { expired: true } : {},
|
|
870
|
+
...input.superseded === true ? { superseded: true } : {},
|
|
871
|
+
...input.createdBefore === void 0 ? {} : { createdBefore: new Date(input.createdBefore) },
|
|
872
|
+
...input.notAccessedSince === void 0 ? {} : { notAccessedSince: new Date(input.notAccessedSince) },
|
|
873
|
+
...types === void 0 ? {} : { types },
|
|
874
|
+
...tags === void 0 ? {} : { tags },
|
|
875
|
+
...input.maxImportance === void 0 ? {} : { maxImportance: input.maxImportance },
|
|
876
|
+
dryRun: input.dryRun === true
|
|
877
|
+
};
|
|
878
|
+
assertPruneFilterHasSelector(filter);
|
|
879
|
+
return filter;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Validate a {@link FindDuplicatesInput} and throw a `"VALIDATION"`
|
|
883
|
+
* {@link MnemocyteError} for malformed `threshold` / `limit` values.
|
|
884
|
+
*/
|
|
885
|
+
function validateFindDuplicatesInput(input) {
|
|
886
|
+
assertNonEmptyString(input.entityId, "entityId");
|
|
887
|
+
if (input.threshold !== void 0) {
|
|
888
|
+
if (!Number.isFinite(input.threshold) || input.threshold < 0 || input.threshold > 1) throw new MnemocyteError("threshold must be a number between 0 and 1.", "VALIDATION");
|
|
889
|
+
}
|
|
890
|
+
if (input.limit !== void 0) assertLimit(input.limit);
|
|
891
|
+
if (input.types !== void 0) assertMemoryTypeArray(input.types, "types");
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Validate a {@link ConsolidateInput} and throw a `"VALIDATION"`
|
|
895
|
+
* {@link MnemocyteError} for malformed inputs (empty `supersededIds`
|
|
896
|
+
* or a `survivorId` appearing in `supersededIds`).
|
|
897
|
+
*/
|
|
898
|
+
function validateConsolidateInput(input) {
|
|
899
|
+
assertNonEmptyString(input.entityId, "entityId");
|
|
900
|
+
assertNonEmptyString(input.survivorId, "survivorId");
|
|
901
|
+
if (input.supersededIds.length === 0) throw new MnemocyteError("supersededIds must contain at least one memory id.", "VALIDATION");
|
|
902
|
+
if (input.supersededIds.includes(input.survivorId)) throw new MnemocyteError("survivorId must not appear in supersededIds.", "VALIDATION");
|
|
903
|
+
for (const id of input.supersededIds) assertNonEmptyString(id, "supersededIds[*]");
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Validate a {@link ListAuditLogInput} and throw a `"VALIDATION"`
|
|
907
|
+
* {@link MnemocyteError} when malformed.
|
|
908
|
+
*/
|
|
909
|
+
function validateListAuditLogInput(input) {
|
|
910
|
+
assertNonEmptyString(input.entityId, "entityId");
|
|
911
|
+
if (input.limit !== void 0) assertLimit(input.limit);
|
|
912
|
+
for (const [field, cursor] of [["beforeCursor", input.beforeCursor], ["afterCursor", input.afterCursor]]) {
|
|
913
|
+
if (cursor === void 0) continue;
|
|
914
|
+
assertAuditLogCursor(cursor, field);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
function assertAuditLogCursor(value, field) {
|
|
918
|
+
if (typeof value !== "object" || value === null) throw new MnemocyteError(`${field} must be an audit-log cursor.`, "VALIDATION");
|
|
919
|
+
const cursor = value;
|
|
920
|
+
assertValidDate(cursor.timestamp, `${field}.timestamp`);
|
|
921
|
+
assertNonEmptyString(cursor.id, `${field}.id`);
|
|
922
|
+
}
|
|
923
|
+
//#endregion
|
|
924
|
+
//#region src/memory/client-core.ts
|
|
925
|
+
function isPositionalRememberManyInput(input) {
|
|
926
|
+
return Array.isArray(input);
|
|
927
|
+
}
|
|
928
|
+
function providerOptions(config, signal) {
|
|
929
|
+
return {
|
|
930
|
+
...signal === void 0 ? {} : { signal },
|
|
931
|
+
...config.provider === void 0 ? {} : { resilience: config.provider }
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
function storeOptions(signal) {
|
|
935
|
+
return signal === void 0 ? void 0 : { signal };
|
|
936
|
+
}
|
|
937
|
+
function snapshotRememberInput(input) {
|
|
938
|
+
return {
|
|
939
|
+
...input,
|
|
940
|
+
...input.tags === void 0 ? {} : { tags: Array.isArray(input.tags) ? [...input.tags] : input.tags },
|
|
941
|
+
metadata: cloneJsonObject(input.metadata ?? {}),
|
|
942
|
+
...input.expiresAt === void 0 ? {} : { expiresAt: input.expiresAt instanceof Date ? new Date(input.expiresAt) : input.expiresAt }
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
function createStoredMemory(config, input, embedding, now) {
|
|
946
|
+
return {
|
|
947
|
+
id: createId(),
|
|
948
|
+
entityId: input.entityId,
|
|
949
|
+
content: input.content,
|
|
950
|
+
type: input.type ?? "fact",
|
|
951
|
+
importance: input.importance ?? "normal",
|
|
952
|
+
tags: normalizeTags(input.tags),
|
|
953
|
+
source: input.source ?? null,
|
|
954
|
+
metadata: input.metadata,
|
|
955
|
+
confidence: input.confidence ?? 1,
|
|
956
|
+
embeddingModel: config.embedder.model,
|
|
957
|
+
embeddingDimensions: config.embedder.dimensions,
|
|
958
|
+
supersededBy: null,
|
|
959
|
+
supersededAt: null,
|
|
960
|
+
expiresAt: input.expiresAt ?? null,
|
|
961
|
+
lastAccessedAt: null,
|
|
962
|
+
accessCount: 0,
|
|
963
|
+
createdAt: now,
|
|
964
|
+
updatedAt: now,
|
|
965
|
+
embedding
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
function auditEvent(entityId, description, metadata, timestamp = /* @__PURE__ */ new Date()) {
|
|
969
|
+
return {
|
|
970
|
+
id: createEventId(),
|
|
971
|
+
entityId,
|
|
972
|
+
description,
|
|
973
|
+
metadata,
|
|
974
|
+
timestamp
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
function normalizeStoreRows(expected, returned, operation) {
|
|
978
|
+
const expectedIds = new Set(expected.map((memory) => memory.id));
|
|
979
|
+
const returnedById = /* @__PURE__ */ new Map();
|
|
980
|
+
for (const row of returned) {
|
|
981
|
+
if (!expectedIds.has(row.id)) throw new MnemocyteError(`${operation} returned an unknown ID.`, "DB");
|
|
982
|
+
if (returnedById.has(row.id)) throw new MnemocyteError(`${operation} returned a duplicate ID.`, "DB");
|
|
983
|
+
returnedById.set(row.id, row);
|
|
984
|
+
}
|
|
985
|
+
return expected.map((row) => {
|
|
986
|
+
const returnedRow = returnedById.get(row.id);
|
|
987
|
+
if (!returnedRow) throw new MnemocyteError(`${operation} did not return every requested ID.`, "DB");
|
|
988
|
+
return returnedRow;
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
function normalizePruneEntityDeletions(result, expectedEntityId) {
|
|
992
|
+
const seenEntityIds = /* @__PURE__ */ new Set();
|
|
993
|
+
let detailedDeletedCount = 0;
|
|
994
|
+
for (const detail of result.deletedByEntity) {
|
|
995
|
+
if (detail.entityId.length === 0 || !Number.isInteger(detail.deletedCount) || detail.deletedCount <= 0 || seenEntityIds.has(detail.entityId) || expectedEntityId !== void 0 && detail.entityId !== expectedEntityId) throw new MnemocyteError("Memory prune returned invalid per-entity deletion details.", "DB");
|
|
996
|
+
seenEntityIds.add(detail.entityId);
|
|
997
|
+
detailedDeletedCount += detail.deletedCount;
|
|
998
|
+
}
|
|
999
|
+
if (detailedDeletedCount !== result.deletedCount || result.dryRun && result.deletedByEntity.length > 0) throw new MnemocyteError("Memory prune deletion details did not match the deleted count.", "DB");
|
|
1000
|
+
return [...result.deletedByEntity].sort((a, b) => a.entityId.localeCompare(b.entityId));
|
|
1001
|
+
}
|
|
1002
|
+
function toDuplicatePair(pair) {
|
|
1003
|
+
return {
|
|
1004
|
+
a: cloneMemory(pair.a),
|
|
1005
|
+
b: cloneMemory(pair.b),
|
|
1006
|
+
similarity: Math.max(0, Math.min(1, pair.similarity))
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
function createMemoryClient(config, store) {
|
|
1010
|
+
let state = "open";
|
|
1011
|
+
let activeOperations = 0;
|
|
1012
|
+
let operationsDrainedPromise;
|
|
1013
|
+
let resolveOperationsDrained;
|
|
1014
|
+
let closePromise;
|
|
1015
|
+
function assertOpen() {
|
|
1016
|
+
if (state === "closed") throw new MnemocyteError("Mnemocyte client is closed.", "DB");
|
|
1017
|
+
}
|
|
1018
|
+
function beginOperation() {
|
|
1019
|
+
if (state !== "open") throw new MnemocyteError("Mnemocyte client is closed.", "DB");
|
|
1020
|
+
activeOperations += 1;
|
|
1021
|
+
let completed = false;
|
|
1022
|
+
return () => {
|
|
1023
|
+
if (completed) return;
|
|
1024
|
+
completed = true;
|
|
1025
|
+
activeOperations -= 1;
|
|
1026
|
+
if (activeOperations === 0 && resolveOperationsDrained) {
|
|
1027
|
+
const resolve = resolveOperationsDrained;
|
|
1028
|
+
resolveOperationsDrained = void 0;
|
|
1029
|
+
operationsDrainedPromise = void 0;
|
|
1030
|
+
resolve();
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
function runOperation(operationName, metadata, operation) {
|
|
1035
|
+
let complete;
|
|
1036
|
+
try {
|
|
1037
|
+
complete = beginOperation();
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
return observe(config, store.backend, operationName, metadata, async () => {
|
|
1040
|
+
throw error;
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
return operation().then((result) => {
|
|
1045
|
+
complete();
|
|
1046
|
+
return result;
|
|
1047
|
+
}, (error) => {
|
|
1048
|
+
complete();
|
|
1049
|
+
throw error;
|
|
1050
|
+
});
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
complete();
|
|
1053
|
+
return Promise.reject(error);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
function waitForActiveOperations() {
|
|
1057
|
+
if (activeOperations === 0) return Promise.resolve();
|
|
1058
|
+
if (!operationsDrainedPromise) operationsDrainedPromise = new Promise((resolve) => {
|
|
1059
|
+
resolveOperationsDrained = resolve;
|
|
1060
|
+
});
|
|
1061
|
+
return operationsDrainedPromise;
|
|
1062
|
+
}
|
|
1063
|
+
async function ensureSchema() {
|
|
1064
|
+
await store.ensureSchema();
|
|
1065
|
+
}
|
|
1066
|
+
async function ensureEmbeddingCompatibility() {
|
|
1067
|
+
await store.ensureEmbeddingCompatibility(config.embedder);
|
|
1068
|
+
}
|
|
1069
|
+
async function recordAudit(events) {
|
|
1070
|
+
if (events.length === 0 || config.audit?.enabled !== true) return;
|
|
1071
|
+
try {
|
|
1072
|
+
await store.addAuditEvents(events);
|
|
1073
|
+
} catch {}
|
|
1074
|
+
}
|
|
1075
|
+
function remember(input) {
|
|
1076
|
+
return runOperation("remember", { entityId: input.entityId }, () => observePrepared(config, store.backend, "remember", { entityId: input.entityId }, () => snapshotRememberInput(input), async (preparedInput) => {
|
|
1077
|
+
assertOpen();
|
|
1078
|
+
validateRememberInput(preparedInput);
|
|
1079
|
+
await ensureEmbeddingCompatibility();
|
|
1080
|
+
const stored = [createStoredMemory(config, preparedInput, await embedOne(config.embedder, preparedInput.content, providerOptions(config, preparedInput.signal)), /* @__PURE__ */ new Date())];
|
|
1081
|
+
const [memory] = normalizeStoreRows(stored, await store.insertMemories(stored), "Memory insert");
|
|
1082
|
+
if (!memory) throw new MnemocyteError("Memory insert did not return the inserted ID.", "DB");
|
|
1083
|
+
await recordAudit([auditEvent(memory.entityId, "memory.created", {
|
|
807
1084
|
memoryId: memory.id,
|
|
808
1085
|
type: memory.type,
|
|
809
1086
|
importance: memory.importance
|
|
810
|
-
});
|
|
811
|
-
return
|
|
1087
|
+
})]);
|
|
1088
|
+
return memory;
|
|
812
1089
|
}, (memory) => ({
|
|
813
1090
|
memoryId: memory.id,
|
|
814
1091
|
count: 1
|
|
815
|
-
}));
|
|
1092
|
+
})));
|
|
816
1093
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1094
|
+
function rememberMany(input) {
|
|
1095
|
+
const count = isPositionalRememberManyInput(input) ? input.length : input.inputs.length;
|
|
1096
|
+
return runOperation("rememberMany", { count }, () => observePrepared(config, store.backend, "rememberMany", { count }, () => {
|
|
1097
|
+
const positional = isPositionalRememberManyInput(input);
|
|
1098
|
+
return {
|
|
1099
|
+
preparedInputs: (positional ? input : input.inputs).map(snapshotRememberInput),
|
|
1100
|
+
signal: positional ? input[0]?.signal : input.signal
|
|
1101
|
+
};
|
|
1102
|
+
}, async ({ preparedInputs, signal }) => {
|
|
1103
|
+
assertOpen();
|
|
1104
|
+
if (preparedInputs.length === 0) return [];
|
|
1105
|
+
for (const item of preparedInputs) validateRememberInput(item);
|
|
1106
|
+
await ensureEmbeddingCompatibility();
|
|
1107
|
+
const embeddings = await embedMany(config.embedder, preparedInputs.map((item) => item.content), providerOptions(config, signal));
|
|
1108
|
+
const now = /* @__PURE__ */ new Date();
|
|
1109
|
+
const stored = preparedInputs.map((item, idx) => createStoredMemory(config, item, embeddings[idx], now));
|
|
1110
|
+
const memories = normalizeStoreRows(stored, await store.insertMemories(stored), "Memory insert");
|
|
1111
|
+
await recordAudit(memories.map((memory) => auditEvent(memory.entityId, "memory.created", {
|
|
1112
|
+
memoryId: memory.id,
|
|
1113
|
+
type: memory.type,
|
|
1114
|
+
importance: memory.importance
|
|
1115
|
+
})));
|
|
1116
|
+
return memories;
|
|
1117
|
+
}, (result) => ({ count: result.length })));
|
|
1118
|
+
}
|
|
1119
|
+
function recall(input) {
|
|
1120
|
+
return observe(config, store.backend, "recall", { entityId: input.entityId }, async () => {
|
|
1121
|
+
assertOpen();
|
|
1122
|
+
validateRecallInput(input);
|
|
1123
|
+
const limit = input.limit ?? config.defaults?.limit ?? 10;
|
|
1124
|
+
const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
|
|
1125
|
+
assertLimit(limit);
|
|
1126
|
+
assertMinScore(minScore);
|
|
1127
|
+
await ensureEmbeddingCompatibility();
|
|
1128
|
+
const queryEmbedding = await embedOne(config.embedder, input.query, providerOptions(config, input.signal));
|
|
1129
|
+
const candidateMultiplier = config.retrieval?.candidateMultiplier ?? 3;
|
|
1130
|
+
const candidateLimit = Math.max(limit, limit * candidateMultiplier);
|
|
1131
|
+
const scoreLexical = createLexicalScorer(input.query);
|
|
1132
|
+
const scoringConfig = createScoringConfig(config.retrieval);
|
|
1133
|
+
const [vectorCandidates, lexicalCandidates] = await Promise.all([store.vectorSearch({
|
|
1134
|
+
...input,
|
|
1135
|
+
embedding: queryEmbedding,
|
|
1136
|
+
limit: candidateLimit,
|
|
1137
|
+
minVectorScore: 0
|
|
1138
|
+
}), store.lexicalSearch({
|
|
1139
|
+
...input,
|
|
1140
|
+
limit: candidateLimit
|
|
1141
|
+
})]);
|
|
1142
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1143
|
+
for (const candidate of vectorCandidates) merged.set(candidate.memory.id, {
|
|
1144
|
+
memory: candidate.memory,
|
|
1145
|
+
vectorScore: candidate.vectorScore,
|
|
1146
|
+
lexicalScore: scoreLexical(candidate.memory.content)
|
|
1147
|
+
});
|
|
1148
|
+
const lexicalOnly = lexicalCandidates.filter((candidate) => !merged.has(candidate.memory.id));
|
|
1149
|
+
const lexicalOnlyEmbeddings = await store.getMemoryEmbeddings(lexicalOnly.map((candidate) => candidate.memory.id));
|
|
1150
|
+
for (const candidate of lexicalCandidates) {
|
|
1151
|
+
const existing = merged.get(candidate.memory.id);
|
|
1152
|
+
if (existing) {
|
|
1153
|
+
existing.lexicalScore = candidate.lexicalScore;
|
|
1154
|
+
continue;
|
|
860
1155
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
validateRecallInput(input);
|
|
868
|
-
const limit = input.limit ?? config.defaults?.limit ?? 10;
|
|
869
|
-
const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
|
|
870
|
-
assertLimit(limit);
|
|
871
|
-
assertMinScore(minScore);
|
|
872
|
-
const queryEmbedding = await embedOne(config.embedder, input.query, {
|
|
873
|
-
...input.signal === void 0 ? {} : { signal: input.signal },
|
|
874
|
-
...config.provider === void 0 ? {} : { resilience: config.provider }
|
|
1156
|
+
const embedding = lexicalOnlyEmbeddings.get(candidate.memory.id);
|
|
1157
|
+
const similarity = embedding ? cosineSimilarity(embedding, queryEmbedding) : 0;
|
|
1158
|
+
merged.set(candidate.memory.id, {
|
|
1159
|
+
memory: candidate.memory,
|
|
1160
|
+
vectorScore: toVectorScore(similarity),
|
|
1161
|
+
lexicalScore: candidate.lexicalScore
|
|
875
1162
|
});
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
|
|
1163
|
+
}
|
|
1164
|
+
const scored = Array.from(merged.values()).map((entry) => toScoredMemoryWithConfig(entry.memory, Math.max(0, entry.vectorScore), Math.max(0, entry.lexicalScore), input, scoringConfig)).filter((memory) => memory.score >= minScore).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1165
|
+
const accessUpdates = normalizeStoreRows(scored, await store.markMemoriesAccessed(scored.map((memory) => memory.id)), "Memory access update");
|
|
1166
|
+
return scored.map((memory, index) => {
|
|
1167
|
+
const update = accessUpdates[index];
|
|
1168
|
+
if (!update) throw new MnemocyteError("Memory access update did not return every requested ID.", "DB");
|
|
1169
|
+
return {
|
|
1170
|
+
...memory,
|
|
1171
|
+
lastAccessedAt: new Date(update.lastAccessedAt),
|
|
1172
|
+
accessCount: update.accessCount,
|
|
1173
|
+
updatedAt: new Date(update.updatedAt)
|
|
1174
|
+
};
|
|
1175
|
+
});
|
|
1176
|
+
}, (result) => ({ count: result.length }));
|
|
1177
|
+
}
|
|
1178
|
+
return {
|
|
1179
|
+
remember,
|
|
1180
|
+
rememberMany,
|
|
1181
|
+
recall(input) {
|
|
1182
|
+
return runOperation("recall", { entityId: input.entityId }, () => recall(input));
|
|
890
1183
|
},
|
|
891
|
-
|
|
892
|
-
return observe(config,
|
|
1184
|
+
buildContext(input) {
|
|
1185
|
+
return runOperation("buildContext", { entityId: input.entityId }, () => observe(config, store.backend, "buildContext", { entityId: input.entityId }, async () => {
|
|
893
1186
|
assertOpen();
|
|
1187
|
+
validateBuildContextInput(input);
|
|
894
1188
|
return buildContext({
|
|
895
1189
|
input,
|
|
896
1190
|
recall: (contextInput) => {
|
|
897
1191
|
const recallInput = contextInputToRecallInput(contextInput);
|
|
898
|
-
return
|
|
1192
|
+
return recall(input.signal === void 0 ? recallInput : {
|
|
899
1193
|
...recallInput,
|
|
900
1194
|
signal: input.signal
|
|
901
1195
|
});
|
|
902
1196
|
}
|
|
903
1197
|
});
|
|
904
|
-
});
|
|
1198
|
+
}));
|
|
905
1199
|
},
|
|
906
|
-
|
|
907
|
-
return
|
|
1200
|
+
forget(input) {
|
|
1201
|
+
return runOperation("forget", {
|
|
1202
|
+
entityId: input.entityId,
|
|
1203
|
+
memoryId: input.memoryId
|
|
1204
|
+
}, () => observe(config, store.backend, "forget", {
|
|
908
1205
|
entityId: input.entityId,
|
|
909
1206
|
memoryId: input.memoryId
|
|
910
1207
|
}, async () => {
|
|
911
1208
|
assertOpen();
|
|
912
1209
|
assertNonEmptyString(input.entityId, "entityId");
|
|
913
1210
|
assertNonEmptyString(input.memoryId, "memoryId");
|
|
914
|
-
|
|
915
|
-
if (!
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
});
|
|
1211
|
+
await ensureSchema();
|
|
1212
|
+
if (!await store.deleteMemory(input.entityId, input.memoryId)) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
|
|
1213
|
+
await recordAudit([auditEvent(input.entityId, "memory.deleted", { memoryId: input.memoryId })]);
|
|
1214
|
+
}));
|
|
919
1215
|
},
|
|
920
|
-
|
|
921
|
-
return observe(config,
|
|
1216
|
+
forgetAll(input) {
|
|
1217
|
+
return runOperation("forgetAll", { entityId: input.entityId }, () => observe(config, store.backend, "forgetAll", { entityId: input.entityId }, async () => {
|
|
922
1218
|
assertOpen();
|
|
923
1219
|
assertNonEmptyString(input.entityId, "entityId");
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1220
|
+
await ensureSchema();
|
|
1221
|
+
const deletedCount = await store.deleteMemoriesForEntity(input.entityId);
|
|
1222
|
+
await recordAudit([auditEvent(input.entityId, "entity.cleared", { count: deletedCount })]);
|
|
1223
|
+
return deletedCount;
|
|
1224
|
+
}, (count) => ({ count })).then(() => void 0));
|
|
1225
|
+
},
|
|
1226
|
+
prune(input) {
|
|
1227
|
+
return runOperation("prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, () => observe(config, store.backend, "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
|
|
1228
|
+
assertOpen();
|
|
1229
|
+
throwIfAborted(input.signal);
|
|
1230
|
+
const filter = validatePruneInput(input);
|
|
1231
|
+
await ensureSchema();
|
|
1232
|
+
const result = await store.prune(filter, storeOptions(input.signal));
|
|
1233
|
+
const deletedByEntity = normalizePruneEntityDeletions(result, input.entityId);
|
|
1234
|
+
if (result.deletedCount > 0 && result.dryRun === false) await recordAudit(deletedByEntity.map((detail) => auditEvent(detail.entityId, "memory.pruned", { count: detail.deletedCount })));
|
|
1235
|
+
return {
|
|
1236
|
+
matchedCount: result.matchedCount,
|
|
1237
|
+
deletedCount: result.deletedCount,
|
|
1238
|
+
dryRun: result.dryRun
|
|
1239
|
+
};
|
|
1240
|
+
}, (result) => ({ count: result.deletedCount })));
|
|
1241
|
+
},
|
|
1242
|
+
findDuplicates(input) {
|
|
1243
|
+
return runOperation("findDuplicates", { entityId: input.entityId }, () => observe(config, store.backend, "findDuplicates", { entityId: input.entityId }, async () => {
|
|
1244
|
+
assertOpen();
|
|
1245
|
+
throwIfAborted(input.signal);
|
|
1246
|
+
validateFindDuplicatesInput(input);
|
|
1247
|
+
await ensureEmbeddingCompatibility();
|
|
1248
|
+
return (await store.findDuplicatePairs(input, storeOptions(input.signal))).map(toDuplicatePair);
|
|
1249
|
+
}, (result) => ({ count: result.length })));
|
|
1250
|
+
},
|
|
1251
|
+
listAuditLog(input) {
|
|
1252
|
+
return runOperation("listAuditLog", { entityId: input.entityId }, () => observe(config, store.backend, "listAuditLog", { entityId: input.entityId }, async () => {
|
|
1253
|
+
assertOpen();
|
|
1254
|
+
throwIfAborted(input.signal);
|
|
1255
|
+
validateListAuditLogInput(input);
|
|
1256
|
+
await ensureSchema();
|
|
1257
|
+
return await store.listAuditLog(input, storeOptions(input.signal));
|
|
1258
|
+
}, (result) => ({ count: result.length })));
|
|
1259
|
+
},
|
|
1260
|
+
stats(input) {
|
|
1261
|
+
return runOperation("stats", input?.entityId ? { entityId: input.entityId } : {}, () => observe(config, store.backend, "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
|
|
1262
|
+
assertOpen();
|
|
1263
|
+
await ensureSchema();
|
|
1264
|
+
return store.stats(input, /* @__PURE__ */ new Date());
|
|
1265
|
+
}));
|
|
1266
|
+
},
|
|
1267
|
+
experimental: { consolidate(input) {
|
|
1268
|
+
return runOperation("consolidate", {
|
|
1269
|
+
entityId: input.entityId,
|
|
1270
|
+
memoryId: input.survivorId
|
|
1271
|
+
}, () => observe(config, store.backend, "consolidate", {
|
|
1272
|
+
entityId: input.entityId,
|
|
1273
|
+
memoryId: input.survivorId
|
|
1274
|
+
}, async () => {
|
|
1275
|
+
assertOpen();
|
|
1276
|
+
throwIfAborted(input.signal);
|
|
1277
|
+
validateConsolidateInput(input);
|
|
1278
|
+
await ensureSchema();
|
|
1279
|
+
const survivor = await store.getMemory(input.entityId, input.survivorId, storeOptions(input.signal));
|
|
1280
|
+
throwIfAborted(input.signal);
|
|
1281
|
+
if (!survivor) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
|
|
1282
|
+
if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
|
|
1283
|
+
const targets = await store.loadConsolidationTargets(input.entityId, input.supersededIds, storeOptions(input.signal));
|
|
1284
|
+
throwIfAborted(input.signal);
|
|
1285
|
+
const foundIds = new Set(targets.map((target) => target.id));
|
|
1286
|
+
for (const id of input.supersededIds) if (!foundIds.has(id)) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
|
|
1287
|
+
if (targets.some((target) => target.supersededBy !== null && target.supersededBy !== survivor.id)) throw new MnemocyteError("Superseded memory already belongs to a different survivor.", "CONFLICT");
|
|
1288
|
+
if (targets.filter((target) => target.supersededBy === null).length === 0) return {
|
|
1289
|
+
survivorId: survivor.id,
|
|
1290
|
+
supersededCount: 0,
|
|
1291
|
+
supersededIds: []
|
|
1292
|
+
};
|
|
1293
|
+
const result = await store.consolidate({
|
|
1294
|
+
entityId: input.entityId,
|
|
1295
|
+
survivorId: survivor.id,
|
|
1296
|
+
survivorTags: survivor.tags,
|
|
1297
|
+
supersededIds: input.supersededIds,
|
|
1298
|
+
mergeTags: input.mergeTags !== false,
|
|
1299
|
+
now: /* @__PURE__ */ new Date(),
|
|
1300
|
+
auditEnabled: config.audit?.enabled === true
|
|
1301
|
+
}, storeOptions(input.signal));
|
|
1302
|
+
return {
|
|
1303
|
+
survivorId: survivor.id,
|
|
1304
|
+
supersededCount: result.supersededIds.length,
|
|
1305
|
+
supersededIds: result.supersededIds
|
|
1306
|
+
};
|
|
1307
|
+
}, (result) => ({ count: result.supersededCount })));
|
|
1308
|
+
} },
|
|
1309
|
+
close() {
|
|
1310
|
+
if (closePromise) return closePromise;
|
|
1311
|
+
state = "closing";
|
|
1312
|
+
closePromise = observe(config, store.backend, "close", {}, async () => {
|
|
1313
|
+
await waitForActiveOperations();
|
|
1314
|
+
try {
|
|
1315
|
+
await store.close();
|
|
1316
|
+
state = "closed";
|
|
1317
|
+
} catch (error) {
|
|
1318
|
+
state = "open";
|
|
1319
|
+
closePromise = void 0;
|
|
1320
|
+
throw error;
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
return closePromise;
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
//#endregion
|
|
1328
|
+
//#region src/memory/deletion.ts
|
|
1329
|
+
function memoryHasDependentsError(cause) {
|
|
1330
|
+
return new MnemocyteError("Memory cannot be deleted while other memories reference it as their consolidation survivor.", "CONFLICT", cause);
|
|
1331
|
+
}
|
|
1332
|
+
//#endregion
|
|
1333
|
+
//#region src/memory/filters.ts
|
|
1334
|
+
function isExpired(memory, now) {
|
|
1335
|
+
return memory.expiresAt !== null && memory.expiresAt.getTime() <= now.getTime();
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Test whether `memory` matches a validated prune filter. All specified
|
|
1339
|
+
* selectors are AND-combined; unspecified selectors do not restrict.
|
|
1340
|
+
*
|
|
1341
|
+
* `now` is supplied by the caller so callers can share a consistent
|
|
1342
|
+
* timestamp across a batch.
|
|
1343
|
+
*/
|
|
1344
|
+
function matchesPruneFilter(memory, input, now) {
|
|
1345
|
+
if (input.entityId !== void 0 && memory.entityId !== input.entityId) return false;
|
|
1346
|
+
if (input.expired === true && !isExpired(memory, now)) return false;
|
|
1347
|
+
if (input.superseded === true && memory.supersededBy === null) return false;
|
|
1348
|
+
if (input.createdBefore !== void 0 && memory.createdAt.getTime() >= input.createdBefore.getTime()) return false;
|
|
1349
|
+
if (input.notAccessedSince !== void 0) {
|
|
1350
|
+
const last = memory.lastAccessedAt;
|
|
1351
|
+
if (last !== null && last.getTime() >= input.notAccessedSince.getTime()) return false;
|
|
1352
|
+
}
|
|
1353
|
+
if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
|
|
1354
|
+
if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
|
|
1355
|
+
if (input.maxImportance !== void 0 && IMPORTANCE_RANK[memory.importance] > IMPORTANCE_RANK[input.maxImportance]) return false;
|
|
1356
|
+
return true;
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Return `true` when a memory should participate in a duplicate scan,
|
|
1360
|
+
* given the filters on `input` and a shared `now` timestamp.
|
|
1361
|
+
*/
|
|
1362
|
+
function matchesDuplicateFilter(memory, input, now) {
|
|
1363
|
+
if (memory.entityId !== input.entityId) return false;
|
|
1364
|
+
if (input.includeSuperseded !== true && memory.supersededBy !== null) return false;
|
|
1365
|
+
if (input.includeExpired !== true && isExpired(memory, now)) return false;
|
|
1366
|
+
if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
|
|
1367
|
+
if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
|
|
1368
|
+
return true;
|
|
1369
|
+
}
|
|
1370
|
+
function matchesRecallFilter(memory, input, now) {
|
|
1371
|
+
if (memory.entityId !== input.entityId) return false;
|
|
1372
|
+
if (!input.includeSuperseded && memory.supersededBy !== null) return false;
|
|
1373
|
+
if (!input.includeExpired && isExpired(memory, now)) return false;
|
|
1374
|
+
if (input.types && !input.types.includes(memory.type)) return false;
|
|
1375
|
+
if (input.tags && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
|
|
1376
|
+
if (input.before && memory.createdAt >= input.before) return false;
|
|
1377
|
+
if (input.after && memory.createdAt <= input.after) return false;
|
|
1378
|
+
return true;
|
|
1379
|
+
}
|
|
1380
|
+
//#endregion
|
|
1381
|
+
//#region src/memory/in-memory.ts
|
|
1382
|
+
function cloneAuditEvent(event) {
|
|
1383
|
+
return {
|
|
1384
|
+
id: event.id,
|
|
1385
|
+
entityId: event.entityId,
|
|
1386
|
+
description: event.description,
|
|
1387
|
+
metadata: cloneJsonObject(event.metadata),
|
|
1388
|
+
timestamp: new Date(event.timestamp)
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
function compareAuditPositions(a, b) {
|
|
1392
|
+
const timestampDifference = a.timestamp.getTime() - b.timestamp.getTime();
|
|
1393
|
+
if (timestampDifference !== 0) return timestampDifference;
|
|
1394
|
+
if (a.id === b.id) return 0;
|
|
1395
|
+
return a.id < b.id ? -1 : 1;
|
|
1396
|
+
}
|
|
1397
|
+
function assertNoDependentMemories(memories, targetIds) {
|
|
1398
|
+
for (const memory of memories.values()) if (memory.supersededBy !== null && targetIds.has(memory.supersededBy)) throw memoryHasDependentsError();
|
|
1399
|
+
}
|
|
1400
|
+
function createInMemoryStore() {
|
|
1401
|
+
const memories = /* @__PURE__ */ new Map();
|
|
1402
|
+
const auditLog = [];
|
|
1403
|
+
return {
|
|
1404
|
+
backend: "in-memory",
|
|
1405
|
+
async ensureSchema() {},
|
|
1406
|
+
async ensureEmbeddingCompatibility() {},
|
|
1407
|
+
async insertMemories(rows) {
|
|
1408
|
+
const inserted = [];
|
|
1409
|
+
for (const row of rows) {
|
|
1410
|
+
memories.set(row.id, row);
|
|
1411
|
+
inserted.push(cloneMemory(row));
|
|
1412
|
+
}
|
|
1413
|
+
return inserted;
|
|
1414
|
+
},
|
|
1415
|
+
async vectorSearch(input) {
|
|
1416
|
+
const now = /* @__PURE__ */ new Date();
|
|
1417
|
+
const minVectorScore = input.minVectorScore ?? 0;
|
|
1418
|
+
return Array.from(memories.values()).filter((memory) => matchesRecallFilter(memory, input, now)).map((memory) => ({
|
|
1419
|
+
memory: cloneMemory(memory),
|
|
1420
|
+
vectorScore: toVectorScore(cosineSimilarity(memory.embedding, input.embedding))
|
|
1421
|
+
})).filter((candidate) => candidate.vectorScore >= minVectorScore).sort((a, b) => b.vectorScore - a.vectorScore).slice(0, input.limit);
|
|
1422
|
+
},
|
|
1423
|
+
async lexicalSearch(input) {
|
|
1424
|
+
const now = /* @__PURE__ */ new Date();
|
|
1425
|
+
const scoreLexical = createLexicalScorer(input.query);
|
|
1426
|
+
return Array.from(memories.values()).filter((memory) => matchesRecallFilter(memory, input, now)).map((memory) => ({
|
|
1427
|
+
memory: cloneMemory(memory),
|
|
1428
|
+
lexicalScore: scoreLexical(memory.content)
|
|
1429
|
+
})).filter((candidate) => candidate.lexicalScore > 0).sort((a, b) => b.lexicalScore - a.lexicalScore).slice(0, input.limit);
|
|
1430
|
+
},
|
|
1431
|
+
async getMemoryEmbeddings(memoryIds) {
|
|
1432
|
+
const result = /* @__PURE__ */ new Map();
|
|
1433
|
+
for (const id of memoryIds) {
|
|
1434
|
+
const memory = memories.get(id);
|
|
1435
|
+
if (memory) result.set(id, [...memory.embedding]);
|
|
1436
|
+
}
|
|
1437
|
+
return result;
|
|
1438
|
+
},
|
|
1439
|
+
async markMemoriesAccessed(memoryIds) {
|
|
1440
|
+
const now = /* @__PURE__ */ new Date();
|
|
1441
|
+
const updates = [];
|
|
1442
|
+
for (const id of memoryIds) {
|
|
1443
|
+
const memory = memories.get(id);
|
|
1444
|
+
if (memory) {
|
|
1445
|
+
memory.lastAccessedAt = now;
|
|
1446
|
+
memory.accessCount += 1;
|
|
1447
|
+
memory.updatedAt = now;
|
|
1448
|
+
updates.push({
|
|
1449
|
+
id: memory.id,
|
|
1450
|
+
lastAccessedAt: now,
|
|
1451
|
+
accessCount: memory.accessCount,
|
|
1452
|
+
updatedAt: now
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
return updates;
|
|
1457
|
+
},
|
|
1458
|
+
async deleteMemory(entityId, memoryId) {
|
|
1459
|
+
const memory = memories.get(memoryId);
|
|
1460
|
+
if (!memory || memory.entityId !== entityId) return false;
|
|
1461
|
+
assertNoDependentMemories(memories, new Set([memoryId]));
|
|
1462
|
+
return memories.delete(memoryId);
|
|
1463
|
+
},
|
|
1464
|
+
async deleteMemoriesForEntity(entityId) {
|
|
1465
|
+
const targetIds = new Set(Array.from(memories.values()).filter((memory) => memory.entityId === entityId).map((memory) => memory.id));
|
|
1466
|
+
assertNoDependentMemories(memories, targetIds);
|
|
1467
|
+
for (const id of targetIds) memories.delete(id);
|
|
1468
|
+
return targetIds.size;
|
|
1469
|
+
},
|
|
1470
|
+
async prune(input, options) {
|
|
1471
|
+
throwIfAborted(options?.signal);
|
|
1472
|
+
assertPruneFilterHasSelector(input);
|
|
1473
|
+
const now = /* @__PURE__ */ new Date();
|
|
1474
|
+
const matched = [];
|
|
1475
|
+
for (const memory of memories.values()) {
|
|
1476
|
+
throwIfAborted(options?.signal);
|
|
1477
|
+
if (matchesPruneFilter(memory, input, now)) matched.push(memory);
|
|
1478
|
+
}
|
|
1479
|
+
const dryRun = input.dryRun === true;
|
|
1480
|
+
let deletedByEntity = [];
|
|
1481
|
+
if (!dryRun) {
|
|
1482
|
+
throwIfAborted(options?.signal);
|
|
1483
|
+
assertNoDependentMemories(memories, new Set(matched.map((memory) => memory.id)));
|
|
1484
|
+
const deletedCounts = /* @__PURE__ */ new Map();
|
|
1485
|
+
for (const memory of matched) {
|
|
1486
|
+
memories.delete(memory.id);
|
|
1487
|
+
deletedCounts.set(memory.entityId, (deletedCounts.get(memory.entityId) ?? 0) + 1);
|
|
928
1488
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1489
|
+
deletedByEntity = Array.from(deletedCounts, ([entityId, deletedCount]) => ({
|
|
1490
|
+
entityId,
|
|
1491
|
+
deletedCount
|
|
1492
|
+
})).sort((a, b) => a.entityId.localeCompare(b.entityId));
|
|
1493
|
+
}
|
|
1494
|
+
return {
|
|
1495
|
+
matchedCount: matched.length,
|
|
1496
|
+
deletedCount: dryRun ? 0 : matched.length,
|
|
1497
|
+
dryRun,
|
|
1498
|
+
deletedByEntity
|
|
1499
|
+
};
|
|
932
1500
|
},
|
|
933
|
-
async
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1501
|
+
async findDuplicatePairs(input, options) {
|
|
1502
|
+
throwIfAborted(options?.signal);
|
|
1503
|
+
const threshold = input.threshold ?? .95;
|
|
1504
|
+
const limit = input.limit ?? 100;
|
|
1505
|
+
const now = /* @__PURE__ */ new Date();
|
|
1506
|
+
const candidates = Array.from(memories.values()).filter((memory) => {
|
|
1507
|
+
throwIfAborted(options?.signal);
|
|
1508
|
+
return matchesDuplicateFilter(memory, input, now);
|
|
1509
|
+
});
|
|
1510
|
+
const pairs = [];
|
|
1511
|
+
for (let i = 0; i < candidates.length; i += 1) {
|
|
1512
|
+
throwIfAborted(options?.signal);
|
|
1513
|
+
const a = candidates[i];
|
|
1514
|
+
if (a === void 0) continue;
|
|
1515
|
+
for (let j = i + 1; j < candidates.length; j += 1) {
|
|
1516
|
+
throwIfAborted(options?.signal);
|
|
1517
|
+
const b = candidates[j];
|
|
1518
|
+
if (b === void 0) continue;
|
|
1519
|
+
const similarity = Math.max(0, Math.min(1, cosineSimilarity(a.embedding, b.embedding)));
|
|
1520
|
+
if (similarity >= threshold) pairs.push({
|
|
1521
|
+
a: cloneMemory(a),
|
|
1522
|
+
b: cloneMemory(b),
|
|
1523
|
+
similarity
|
|
1524
|
+
});
|
|
944
1525
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
dryRun
|
|
949
|
-
};
|
|
950
|
-
}, (result) => ({ count: result.deletedCount }));
|
|
1526
|
+
}
|
|
1527
|
+
pairs.sort((x, y) => y.similarity - x.similarity);
|
|
1528
|
+
return pairs.slice(0, limit);
|
|
951
1529
|
},
|
|
952
|
-
async
|
|
953
|
-
|
|
954
|
-
assertOpen();
|
|
955
|
-
validateFindDuplicatesInput(input);
|
|
956
|
-
const threshold = input.threshold ?? .95;
|
|
957
|
-
const limit = input.limit ?? 100;
|
|
958
|
-
const now = /* @__PURE__ */ new Date();
|
|
959
|
-
const candidates = Array.from(memories.values()).filter((memory) => matchesDuplicateFilter(memory, input, now));
|
|
960
|
-
const pairs = [];
|
|
961
|
-
for (let i = 0; i < candidates.length; i += 1) {
|
|
962
|
-
const a = candidates[i];
|
|
963
|
-
if (a === void 0) continue;
|
|
964
|
-
for (let j = i + 1; j < candidates.length; j += 1) {
|
|
965
|
-
const b = candidates[j];
|
|
966
|
-
if (b === void 0) continue;
|
|
967
|
-
const similarity = Math.max(0, Math.min(1, cosineSimilarity(a.embedding, b.embedding)));
|
|
968
|
-
if (similarity >= threshold) pairs.push({
|
|
969
|
-
a: cloneMemory(a),
|
|
970
|
-
b: cloneMemory(b),
|
|
971
|
-
similarity
|
|
972
|
-
});
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
pairs.sort((x, y) => y.similarity - x.similarity);
|
|
976
|
-
return pairs.slice(0, limit);
|
|
977
|
-
}, (result) => ({ count: result.length }));
|
|
1530
|
+
async addAuditEvents(events) {
|
|
1531
|
+
auditLog.push(...events.map(cloneAuditEvent));
|
|
978
1532
|
},
|
|
979
|
-
async listAuditLog(input) {
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
const dt = b.event.timestamp.getTime() - a.event.timestamp.getTime();
|
|
994
|
-
if (dt !== 0) return dt;
|
|
995
|
-
return b.idx - a.idx;
|
|
996
|
-
}).slice(0, limit).map(({ event }) => cloneAuditEvent(event));
|
|
997
|
-
}, (result) => ({ count: result.length }));
|
|
1533
|
+
async listAuditLog(input, options) {
|
|
1534
|
+
throwIfAborted(options?.signal);
|
|
1535
|
+
const limit = input.limit ?? 50;
|
|
1536
|
+
return auditLog.filter((event) => {
|
|
1537
|
+
throwIfAborted(options?.signal);
|
|
1538
|
+
if (event.entityId !== input.entityId) return false;
|
|
1539
|
+
if (input.before !== void 0 && event.timestamp.getTime() >= input.before.getTime()) return false;
|
|
1540
|
+
if (input.after !== void 0 && event.timestamp.getTime() <= input.after.getTime()) return false;
|
|
1541
|
+
if (input.beforeCursor !== void 0 && compareAuditPositions(event, input.beforeCursor) >= 0) return false;
|
|
1542
|
+
if (input.afterCursor !== void 0 && compareAuditPositions(event, input.afterCursor) <= 0) return false;
|
|
1543
|
+
return true;
|
|
1544
|
+
}).sort((a, b) => {
|
|
1545
|
+
return compareAuditPositions(b, a);
|
|
1546
|
+
}).slice(0, limit).map(cloneAuditEvent);
|
|
998
1547
|
},
|
|
999
|
-
async
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
const
|
|
1008
|
-
if (
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
};
|
|
1015
|
-
return {
|
|
1016
|
-
entityCount: new Set(allMemories.map((memory) => memory.entityId)).size,
|
|
1017
|
-
memoryCount: selected.length,
|
|
1018
|
-
activeMemoryCount,
|
|
1019
|
-
expiredMemoryCount,
|
|
1020
|
-
supersededMemoryCount
|
|
1021
|
-
};
|
|
1548
|
+
async getMemory(entityId, memoryId, options) {
|
|
1549
|
+
throwIfAborted(options?.signal);
|
|
1550
|
+
const memory = memories.get(memoryId);
|
|
1551
|
+
return memory && memory.entityId === entityId ? cloneMemory(memory) : null;
|
|
1552
|
+
},
|
|
1553
|
+
async loadConsolidationTargets(entityId, ids, options) {
|
|
1554
|
+
return ids.flatMap((id) => {
|
|
1555
|
+
throwIfAborted(options?.signal);
|
|
1556
|
+
const memory = memories.get(id);
|
|
1557
|
+
if (!memory || memory.entityId !== entityId) return [];
|
|
1558
|
+
return [{
|
|
1559
|
+
id: memory.id,
|
|
1560
|
+
tags: [...memory.tags],
|
|
1561
|
+
supersededBy: memory.supersededBy
|
|
1562
|
+
}];
|
|
1022
1563
|
});
|
|
1023
1564
|
},
|
|
1024
|
-
|
|
1565
|
+
async consolidate(input, options) {
|
|
1566
|
+
throwIfAborted(options?.signal);
|
|
1567
|
+
const survivor = memories.get(input.survivorId);
|
|
1568
|
+
const newlySuperseded = [];
|
|
1569
|
+
const targets = [];
|
|
1570
|
+
for (const id of input.supersededIds) {
|
|
1571
|
+
throwIfAborted(options?.signal);
|
|
1572
|
+
const memory = memories.get(id);
|
|
1573
|
+
if (!memory || memory.entityId !== input.entityId) continue;
|
|
1574
|
+
targets.push(memory);
|
|
1575
|
+
}
|
|
1576
|
+
if (targets.some((memory) => memory.supersededBy !== null && memory.supersededBy !== input.survivorId)) throw new MnemocyteError("Superseded memory already belongs to a different survivor.", "CONFLICT");
|
|
1577
|
+
for (const memory of targets) if (memory.supersededBy === null) newlySuperseded.push(memory);
|
|
1578
|
+
let mergedSurvivorTags;
|
|
1579
|
+
if (survivor && input.mergeTags && newlySuperseded.length > 0) {
|
|
1580
|
+
const mergedTags = new Set(input.survivorTags);
|
|
1581
|
+
for (const memory of newlySuperseded) {
|
|
1582
|
+
throwIfAborted(options?.signal);
|
|
1583
|
+
for (const tag of memory.tags) mergedTags.add(tag);
|
|
1584
|
+
}
|
|
1585
|
+
if (mergedTags.size !== survivor.tags.length) mergedSurvivorTags = [...mergedTags];
|
|
1586
|
+
}
|
|
1587
|
+
const auditEvents = input.auditEnabled ? newlySuperseded.map((memory) => ({
|
|
1588
|
+
id: createEventId(),
|
|
1589
|
+
entityId: input.entityId,
|
|
1590
|
+
description: "memory.superseded",
|
|
1591
|
+
metadata: {
|
|
1592
|
+
memoryId: memory.id,
|
|
1593
|
+
supersededBy: input.survivorId
|
|
1594
|
+
},
|
|
1595
|
+
timestamp: input.now
|
|
1596
|
+
})) : [];
|
|
1597
|
+
throwIfAborted(options?.signal);
|
|
1598
|
+
for (const memory of newlySuperseded) {
|
|
1599
|
+
memory.supersededBy = input.survivorId;
|
|
1600
|
+
memory.supersededAt = input.now;
|
|
1601
|
+
memory.updatedAt = input.now;
|
|
1602
|
+
}
|
|
1603
|
+
if (survivor && mergedSurvivorTags) {
|
|
1604
|
+
survivor.tags = mergedSurvivorTags;
|
|
1605
|
+
survivor.updatedAt = input.now;
|
|
1606
|
+
}
|
|
1607
|
+
if (input.auditEnabled) auditLog.push(...auditEvents);
|
|
1608
|
+
return { supersededIds: newlySuperseded.map((memory) => memory.id) };
|
|
1609
|
+
},
|
|
1610
|
+
async stats(input, now) {
|
|
1611
|
+
const allMemories = Array.from(memories.values());
|
|
1612
|
+
const selected = input?.entityId ? allMemories.filter((memory) => memory.entityId === input.entityId) : allMemories;
|
|
1613
|
+
const activeMemoryCount = selected.filter((memory) => !isExpired(memory, now) && memory.supersededBy === null).length;
|
|
1614
|
+
const expiredMemoryCount = selected.filter((memory) => isExpired(memory, now)).length;
|
|
1615
|
+
const supersededMemoryCount = selected.filter((memory) => memory.supersededBy !== null).length;
|
|
1616
|
+
if (input?.entityId) return {
|
|
1617
|
+
entityId: input.entityId,
|
|
1618
|
+
memoryCount: selected.length,
|
|
1619
|
+
activeMemoryCount,
|
|
1620
|
+
expiredMemoryCount,
|
|
1621
|
+
supersededMemoryCount
|
|
1622
|
+
};
|
|
1623
|
+
return {
|
|
1624
|
+
entityCount: new Set(allMemories.map((memory) => memory.entityId)).size,
|
|
1625
|
+
memoryCount: selected.length,
|
|
1626
|
+
activeMemoryCount,
|
|
1627
|
+
expiredMemoryCount,
|
|
1628
|
+
supersededMemoryCount
|
|
1629
|
+
};
|
|
1630
|
+
},
|
|
1025
1631
|
async close() {
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
memories.clear();
|
|
1029
|
-
});
|
|
1632
|
+
memories.clear();
|
|
1633
|
+
auditLog.length = 0;
|
|
1030
1634
|
}
|
|
1031
1635
|
};
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
for (const loser of losers) {
|
|
1054
|
-
loser.supersededBy = survivor.id;
|
|
1055
|
-
loser.supersededAt = now;
|
|
1056
|
-
loser.updatedAt = now;
|
|
1057
|
-
newSupersededIds.push(loser.id);
|
|
1058
|
-
recordAudit(input.entityId, "memory.superseded", {
|
|
1059
|
-
memoryId: loser.id,
|
|
1060
|
-
supersededBy: survivor.id
|
|
1061
|
-
});
|
|
1062
|
-
}
|
|
1063
|
-
if (input.mergeTags !== false && losers.length > 0) {
|
|
1064
|
-
const mergedTags = new Set(survivor.tags);
|
|
1065
|
-
for (const loser of losers) for (const tag of loser.tags) mergedTags.add(tag);
|
|
1066
|
-
if (mergedTags.size !== survivor.tags.length) {
|
|
1067
|
-
survivor.tags = [...mergedTags];
|
|
1068
|
-
survivor.updatedAt = now;
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
return {
|
|
1072
|
-
survivorId: survivor.id,
|
|
1073
|
-
supersededCount: newSupersededIds.length,
|
|
1074
|
-
supersededIds: newSupersededIds
|
|
1075
|
-
};
|
|
1076
|
-
}, (result) => ({ count: result.supersededCount }));
|
|
1077
|
-
} };
|
|
1636
|
+
}
|
|
1637
|
+
function createInMemoryClient(config) {
|
|
1638
|
+
return createMemoryClient(config, createInMemoryStore());
|
|
1639
|
+
}
|
|
1640
|
+
//#endregion
|
|
1641
|
+
//#region src/db/cancellation.ts
|
|
1642
|
+
const postgresDialect = new PgDialect();
|
|
1643
|
+
async function awaitCancelableQuery(query, signal) {
|
|
1644
|
+
throwIfAborted(signal);
|
|
1645
|
+
if (!signal) return await query;
|
|
1646
|
+
const onAbort = () => {
|
|
1647
|
+
query.cancel();
|
|
1648
|
+
};
|
|
1649
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1650
|
+
try {
|
|
1651
|
+
return await query;
|
|
1652
|
+
} catch (error) {
|
|
1653
|
+
if (signal.aborted) throw new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason);
|
|
1654
|
+
throw error;
|
|
1655
|
+
} finally {
|
|
1656
|
+
signal.removeEventListener("abort", onAbort);
|
|
1078
1657
|
}
|
|
1079
1658
|
}
|
|
1659
|
+
async function executeCancelableSql(db, statement, signal) {
|
|
1660
|
+
const query = postgresDialect.sqlToQuery(statement);
|
|
1661
|
+
const client = db.$client;
|
|
1662
|
+
if (!client) throw new MnemocyteError("Cancelable Postgres query requires the postgres.js client.", "DB");
|
|
1663
|
+
return await awaitCancelableQuery(client.unsafe(query.sql, query.params), signal);
|
|
1664
|
+
}
|
|
1080
1665
|
//#endregion
|
|
1081
1666
|
//#region src/db/queries/events.ts
|
|
1667
|
+
function timestampParam$1(value) {
|
|
1668
|
+
return sql`${value.toISOString()}::timestamptz`;
|
|
1669
|
+
}
|
|
1082
1670
|
async function insertEvent(db, row) {
|
|
1083
1671
|
const inserted = (await db.insert(eventsTable).values(row).returning())[0];
|
|
1084
1672
|
if (!inserted) throw new MnemocyteError("Event insert returned no rows.", "DB");
|
|
1085
1673
|
return inserted;
|
|
1086
1674
|
}
|
|
1087
|
-
async function listEvents(db, filter) {
|
|
1088
|
-
return db
|
|
1675
|
+
async function listEvents(db, filter, signal) {
|
|
1676
|
+
return executeCancelableSql(db, sql`
|
|
1677
|
+
SELECT
|
|
1678
|
+
id,
|
|
1679
|
+
entity_id AS "entityId",
|
|
1680
|
+
description,
|
|
1681
|
+
metadata,
|
|
1682
|
+
timestamp
|
|
1683
|
+
FROM mnemocyte_events
|
|
1684
|
+
WHERE entity_id = ${filter.entityId}
|
|
1685
|
+
${filter.before ? sql`AND timestamp < ${timestampParam$1(filter.before)}` : sql``}
|
|
1686
|
+
${filter.after ? sql`AND timestamp > ${timestampParam$1(filter.after)}` : sql``}
|
|
1687
|
+
${filter.beforeCursor ? sql`AND (timestamp, id) < (${timestampParam$1(filter.beforeCursor.timestamp)}, ${filter.beforeCursor.id})` : sql``}
|
|
1688
|
+
${filter.afterCursor ? sql`AND (timestamp, id) > (${timestampParam$1(filter.afterCursor.timestamp)}, ${filter.afterCursor.id})` : sql``}
|
|
1689
|
+
ORDER BY timestamp DESC, id DESC
|
|
1690
|
+
LIMIT ${filter.limit ?? 50}
|
|
1691
|
+
`, signal);
|
|
1089
1692
|
}
|
|
1090
1693
|
//#endregion
|
|
1091
1694
|
//#region src/db/queries/memories.ts
|
|
1092
1695
|
function vectorLiteral(embedding) {
|
|
1093
1696
|
return `[${embedding.map(formatVectorComponent).join(",")}]`;
|
|
1094
1697
|
}
|
|
1095
|
-
function
|
|
1096
|
-
|
|
1097
|
-
return
|
|
1698
|
+
function clampedVectorScore(embedding) {
|
|
1699
|
+
const rawScore = sql`1 - (embedding <=> ${embedding}::vector)`;
|
|
1700
|
+
return sql`
|
|
1701
|
+
CASE
|
|
1702
|
+
WHEN ${rawScore} = 'NaN'::double precision
|
|
1703
|
+
OR ${rawScore} = 'Infinity'::double precision
|
|
1704
|
+
OR ${rawScore} = '-Infinity'::double precision
|
|
1705
|
+
THEN 0::double precision
|
|
1706
|
+
ELSE GREATEST(
|
|
1707
|
+
0::double precision,
|
|
1708
|
+
LEAST(1::double precision, ${rawScore})
|
|
1709
|
+
)
|
|
1710
|
+
END
|
|
1711
|
+
`;
|
|
1098
1712
|
}
|
|
1099
1713
|
function timestampParam(value) {
|
|
1100
1714
|
return sql`${value.toISOString()}::timestamptz`;
|
|
@@ -1137,11 +1751,6 @@ function emptyMemoryStatsCounts() {
|
|
|
1137
1751
|
supersededMemoryCount: 0
|
|
1138
1752
|
};
|
|
1139
1753
|
}
|
|
1140
|
-
async function insertMemory(db, row) {
|
|
1141
|
-
const inserted = (await db.insert(memoriesTable).values(row).returning())[0];
|
|
1142
|
-
if (!inserted) throw new MnemocyteError("Memory insert returned no rows.", "DB");
|
|
1143
|
-
return inserted;
|
|
1144
|
-
}
|
|
1145
1754
|
async function insertMemories(db, rows) {
|
|
1146
1755
|
if (rows.length === 0) return [];
|
|
1147
1756
|
return db.insert(memoriesTable).values(rows).returning();
|
|
@@ -1162,78 +1771,136 @@ async function getGlobalMemoryStatsCounts(db, now) {
|
|
|
1162
1771
|
};
|
|
1163
1772
|
}
|
|
1164
1773
|
async function deleteMemory(db, entityId, memoryId) {
|
|
1165
|
-
return (
|
|
1774
|
+
return deleteMemoriesWithDependentGuard(db, and(eq(memoriesTable.entityId, entityId), eq(memoriesTable.id, memoryId)));
|
|
1166
1775
|
}
|
|
1167
1776
|
async function deleteMemoriesForEntity(db, entityId) {
|
|
1168
|
-
return (
|
|
1777
|
+
return deleteMemoriesWithDependentGuard(db, eq(memoriesTable.entityId, entityId));
|
|
1169
1778
|
}
|
|
1170
1779
|
function pruneConditions(filter) {
|
|
1171
1780
|
return and(filter.entityId !== void 0 ? eq(memoriesTable.entityId, filter.entityId) : void 0, filter.expired ? and(isNotNull(memoriesTable.expiresAt), sql`${memoriesTable.expiresAt} <= now()`) : void 0, filter.superseded ? isNotNull(memoriesTable.supersededBy) : void 0, filter.createdBefore ? lt(memoriesTable.createdAt, filter.createdBefore) : void 0, filter.notAccessedSince ? or(isNull(memoriesTable.lastAccessedAt), lt(memoriesTable.lastAccessedAt, filter.notAccessedSince)) : void 0, filter.types && filter.types.length > 0 ? inArray(memoriesTable.type, [...filter.types]) : void 0, tagsContainAll(filter.tags), filter.maxImportanceLevels && filter.maxImportanceLevels.length > 0 ? inArray(memoriesTable.importance, [...filter.maxImportanceLevels]) : void 0);
|
|
1172
1781
|
}
|
|
1173
|
-
async function countPruneMatches(db, filter) {
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1782
|
+
async function countPruneMatches(db, filter, signal) {
|
|
1783
|
+
const conditions = pruneConditions(filter);
|
|
1784
|
+
const rows = await executeCancelableSql(db, sql`
|
|
1785
|
+
SELECT count(*)::int AS count
|
|
1786
|
+
FROM ${memoriesTable}
|
|
1787
|
+
${conditions ? sql`WHERE ${conditions}` : sql``}
|
|
1788
|
+
`, signal);
|
|
1789
|
+
return Number(rows[0]?.count ?? 0);
|
|
1790
|
+
}
|
|
1791
|
+
async function pruneMemories(db, filter, signal) {
|
|
1792
|
+
return deleteMemoriesWithDependentGuard(db, pruneConditions(filter), signal);
|
|
1793
|
+
}
|
|
1794
|
+
async function deleteMemoriesWithDependentGuard(db, conditions, signal) {
|
|
1795
|
+
const rows = await executeCancelableSql(db, sql`
|
|
1796
|
+
WITH candidates AS MATERIALIZED (
|
|
1797
|
+
SELECT id, entity_id
|
|
1798
|
+
FROM ${memoriesTable}
|
|
1799
|
+
${conditions ? sql`WHERE ${conditions}` : sql``}
|
|
1800
|
+
),
|
|
1801
|
+
dependents AS MATERIALIZED (
|
|
1802
|
+
SELECT 1
|
|
1803
|
+
FROM ${memoriesTable} AS dependent
|
|
1804
|
+
INNER JOIN candidates AS target
|
|
1805
|
+
ON dependent.superseded_by = target.id
|
|
1806
|
+
LIMIT 1
|
|
1807
|
+
),
|
|
1808
|
+
deleted AS (
|
|
1809
|
+
DELETE FROM ${memoriesTable}
|
|
1810
|
+
WHERE ${memoriesTable.id} IN (SELECT id FROM candidates)
|
|
1811
|
+
AND NOT EXISTS (SELECT 1 FROM dependents)
|
|
1812
|
+
RETURNING id, entity_id
|
|
1813
|
+
),
|
|
1814
|
+
deleted_counts AS (
|
|
1815
|
+
SELECT entity_id, count(*)::int AS deleted_count
|
|
1816
|
+
FROM deleted
|
|
1817
|
+
GROUP BY entity_id
|
|
1818
|
+
),
|
|
1819
|
+
summary AS (
|
|
1820
|
+
SELECT
|
|
1821
|
+
(SELECT count(*)::int FROM candidates) AS matched_count,
|
|
1822
|
+
(SELECT count(*)::int FROM deleted) AS deleted_count,
|
|
1823
|
+
EXISTS (SELECT 1 FROM dependents) AS has_dependents
|
|
1824
|
+
)
|
|
1825
|
+
SELECT
|
|
1826
|
+
summary.matched_count AS "matchedCount",
|
|
1827
|
+
summary.deleted_count AS "deletedCount",
|
|
1828
|
+
summary.has_dependents AS "hasDependents",
|
|
1829
|
+
deleted_counts.entity_id AS "entityId",
|
|
1830
|
+
deleted_counts.deleted_count AS "entityDeletedCount"
|
|
1831
|
+
FROM summary
|
|
1832
|
+
LEFT JOIN deleted_counts ON true
|
|
1833
|
+
ORDER BY deleted_counts.entity_id
|
|
1834
|
+
`, signal);
|
|
1835
|
+
const row = rows[0];
|
|
1836
|
+
return {
|
|
1837
|
+
matchedCount: Number(row?.matchedCount ?? 0),
|
|
1838
|
+
deletedCount: Number(row?.deletedCount ?? 0),
|
|
1839
|
+
hasDependents: row?.hasDependents === true,
|
|
1840
|
+
deletedByEntity: rows.flatMap((detail) => detail.entityId === null ? [] : [{
|
|
1841
|
+
entityId: detail.entityId,
|
|
1842
|
+
deletedCount: Number(detail.entityDeletedCount ?? 0)
|
|
1843
|
+
}])
|
|
1844
|
+
};
|
|
1178
1845
|
}
|
|
1179
|
-
async function findDuplicatePairs(db, input) {
|
|
1180
|
-
return (await db
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1846
|
+
async function findDuplicatePairs(db, input, signal) {
|
|
1847
|
+
return (await executeCancelableSql(db, sql`
|
|
1848
|
+
SELECT
|
|
1849
|
+
a.id AS "aId",
|
|
1850
|
+
a.entity_id AS "aEntityId",
|
|
1851
|
+
a.content AS "aContent",
|
|
1852
|
+
a.type AS "aType",
|
|
1853
|
+
a.importance AS "aImportance",
|
|
1854
|
+
a.tags AS "aTags",
|
|
1855
|
+
a.source AS "aSource",
|
|
1856
|
+
a.metadata AS "aMetadata",
|
|
1857
|
+
a.confidence AS "aConfidence",
|
|
1858
|
+
a.embedding AS "aEmbedding",
|
|
1859
|
+
a.embedding_model AS "aEmbeddingModel",
|
|
1860
|
+
a.embedding_dimensions AS "aEmbeddingDimensions",
|
|
1861
|
+
a.superseded_by AS "aSupersededBy",
|
|
1862
|
+
a.superseded_at AS "aSupersededAt",
|
|
1863
|
+
a.expires_at AS "aExpiresAt",
|
|
1864
|
+
a.last_accessed_at AS "aLastAccessedAt",
|
|
1865
|
+
a.access_count AS "aAccessCount",
|
|
1866
|
+
a.created_at AS "aCreatedAt",
|
|
1867
|
+
a.updated_at AS "aUpdatedAt",
|
|
1868
|
+
b.id AS "bId",
|
|
1869
|
+
b.entity_id AS "bEntityId",
|
|
1870
|
+
b.content AS "bContent",
|
|
1871
|
+
b.type AS "bType",
|
|
1872
|
+
b.importance AS "bImportance",
|
|
1873
|
+
b.tags AS "bTags",
|
|
1874
|
+
b.source AS "bSource",
|
|
1875
|
+
b.metadata AS "bMetadata",
|
|
1876
|
+
b.confidence AS "bConfidence",
|
|
1877
|
+
b.embedding AS "bEmbedding",
|
|
1878
|
+
b.embedding_model AS "bEmbeddingModel",
|
|
1879
|
+
b.embedding_dimensions AS "bEmbeddingDimensions",
|
|
1880
|
+
b.superseded_by AS "bSupersededBy",
|
|
1881
|
+
b.superseded_at AS "bSupersededAt",
|
|
1882
|
+
b.expires_at AS "bExpiresAt",
|
|
1883
|
+
b.last_accessed_at AS "bLastAccessedAt",
|
|
1884
|
+
b.access_count AS "bAccessCount",
|
|
1885
|
+
b.created_at AS "bCreatedAt",
|
|
1886
|
+
b.updated_at AS "bUpdatedAt",
|
|
1887
|
+
1 - (a.embedding <=> b.embedding) AS "similarity"
|
|
1888
|
+
FROM mnemocyte_memories a
|
|
1889
|
+
JOIN mnemocyte_memories b
|
|
1890
|
+
ON b.entity_id = a.entity_id
|
|
1891
|
+
AND a.id < b.id
|
|
1892
|
+
AND a.embedding IS NOT NULL
|
|
1893
|
+
AND b.embedding IS NOT NULL
|
|
1894
|
+
WHERE
|
|
1895
|
+
a.entity_id = ${input.entityId}
|
|
1896
|
+
${input.includeSuperseded ? sql`` : sql`AND a.superseded_by IS NULL AND b.superseded_by IS NULL`}
|
|
1897
|
+
${input.includeExpired ? sql`` : sql`AND (a.expires_at IS NULL OR a.expires_at > now()) AND (b.expires_at IS NULL OR b.expires_at > now())`}
|
|
1898
|
+
${input.types && input.types.length > 0 ? sql`AND a.type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)}) AND b.type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)})` : sql``}
|
|
1899
|
+
${duplicateTagsFilter(input.tags)}
|
|
1900
|
+
AND 1 - (a.embedding <=> b.embedding) >= ${input.threshold}
|
|
1901
|
+
ORDER BY "similarity" DESC
|
|
1902
|
+
LIMIT ${input.limit}
|
|
1903
|
+
`, signal)).map((row) => {
|
|
1237
1904
|
return {
|
|
1238
1905
|
a: {
|
|
1239
1906
|
id: row.aId,
|
|
@@ -1289,6 +1956,14 @@ async function loadConsolidationTargets(db, entityId, ids) {
|
|
|
1289
1956
|
supersededBy: memoriesTable.supersededBy
|
|
1290
1957
|
}).from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), inArray(memoriesTable.id, [...ids])));
|
|
1291
1958
|
}
|
|
1959
|
+
async function lockConsolidationTargets(db, entityId, ids) {
|
|
1960
|
+
if (ids.length === 0) return [];
|
|
1961
|
+
return db.select({
|
|
1962
|
+
id: memoriesTable.id,
|
|
1963
|
+
tags: memoriesTable.tags,
|
|
1964
|
+
supersededBy: memoriesTable.supersededBy
|
|
1965
|
+
}).from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), inArray(memoriesTable.id, [...ids]))).orderBy(memoriesTable.id).for("update");
|
|
1966
|
+
}
|
|
1292
1967
|
async function markMemoriesSuperseded(db, params) {
|
|
1293
1968
|
if (params.ids.length === 0) return [];
|
|
1294
1969
|
return await db.update(memoriesTable).set({
|
|
@@ -1306,14 +1981,19 @@ async function setMemoryTags(db, params) {
|
|
|
1306
1981
|
updatedAt: params.now
|
|
1307
1982
|
}).where(and(eq(memoriesTable.entityId, params.entityId), eq(memoriesTable.id, params.memoryId)));
|
|
1308
1983
|
}
|
|
1309
|
-
async function
|
|
1984
|
+
async function markMemoriesAccessed(db, memoryIds) {
|
|
1310
1985
|
const now = /* @__PURE__ */ new Date();
|
|
1311
|
-
if (memoryIds.length === 0) return;
|
|
1312
|
-
|
|
1986
|
+
if (memoryIds.length === 0) return [];
|
|
1987
|
+
return db.update(memoriesTable).set({
|
|
1313
1988
|
lastAccessedAt: now,
|
|
1314
1989
|
accessCount: sql`${memoriesTable.accessCount} + 1`,
|
|
1315
1990
|
updatedAt: now
|
|
1316
|
-
}).where(inArray(memoriesTable.id, [...memoryIds]))
|
|
1991
|
+
}).where(inArray(memoriesTable.id, [...memoryIds])).returning({
|
|
1992
|
+
id: memoriesTable.id,
|
|
1993
|
+
lastAccessedAt: memoriesTable.lastAccessedAt,
|
|
1994
|
+
accessCount: memoriesTable.accessCount,
|
|
1995
|
+
updatedAt: memoriesTable.updatedAt
|
|
1996
|
+
});
|
|
1317
1997
|
}
|
|
1318
1998
|
async function getMemoryEmbeddings(db, memoryIds) {
|
|
1319
1999
|
if (memoryIds.length === 0) return /* @__PURE__ */ new Map();
|
|
@@ -1325,7 +2005,8 @@ async function getMemoryEmbeddings(db, memoryIds) {
|
|
|
1325
2005
|
}
|
|
1326
2006
|
async function vectorSearch(db, input) {
|
|
1327
2007
|
const embedding = vectorLiteral(input.embedding);
|
|
1328
|
-
const
|
|
2008
|
+
const vectorScore = clampedVectorScore(embedding);
|
|
2009
|
+
const minVectorScore = input.minVectorScore ?? 0;
|
|
1329
2010
|
return await db.execute(sql`
|
|
1330
2011
|
SELECT
|
|
1331
2012
|
id,
|
|
@@ -1346,7 +2027,7 @@ async function vectorSearch(db, input) {
|
|
|
1346
2027
|
access_count AS "accessCount",
|
|
1347
2028
|
created_at AS "createdAt",
|
|
1348
2029
|
updated_at AS "updatedAt",
|
|
1349
|
-
|
|
2030
|
+
${vectorScore} AS "vectorScore"
|
|
1350
2031
|
FROM mnemocyte_memories
|
|
1351
2032
|
WHERE
|
|
1352
2033
|
entity_id = ${input.entityId}
|
|
@@ -1357,7 +2038,7 @@ async function vectorSearch(db, input) {
|
|
|
1357
2038
|
${rawTagsFilter(input.tags)}
|
|
1358
2039
|
${input.before ? sql`AND created_at < ${timestampParam(input.before)}` : sql``}
|
|
1359
2040
|
${input.after ? sql`AND created_at > ${timestampParam(input.after)}` : sql``}
|
|
1360
|
-
AND
|
|
2041
|
+
AND ${vectorScore} >= ${minVectorScore}
|
|
1361
2042
|
ORDER BY embedding <=> ${embedding}::vector
|
|
1362
2043
|
LIMIT ${input.limit}
|
|
1363
2044
|
`);
|
|
@@ -1401,74 +2082,100 @@ async function lexicalSearch(db, input) {
|
|
|
1401
2082
|
async function getInstallationMeta(db) {
|
|
1402
2083
|
return (await db.select().from(metaTable).where(eq(metaTable.key, "installation")).limit(1))[0] ?? null;
|
|
1403
2084
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
...input.resilience === void 0 ? {} : { resilience: input.resilience }
|
|
1410
|
-
});
|
|
1411
|
-
const candidateMultiplier = input.retrieval?.candidateMultiplier ?? 3;
|
|
1412
|
-
const candidateLimit = Math.max(input.limit, input.limit * candidateMultiplier);
|
|
1413
|
-
const scoreLexical = createLexicalScorer(input.input.query);
|
|
1414
|
-
const scoringConfig = createScoringConfig(input.retrieval);
|
|
1415
|
-
const [vectorRows, lexicalRows] = await Promise.all([vectorSearch(input.db, {
|
|
1416
|
-
...input.input,
|
|
1417
|
-
embedding: queryEmbedding,
|
|
1418
|
-
limit: candidateLimit,
|
|
1419
|
-
minScore: 0
|
|
1420
|
-
}), lexicalSearch(input.db, {
|
|
1421
|
-
...input.input,
|
|
1422
|
-
limit: candidateLimit
|
|
1423
|
-
})]);
|
|
1424
|
-
const merged = /* @__PURE__ */ new Map();
|
|
1425
|
-
for (const row of vectorRows) merged.set(row.id, {
|
|
1426
|
-
row,
|
|
1427
|
-
vectorScore: row.vectorScore,
|
|
1428
|
-
lexicalScore: scoreLexical(row.content)
|
|
1429
|
-
});
|
|
1430
|
-
const lexicalOnlyRows = lexicalRows.filter((row) => !merged.has(row.id));
|
|
1431
|
-
const lexicalOnlyEmbeddings = await getMemoryEmbeddings(input.db, lexicalOnlyRows.map((row) => row.id));
|
|
1432
|
-
for (const row of lexicalRows) {
|
|
1433
|
-
const existing = merged.get(row.id);
|
|
1434
|
-
if (existing) existing.lexicalScore = row.lexicalScore;
|
|
1435
|
-
else {
|
|
1436
|
-
const embedding = lexicalOnlyEmbeddings.get(row.id);
|
|
1437
|
-
const sim = embedding ? cosineSimilarity(embedding, queryEmbedding) : 0;
|
|
1438
|
-
merged.set(row.id, {
|
|
1439
|
-
row,
|
|
1440
|
-
vectorScore: Math.max(0, sim),
|
|
1441
|
-
lexicalScore: row.lexicalScore
|
|
1442
|
-
});
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
const scored = Array.from(merged.values()).map((entry) => toScoredMemoryWithConfig(rowToMemory(entry.row), Math.max(0, entry.vectorScore), Math.max(0, entry.lexicalScore), input.input, scoringConfig)).filter((memory) => memory.score >= input.minScore).sort((a, b) => b.score - a.score).slice(0, input.limit);
|
|
1446
|
-
await markMemoryAccessed(input.db, scored.map((memory) => memory.id));
|
|
1447
|
-
return scored;
|
|
2085
|
+
async function getStoredEmbeddingModels(db) {
|
|
2086
|
+
return (await db.selectDistinct({ embeddingModel: memoriesTable.embeddingModel }).from(memoriesTable).limit(2)).map((row) => row.embeddingModel);
|
|
2087
|
+
}
|
|
2088
|
+
async function recordInstallationEmbeddingModel(db, embeddingModel) {
|
|
2089
|
+
return (await db.update(metaTable).set({ embeddingModel }).where(and(eq(metaTable.key, "installation"), isNull(metaTable.embeddingModel))).returning())[0] ?? null;
|
|
1448
2090
|
}
|
|
1449
2091
|
//#endregion
|
|
1450
|
-
//#region src/memory/postgres.ts
|
|
1451
|
-
function
|
|
2092
|
+
//#region src/memory/postgres-records.ts
|
|
2093
|
+
function toDate(value) {
|
|
2094
|
+
return value instanceof Date ? value : new Date(value);
|
|
2095
|
+
}
|
|
2096
|
+
function toNullableDate(value) {
|
|
2097
|
+
return value === null ? null : toDate(value);
|
|
2098
|
+
}
|
|
2099
|
+
function rowToMemory(row) {
|
|
1452
2100
|
return {
|
|
1453
2101
|
id: row.id,
|
|
1454
2102
|
entityId: row.entityId,
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
2103
|
+
content: row.content,
|
|
2104
|
+
type: row.type,
|
|
2105
|
+
importance: row.importance,
|
|
2106
|
+
tags: row.tags,
|
|
2107
|
+
source: row.source,
|
|
2108
|
+
metadata: cloneJsonObject(row.metadata),
|
|
2109
|
+
confidence: row.confidence,
|
|
2110
|
+
embeddingModel: row.embeddingModel,
|
|
2111
|
+
embeddingDimensions: row.embeddingDimensions,
|
|
2112
|
+
supersededBy: row.supersededBy,
|
|
2113
|
+
supersededAt: toNullableDate(row.supersededAt),
|
|
2114
|
+
expiresAt: toNullableDate(row.expiresAt),
|
|
2115
|
+
lastAccessedAt: toNullableDate(row.lastAccessedAt),
|
|
2116
|
+
accessCount: row.accessCount,
|
|
2117
|
+
createdAt: toDate(row.createdAt),
|
|
2118
|
+
updatedAt: toDate(row.updatedAt)
|
|
1458
2119
|
};
|
|
1459
2120
|
}
|
|
2121
|
+
//#endregion
|
|
2122
|
+
//#region src/memory/postgres.ts
|
|
1460
2123
|
const IMPORTANCE_LEVELS = [
|
|
1461
2124
|
"low",
|
|
1462
2125
|
"normal",
|
|
1463
2126
|
"high",
|
|
1464
2127
|
"critical"
|
|
1465
2128
|
];
|
|
2129
|
+
const MIGRATION_ERROR_CODES = new Set([
|
|
2130
|
+
"42P01",
|
|
2131
|
+
"42703",
|
|
2132
|
+
"42704",
|
|
2133
|
+
"42883"
|
|
2134
|
+
]);
|
|
2135
|
+
const CONSOLIDATION_SURVIVOR_FOREIGN_KEY = "mnemocyte_memories_superseded_by_mnemocyte_memories_id_fk";
|
|
2136
|
+
function rowToAuditEvent(row) {
|
|
2137
|
+
return {
|
|
2138
|
+
id: row.id,
|
|
2139
|
+
entityId: row.entityId,
|
|
2140
|
+
description: row.description,
|
|
2141
|
+
metadata: cloneJsonObject(row.metadata),
|
|
2142
|
+
timestamp: new Date(row.timestamp)
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
1466
2145
|
function importanceCeilingLevels(max) {
|
|
1467
2146
|
return IMPORTANCE_LEVELS.filter((level) => IMPORTANCE_RANK[level] <= IMPORTANCE_RANK[max]);
|
|
1468
2147
|
}
|
|
1469
2148
|
function hasPostgresErrorCode(error, code) {
|
|
1470
2149
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
1471
2150
|
}
|
|
2151
|
+
function hasPostgresConstraintName(error, name) {
|
|
2152
|
+
return typeof error === "object" && error !== null && "constraint_name" in error && error.constraint_name === name;
|
|
2153
|
+
}
|
|
2154
|
+
function getPostgresErrorCode(error) {
|
|
2155
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
2156
|
+
}
|
|
2157
|
+
function normalizePostgresError(error) {
|
|
2158
|
+
if (error instanceof MnemocyteError) return error;
|
|
2159
|
+
const code = getPostgresErrorCode(error);
|
|
2160
|
+
if (code && MIGRATION_ERROR_CODES.has(code)) return new MnemocyteError("Postgres schema is missing or incompatible. Apply the bundled Mnemocyte migrations before using the Postgres backend.", "MIGRATION", error);
|
|
2161
|
+
return new MnemocyteError("Postgres operation failed.", "DB", error);
|
|
2162
|
+
}
|
|
2163
|
+
async function runPostgresOperation(operation) {
|
|
2164
|
+
try {
|
|
2165
|
+
return await operation();
|
|
2166
|
+
} catch (error) {
|
|
2167
|
+
throw normalizePostgresError(error);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
async function runPostgresDeleteOperation(operation) {
|
|
2171
|
+
try {
|
|
2172
|
+
return await operation();
|
|
2173
|
+
} catch (error) {
|
|
2174
|
+
if (error instanceof MnemocyteError) throw error;
|
|
2175
|
+
if (hasPostgresErrorCode(error, "23503") && hasPostgresConstraintName(error, CONSOLIDATION_SURVIVOR_FOREIGN_KEY)) throw memoryHasDependentsError(error);
|
|
2176
|
+
throw normalizePostgresError(error);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
1472
2179
|
function toPruneFilter(input) {
|
|
1473
2180
|
const filter = {};
|
|
1474
2181
|
if (input.entityId !== void 0) filter.entityId = input.entityId;
|
|
@@ -1481,250 +2188,236 @@ function toPruneFilter(input) {
|
|
|
1481
2188
|
if (input.maxImportance !== void 0) filter.maxImportanceLevels = importanceCeilingLevels(input.maxImportance);
|
|
1482
2189
|
return filter;
|
|
1483
2190
|
}
|
|
1484
|
-
function
|
|
1485
|
-
|
|
2191
|
+
function toMemoryRow(memory) {
|
|
2192
|
+
return {
|
|
2193
|
+
...memory,
|
|
2194
|
+
tags: [...memory.tags],
|
|
2195
|
+
embedding: [...memory.embedding]
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
function createPostgresStore(handle) {
|
|
1486
2199
|
let schemaValidated = false;
|
|
1487
2200
|
let schemaValidationPromise;
|
|
1488
|
-
function
|
|
1489
|
-
if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
|
|
1490
|
-
}
|
|
1491
|
-
async function ensureSchemaMatchesEmbedder() {
|
|
2201
|
+
async function ensureEmbeddingCompatibility(embedder) {
|
|
1492
2202
|
if (schemaValidated) return;
|
|
1493
|
-
if (!schemaValidationPromise) schemaValidationPromise =
|
|
2203
|
+
if (!schemaValidationPromise) schemaValidationPromise = validateEmbeddingCompatibility(embedder).catch((error) => {
|
|
1494
2204
|
schemaValidationPromise = void 0;
|
|
1495
2205
|
throw error;
|
|
1496
2206
|
});
|
|
1497
2207
|
await schemaValidationPromise;
|
|
1498
2208
|
}
|
|
1499
|
-
async function
|
|
2209
|
+
async function validateEmbeddingCompatibility(embedder) {
|
|
1500
2210
|
let meta;
|
|
1501
2211
|
try {
|
|
1502
2212
|
meta = await getInstallationMeta(handle.db);
|
|
1503
2213
|
} catch (error) {
|
|
1504
|
-
if (hasPostgresErrorCode(error, "42P01")) throw new MnemocyteError("
|
|
1505
|
-
throw error;
|
|
2214
|
+
if (hasPostgresErrorCode(error, "42P01") || hasPostgresErrorCode(error, "42703")) throw new MnemocyteError("Postgres embedding metadata is missing or outdated. Apply the bundled migrations through 0002_add_embedding_model.sql, or render a fresh dimension-specific initial migration, before using embedding-dependent operations.", "MIGRATION", error);
|
|
2215
|
+
throw normalizePostgresError(error);
|
|
1506
2216
|
}
|
|
1507
|
-
if (!meta) throw new MnemocyteError("mnemocyte_meta is missing the \"installation\" row. Apply the
|
|
1508
|
-
if (meta.embeddingDimensions !==
|
|
2217
|
+
if (!meta) throw new MnemocyteError("mnemocyte_meta is missing the \"installation\" row. Apply the bundled migrations, or render a fresh dimension-specific initial migration, before using embedding-dependent operations.", "MIGRATION");
|
|
2218
|
+
if (meta.embeddingDimensions !== embedder.dimensions) throw new MnemocyteError(`embedder.dimensions (${embedder.dimensions}) must match mnemocyte_meta.embedding_dimensions (${meta.embeddingDimensions}). Render and apply a migration for the selected embedding dimension, or configure an embedder that matches this installation.`, "CONFIG");
|
|
2219
|
+
let installationModel = meta.embeddingModel;
|
|
2220
|
+
if (installationModel === null) {
|
|
2221
|
+
try {
|
|
2222
|
+
const storedModels = await getStoredEmbeddingModels(handle.db);
|
|
2223
|
+
if (storedModels.length > 1) throw new MnemocyteError("mnemocyte_meta.embedding_model is unset, but stored memories contain multiple embedding_model values. Re-embed or remove the mixed historical rows, then explicitly record the installation model before using embedding-dependent operations.", "MIGRATION");
|
|
2224
|
+
const inferredModel = storedModels[0] ?? embedder.model;
|
|
2225
|
+
const recorded = await recordInstallationEmbeddingModel(handle.db, inferredModel);
|
|
2226
|
+
if (recorded) installationModel = recorded.embeddingModel;
|
|
2227
|
+
else installationModel = (await getInstallationMeta(handle.db))?.embeddingModel ?? null;
|
|
2228
|
+
} catch (error) {
|
|
2229
|
+
throw normalizePostgresError(error);
|
|
2230
|
+
}
|
|
2231
|
+
if (installationModel === null) throw new MnemocyteError("mnemocyte_meta.embedding_model could not be recorded. Verify the installation metadata row and retry.", "MIGRATION");
|
|
2232
|
+
}
|
|
2233
|
+
if (installationModel !== embedder.model) throw new MnemocyteError(`embedder.model ("${embedder.model}") must match mnemocyte_meta.embedding_model ("${installationModel}"). Configure the recorded model, or explicitly re-embed existing memories and update the installation metadata.`, "CONFIG");
|
|
1509
2234
|
schemaValidated = true;
|
|
1510
2235
|
}
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
metadata,
|
|
1519
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
1520
|
-
});
|
|
1521
|
-
} catch {}
|
|
1522
|
-
}
|
|
1523
|
-
async function remember(input) {
|
|
1524
|
-
return observe(config, "postgres", "remember", { entityId: input.entityId }, async () => {
|
|
1525
|
-
assertOpen();
|
|
1526
|
-
validateRememberInput(input);
|
|
1527
|
-
await ensureSchemaMatchesEmbedder();
|
|
1528
|
-
const embedding = await embedOne(config.embedder, input.content, {
|
|
1529
|
-
...input.signal === void 0 ? {} : { signal: input.signal },
|
|
1530
|
-
...config.provider === void 0 ? {} : { resilience: config.provider }
|
|
2236
|
+
return {
|
|
2237
|
+
backend: "postgres",
|
|
2238
|
+
async ensureSchema() {},
|
|
2239
|
+
ensureEmbeddingCompatibility,
|
|
2240
|
+
async insertMemories(memories) {
|
|
2241
|
+
return runPostgresOperation(async () => {
|
|
2242
|
+
return (await insertMemories(handle.db, memories.map(toMemoryRow))).map(rowToMemory);
|
|
1531
2243
|
});
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
tags: normalizeTags(input.tags),
|
|
1540
|
-
source: input.source ?? null,
|
|
1541
|
-
metadata: input.metadata ?? {},
|
|
1542
|
-
confidence: input.confidence ?? 1,
|
|
1543
|
-
embedding,
|
|
1544
|
-
embeddingModel: config.embedder.model,
|
|
1545
|
-
embeddingDimensions: config.embedder.dimensions,
|
|
1546
|
-
supersededBy: null,
|
|
1547
|
-
supersededAt: null,
|
|
1548
|
-
expiresAt: input.expiresAt ?? null,
|
|
1549
|
-
lastAccessedAt: null,
|
|
1550
|
-
accessCount: 0,
|
|
1551
|
-
createdAt: now,
|
|
1552
|
-
updatedAt: now
|
|
1553
|
-
}));
|
|
1554
|
-
await recordAudit(memory.entityId, "memory.created", {
|
|
1555
|
-
memoryId: memory.id,
|
|
1556
|
-
type: memory.type,
|
|
1557
|
-
importance: memory.importance
|
|
2244
|
+
},
|
|
2245
|
+
async vectorSearch(input) {
|
|
2246
|
+
return runPostgresOperation(async () => {
|
|
2247
|
+
return (await vectorSearch(handle.db, input)).map((row) => ({
|
|
2248
|
+
memory: rowToMemory(row),
|
|
2249
|
+
vectorScore: row.vectorScore
|
|
2250
|
+
}));
|
|
1558
2251
|
});
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
const client = {
|
|
1566
|
-
remember,
|
|
1567
|
-
async rememberMany(inputs) {
|
|
1568
|
-
return observe(config, "postgres", "rememberMany", { count: inputs.length }, async () => {
|
|
1569
|
-
assertOpen();
|
|
1570
|
-
if (inputs.length === 0) return [];
|
|
1571
|
-
for (const input of inputs) validateRememberInput(input);
|
|
1572
|
-
await ensureSchemaMatchesEmbedder();
|
|
1573
|
-
const embeddings = await embedMany(config.embedder, inputs.map((i) => i.content), {
|
|
1574
|
-
...inputs[0]?.signal === void 0 ? {} : { signal: inputs[0].signal },
|
|
1575
|
-
...config.provider === void 0 ? {} : { resilience: config.provider }
|
|
1576
|
-
});
|
|
1577
|
-
const now = /* @__PURE__ */ new Date();
|
|
1578
|
-
const rows = inputs.map((input, idx) => ({
|
|
1579
|
-
id: createId(),
|
|
1580
|
-
entityId: input.entityId,
|
|
1581
|
-
content: input.content,
|
|
1582
|
-
type: input.type ?? "fact",
|
|
1583
|
-
importance: input.importance ?? "normal",
|
|
1584
|
-
tags: normalizeTags(input.tags),
|
|
1585
|
-
source: input.source ?? null,
|
|
1586
|
-
metadata: input.metadata ?? {},
|
|
1587
|
-
confidence: input.confidence ?? 1,
|
|
1588
|
-
embedding: embeddings[idx],
|
|
1589
|
-
embeddingModel: config.embedder.model,
|
|
1590
|
-
embeddingDimensions: config.embedder.dimensions,
|
|
1591
|
-
supersededBy: null,
|
|
1592
|
-
supersededAt: null,
|
|
1593
|
-
expiresAt: input.expiresAt ?? null,
|
|
1594
|
-
lastAccessedAt: null,
|
|
1595
|
-
accessCount: 0,
|
|
1596
|
-
createdAt: now,
|
|
1597
|
-
updatedAt: now
|
|
2252
|
+
},
|
|
2253
|
+
async lexicalSearch(input) {
|
|
2254
|
+
return runPostgresOperation(async () => {
|
|
2255
|
+
return (await lexicalSearch(handle.db, input)).map((row) => ({
|
|
2256
|
+
memory: rowToMemory(row),
|
|
2257
|
+
lexicalScore: row.lexicalScore
|
|
1598
2258
|
}));
|
|
1599
|
-
|
|
1600
|
-
for (const memory of memories) recordAudit(memory.entityId, "memory.created", {
|
|
1601
|
-
memoryId: memory.id,
|
|
1602
|
-
type: memory.type,
|
|
1603
|
-
importance: memory.importance
|
|
1604
|
-
});
|
|
1605
|
-
return memories;
|
|
1606
|
-
}, (result) => ({ count: result.length }));
|
|
2259
|
+
});
|
|
1607
2260
|
},
|
|
1608
|
-
async
|
|
1609
|
-
return
|
|
1610
|
-
assertOpen();
|
|
1611
|
-
validateRecallInput(input);
|
|
1612
|
-
const limit = input.limit ?? config.defaults?.limit ?? 10;
|
|
1613
|
-
const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
|
|
1614
|
-
assertLimit(limit);
|
|
1615
|
-
assertMinScore(minScore);
|
|
1616
|
-
await ensureSchemaMatchesEmbedder();
|
|
1617
|
-
return hybridRecall({
|
|
1618
|
-
db: handle.db,
|
|
1619
|
-
embedder: config.embedder,
|
|
1620
|
-
input,
|
|
1621
|
-
limit,
|
|
1622
|
-
minScore,
|
|
1623
|
-
retrieval: config.retrieval,
|
|
1624
|
-
...input.signal === void 0 ? {} : { signal: input.signal },
|
|
1625
|
-
...config.provider === void 0 ? {} : { resilience: config.provider }
|
|
1626
|
-
});
|
|
1627
|
-
}, (result) => ({ count: result.length }));
|
|
2261
|
+
async getMemoryEmbeddings(memoryIds) {
|
|
2262
|
+
return runPostgresOperation(() => getMemoryEmbeddings(handle.db, memoryIds));
|
|
1628
2263
|
},
|
|
1629
|
-
async
|
|
1630
|
-
return
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
return client.recall(input.signal === void 0 ? recallInput : {
|
|
1638
|
-
...recallInput,
|
|
1639
|
-
signal: input.signal
|
|
1640
|
-
});
|
|
1641
|
-
}
|
|
2264
|
+
async markMemoriesAccessed(memoryIds) {
|
|
2265
|
+
return runPostgresOperation(async () => {
|
|
2266
|
+
return (await markMemoriesAccessed(handle.db, memoryIds)).map((row) => {
|
|
2267
|
+
if (row.lastAccessedAt === null) throw new MnemocyteError("Postgres access update returned no timestamp.", "DB");
|
|
2268
|
+
return {
|
|
2269
|
+
...row,
|
|
2270
|
+
lastAccessedAt: row.lastAccessedAt
|
|
2271
|
+
};
|
|
1642
2272
|
});
|
|
1643
2273
|
});
|
|
1644
2274
|
},
|
|
1645
|
-
async
|
|
1646
|
-
return
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
assertOpen();
|
|
1651
|
-
assertNonEmptyString(input.entityId, "entityId");
|
|
1652
|
-
assertNonEmptyString(input.memoryId, "memoryId");
|
|
1653
|
-
await ensureSchemaMatchesEmbedder();
|
|
1654
|
-
if (!await deleteMemory(handle.db, input.entityId, input.memoryId)) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
|
|
1655
|
-
await recordAudit(input.entityId, "memory.deleted", { memoryId: input.memoryId });
|
|
2275
|
+
async deleteMemory(entityId, memoryId) {
|
|
2276
|
+
return runPostgresDeleteOperation(async () => {
|
|
2277
|
+
const result = await deleteMemory(handle.db, entityId, memoryId);
|
|
2278
|
+
if (result.hasDependents) throw memoryHasDependentsError();
|
|
2279
|
+
return result.deletedCount > 0;
|
|
1656
2280
|
});
|
|
1657
2281
|
},
|
|
1658
|
-
async
|
|
1659
|
-
return
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
await recordAudit(input.entityId, "entity.cleared", { count: deletedCount });
|
|
1665
|
-
return deletedCount;
|
|
1666
|
-
}, (count) => ({ count })).then(() => void 0);
|
|
2282
|
+
async deleteMemoriesForEntity(entityId) {
|
|
2283
|
+
return runPostgresDeleteOperation(async () => {
|
|
2284
|
+
const result = await deleteMemoriesForEntity(handle.db, entityId);
|
|
2285
|
+
if (result.hasDependents) throw memoryHasDependentsError();
|
|
2286
|
+
return result.deletedCount;
|
|
2287
|
+
});
|
|
1667
2288
|
},
|
|
1668
|
-
async prune(input) {
|
|
1669
|
-
return
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
await ensureSchemaMatchesEmbedder();
|
|
2289
|
+
async prune(input, options) {
|
|
2290
|
+
return runPostgresDeleteOperation(async () => {
|
|
2291
|
+
throwIfAborted(options?.signal);
|
|
2292
|
+
assertPruneFilterHasSelector(input);
|
|
1673
2293
|
const filter = toPruneFilter(input);
|
|
1674
2294
|
if (input.dryRun === true) return {
|
|
1675
|
-
matchedCount: await countPruneMatches(handle.db, filter),
|
|
2295
|
+
matchedCount: await countPruneMatches(handle.db, filter, options?.signal),
|
|
1676
2296
|
deletedCount: 0,
|
|
1677
|
-
dryRun: true
|
|
2297
|
+
dryRun: true,
|
|
2298
|
+
deletedByEntity: []
|
|
1678
2299
|
};
|
|
1679
|
-
const
|
|
1680
|
-
if (
|
|
2300
|
+
const result = await pruneMemories(handle.db, filter, options?.signal);
|
|
2301
|
+
if (result.hasDependents) throw memoryHasDependentsError();
|
|
1681
2302
|
return {
|
|
1682
|
-
matchedCount:
|
|
1683
|
-
deletedCount,
|
|
1684
|
-
dryRun: false
|
|
2303
|
+
matchedCount: result.matchedCount,
|
|
2304
|
+
deletedCount: result.deletedCount,
|
|
2305
|
+
dryRun: false,
|
|
2306
|
+
deletedByEntity: result.deletedByEntity
|
|
1685
2307
|
};
|
|
1686
|
-
}
|
|
2308
|
+
});
|
|
1687
2309
|
},
|
|
1688
|
-
async
|
|
1689
|
-
return
|
|
1690
|
-
assertOpen();
|
|
1691
|
-
validateFindDuplicatesInput(input);
|
|
1692
|
-
await ensureSchemaMatchesEmbedder();
|
|
1693
|
-
const threshold = input.threshold ?? .95;
|
|
1694
|
-
const limit = input.limit ?? 100;
|
|
2310
|
+
async findDuplicatePairs(input, options) {
|
|
2311
|
+
return runPostgresOperation(async () => {
|
|
1695
2312
|
return (await findDuplicatePairs(handle.db, {
|
|
1696
2313
|
entityId: input.entityId,
|
|
1697
|
-
threshold,
|
|
1698
|
-
limit,
|
|
2314
|
+
threshold: input.threshold ?? .95,
|
|
2315
|
+
limit: input.limit ?? 100,
|
|
1699
2316
|
...input.types === void 0 ? {} : { types: input.types },
|
|
1700
2317
|
...input.tags === void 0 ? {} : { tags: input.tags },
|
|
1701
2318
|
...input.includeSuperseded === void 0 ? {} : { includeSuperseded: input.includeSuperseded },
|
|
1702
2319
|
...input.includeExpired === void 0 ? {} : { includeExpired: input.includeExpired }
|
|
1703
|
-
})).map((row) => ({
|
|
2320
|
+
}, options?.signal)).map((row) => ({
|
|
1704
2321
|
a: rowToMemory(row.a),
|
|
1705
2322
|
b: rowToMemory(row.b),
|
|
1706
2323
|
similarity: Math.max(0, Math.min(1, row.similarity))
|
|
1707
2324
|
}));
|
|
1708
|
-
}
|
|
2325
|
+
});
|
|
1709
2326
|
},
|
|
1710
|
-
async
|
|
1711
|
-
return
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
2327
|
+
async addAuditEvents(events) {
|
|
2328
|
+
return runPostgresOperation(async () => {
|
|
2329
|
+
for (const event of events) await insertEvent(handle.db, {
|
|
2330
|
+
id: event.id,
|
|
2331
|
+
entityId: event.entityId,
|
|
2332
|
+
description: event.description,
|
|
2333
|
+
metadata: cloneJsonObject(event.metadata),
|
|
2334
|
+
timestamp: event.timestamp
|
|
2335
|
+
});
|
|
2336
|
+
});
|
|
2337
|
+
},
|
|
2338
|
+
async listAuditLog(input, options) {
|
|
2339
|
+
return runPostgresOperation(async () => {
|
|
1715
2340
|
return (await listEvents(handle.db, {
|
|
1716
2341
|
entityId: input.entityId,
|
|
1717
2342
|
limit: input.limit ?? 50,
|
|
1718
2343
|
...input.before === void 0 ? {} : { before: input.before },
|
|
1719
|
-
...input.after === void 0 ? {} : { after: input.after }
|
|
1720
|
-
|
|
1721
|
-
|
|
2344
|
+
...input.after === void 0 ? {} : { after: input.after },
|
|
2345
|
+
...input.beforeCursor === void 0 ? {} : { beforeCursor: input.beforeCursor },
|
|
2346
|
+
...input.afterCursor === void 0 ? {} : { afterCursor: input.afterCursor }
|
|
2347
|
+
}, options?.signal)).map(rowToAuditEvent);
|
|
2348
|
+
});
|
|
1722
2349
|
},
|
|
1723
|
-
async
|
|
1724
|
-
return
|
|
1725
|
-
|
|
1726
|
-
await
|
|
1727
|
-
|
|
2350
|
+
async getMemory(entityId, memoryId, options) {
|
|
2351
|
+
return runPostgresOperation(async () => {
|
|
2352
|
+
throwIfAborted(options?.signal);
|
|
2353
|
+
const row = await getMemoryById(handle.db, entityId, memoryId);
|
|
2354
|
+
throwIfAborted(options?.signal);
|
|
2355
|
+
return row ? rowToMemory(row) : null;
|
|
2356
|
+
});
|
|
2357
|
+
},
|
|
2358
|
+
async loadConsolidationTargets(entityId, ids, options) {
|
|
2359
|
+
return runPostgresOperation(async () => {
|
|
2360
|
+
throwIfAborted(options?.signal);
|
|
2361
|
+
const targets = await loadConsolidationTargets(handle.db, entityId, ids);
|
|
2362
|
+
throwIfAborted(options?.signal);
|
|
2363
|
+
return targets;
|
|
2364
|
+
});
|
|
2365
|
+
},
|
|
2366
|
+
async consolidate(input, options) {
|
|
2367
|
+
return runPostgresOperation(async () => {
|
|
2368
|
+
throwIfAborted(options?.signal);
|
|
2369
|
+
return { supersededIds: await handle.db.transaction(async (tx) => {
|
|
2370
|
+
throwIfAborted(options?.signal);
|
|
2371
|
+
const targets = await lockConsolidationTargets(tx, input.entityId, input.supersededIds);
|
|
2372
|
+
throwIfAborted(options?.signal);
|
|
2373
|
+
if (targets.some((target) => target.supersededBy !== null && target.supersededBy !== input.survivorId)) throw new MnemocyteError("Superseded memory already belongs to a different survivor.", "CONFLICT");
|
|
2374
|
+
const activeIds = targets.filter((target) => target.supersededBy === null).map((target) => target.id);
|
|
2375
|
+
const updated = await markMemoriesSuperseded(tx, {
|
|
2376
|
+
survivorId: input.survivorId,
|
|
2377
|
+
entityId: input.entityId,
|
|
2378
|
+
ids: activeIds,
|
|
2379
|
+
now: input.now
|
|
2380
|
+
});
|
|
2381
|
+
throwIfAborted(options?.signal);
|
|
2382
|
+
const ids = updated.map((row) => row.id);
|
|
2383
|
+
if (input.auditEnabled) for (const id of ids) {
|
|
2384
|
+
throwIfAborted(options?.signal);
|
|
2385
|
+
await insertEvent(tx, {
|
|
2386
|
+
id: createEventId(),
|
|
2387
|
+
entityId: input.entityId,
|
|
2388
|
+
description: "memory.superseded",
|
|
2389
|
+
metadata: {
|
|
2390
|
+
memoryId: id,
|
|
2391
|
+
supersededBy: input.survivorId
|
|
2392
|
+
},
|
|
2393
|
+
timestamp: input.now
|
|
2394
|
+
});
|
|
2395
|
+
throwIfAborted(options?.signal);
|
|
2396
|
+
}
|
|
2397
|
+
if (input.mergeTags && updated.length > 0) {
|
|
2398
|
+
const mergedTags = new Set(input.survivorTags);
|
|
2399
|
+
for (const row of updated) {
|
|
2400
|
+
throwIfAborted(options?.signal);
|
|
2401
|
+
for (const tag of row.tags) mergedTags.add(tag);
|
|
2402
|
+
}
|
|
2403
|
+
if (mergedTags.size !== input.survivorTags.length) {
|
|
2404
|
+
throwIfAborted(options?.signal);
|
|
2405
|
+
await setMemoryTags(tx, {
|
|
2406
|
+
entityId: input.entityId,
|
|
2407
|
+
memoryId: input.survivorId,
|
|
2408
|
+
tags: [...mergedTags],
|
|
2409
|
+
now: input.now
|
|
2410
|
+
});
|
|
2411
|
+
throwIfAborted(options?.signal);
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
throwIfAborted(options?.signal);
|
|
2415
|
+
return ids;
|
|
2416
|
+
}) };
|
|
2417
|
+
});
|
|
2418
|
+
},
|
|
2419
|
+
async stats(input, now) {
|
|
2420
|
+
return runPostgresOperation(async () => {
|
|
1728
2421
|
if (input?.entityId) {
|
|
1729
2422
|
const counts = await getEntityMemoryStatsCounts(handle.db, input.entityId, now);
|
|
1730
2423
|
return {
|
|
@@ -1735,78 +2428,13 @@ function createPostgresClient(config, handle) {
|
|
|
1735
2428
|
return await getGlobalMemoryStatsCounts(handle.db, now);
|
|
1736
2429
|
});
|
|
1737
2430
|
},
|
|
1738
|
-
experimental: createExperimental(),
|
|
1739
2431
|
async close() {
|
|
1740
|
-
return
|
|
1741
|
-
closed = true;
|
|
1742
|
-
await handle.close();
|
|
1743
|
-
});
|
|
2432
|
+
return runPostgresOperation(() => handle.close());
|
|
1744
2433
|
}
|
|
1745
2434
|
};
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
return observe(config, "postgres", "consolidate", {
|
|
1750
|
-
entityId: input.entityId,
|
|
1751
|
-
memoryId: input.survivorId
|
|
1752
|
-
}, async () => {
|
|
1753
|
-
assertOpen();
|
|
1754
|
-
validateConsolidateInput(input);
|
|
1755
|
-
await ensureSchemaMatchesEmbedder();
|
|
1756
|
-
const survivor = await getMemoryById(handle.db, input.entityId, input.survivorId);
|
|
1757
|
-
if (!survivor) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
|
|
1758
|
-
if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
|
|
1759
|
-
const targets = await loadConsolidationTargets(handle.db, input.entityId, input.supersededIds);
|
|
1760
|
-
const foundIds = new Set(targets.map((t) => t.id));
|
|
1761
|
-
for (const id of input.supersededIds) if (!foundIds.has(id)) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
|
|
1762
|
-
const losers = targets.filter((target) => target.supersededBy === null);
|
|
1763
|
-
if (losers.length === 0) return {
|
|
1764
|
-
survivorId: survivor.id,
|
|
1765
|
-
supersededCount: 0,
|
|
1766
|
-
supersededIds: []
|
|
1767
|
-
};
|
|
1768
|
-
const now = /* @__PURE__ */ new Date();
|
|
1769
|
-
const { newSupersededIds } = await handle.db.transaction(async (tx) => {
|
|
1770
|
-
const updated = await markMemoriesSuperseded(tx, {
|
|
1771
|
-
survivorId: survivor.id,
|
|
1772
|
-
entityId: input.entityId,
|
|
1773
|
-
ids: losers.map((loser) => loser.id),
|
|
1774
|
-
now
|
|
1775
|
-
});
|
|
1776
|
-
const newSupersededIds = updated.map((row) => row.id);
|
|
1777
|
-
if (config.audit?.enabled === true) for (const id of newSupersededIds) await insertEvent(tx, {
|
|
1778
|
-
id: createEventId(),
|
|
1779
|
-
entityId: input.entityId,
|
|
1780
|
-
description: "memory.superseded",
|
|
1781
|
-
metadata: {
|
|
1782
|
-
memoryId: id,
|
|
1783
|
-
supersededBy: survivor.id
|
|
1784
|
-
},
|
|
1785
|
-
timestamp: now
|
|
1786
|
-
});
|
|
1787
|
-
if (input.mergeTags !== false && updated.length > 0) {
|
|
1788
|
-
const mergedTags = new Set(survivor.tags);
|
|
1789
|
-
for (const row of updated) for (const tag of row.tags) mergedTags.add(tag);
|
|
1790
|
-
if (mergedTags.size !== survivor.tags.length) await setMemoryTags(tx, {
|
|
1791
|
-
entityId: input.entityId,
|
|
1792
|
-
memoryId: survivor.id,
|
|
1793
|
-
tags: [...mergedTags],
|
|
1794
|
-
now
|
|
1795
|
-
});
|
|
1796
|
-
}
|
|
1797
|
-
return {
|
|
1798
|
-
updated,
|
|
1799
|
-
newSupersededIds
|
|
1800
|
-
};
|
|
1801
|
-
});
|
|
1802
|
-
return {
|
|
1803
|
-
survivorId: survivor.id,
|
|
1804
|
-
supersededCount: newSupersededIds.length,
|
|
1805
|
-
supersededIds: newSupersededIds
|
|
1806
|
-
};
|
|
1807
|
-
}, (result) => ({ count: result.supersededCount }));
|
|
1808
|
-
} };
|
|
1809
|
-
}
|
|
2435
|
+
}
|
|
2436
|
+
function createPostgresClient(config, handle) {
|
|
2437
|
+
return createMemoryClient(config, createPostgresStore(handle));
|
|
1810
2438
|
}
|
|
1811
2439
|
//#endregion
|
|
1812
2440
|
//#region src/client.ts
|
|
@@ -1824,8 +2452,10 @@ function createPostgresClient(config, handle) {
|
|
|
1824
2452
|
*
|
|
1825
2453
|
* @param config - Client configuration. {@link MnemocyteConfig.embedder} is required.
|
|
1826
2454
|
* @returns A {@link MnemocyteClient} instance.
|
|
1827
|
-
* @throws {MnemocyteError} With code `"CONFIG"` if `embedder
|
|
1828
|
-
*
|
|
2455
|
+
* @throws {MnemocyteError} With code `"CONFIG"` if `embedder`, retrieval
|
|
2456
|
+
* tuning, provider resilience, or the database URL is malformed or does not
|
|
2457
|
+
* use the `postgres:` / `postgresql:` protocol, or `"VALIDATION"` if
|
|
2458
|
+
* `databaseUrl` is explicitly empty.
|
|
1829
2459
|
*
|
|
1830
2460
|
* @example Postgres-backed client
|
|
1831
2461
|
* ```ts
|
|
@@ -1837,7 +2467,9 @@ function createPostgresClient(config, handle) {
|
|
|
1837
2467
|
*/
|
|
1838
2468
|
function createMnemocyte(config) {
|
|
1839
2469
|
assertEmbedder(config.embedder);
|
|
1840
|
-
|
|
2470
|
+
validateRetrievalConfig(config.retrieval);
|
|
2471
|
+
validateProviderResilienceConfig(config.provider);
|
|
2472
|
+
if (config.databaseUrl !== void 0) {
|
|
1841
2473
|
assertNonEmptyString(config.databaseUrl, "databaseUrl");
|
|
1842
2474
|
return createPostgresClient(config, createDatabase(config.databaseUrl));
|
|
1843
2475
|
}
|