mnemocyte 0.2.0 → 0.3.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 +130 -19
- package/dist/embedders/openai.d.mts +1 -1
- package/dist/embedders/openai.mjs +1 -1
- package/dist/{errors-BG3-mNur.mjs → errors-B_7ZdjEO.mjs} +4 -3
- package/dist/{errors-BG3-mNur.mjs.map → errors-B_7ZdjEO.mjs.map} +1 -1
- package/dist/index.d.mts +8 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +924 -805
- package/dist/index.mjs.map +1 -1
- package/dist/{types-DXEGcc47.d.mts → types-gOPLre4V.d.mts} +61 -27
- package/dist/types-gOPLre4V.d.mts.map +1 -0
- package/package.json +4 -3
- package/dist/types-DXEGcc47.d.mts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
4
|
import { and, count, countDistinct, desc, eq, gt, inArray, isNotNull, isNull, lt, or, sql } from "drizzle-orm";
|
|
@@ -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.toFixed(17);
|
|
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,7 +67,7 @@ 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", {
|
|
@@ -81,7 +83,12 @@ function parseSslFromUrl(url) {
|
|
|
81
83
|
if (mode === "verify-full") return "verify-full";
|
|
82
84
|
}
|
|
83
85
|
function createDatabase(databaseUrl) {
|
|
84
|
-
|
|
86
|
+
let url;
|
|
87
|
+
try {
|
|
88
|
+
url = new URL(databaseUrl);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
throw new MnemocyteError("databaseUrl must be a valid Postgres connection URL.", "CONFIG", error);
|
|
91
|
+
}
|
|
85
92
|
const ssl = parseSslFromUrl(url);
|
|
86
93
|
const isPooler = url.port === "6543" || url.searchParams.get("pgbouncer") === "true";
|
|
87
94
|
const client = postgres(databaseUrl, {
|
|
@@ -99,6 +106,203 @@ function createDatabase(databaseUrl) {
|
|
|
99
106
|
};
|
|
100
107
|
}
|
|
101
108
|
//#endregion
|
|
109
|
+
//#region src/memory/json.ts
|
|
110
|
+
function invalidJsonValue(path, cause) {
|
|
111
|
+
return new MnemocyteError(`${path} must contain only JSON-compatible value data.`, "VALIDATION", cause);
|
|
112
|
+
}
|
|
113
|
+
function cloneJsonValue(value, path, ancestors) {
|
|
114
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
115
|
+
if (typeof value === "number") {
|
|
116
|
+
if (!Number.isFinite(value)) throw invalidJsonValue(path);
|
|
117
|
+
return Object.is(value, -0) ? 0 : value;
|
|
118
|
+
}
|
|
119
|
+
if (typeof value !== "object") throw invalidJsonValue(path);
|
|
120
|
+
if (ancestors.has(value)) throw invalidJsonValue(path);
|
|
121
|
+
ancestors.add(value);
|
|
122
|
+
try {
|
|
123
|
+
if (Array.isArray(value)) {
|
|
124
|
+
if (Object.getOwnPropertySymbols(value).length > 0 || Object.keys(value).some((key) => !/^(0|[1-9]\d*)$/.test(key))) throw invalidJsonValue(path);
|
|
125
|
+
return Array.from({ length: value.length }, (_, index) => {
|
|
126
|
+
if (!Object.hasOwn(value, index)) throw invalidJsonValue(`${path}[${index}]`);
|
|
127
|
+
return cloneJsonValue(value[index], `${path}[${index}]`, ancestors);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const prototype = Object.getPrototypeOf(value);
|
|
131
|
+
if (prototype !== Object.prototype && prototype !== null) throw invalidJsonValue(path);
|
|
132
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
133
|
+
const entries = [];
|
|
134
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
135
|
+
if (typeof key !== "string") throw invalidJsonValue(path);
|
|
136
|
+
const descriptor = descriptors[key];
|
|
137
|
+
if (descriptor === void 0 || descriptor.enumerable !== true || !("value" in descriptor)) throw invalidJsonValue(`${path}.${key}`);
|
|
138
|
+
entries.push([key, cloneJsonValue(descriptor.value, `${path}.${key}`, ancestors)]);
|
|
139
|
+
}
|
|
140
|
+
return Object.fromEntries(entries);
|
|
141
|
+
} finally {
|
|
142
|
+
ancestors.delete(value);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Validate and deep-clone a JSON-compatible object. */
|
|
146
|
+
function cloneJsonObject(value, path = "metadata") {
|
|
147
|
+
try {
|
|
148
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw invalidJsonValue(path);
|
|
149
|
+
return cloneJsonValue(value, path, /* @__PURE__ */ new Set());
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error instanceof MnemocyteError) throw error;
|
|
152
|
+
throw invalidJsonValue(path, error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Assert that a runtime value is a JSON-compatible object. */
|
|
156
|
+
function validateJsonObject(value, path = "metadata") {
|
|
157
|
+
cloneJsonObject(value, path);
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/memory/records.ts
|
|
161
|
+
function normalizeTags(tags) {
|
|
162
|
+
if (!tags) return [];
|
|
163
|
+
return [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))];
|
|
164
|
+
}
|
|
165
|
+
function createId() {
|
|
166
|
+
return `mem_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
167
|
+
}
|
|
168
|
+
/** Generate a unique audit-event id (`evt_*`). */
|
|
169
|
+
function createEventId() {
|
|
170
|
+
return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
171
|
+
}
|
|
172
|
+
function cloneMemory(memory) {
|
|
173
|
+
return {
|
|
174
|
+
id: memory.id,
|
|
175
|
+
entityId: memory.entityId,
|
|
176
|
+
content: memory.content,
|
|
177
|
+
type: memory.type,
|
|
178
|
+
importance: memory.importance,
|
|
179
|
+
tags: [...memory.tags],
|
|
180
|
+
source: memory.source,
|
|
181
|
+
metadata: cloneJsonObject(memory.metadata),
|
|
182
|
+
confidence: memory.confidence,
|
|
183
|
+
embeddingModel: memory.embeddingModel,
|
|
184
|
+
embeddingDimensions: memory.embeddingDimensions,
|
|
185
|
+
supersededBy: memory.supersededBy,
|
|
186
|
+
supersededAt: memory.supersededAt ? new Date(memory.supersededAt) : null,
|
|
187
|
+
expiresAt: memory.expiresAt ? new Date(memory.expiresAt) : null,
|
|
188
|
+
lastAccessedAt: memory.lastAccessedAt ? new Date(memory.lastAccessedAt) : null,
|
|
189
|
+
accessCount: memory.accessCount,
|
|
190
|
+
createdAt: new Date(memory.createdAt),
|
|
191
|
+
updatedAt: new Date(memory.updatedAt)
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/retrieval/scorer.ts
|
|
196
|
+
const IMPORTANCE_SCORES = {
|
|
197
|
+
low: 0,
|
|
198
|
+
normal: .5,
|
|
199
|
+
high: .8,
|
|
200
|
+
critical: 1
|
|
201
|
+
};
|
|
202
|
+
const DEFAULT_RETRIEVAL_WEIGHTS = {
|
|
203
|
+
vector: .55,
|
|
204
|
+
lexical: .2,
|
|
205
|
+
recency: .1,
|
|
206
|
+
confidence: .05,
|
|
207
|
+
access: .05,
|
|
208
|
+
importance: .05
|
|
209
|
+
};
|
|
210
|
+
const DEFAULT_RECENCY_HALF_LIFE_DAYS = 90;
|
|
211
|
+
const DEFAULT_ACCESS_SATURATION = 10;
|
|
212
|
+
function cosineSimilarity(a, b) {
|
|
213
|
+
let dot = 0;
|
|
214
|
+
let aMagnitude = 0;
|
|
215
|
+
let bMagnitude = 0;
|
|
216
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
217
|
+
const av = a[i] ?? 0;
|
|
218
|
+
const bv = b[i] ?? 0;
|
|
219
|
+
dot += av * bv;
|
|
220
|
+
aMagnitude += av * av;
|
|
221
|
+
bMagnitude += bv * bv;
|
|
222
|
+
}
|
|
223
|
+
if (aMagnitude === 0 || bMagnitude === 0) return 0;
|
|
224
|
+
return dot / (Math.sqrt(aMagnitude) * Math.sqrt(bMagnitude));
|
|
225
|
+
}
|
|
226
|
+
function tokenizeQuery(query) {
|
|
227
|
+
return query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
228
|
+
}
|
|
229
|
+
function lexicalScoreWithTerms(content, terms) {
|
|
230
|
+
if (terms.length === 0) return 0;
|
|
231
|
+
const lowerContent = content.toLowerCase();
|
|
232
|
+
return terms.filter((term) => lowerContent.includes(term)).length / terms.length;
|
|
233
|
+
}
|
|
234
|
+
function createLexicalScorer(query) {
|
|
235
|
+
const terms = tokenizeQuery(query);
|
|
236
|
+
return (content) => lexicalScoreWithTerms(content, terms);
|
|
237
|
+
}
|
|
238
|
+
function clampScore(value) {
|
|
239
|
+
if (!Number.isFinite(value)) return 0;
|
|
240
|
+
return Math.max(0, Math.min(1, value));
|
|
241
|
+
}
|
|
242
|
+
function normalizeWeights(weights) {
|
|
243
|
+
const merged = {
|
|
244
|
+
...DEFAULT_RETRIEVAL_WEIGHTS,
|
|
245
|
+
...weights
|
|
246
|
+
};
|
|
247
|
+
const total = merged.vector + merged.lexical + merged.recency + merged.confidence + merged.access + merged.importance;
|
|
248
|
+
if (total <= 0) return { ...DEFAULT_RETRIEVAL_WEIGHTS };
|
|
249
|
+
return {
|
|
250
|
+
vector: merged.vector / total,
|
|
251
|
+
lexical: merged.lexical / total,
|
|
252
|
+
recency: merged.recency / total,
|
|
253
|
+
confidence: merged.confidence / total,
|
|
254
|
+
access: merged.access / total,
|
|
255
|
+
importance: merged.importance / total
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function createScoringConfig(config) {
|
|
259
|
+
return {
|
|
260
|
+
weights: normalizeWeights(config?.weights),
|
|
261
|
+
recencyHalfLifeDays: config?.recencyHalfLifeDays ?? DEFAULT_RECENCY_HALF_LIFE_DAYS,
|
|
262
|
+
accessSaturation: config?.accessSaturation ?? DEFAULT_ACCESS_SATURATION
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function recencyScore(createdAt, halfLifeDays) {
|
|
266
|
+
const ageMs = Date.now() - createdAt.getTime();
|
|
267
|
+
const ageDays = Math.max(0, ageMs / 864e5);
|
|
268
|
+
return Math.exp(-Math.LN2 * ageDays / halfLifeDays);
|
|
269
|
+
}
|
|
270
|
+
function accessScore(accessCount, saturation) {
|
|
271
|
+
if (accessCount <= 0) return 0;
|
|
272
|
+
return Math.min(1, Math.log1p(accessCount) / Math.log1p(saturation));
|
|
273
|
+
}
|
|
274
|
+
function toScoredMemoryWithConfig(memory, vector, lexical, input, config) {
|
|
275
|
+
const { weights } = config;
|
|
276
|
+
const recency = recencyScore(memory.createdAt, config.recencyHalfLifeDays);
|
|
277
|
+
const confidence = clampScore(memory.confidence);
|
|
278
|
+
const access = accessScore(memory.accessCount, config.accessSaturation);
|
|
279
|
+
const importance = IMPORTANCE_SCORES[memory.importance];
|
|
280
|
+
const score = Math.max(0, Math.min(1, clampScore(vector) * weights.vector + clampScore(lexical) * weights.lexical + recency * weights.recency + confidence * weights.confidence + access * weights.access + importance * weights.importance));
|
|
281
|
+
return {
|
|
282
|
+
...cloneMemory(memory),
|
|
283
|
+
score,
|
|
284
|
+
scores: {
|
|
285
|
+
vector: clampScore(vector),
|
|
286
|
+
lexical: clampScore(lexical),
|
|
287
|
+
recency,
|
|
288
|
+
confidence,
|
|
289
|
+
access,
|
|
290
|
+
importance
|
|
291
|
+
},
|
|
292
|
+
explanation: input.explain ? {
|
|
293
|
+
vectorScore: clampScore(vector),
|
|
294
|
+
lexicalScore: clampScore(lexical),
|
|
295
|
+
recencyScore: recency,
|
|
296
|
+
confidenceScore: confidence,
|
|
297
|
+
accessScore: access,
|
|
298
|
+
importanceScore: importance,
|
|
299
|
+
importanceBoost: importance * weights.importance,
|
|
300
|
+
weights,
|
|
301
|
+
finalScore: score
|
|
302
|
+
} : null
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
//#endregion
|
|
102
306
|
//#region src/context/formatter.ts
|
|
103
307
|
function escapeXml(value) {
|
|
104
308
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
@@ -250,6 +454,17 @@ async function observe(config, backend, operation, metadata, action, successMeta
|
|
|
250
454
|
throw error;
|
|
251
455
|
}
|
|
252
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Ordering of {@link ImportanceLevel} from least to most important. Used
|
|
459
|
+
* by `prune({ maxImportance })` to compare a memory's importance against
|
|
460
|
+
* the caller-supplied ceiling.
|
|
461
|
+
*/
|
|
462
|
+
const IMPORTANCE_RANK = {
|
|
463
|
+
low: 0,
|
|
464
|
+
normal: 1,
|
|
465
|
+
high: 2,
|
|
466
|
+
critical: 3
|
|
467
|
+
};
|
|
253
468
|
//#endregion
|
|
254
469
|
//#region src/resilience.ts
|
|
255
470
|
/**
|
|
@@ -336,37 +551,44 @@ function delay(ms, signal) {
|
|
|
336
551
|
}
|
|
337
552
|
});
|
|
338
553
|
}
|
|
554
|
+
function timeoutError(timeoutMs) {
|
|
555
|
+
return new MnemocyteError(`Provider call timed out after ${timeoutMs}ms.`, "TIMEOUT");
|
|
556
|
+
}
|
|
339
557
|
/**
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
558
|
+
* Run one provider attempt with a signal linked to the caller's signal and
|
|
559
|
+
* Mnemocyte's configured timeout. Timeout aborts are actively propagated to
|
|
560
|
+
* providers that honor `AbortSignal`.
|
|
343
561
|
*/
|
|
344
|
-
function
|
|
345
|
-
if (timeoutMs <= 0 && !signal) return
|
|
562
|
+
function runAttempt(action, timeoutMs, signal) {
|
|
563
|
+
if (timeoutMs <= 0 && !signal) return action(void 0);
|
|
346
564
|
return new Promise((resolve, reject) => {
|
|
347
565
|
let settled = false;
|
|
566
|
+
const controller = new AbortController();
|
|
348
567
|
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
349
568
|
if (settled) return;
|
|
350
569
|
settled = true;
|
|
351
570
|
signal?.removeEventListener("abort", onAbort);
|
|
352
|
-
|
|
571
|
+
controller.abort(timeoutError(timeoutMs));
|
|
572
|
+
reject(timeoutError(timeoutMs));
|
|
353
573
|
}, timeoutMs) : void 0;
|
|
354
574
|
const onAbort = () => {
|
|
355
575
|
if (settled) return;
|
|
356
576
|
settled = true;
|
|
357
577
|
if (timer) clearTimeout(timer);
|
|
578
|
+
controller.abort(signal?.reason);
|
|
358
579
|
reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal?.reason));
|
|
359
580
|
};
|
|
360
581
|
if (signal) {
|
|
361
582
|
if (signal.aborted) {
|
|
362
583
|
if (timer) clearTimeout(timer);
|
|
363
584
|
settled = true;
|
|
585
|
+
controller.abort(signal.reason);
|
|
364
586
|
reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason));
|
|
365
587
|
return;
|
|
366
588
|
}
|
|
367
589
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
368
590
|
}
|
|
369
|
-
|
|
591
|
+
action(controller.signal).then((value) => {
|
|
370
592
|
if (settled) return;
|
|
371
593
|
settled = true;
|
|
372
594
|
if (timer) clearTimeout(timer);
|
|
@@ -405,7 +627,7 @@ async function withResilience(action, options = {}) {
|
|
|
405
627
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
406
628
|
throwIfAborted(externalSignal);
|
|
407
629
|
try {
|
|
408
|
-
return await
|
|
630
|
+
return await runAttempt(action, timeoutMs, externalSignal);
|
|
409
631
|
} catch (error) {
|
|
410
632
|
lastError = error;
|
|
411
633
|
if (error instanceof MnemocyteError && error.code === "ABORTED") throw error;
|
|
@@ -416,84 +638,11 @@ async function withResilience(action, options = {}) {
|
|
|
416
638
|
}
|
|
417
639
|
throw lastError;
|
|
418
640
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const IMPORTANCE_RANK = {
|
|
425
|
-
low: 0,
|
|
426
|
-
normal: 1,
|
|
427
|
-
high: 2,
|
|
428
|
-
critical: 3
|
|
429
|
-
};
|
|
430
|
-
function assertNonEmptyString(value, field) {
|
|
431
|
-
if (value.trim().length === 0) throw new MnemocyteError(`${field} must be a non-empty string.`, "VALIDATION");
|
|
432
|
-
}
|
|
433
|
-
function assertLimit(value) {
|
|
434
|
-
if (!Number.isInteger(value) || value < 1) throw new MnemocyteError("limit must be a positive integer.", "VALIDATION");
|
|
435
|
-
}
|
|
436
|
-
function assertMinScore(value) {
|
|
437
|
-
if (!Number.isFinite(value) || value < 0 || value > 1) throw new MnemocyteError("minScore must be a number between 0 and 1.", "VALIDATION");
|
|
438
|
-
}
|
|
439
|
-
function assertEmbedder(embedder) {
|
|
440
|
-
if (!embedder || typeof embedder.embed !== "function") throw new MnemocyteError("embedder with an embed function is required.", "CONFIG");
|
|
441
|
-
assertNonEmptyString(embedder.model, "embedder.model");
|
|
442
|
-
if (!Number.isInteger(embedder.dimensions) || embedder.dimensions < 1) throw new MnemocyteError("embedder.dimensions must be a positive integer.", "CONFIG");
|
|
443
|
-
}
|
|
444
|
-
function normalizeTags(tags) {
|
|
445
|
-
if (!tags) return [];
|
|
446
|
-
return [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))];
|
|
447
|
-
}
|
|
448
|
-
function createId() {
|
|
449
|
-
return `mem_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
450
|
-
}
|
|
451
|
-
/** Generate a unique audit-event id (`evt_*`). */
|
|
452
|
-
function createEventId() {
|
|
453
|
-
return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
454
|
-
}
|
|
455
|
-
function isExpired(memory, now) {
|
|
456
|
-
return memory.expiresAt !== null && memory.expiresAt.getTime() <= now.getTime();
|
|
457
|
-
}
|
|
458
|
-
function cloneMemory(memory) {
|
|
459
|
-
return {
|
|
460
|
-
...memory,
|
|
461
|
-
tags: [...memory.tags],
|
|
462
|
-
metadata: { ...memory.metadata },
|
|
463
|
-
supersededAt: memory.supersededAt ? new Date(memory.supersededAt) : null,
|
|
464
|
-
expiresAt: memory.expiresAt ? new Date(memory.expiresAt) : null,
|
|
465
|
-
lastAccessedAt: memory.lastAccessedAt ? new Date(memory.lastAccessedAt) : null,
|
|
466
|
-
createdAt: new Date(memory.createdAt),
|
|
467
|
-
updatedAt: new Date(memory.updatedAt)
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
function toDate(value) {
|
|
471
|
-
return value instanceof Date ? value : new Date(value);
|
|
472
|
-
}
|
|
473
|
-
function toNullableDate(value) {
|
|
474
|
-
return value === null ? null : toDate(value);
|
|
475
|
-
}
|
|
476
|
-
function rowToMemory(row) {
|
|
477
|
-
return {
|
|
478
|
-
id: row.id,
|
|
479
|
-
entityId: row.entityId,
|
|
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
|
-
};
|
|
641
|
+
//#endregion
|
|
642
|
+
//#region src/memory/embeddings.ts
|
|
643
|
+
function validateEmbedding(embedding, dimensions) {
|
|
644
|
+
if (embedding.length !== dimensions) throw new MnemocyteError("Embedder returned an embedding with unexpected dimensions.", "EMBEDDING");
|
|
645
|
+
if (embedding.some((component) => !Number.isFinite(component))) throw new MnemocyteError("Embedder returned an embedding with non-finite values.", "EMBEDDING");
|
|
497
646
|
}
|
|
498
647
|
async function embedOne(embedder, text, options = {}) {
|
|
499
648
|
let embeddings;
|
|
@@ -508,7 +657,7 @@ async function embedOne(embedder, text, options = {}) {
|
|
|
508
657
|
}
|
|
509
658
|
const embedding = embeddings[0];
|
|
510
659
|
if (!embedding) throw new MnemocyteError("Embedder returned no embedding.", "EMBEDDING");
|
|
511
|
-
|
|
660
|
+
validateEmbedding(embedding, embedder.dimensions);
|
|
512
661
|
return embedding;
|
|
513
662
|
}
|
|
514
663
|
async function embedMany(embedder, texts, options = {}) {
|
|
@@ -524,12 +673,56 @@ async function embedMany(embedder, texts, options = {}) {
|
|
|
524
673
|
throw new MnemocyteError("Failed to embed texts.", "EMBEDDING", error);
|
|
525
674
|
}
|
|
526
675
|
if (embeddings.length !== texts.length) throw new MnemocyteError(`Embedder returned ${embeddings.length} embeddings for ${texts.length} texts.`, "EMBEDDING");
|
|
527
|
-
for (const embedding of embeddings)
|
|
676
|
+
for (const embedding of embeddings) validateEmbedding(embedding, embedder.dimensions);
|
|
528
677
|
return embeddings;
|
|
529
678
|
}
|
|
679
|
+
//#endregion
|
|
680
|
+
//#region src/memory/validation.ts
|
|
681
|
+
function assertNonEmptyString(value, field, code = "VALIDATION") {
|
|
682
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new MnemocyteError(`${field} must be a non-empty string.`, code);
|
|
683
|
+
}
|
|
684
|
+
function assertLimit(value) {
|
|
685
|
+
if (!Number.isInteger(value) || value < 1) throw new MnemocyteError("limit must be a positive integer.", "VALIDATION");
|
|
686
|
+
}
|
|
687
|
+
function assertMinScore(value) {
|
|
688
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) throw new MnemocyteError("minScore must be a number between 0 and 1.", "VALIDATION");
|
|
689
|
+
}
|
|
690
|
+
function assertEmbedder(embedder) {
|
|
691
|
+
if (!embedder || typeof embedder.embed !== "function") throw new MnemocyteError("embedder with an embed function is required.", "CONFIG");
|
|
692
|
+
assertNonEmptyString(embedder.model, "embedder.model", "CONFIG");
|
|
693
|
+
if (!Number.isInteger(embedder.dimensions) || embedder.dimensions < 1) throw new MnemocyteError("embedder.dimensions must be a positive integer.", "CONFIG");
|
|
694
|
+
}
|
|
695
|
+
const RETRIEVAL_WEIGHT_KEYS = [
|
|
696
|
+
"vector",
|
|
697
|
+
"lexical",
|
|
698
|
+
"recency",
|
|
699
|
+
"confidence",
|
|
700
|
+
"access",
|
|
701
|
+
"importance"
|
|
702
|
+
];
|
|
703
|
+
function assertPositiveFiniteConfigValue(value, field) {
|
|
704
|
+
if (!Number.isFinite(value) || value <= 0) throw new MnemocyteError(`${field} must be a positive finite number.`, "CONFIG");
|
|
705
|
+
}
|
|
706
|
+
function validateRetrievalConfig(config) {
|
|
707
|
+
if (config === void 0) return;
|
|
708
|
+
let weightTotal = 0;
|
|
709
|
+
for (const key of RETRIEVAL_WEIGHT_KEYS) {
|
|
710
|
+
const weight = config.weights?.[key] ?? DEFAULT_RETRIEVAL_WEIGHTS[key];
|
|
711
|
+
if (!Number.isFinite(weight) || weight < 0) throw new MnemocyteError(`retrieval.weights.${key} must be a non-negative finite number.`, "CONFIG");
|
|
712
|
+
weightTotal += weight;
|
|
713
|
+
}
|
|
714
|
+
if (weightTotal === 0) throw new MnemocyteError("retrieval.weights must have an effective total greater than zero.", "CONFIG");
|
|
715
|
+
if (config.recencyHalfLifeDays !== void 0) assertPositiveFiniteConfigValue(config.recencyHalfLifeDays, "retrieval.recencyHalfLifeDays");
|
|
716
|
+
if (config.accessSaturation !== void 0) assertPositiveFiniteConfigValue(config.accessSaturation, "retrieval.accessSaturation");
|
|
717
|
+
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");
|
|
718
|
+
}
|
|
719
|
+
function validateBuildContextInput(input) {
|
|
720
|
+
if (input.maxTokens !== void 0 && (!Number.isInteger(input.maxTokens) || input.maxTokens < 1)) throw new MnemocyteError("maxTokens must be a positive integer when supplied.", "VALIDATION");
|
|
721
|
+
}
|
|
530
722
|
function validateRememberInput(input) {
|
|
531
723
|
assertNonEmptyString(input.entityId, "entityId");
|
|
532
724
|
assertNonEmptyString(input.content, "content");
|
|
725
|
+
if (input.metadata !== void 0) validateJsonObject(input.metadata);
|
|
533
726
|
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
727
|
}
|
|
535
728
|
function validateRecallInput(input) {
|
|
@@ -560,27 +753,6 @@ function validatePruneInput(input) {
|
|
|
560
753
|
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
754
|
}
|
|
562
755
|
/**
|
|
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
756
|
* Validate a {@link FindDuplicatesInput} and throw a `"VALIDATION"`
|
|
585
757
|
* {@link MnemocyteError} for malformed `threshold` / `limit` values.
|
|
586
758
|
*/
|
|
@@ -592,18 +764,6 @@ function validateFindDuplicatesInput(input) {
|
|
|
592
764
|
if (input.limit !== void 0) assertLimit(input.limit);
|
|
593
765
|
}
|
|
594
766
|
/**
|
|
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
767
|
* Validate a {@link ConsolidateInput} and throw a `"VALIDATION"`
|
|
608
768
|
* {@link MnemocyteError} for malformed inputs (empty `supersededIds`
|
|
609
769
|
* or a `survivorId` appearing in `supersededIds`).
|
|
@@ -623,274 +783,178 @@ function validateListAuditLogInput(input) {
|
|
|
623
783
|
assertNonEmptyString(input.entityId, "entityId");
|
|
624
784
|
if (input.limit !== void 0) assertLimit(input.limit);
|
|
625
785
|
}
|
|
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
786
|
//#endregion
|
|
637
|
-
//#region src/
|
|
638
|
-
|
|
639
|
-
|
|
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);
|
|
787
|
+
//#region src/memory/client-core.ts
|
|
788
|
+
function isPositionalRememberManyInput(input) {
|
|
789
|
+
return Array.isArray(input);
|
|
670
790
|
}
|
|
671
|
-
function
|
|
672
|
-
if (terms.length === 0) return 0;
|
|
673
|
-
const lowerContent = content.toLowerCase();
|
|
674
|
-
return terms.filter((term) => lowerContent.includes(term)).length / terms.length;
|
|
675
|
-
}
|
|
676
|
-
function createLexicalScorer(query) {
|
|
677
|
-
const terms = tokenizeQuery(query);
|
|
678
|
-
return (content) => lexicalScoreWithTerms(content, terms);
|
|
679
|
-
}
|
|
680
|
-
function clampScore(value) {
|
|
681
|
-
if (!Number.isFinite(value)) return 0;
|
|
682
|
-
return Math.max(0, Math.min(1, value));
|
|
683
|
-
}
|
|
684
|
-
function normalizeWeights(weights) {
|
|
685
|
-
const merged = {
|
|
686
|
-
...DEFAULT_WEIGHTS,
|
|
687
|
-
...weights
|
|
688
|
-
};
|
|
689
|
-
const total = merged.vector + merged.lexical + merged.recency + merged.confidence + merged.access + merged.importance;
|
|
690
|
-
if (total <= 0) return DEFAULT_WEIGHTS;
|
|
791
|
+
function providerOptions(config, signal) {
|
|
691
792
|
return {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
recency: merged.recency / total,
|
|
695
|
-
confidence: merged.confidence / total,
|
|
696
|
-
access: merged.access / total,
|
|
697
|
-
importance: merged.importance / total
|
|
793
|
+
...signal === void 0 ? {} : { signal },
|
|
794
|
+
...config.provider === void 0 ? {} : { resilience: config.provider }
|
|
698
795
|
};
|
|
699
796
|
}
|
|
700
|
-
function
|
|
797
|
+
function createStoredMemory(config, input, embedding, now) {
|
|
701
798
|
return {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
799
|
+
id: createId(),
|
|
800
|
+
entityId: input.entityId,
|
|
801
|
+
content: input.content,
|
|
802
|
+
type: input.type ?? "fact",
|
|
803
|
+
importance: input.importance ?? "normal",
|
|
804
|
+
tags: normalizeTags(input.tags),
|
|
805
|
+
source: input.source ?? null,
|
|
806
|
+
metadata: cloneJsonObject(input.metadata ?? {}),
|
|
807
|
+
confidence: input.confidence ?? 1,
|
|
808
|
+
embeddingModel: config.embedder.model,
|
|
809
|
+
embeddingDimensions: config.embedder.dimensions,
|
|
810
|
+
supersededBy: null,
|
|
811
|
+
supersededAt: null,
|
|
812
|
+
expiresAt: input.expiresAt ?? null,
|
|
813
|
+
lastAccessedAt: null,
|
|
814
|
+
accessCount: 0,
|
|
815
|
+
createdAt: now,
|
|
816
|
+
updatedAt: now,
|
|
817
|
+
embedding
|
|
705
818
|
};
|
|
706
819
|
}
|
|
707
|
-
function
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
820
|
+
function auditEvent(entityId, description, metadata, timestamp = /* @__PURE__ */ new Date()) {
|
|
821
|
+
return {
|
|
822
|
+
id: createEventId(),
|
|
823
|
+
entityId,
|
|
824
|
+
description,
|
|
825
|
+
metadata: cloneJsonObject(metadata),
|
|
826
|
+
timestamp
|
|
827
|
+
};
|
|
715
828
|
}
|
|
716
|
-
function
|
|
717
|
-
const { weights } = config;
|
|
718
|
-
const recency = recencyScore(memory.createdAt, config.recencyHalfLifeDays);
|
|
719
|
-
const confidence = clampScore(memory.confidence);
|
|
720
|
-
const access = accessScore(memory.accessCount, config.accessSaturation);
|
|
721
|
-
const importance = IMPORTANCE_SCORES[memory.importance];
|
|
722
|
-
const score = Math.max(0, Math.min(1, clampScore(vector) * weights.vector + clampScore(lexical) * weights.lexical + recency * weights.recency + confidence * weights.confidence + access * weights.access + importance * weights.importance));
|
|
829
|
+
function toDuplicatePair(pair) {
|
|
723
830
|
return {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
vector: clampScore(vector),
|
|
728
|
-
lexical: clampScore(lexical),
|
|
729
|
-
recency,
|
|
730
|
-
confidence,
|
|
731
|
-
access,
|
|
732
|
-
importance
|
|
733
|
-
},
|
|
734
|
-
explanation: input.explain ? {
|
|
735
|
-
vectorScore: clampScore(vector),
|
|
736
|
-
lexicalScore: clampScore(lexical),
|
|
737
|
-
recencyScore: recency,
|
|
738
|
-
confidenceScore: confidence,
|
|
739
|
-
accessScore: access,
|
|
740
|
-
importanceScore: importance,
|
|
741
|
-
importanceBoost: importance * weights.importance,
|
|
742
|
-
weights,
|
|
743
|
-
finalScore: score
|
|
744
|
-
} : null
|
|
831
|
+
a: cloneMemory(pair.a),
|
|
832
|
+
b: cloneMemory(pair.b),
|
|
833
|
+
similarity: Math.max(0, Math.min(1, pair.similarity))
|
|
745
834
|
};
|
|
746
835
|
}
|
|
747
|
-
|
|
748
|
-
//#region src/memory/in-memory.ts
|
|
749
|
-
function createInMemoryClient(config) {
|
|
750
|
-
const memories = /* @__PURE__ */ new Map();
|
|
751
|
-
const auditLog = [];
|
|
836
|
+
function createMemoryClient(config, store) {
|
|
752
837
|
let closed = false;
|
|
753
838
|
function assertOpen() {
|
|
754
839
|
if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
|
|
755
840
|
}
|
|
756
|
-
function
|
|
757
|
-
|
|
758
|
-
auditLog.push({
|
|
759
|
-
id: createEventId(),
|
|
760
|
-
entityId,
|
|
761
|
-
description,
|
|
762
|
-
metadata,
|
|
763
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
764
|
-
});
|
|
841
|
+
async function ensureSchema() {
|
|
842
|
+
await store.ensureSchema();
|
|
765
843
|
}
|
|
766
|
-
function
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
}
|
|
844
|
+
async function ensureEmbeddingCompatibility() {
|
|
845
|
+
await store.ensureEmbeddingCompatibility(config.embedder);
|
|
846
|
+
}
|
|
847
|
+
async function recordAudit(events) {
|
|
848
|
+
if (events.length === 0 || config.audit?.enabled !== true) return;
|
|
849
|
+
try {
|
|
850
|
+
await store.addAuditEvents(events);
|
|
851
|
+
} catch {}
|
|
774
852
|
}
|
|
775
853
|
async function remember(input) {
|
|
776
|
-
|
|
854
|
+
const preparedInput = {
|
|
855
|
+
...input,
|
|
856
|
+
metadata: cloneJsonObject(input.metadata ?? {})
|
|
857
|
+
};
|
|
858
|
+
return observe(config, store.backend, "remember", { entityId: preparedInput.entityId }, async () => {
|
|
777
859
|
assertOpen();
|
|
778
|
-
validateRememberInput(
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const memory = {
|
|
785
|
-
id: createId(),
|
|
786
|
-
entityId: input.entityId,
|
|
787
|
-
content: input.content,
|
|
788
|
-
type: input.type ?? "fact",
|
|
789
|
-
importance: input.importance ?? "normal",
|
|
790
|
-
tags: normalizeTags(input.tags),
|
|
791
|
-
source: input.source ?? null,
|
|
792
|
-
metadata: input.metadata ?? {},
|
|
793
|
-
confidence: input.confidence ?? 1,
|
|
794
|
-
embeddingModel: config.embedder.model,
|
|
795
|
-
embeddingDimensions: config.embedder.dimensions,
|
|
796
|
-
supersededBy: null,
|
|
797
|
-
supersededAt: null,
|
|
798
|
-
expiresAt: input.expiresAt ?? null,
|
|
799
|
-
lastAccessedAt: null,
|
|
800
|
-
accessCount: 0,
|
|
801
|
-
createdAt: now,
|
|
802
|
-
updatedAt: now,
|
|
803
|
-
embedding
|
|
804
|
-
};
|
|
805
|
-
memories.set(memory.id, memory);
|
|
806
|
-
recordAudit(memory.entityId, "memory.created", {
|
|
860
|
+
validateRememberInput(preparedInput);
|
|
861
|
+
await ensureEmbeddingCompatibility();
|
|
862
|
+
const embedding = await embedOne(config.embedder, preparedInput.content, providerOptions(config, preparedInput.signal));
|
|
863
|
+
const [memory] = await store.insertMemories([createStoredMemory(config, preparedInput, embedding, /* @__PURE__ */ new Date())]);
|
|
864
|
+
if (!memory) throw new MnemocyteError("Memory insert returned no rows.", "DB");
|
|
865
|
+
await recordAudit([auditEvent(memory.entityId, "memory.created", {
|
|
807
866
|
memoryId: memory.id,
|
|
808
867
|
type: memory.type,
|
|
809
868
|
importance: memory.importance
|
|
810
|
-
});
|
|
869
|
+
})]);
|
|
811
870
|
return cloneMemory(memory);
|
|
812
871
|
}, (memory) => ({
|
|
813
872
|
memoryId: memory.id,
|
|
814
873
|
count: 1
|
|
815
874
|
}));
|
|
816
875
|
}
|
|
876
|
+
async function rememberMany(input) {
|
|
877
|
+
const positional = isPositionalRememberManyInput(input);
|
|
878
|
+
const inputs = positional ? input : input.inputs;
|
|
879
|
+
const signal = positional ? input[0]?.signal : input.signal;
|
|
880
|
+
const preparedInputs = inputs.map((item) => ({
|
|
881
|
+
...item,
|
|
882
|
+
metadata: cloneJsonObject(item.metadata ?? {})
|
|
883
|
+
}));
|
|
884
|
+
return observe(config, store.backend, "rememberMany", { count: preparedInputs.length }, async () => {
|
|
885
|
+
assertOpen();
|
|
886
|
+
if (preparedInputs.length === 0) return [];
|
|
887
|
+
for (const item of preparedInputs) validateRememberInput(item);
|
|
888
|
+
await ensureEmbeddingCompatibility();
|
|
889
|
+
const embeddings = await embedMany(config.embedder, preparedInputs.map((item) => item.content), providerOptions(config, signal));
|
|
890
|
+
const now = /* @__PURE__ */ new Date();
|
|
891
|
+
const stored = preparedInputs.map((item, idx) => createStoredMemory(config, item, embeddings[idx], now));
|
|
892
|
+
const memories = await store.insertMemories(stored);
|
|
893
|
+
await recordAudit(memories.map((memory) => auditEvent(memory.entityId, "memory.created", {
|
|
894
|
+
memoryId: memory.id,
|
|
895
|
+
type: memory.type,
|
|
896
|
+
importance: memory.importance
|
|
897
|
+
})));
|
|
898
|
+
return memories.map(cloneMemory);
|
|
899
|
+
}, (result) => ({ count: result.length }));
|
|
900
|
+
}
|
|
817
901
|
const client = {
|
|
818
902
|
remember,
|
|
819
|
-
|
|
820
|
-
return observe(config, "in-memory", "rememberMany", { count: inputs.length }, async () => {
|
|
821
|
-
assertOpen();
|
|
822
|
-
if (inputs.length === 0) return [];
|
|
823
|
-
for (const input of inputs) validateRememberInput(input);
|
|
824
|
-
const embeddings = await embedMany(config.embedder, inputs.map((i) => i.content), {
|
|
825
|
-
...inputs[0]?.signal === void 0 ? {} : { signal: inputs[0].signal },
|
|
826
|
-
...config.provider === void 0 ? {} : { resilience: config.provider }
|
|
827
|
-
});
|
|
828
|
-
const now = /* @__PURE__ */ new Date();
|
|
829
|
-
const result = [];
|
|
830
|
-
for (let i = 0; i < inputs.length; i++) {
|
|
831
|
-
const input = inputs[i];
|
|
832
|
-
const memory = {
|
|
833
|
-
id: createId(),
|
|
834
|
-
entityId: input.entityId,
|
|
835
|
-
content: input.content,
|
|
836
|
-
type: input.type ?? "fact",
|
|
837
|
-
importance: input.importance ?? "normal",
|
|
838
|
-
tags: normalizeTags(input.tags),
|
|
839
|
-
source: input.source ?? null,
|
|
840
|
-
metadata: input.metadata ?? {},
|
|
841
|
-
confidence: input.confidence ?? 1,
|
|
842
|
-
embeddingModel: config.embedder.model,
|
|
843
|
-
embeddingDimensions: config.embedder.dimensions,
|
|
844
|
-
supersededBy: null,
|
|
845
|
-
supersededAt: null,
|
|
846
|
-
expiresAt: input.expiresAt ?? null,
|
|
847
|
-
lastAccessedAt: null,
|
|
848
|
-
accessCount: 0,
|
|
849
|
-
createdAt: now,
|
|
850
|
-
updatedAt: now,
|
|
851
|
-
embedding: embeddings[i]
|
|
852
|
-
};
|
|
853
|
-
memories.set(memory.id, memory);
|
|
854
|
-
recordAudit(memory.entityId, "memory.created", {
|
|
855
|
-
memoryId: memory.id,
|
|
856
|
-
type: memory.type,
|
|
857
|
-
importance: memory.importance
|
|
858
|
-
});
|
|
859
|
-
result.push(cloneMemory(memory));
|
|
860
|
-
}
|
|
861
|
-
return result;
|
|
862
|
-
}, (result) => ({ count: result.length }));
|
|
863
|
-
},
|
|
903
|
+
rememberMany,
|
|
864
904
|
async recall(input) {
|
|
865
|
-
return observe(config,
|
|
905
|
+
return observe(config, store.backend, "recall", { entityId: input.entityId }, async () => {
|
|
866
906
|
assertOpen();
|
|
867
907
|
validateRecallInput(input);
|
|
868
908
|
const limit = input.limit ?? config.defaults?.limit ?? 10;
|
|
869
909
|
const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
|
|
870
910
|
assertLimit(limit);
|
|
871
911
|
assertMinScore(minScore);
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
const now = /* @__PURE__ */ new Date();
|
|
912
|
+
await ensureEmbeddingCompatibility();
|
|
913
|
+
const queryEmbedding = await embedOne(config.embedder, input.query, providerOptions(config, input.signal));
|
|
914
|
+
const candidateMultiplier = config.retrieval?.candidateMultiplier ?? 3;
|
|
915
|
+
const candidateLimit = Math.max(limit, limit * candidateMultiplier);
|
|
877
916
|
const scoreLexical = createLexicalScorer(input.query);
|
|
878
917
|
const scoringConfig = createScoringConfig(config.retrieval);
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
918
|
+
const [vectorCandidates, lexicalCandidates] = await Promise.all([store.vectorSearch({
|
|
919
|
+
...input,
|
|
920
|
+
embedding: queryEmbedding,
|
|
921
|
+
limit: candidateLimit,
|
|
922
|
+
minScore: 0
|
|
923
|
+
}), store.lexicalSearch({
|
|
924
|
+
...input,
|
|
925
|
+
limit: candidateLimit
|
|
926
|
+
})]);
|
|
927
|
+
const merged = /* @__PURE__ */ new Map();
|
|
928
|
+
for (const candidate of vectorCandidates) merged.set(candidate.memory.id, {
|
|
929
|
+
memory: candidate.memory,
|
|
930
|
+
vectorScore: candidate.vectorScore,
|
|
931
|
+
lexicalScore: scoreLexical(candidate.memory.content)
|
|
932
|
+
});
|
|
933
|
+
const lexicalOnly = lexicalCandidates.filter((candidate) => !merged.has(candidate.memory.id));
|
|
934
|
+
const lexicalOnlyEmbeddings = await store.getMemoryEmbeddings(lexicalOnly.map((candidate) => candidate.memory.id));
|
|
935
|
+
for (const candidate of lexicalCandidates) {
|
|
936
|
+
const existing = merged.get(candidate.memory.id);
|
|
937
|
+
if (existing) {
|
|
938
|
+
existing.lexicalScore = candidate.lexicalScore;
|
|
939
|
+
continue;
|
|
886
940
|
}
|
|
941
|
+
const embedding = lexicalOnlyEmbeddings.get(candidate.memory.id);
|
|
942
|
+
const similarity = embedding ? cosineSimilarity(embedding, queryEmbedding) : 0;
|
|
943
|
+
merged.set(candidate.memory.id, {
|
|
944
|
+
memory: candidate.memory,
|
|
945
|
+
vectorScore: Math.max(0, similarity),
|
|
946
|
+
lexicalScore: candidate.lexicalScore
|
|
947
|
+
});
|
|
887
948
|
}
|
|
949
|
+
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);
|
|
950
|
+
await store.markMemoriesAccessed(scored.map((memory) => memory.id));
|
|
888
951
|
return scored;
|
|
889
952
|
}, (result) => ({ count: result.length }));
|
|
890
953
|
},
|
|
891
954
|
async buildContext(input) {
|
|
892
|
-
return observe(config,
|
|
955
|
+
return observe(config, store.backend, "buildContext", { entityId: input.entityId }, async () => {
|
|
893
956
|
assertOpen();
|
|
957
|
+
validateBuildContextInput(input);
|
|
894
958
|
return buildContext({
|
|
895
959
|
input,
|
|
896
960
|
recall: (contextInput) => {
|
|
@@ -904,178 +968,375 @@ function createInMemoryClient(config) {
|
|
|
904
968
|
});
|
|
905
969
|
},
|
|
906
970
|
async forget(input) {
|
|
907
|
-
return observe(config,
|
|
971
|
+
return observe(config, store.backend, "forget", {
|
|
908
972
|
entityId: input.entityId,
|
|
909
973
|
memoryId: input.memoryId
|
|
910
974
|
}, async () => {
|
|
911
975
|
assertOpen();
|
|
912
976
|
assertNonEmptyString(input.entityId, "entityId");
|
|
913
977
|
assertNonEmptyString(input.memoryId, "memoryId");
|
|
914
|
-
|
|
915
|
-
if (!
|
|
916
|
-
|
|
917
|
-
recordAudit(memory.entityId, "memory.deleted", { memoryId: memory.id });
|
|
978
|
+
await ensureSchema();
|
|
979
|
+
if (!await store.deleteMemory(input.entityId, input.memoryId)) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
|
|
980
|
+
await recordAudit([auditEvent(input.entityId, "memory.deleted", { memoryId: input.memoryId })]);
|
|
918
981
|
});
|
|
919
982
|
},
|
|
920
983
|
async forgetAll(input) {
|
|
921
|
-
return observe(config,
|
|
984
|
+
return observe(config, store.backend, "forgetAll", { entityId: input.entityId }, async () => {
|
|
922
985
|
assertOpen();
|
|
923
986
|
assertNonEmptyString(input.entityId, "entityId");
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
}
|
|
929
|
-
recordAudit(input.entityId, "entity.cleared", { count: deleted });
|
|
930
|
-
return deleted;
|
|
987
|
+
await ensureSchema();
|
|
988
|
+
const deletedCount = await store.deleteMemoriesForEntity(input.entityId);
|
|
989
|
+
await recordAudit([auditEvent(input.entityId, "entity.cleared", { count: deletedCount })]);
|
|
990
|
+
return deletedCount;
|
|
931
991
|
}, (count) => ({ count })).then(() => void 0);
|
|
932
992
|
},
|
|
933
993
|
async prune(input) {
|
|
934
|
-
return observe(config,
|
|
994
|
+
return observe(config, store.backend, "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
|
|
935
995
|
assertOpen();
|
|
936
996
|
validatePruneInput(input);
|
|
937
|
-
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
997
|
+
await ensureSchema();
|
|
998
|
+
const result = await store.prune(input);
|
|
999
|
+
if (result.deletedCount > 0 && input.entityId !== void 0 && result.dryRun === false) await recordAudit([auditEvent(input.entityId, "memory.pruned", { count: result.deletedCount })]);
|
|
1000
|
+
return result;
|
|
1001
|
+
}, (result) => ({ count: result.deletedCount }));
|
|
1002
|
+
},
|
|
1003
|
+
async findDuplicates(input) {
|
|
1004
|
+
return observe(config, store.backend, "findDuplicates", { entityId: input.entityId }, async () => {
|
|
1005
|
+
assertOpen();
|
|
1006
|
+
validateFindDuplicatesInput(input);
|
|
1007
|
+
await ensureEmbeddingCompatibility();
|
|
1008
|
+
return (await store.findDuplicatePairs(input)).map(toDuplicatePair);
|
|
1009
|
+
}, (result) => ({ count: result.length }));
|
|
1010
|
+
},
|
|
1011
|
+
async listAuditLog(input) {
|
|
1012
|
+
return observe(config, store.backend, "listAuditLog", { entityId: input.entityId }, async () => {
|
|
1013
|
+
assertOpen();
|
|
1014
|
+
validateListAuditLogInput(input);
|
|
1015
|
+
await ensureSchema();
|
|
1016
|
+
return (await store.listAuditLog(input)).map((event) => ({
|
|
1017
|
+
id: event.id,
|
|
1018
|
+
entityId: event.entityId,
|
|
1019
|
+
description: event.description,
|
|
1020
|
+
metadata: cloneJsonObject(event.metadata),
|
|
1021
|
+
timestamp: new Date(event.timestamp)
|
|
1022
|
+
}));
|
|
1023
|
+
}, (result) => ({ count: result.length }));
|
|
1024
|
+
},
|
|
1025
|
+
async stats(input) {
|
|
1026
|
+
return observe(config, store.backend, "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
|
|
1027
|
+
assertOpen();
|
|
1028
|
+
await ensureSchema();
|
|
1029
|
+
return store.stats(input, /* @__PURE__ */ new Date());
|
|
1030
|
+
});
|
|
1031
|
+
},
|
|
1032
|
+
experimental: { async consolidate(input) {
|
|
1033
|
+
return observe(config, store.backend, "consolidate", {
|
|
1034
|
+
entityId: input.entityId,
|
|
1035
|
+
memoryId: input.survivorId
|
|
1036
|
+
}, async () => {
|
|
1037
|
+
assertOpen();
|
|
1038
|
+
validateConsolidateInput(input);
|
|
1039
|
+
await ensureSchema();
|
|
1040
|
+
const survivor = await store.getMemory(input.entityId, input.survivorId);
|
|
1041
|
+
if (!survivor) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
|
|
1042
|
+
if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
|
|
1043
|
+
const targets = await store.loadConsolidationTargets(input.entityId, input.supersededIds);
|
|
1044
|
+
const foundIds = new Set(targets.map((target) => target.id));
|
|
1045
|
+
for (const id of input.supersededIds) if (!foundIds.has(id)) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
|
|
1046
|
+
const losers = targets.filter((target) => target.supersededBy === null);
|
|
1047
|
+
if (losers.length === 0) return {
|
|
1048
|
+
survivorId: survivor.id,
|
|
1049
|
+
supersededCount: 0,
|
|
1050
|
+
supersededIds: []
|
|
1051
|
+
};
|
|
1052
|
+
const result = await store.consolidate({
|
|
1053
|
+
entityId: input.entityId,
|
|
1054
|
+
survivorId: survivor.id,
|
|
1055
|
+
survivorTags: survivor.tags,
|
|
1056
|
+
supersededIds: losers.map((loser) => loser.id),
|
|
1057
|
+
mergeTags: input.mergeTags !== false,
|
|
1058
|
+
now: /* @__PURE__ */ new Date(),
|
|
1059
|
+
auditEnabled: config.audit?.enabled === true
|
|
1060
|
+
});
|
|
945
1061
|
return {
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1062
|
+
survivorId: survivor.id,
|
|
1063
|
+
supersededCount: result.supersededIds.length,
|
|
1064
|
+
supersededIds: result.supersededIds
|
|
949
1065
|
};
|
|
950
|
-
}, (result) => ({ count: result.
|
|
1066
|
+
}, (result) => ({ count: result.supersededCount }));
|
|
1067
|
+
} },
|
|
1068
|
+
async close() {
|
|
1069
|
+
return observe(config, store.backend, "close", {}, async () => {
|
|
1070
|
+
if (!closed) {
|
|
1071
|
+
closed = true;
|
|
1072
|
+
await store.close();
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
return client;
|
|
1078
|
+
}
|
|
1079
|
+
//#endregion
|
|
1080
|
+
//#region src/memory/filters.ts
|
|
1081
|
+
function isExpired(memory, now) {
|
|
1082
|
+
return memory.expiresAt !== null && memory.expiresAt.getTime() <= now.getTime();
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Test whether `memory` matches a {@link PruneInput}. All specified
|
|
1086
|
+
* selectors are AND-combined; unspecified selectors do not restrict.
|
|
1087
|
+
*
|
|
1088
|
+
* `now` is supplied by the caller so callers can share a consistent
|
|
1089
|
+
* timestamp across a batch.
|
|
1090
|
+
*/
|
|
1091
|
+
function matchesPruneFilter(memory, input, now) {
|
|
1092
|
+
if (input.entityId !== void 0 && memory.entityId !== input.entityId) return false;
|
|
1093
|
+
if (input.expired === true && !isExpired(memory, now)) return false;
|
|
1094
|
+
if (input.superseded === true && memory.supersededBy === null) return false;
|
|
1095
|
+
if (input.createdBefore !== void 0 && memory.createdAt.getTime() >= input.createdBefore.getTime()) return false;
|
|
1096
|
+
if (input.notAccessedSince !== void 0) {
|
|
1097
|
+
const last = memory.lastAccessedAt;
|
|
1098
|
+
if (last !== null && last.getTime() >= input.notAccessedSince.getTime()) return false;
|
|
1099
|
+
}
|
|
1100
|
+
if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
|
|
1101
|
+
if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
|
|
1102
|
+
if (input.maxImportance !== void 0 && IMPORTANCE_RANK[memory.importance] > IMPORTANCE_RANK[input.maxImportance]) return false;
|
|
1103
|
+
return true;
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Return `true` when a memory should participate in a duplicate scan,
|
|
1107
|
+
* given the filters on `input` and a shared `now` timestamp.
|
|
1108
|
+
*/
|
|
1109
|
+
function matchesDuplicateFilter(memory, input, now) {
|
|
1110
|
+
if (memory.entityId !== input.entityId) return false;
|
|
1111
|
+
if (input.includeSuperseded !== true && memory.supersededBy !== null) return false;
|
|
1112
|
+
if (input.includeExpired !== true && isExpired(memory, now)) return false;
|
|
1113
|
+
if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
|
|
1114
|
+
if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
|
|
1115
|
+
return true;
|
|
1116
|
+
}
|
|
1117
|
+
function matchesRecallFilter(memory, input, now) {
|
|
1118
|
+
if (memory.entityId !== input.entityId) return false;
|
|
1119
|
+
if (!input.includeSuperseded && memory.supersededBy !== null) return false;
|
|
1120
|
+
if (!input.includeExpired && isExpired(memory, now)) return false;
|
|
1121
|
+
if (input.types && !input.types.includes(memory.type)) return false;
|
|
1122
|
+
if (input.tags && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
|
|
1123
|
+
if (input.before && memory.createdAt >= input.before) return false;
|
|
1124
|
+
if (input.after && memory.createdAt <= input.after) return false;
|
|
1125
|
+
return true;
|
|
1126
|
+
}
|
|
1127
|
+
//#endregion
|
|
1128
|
+
//#region src/memory/in-memory.ts
|
|
1129
|
+
function cloneStoredMemory(memory) {
|
|
1130
|
+
return {
|
|
1131
|
+
...cloneMemory(memory),
|
|
1132
|
+
embedding: [...memory.embedding]
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
function cloneAuditEvent(event) {
|
|
1136
|
+
return {
|
|
1137
|
+
id: event.id,
|
|
1138
|
+
entityId: event.entityId,
|
|
1139
|
+
description: event.description,
|
|
1140
|
+
metadata: cloneJsonObject(event.metadata),
|
|
1141
|
+
timestamp: new Date(event.timestamp)
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
function createInMemoryStore() {
|
|
1145
|
+
const memories = /* @__PURE__ */ new Map();
|
|
1146
|
+
const auditLog = [];
|
|
1147
|
+
return {
|
|
1148
|
+
backend: "in-memory",
|
|
1149
|
+
async ensureSchema() {},
|
|
1150
|
+
async ensureEmbeddingCompatibility() {},
|
|
1151
|
+
async insertMemories(rows) {
|
|
1152
|
+
const inserted = [];
|
|
1153
|
+
for (const row of rows) {
|
|
1154
|
+
const stored = cloneStoredMemory(row);
|
|
1155
|
+
memories.set(stored.id, stored);
|
|
1156
|
+
inserted.push(cloneMemory(stored));
|
|
1157
|
+
}
|
|
1158
|
+
return inserted;
|
|
1159
|
+
},
|
|
1160
|
+
async vectorSearch(input) {
|
|
1161
|
+
const now = /* @__PURE__ */ new Date();
|
|
1162
|
+
const minScore = input.minScore ?? 0;
|
|
1163
|
+
return Array.from(memories.values()).filter((memory) => matchesRecallFilter(memory, input, now)).map((memory) => ({
|
|
1164
|
+
memory: cloneMemory(memory),
|
|
1165
|
+
vectorScore: Math.max(0, cosineSimilarity(memory.embedding, input.embedding))
|
|
1166
|
+
})).filter((candidate) => candidate.vectorScore >= minScore).sort((a, b) => b.vectorScore - a.vectorScore).slice(0, input.limit);
|
|
1167
|
+
},
|
|
1168
|
+
async lexicalSearch(input) {
|
|
1169
|
+
const now = /* @__PURE__ */ new Date();
|
|
1170
|
+
const scoreLexical = createLexicalScorer(input.query);
|
|
1171
|
+
return Array.from(memories.values()).filter((memory) => matchesRecallFilter(memory, input, now)).map((memory) => ({
|
|
1172
|
+
memory: cloneMemory(memory),
|
|
1173
|
+
lexicalScore: scoreLexical(memory.content)
|
|
1174
|
+
})).filter((candidate) => candidate.lexicalScore > 0).sort((a, b) => b.lexicalScore - a.lexicalScore).slice(0, input.limit);
|
|
1175
|
+
},
|
|
1176
|
+
async getMemoryEmbeddings(memoryIds) {
|
|
1177
|
+
const result = /* @__PURE__ */ new Map();
|
|
1178
|
+
for (const id of memoryIds) {
|
|
1179
|
+
const memory = memories.get(id);
|
|
1180
|
+
if (memory) result.set(id, [...memory.embedding]);
|
|
1181
|
+
}
|
|
1182
|
+
return result;
|
|
1183
|
+
},
|
|
1184
|
+
async markMemoriesAccessed(memoryIds) {
|
|
1185
|
+
const now = /* @__PURE__ */ new Date();
|
|
1186
|
+
for (const id of memoryIds) {
|
|
1187
|
+
const memory = memories.get(id);
|
|
1188
|
+
if (memory) {
|
|
1189
|
+
memory.lastAccessedAt = now;
|
|
1190
|
+
memory.accessCount += 1;
|
|
1191
|
+
memory.updatedAt = now;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
},
|
|
1195
|
+
async deleteMemory(entityId, memoryId) {
|
|
1196
|
+
const memory = memories.get(memoryId);
|
|
1197
|
+
if (!memory || memory.entityId !== entityId) return false;
|
|
1198
|
+
return memories.delete(memoryId);
|
|
1199
|
+
},
|
|
1200
|
+
async deleteMemoriesForEntity(entityId) {
|
|
1201
|
+
let deleted = 0;
|
|
1202
|
+
for (const [id, memory] of memories) if (memory.entityId === entityId) {
|
|
1203
|
+
memories.delete(id);
|
|
1204
|
+
deleted += 1;
|
|
1205
|
+
}
|
|
1206
|
+
return deleted;
|
|
951
1207
|
},
|
|
952
|
-
async
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1208
|
+
async prune(input) {
|
|
1209
|
+
const now = /* @__PURE__ */ new Date();
|
|
1210
|
+
const matched = [];
|
|
1211
|
+
for (const memory of memories.values()) if (matchesPruneFilter(memory, input, now)) matched.push(memory.id);
|
|
1212
|
+
const dryRun = input.dryRun === true;
|
|
1213
|
+
if (!dryRun) for (const id of matched) memories.delete(id);
|
|
1214
|
+
return {
|
|
1215
|
+
matchedCount: matched.length,
|
|
1216
|
+
deletedCount: dryRun ? 0 : matched.length,
|
|
1217
|
+
dryRun
|
|
1218
|
+
};
|
|
1219
|
+
},
|
|
1220
|
+
async findDuplicatePairs(input) {
|
|
1221
|
+
const threshold = input.threshold ?? .95;
|
|
1222
|
+
const limit = input.limit ?? 100;
|
|
1223
|
+
const now = /* @__PURE__ */ new Date();
|
|
1224
|
+
const candidates = Array.from(memories.values()).filter((memory) => matchesDuplicateFilter(memory, input, now));
|
|
1225
|
+
const pairs = [];
|
|
1226
|
+
for (let i = 0; i < candidates.length; i += 1) {
|
|
1227
|
+
const a = candidates[i];
|
|
1228
|
+
if (a === void 0) continue;
|
|
1229
|
+
for (let j = i + 1; j < candidates.length; j += 1) {
|
|
1230
|
+
const b = candidates[j];
|
|
1231
|
+
if (b === void 0) continue;
|
|
1232
|
+
const similarity = Math.max(0, Math.min(1, cosineSimilarity(a.embedding, b.embedding)));
|
|
1233
|
+
if (similarity >= threshold) pairs.push({
|
|
1234
|
+
a: cloneMemory(a),
|
|
1235
|
+
b: cloneMemory(b),
|
|
1236
|
+
similarity
|
|
1237
|
+
});
|
|
974
1238
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1239
|
+
}
|
|
1240
|
+
pairs.sort((x, y) => y.similarity - x.similarity);
|
|
1241
|
+
return pairs.slice(0, limit);
|
|
1242
|
+
},
|
|
1243
|
+
async addAuditEvents(events) {
|
|
1244
|
+
auditLog.push(...events.map(cloneAuditEvent));
|
|
978
1245
|
},
|
|
979
1246
|
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 }));
|
|
1247
|
+
const limit = input.limit ?? 50;
|
|
1248
|
+
return auditLog.map((event, idx) => ({
|
|
1249
|
+
event,
|
|
1250
|
+
idx
|
|
1251
|
+
})).filter(({ event }) => {
|
|
1252
|
+
if (event.entityId !== input.entityId) return false;
|
|
1253
|
+
if (input.before !== void 0 && event.timestamp.getTime() >= input.before.getTime()) return false;
|
|
1254
|
+
if (input.after !== void 0 && event.timestamp.getTime() <= input.after.getTime()) return false;
|
|
1255
|
+
return true;
|
|
1256
|
+
}).sort((a, b) => {
|
|
1257
|
+
const dt = b.event.timestamp.getTime() - a.event.timestamp.getTime();
|
|
1258
|
+
return dt === 0 ? b.idx - a.idx : dt;
|
|
1259
|
+
}).slice(0, limit).map(({ event }) => cloneAuditEvent(event));
|
|
998
1260
|
},
|
|
999
|
-
async
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
const now = /* @__PURE__ */ new Date();
|
|
1003
|
-
const allMemories = Array.from(memories.values());
|
|
1004
|
-
const selected = input?.entityId ? allMemories.filter((memory) => memory.entityId === input.entityId) : allMemories;
|
|
1005
|
-
const activeMemoryCount = selected.filter((memory) => !isExpired(memory, now) && memory.supersededBy === null).length;
|
|
1006
|
-
const expiredMemoryCount = selected.filter((memory) => isExpired(memory, now)).length;
|
|
1007
|
-
const supersededMemoryCount = selected.filter((memory) => memory.supersededBy !== null).length;
|
|
1008
|
-
if (input?.entityId) return {
|
|
1009
|
-
entityId: input.entityId,
|
|
1010
|
-
memoryCount: selected.length,
|
|
1011
|
-
activeMemoryCount,
|
|
1012
|
-
expiredMemoryCount,
|
|
1013
|
-
supersededMemoryCount
|
|
1014
|
-
};
|
|
1015
|
-
return {
|
|
1016
|
-
entityCount: new Set(allMemories.map((memory) => memory.entityId)).size,
|
|
1017
|
-
memoryCount: selected.length,
|
|
1018
|
-
activeMemoryCount,
|
|
1019
|
-
expiredMemoryCount,
|
|
1020
|
-
supersededMemoryCount
|
|
1021
|
-
};
|
|
1022
|
-
});
|
|
1261
|
+
async getMemory(entityId, memoryId) {
|
|
1262
|
+
const memory = memories.get(memoryId);
|
|
1263
|
+
return memory && memory.entityId === entityId ? cloneMemory(memory) : null;
|
|
1023
1264
|
},
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1265
|
+
async loadConsolidationTargets(entityId, ids) {
|
|
1266
|
+
return ids.flatMap((id) => {
|
|
1267
|
+
const memory = memories.get(id);
|
|
1268
|
+
if (!memory || memory.entityId !== entityId) return [];
|
|
1269
|
+
return [{
|
|
1270
|
+
id: memory.id,
|
|
1271
|
+
tags: [...memory.tags],
|
|
1272
|
+
supersededBy: memory.supersededBy
|
|
1273
|
+
}];
|
|
1029
1274
|
});
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
entityId
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
const
|
|
1045
|
-
for (const id of
|
|
1275
|
+
},
|
|
1276
|
+
async consolidate(input) {
|
|
1277
|
+
const survivor = memories.get(input.survivorId);
|
|
1278
|
+
const newlySuperseded = [];
|
|
1279
|
+
for (const id of input.supersededIds) {
|
|
1280
|
+
const memory = memories.get(id);
|
|
1281
|
+
if (!memory || memory.entityId !== input.entityId) continue;
|
|
1282
|
+
if (memory.supersededBy !== null) continue;
|
|
1283
|
+
memory.supersededBy = input.survivorId;
|
|
1284
|
+
memory.supersededAt = input.now;
|
|
1285
|
+
memory.updatedAt = input.now;
|
|
1286
|
+
newlySuperseded.push(memory.id);
|
|
1287
|
+
}
|
|
1288
|
+
if (survivor && input.mergeTags && newlySuperseded.length > 0) {
|
|
1289
|
+
const mergedTags = new Set(input.survivorTags);
|
|
1290
|
+
for (const id of newlySuperseded) {
|
|
1046
1291
|
const memory = memories.get(id);
|
|
1047
|
-
if (
|
|
1048
|
-
if (memory.supersededBy !== null) continue;
|
|
1049
|
-
losers.push(memory);
|
|
1050
|
-
}
|
|
1051
|
-
const now = /* @__PURE__ */ new Date();
|
|
1052
|
-
const newSupersededIds = [];
|
|
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
|
-
});
|
|
1292
|
+
if (memory) for (const tag of memory.tags) mergedTags.add(tag);
|
|
1062
1293
|
}
|
|
1063
|
-
if (
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
if (mergedTags.size !== survivor.tags.length) {
|
|
1067
|
-
survivor.tags = [...mergedTags];
|
|
1068
|
-
survivor.updatedAt = now;
|
|
1069
|
-
}
|
|
1294
|
+
if (mergedTags.size !== survivor.tags.length) {
|
|
1295
|
+
survivor.tags = [...mergedTags];
|
|
1296
|
+
survivor.updatedAt = input.now;
|
|
1070
1297
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1298
|
+
}
|
|
1299
|
+
if (input.auditEnabled) for (const id of newlySuperseded) auditLog.push({
|
|
1300
|
+
id: createEventId(),
|
|
1301
|
+
entityId: input.entityId,
|
|
1302
|
+
description: "memory.superseded",
|
|
1303
|
+
metadata: {
|
|
1304
|
+
memoryId: id,
|
|
1305
|
+
supersededBy: input.survivorId
|
|
1306
|
+
},
|
|
1307
|
+
timestamp: input.now
|
|
1308
|
+
});
|
|
1309
|
+
return { supersededIds: newlySuperseded };
|
|
1310
|
+
},
|
|
1311
|
+
async stats(input, now) {
|
|
1312
|
+
const allMemories = Array.from(memories.values());
|
|
1313
|
+
const selected = input?.entityId ? allMemories.filter((memory) => memory.entityId === input.entityId) : allMemories;
|
|
1314
|
+
const activeMemoryCount = selected.filter((memory) => !isExpired(memory, now) && memory.supersededBy === null).length;
|
|
1315
|
+
const expiredMemoryCount = selected.filter((memory) => isExpired(memory, now)).length;
|
|
1316
|
+
const supersededMemoryCount = selected.filter((memory) => memory.supersededBy !== null).length;
|
|
1317
|
+
if (input?.entityId) return {
|
|
1318
|
+
entityId: input.entityId,
|
|
1319
|
+
memoryCount: selected.length,
|
|
1320
|
+
activeMemoryCount,
|
|
1321
|
+
expiredMemoryCount,
|
|
1322
|
+
supersededMemoryCount
|
|
1323
|
+
};
|
|
1324
|
+
return {
|
|
1325
|
+
entityCount: new Set(allMemories.map((memory) => memory.entityId)).size,
|
|
1326
|
+
memoryCount: selected.length,
|
|
1327
|
+
activeMemoryCount,
|
|
1328
|
+
expiredMemoryCount,
|
|
1329
|
+
supersededMemoryCount
|
|
1330
|
+
};
|
|
1331
|
+
},
|
|
1332
|
+
async close() {
|
|
1333
|
+
memories.clear();
|
|
1334
|
+
auditLog.length = 0;
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
function createInMemoryClient(config) {
|
|
1339
|
+
return createMemoryClient(config, createInMemoryStore());
|
|
1079
1340
|
}
|
|
1080
1341
|
//#endregion
|
|
1081
1342
|
//#region src/db/queries/events.ts
|
|
@@ -1092,10 +1353,6 @@ async function listEvents(db, filter) {
|
|
|
1092
1353
|
function vectorLiteral(embedding) {
|
|
1093
1354
|
return `[${embedding.map(formatVectorComponent).join(",")}]`;
|
|
1094
1355
|
}
|
|
1095
|
-
function formatVectorComponent(value) {
|
|
1096
|
-
if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
|
|
1097
|
-
return Object.is(value, -0) ? "0" : value.toFixed(17);
|
|
1098
|
-
}
|
|
1099
1356
|
function timestampParam(value) {
|
|
1100
1357
|
return sql`${value.toISOString()}::timestamptz`;
|
|
1101
1358
|
}
|
|
@@ -1137,11 +1394,6 @@ function emptyMemoryStatsCounts() {
|
|
|
1137
1394
|
supersededMemoryCount: 0
|
|
1138
1395
|
};
|
|
1139
1396
|
}
|
|
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
1397
|
async function insertMemories(db, rows) {
|
|
1146
1398
|
if (rows.length === 0) return [];
|
|
1147
1399
|
return db.insert(memoriesTable).values(rows).returning();
|
|
@@ -1306,7 +1558,7 @@ async function setMemoryTags(db, params) {
|
|
|
1306
1558
|
updatedAt: params.now
|
|
1307
1559
|
}).where(and(eq(memoriesTable.entityId, params.entityId), eq(memoriesTable.id, params.memoryId)));
|
|
1308
1560
|
}
|
|
1309
|
-
async function
|
|
1561
|
+
async function markMemoriesAccessed(db, memoryIds) {
|
|
1310
1562
|
const now = /* @__PURE__ */ new Date();
|
|
1311
1563
|
if (memoryIds.length === 0) return;
|
|
1312
1564
|
await db.update(memoriesTable).set({
|
|
@@ -1402,73 +1654,80 @@ async function getInstallationMeta(db) {
|
|
|
1402
1654
|
return (await db.select().from(metaTable).where(eq(metaTable.key, "installation")).limit(1))[0] ?? null;
|
|
1403
1655
|
}
|
|
1404
1656
|
//#endregion
|
|
1405
|
-
//#region src/
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
...input.signal === void 0 ? {} : { signal: input.signal },
|
|
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;
|
|
1657
|
+
//#region src/memory/postgres-records.ts
|
|
1658
|
+
function toDate(value) {
|
|
1659
|
+
return value instanceof Date ? value : new Date(value);
|
|
1448
1660
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1661
|
+
function toNullableDate(value) {
|
|
1662
|
+
return value === null ? null : toDate(value);
|
|
1663
|
+
}
|
|
1664
|
+
function rowToMemory(row) {
|
|
1452
1665
|
return {
|
|
1453
1666
|
id: row.id,
|
|
1454
1667
|
entityId: row.entityId,
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1668
|
+
content: row.content,
|
|
1669
|
+
type: row.type,
|
|
1670
|
+
importance: row.importance,
|
|
1671
|
+
tags: row.tags,
|
|
1672
|
+
source: row.source,
|
|
1673
|
+
metadata: cloneJsonObject(row.metadata),
|
|
1674
|
+
confidence: row.confidence,
|
|
1675
|
+
embeddingModel: row.embeddingModel,
|
|
1676
|
+
embeddingDimensions: row.embeddingDimensions,
|
|
1677
|
+
supersededBy: row.supersededBy,
|
|
1678
|
+
supersededAt: toNullableDate(row.supersededAt),
|
|
1679
|
+
expiresAt: toNullableDate(row.expiresAt),
|
|
1680
|
+
lastAccessedAt: toNullableDate(row.lastAccessedAt),
|
|
1681
|
+
accessCount: row.accessCount,
|
|
1682
|
+
createdAt: toDate(row.createdAt),
|
|
1683
|
+
updatedAt: toDate(row.updatedAt)
|
|
1458
1684
|
};
|
|
1459
1685
|
}
|
|
1686
|
+
//#endregion
|
|
1687
|
+
//#region src/memory/postgres.ts
|
|
1460
1688
|
const IMPORTANCE_LEVELS = [
|
|
1461
1689
|
"low",
|
|
1462
1690
|
"normal",
|
|
1463
1691
|
"high",
|
|
1464
1692
|
"critical"
|
|
1465
1693
|
];
|
|
1694
|
+
const MIGRATION_ERROR_CODES = new Set([
|
|
1695
|
+
"42P01",
|
|
1696
|
+
"42703",
|
|
1697
|
+
"42704",
|
|
1698
|
+
"42883"
|
|
1699
|
+
]);
|
|
1700
|
+
function rowToAuditEvent(row) {
|
|
1701
|
+
return {
|
|
1702
|
+
id: row.id,
|
|
1703
|
+
entityId: row.entityId,
|
|
1704
|
+
description: row.description,
|
|
1705
|
+
metadata: cloneJsonObject(row.metadata),
|
|
1706
|
+
timestamp: row.timestamp
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1466
1709
|
function importanceCeilingLevels(max) {
|
|
1467
1710
|
return IMPORTANCE_LEVELS.filter((level) => IMPORTANCE_RANK[level] <= IMPORTANCE_RANK[max]);
|
|
1468
1711
|
}
|
|
1469
1712
|
function hasPostgresErrorCode(error, code) {
|
|
1470
1713
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
1471
1714
|
}
|
|
1715
|
+
function getPostgresErrorCode(error) {
|
|
1716
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
1717
|
+
}
|
|
1718
|
+
function normalizePostgresError(error) {
|
|
1719
|
+
if (error instanceof MnemocyteError) return error;
|
|
1720
|
+
const code = getPostgresErrorCode(error);
|
|
1721
|
+
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);
|
|
1722
|
+
return new MnemocyteError("Postgres operation failed.", "DB", error);
|
|
1723
|
+
}
|
|
1724
|
+
async function runPostgresOperation(operation) {
|
|
1725
|
+
try {
|
|
1726
|
+
return await operation();
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
throw normalizePostgresError(error);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1472
1731
|
function toPruneFilter(input) {
|
|
1473
1732
|
const filter = {};
|
|
1474
1733
|
if (input.entityId !== void 0) filter.entityId = input.entityId;
|
|
@@ -1481,195 +1740,76 @@ function toPruneFilter(input) {
|
|
|
1481
1740
|
if (input.maxImportance !== void 0) filter.maxImportanceLevels = importanceCeilingLevels(input.maxImportance);
|
|
1482
1741
|
return filter;
|
|
1483
1742
|
}
|
|
1484
|
-
function
|
|
1485
|
-
|
|
1743
|
+
function toMemoryRow(memory) {
|
|
1744
|
+
return {
|
|
1745
|
+
...memory,
|
|
1746
|
+
tags: [...memory.tags],
|
|
1747
|
+
metadata: cloneJsonObject(memory.metadata),
|
|
1748
|
+
embedding: [...memory.embedding]
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
function createPostgresStore(handle) {
|
|
1486
1752
|
let schemaValidated = false;
|
|
1487
1753
|
let schemaValidationPromise;
|
|
1488
|
-
function
|
|
1489
|
-
if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
|
|
1490
|
-
}
|
|
1491
|
-
async function ensureSchemaMatchesEmbedder() {
|
|
1754
|
+
async function ensureEmbeddingCompatibility(embedder) {
|
|
1492
1755
|
if (schemaValidated) return;
|
|
1493
|
-
if (!schemaValidationPromise) schemaValidationPromise =
|
|
1756
|
+
if (!schemaValidationPromise) schemaValidationPromise = validateEmbeddingCompatibility(embedder).catch((error) => {
|
|
1494
1757
|
schemaValidationPromise = void 0;
|
|
1495
1758
|
throw error;
|
|
1496
1759
|
});
|
|
1497
1760
|
await schemaValidationPromise;
|
|
1498
1761
|
}
|
|
1499
|
-
async function
|
|
1762
|
+
async function validateEmbeddingCompatibility(embedder) {
|
|
1500
1763
|
let meta;
|
|
1501
1764
|
try {
|
|
1502
1765
|
meta = await getInstallationMeta(handle.db);
|
|
1503
1766
|
} catch (error) {
|
|
1504
1767
|
if (hasPostgresErrorCode(error, "42P01")) throw new MnemocyteError("mnemocyte_meta is missing. Apply the v0.2.0 migration or render a dimension-specific initial migration before using the Postgres backend.", "MIGRATION", error);
|
|
1505
|
-
throw error;
|
|
1768
|
+
throw normalizePostgresError(error);
|
|
1506
1769
|
}
|
|
1507
1770
|
if (!meta) throw new MnemocyteError("mnemocyte_meta is missing the \"installation\" row. Apply the v0.2.0 migration or render a dimension-specific initial migration before using the Postgres backend.", "MIGRATION");
|
|
1508
|
-
if (meta.embeddingDimensions !==
|
|
1771
|
+
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");
|
|
1509
1772
|
schemaValidated = true;
|
|
1510
1773
|
}
|
|
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 }
|
|
1774
|
+
return {
|
|
1775
|
+
backend: "postgres",
|
|
1776
|
+
async ensureSchema() {},
|
|
1777
|
+
ensureEmbeddingCompatibility,
|
|
1778
|
+
async insertMemories(memories) {
|
|
1779
|
+
return runPostgresOperation(async () => {
|
|
1780
|
+
return (await insertMemories(handle.db, memories.map(toMemoryRow))).map(rowToMemory);
|
|
1531
1781
|
});
|
|
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
|
|
1782
|
+
},
|
|
1783
|
+
async vectorSearch(input) {
|
|
1784
|
+
return runPostgresOperation(async () => {
|
|
1785
|
+
return (await vectorSearch(handle.db, input)).map((row) => ({
|
|
1786
|
+
memory: rowToMemory(row),
|
|
1787
|
+
vectorScore: row.vectorScore
|
|
1788
|
+
}));
|
|
1558
1789
|
});
|
|
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
|
|
1790
|
+
},
|
|
1791
|
+
async lexicalSearch(input) {
|
|
1792
|
+
return runPostgresOperation(async () => {
|
|
1793
|
+
return (await lexicalSearch(handle.db, input)).map((row) => ({
|
|
1794
|
+
memory: rowToMemory(row),
|
|
1795
|
+
lexicalScore: row.lexicalScore
|
|
1598
1796
|
}));
|
|
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 }));
|
|
1797
|
+
});
|
|
1607
1798
|
},
|
|
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 }));
|
|
1799
|
+
async getMemoryEmbeddings(memoryIds) {
|
|
1800
|
+
return runPostgresOperation(() => getMemoryEmbeddings(handle.db, memoryIds));
|
|
1628
1801
|
},
|
|
1629
|
-
async
|
|
1630
|
-
return
|
|
1631
|
-
assertOpen();
|
|
1632
|
-
await ensureSchemaMatchesEmbedder();
|
|
1633
|
-
return buildContext({
|
|
1634
|
-
input,
|
|
1635
|
-
recall: (contextInput) => {
|
|
1636
|
-
const recallInput = contextInputToRecallInput(contextInput);
|
|
1637
|
-
return client.recall(input.signal === void 0 ? recallInput : {
|
|
1638
|
-
...recallInput,
|
|
1639
|
-
signal: input.signal
|
|
1640
|
-
});
|
|
1641
|
-
}
|
|
1642
|
-
});
|
|
1643
|
-
});
|
|
1802
|
+
async markMemoriesAccessed(memoryIds) {
|
|
1803
|
+
return runPostgresOperation(() => markMemoriesAccessed(handle.db, memoryIds));
|
|
1644
1804
|
},
|
|
1645
|
-
async
|
|
1646
|
-
return
|
|
1647
|
-
entityId: input.entityId,
|
|
1648
|
-
memoryId: input.memoryId
|
|
1649
|
-
}, async () => {
|
|
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 });
|
|
1656
|
-
});
|
|
1805
|
+
async deleteMemory(entityId, memoryId) {
|
|
1806
|
+
return runPostgresOperation(() => deleteMemory(handle.db, entityId, memoryId));
|
|
1657
1807
|
},
|
|
1658
|
-
async
|
|
1659
|
-
return
|
|
1660
|
-
assertOpen();
|
|
1661
|
-
assertNonEmptyString(input.entityId, "entityId");
|
|
1662
|
-
await ensureSchemaMatchesEmbedder();
|
|
1663
|
-
const deletedCount = await deleteMemoriesForEntity(handle.db, input.entityId);
|
|
1664
|
-
await recordAudit(input.entityId, "entity.cleared", { count: deletedCount });
|
|
1665
|
-
return deletedCount;
|
|
1666
|
-
}, (count) => ({ count })).then(() => void 0);
|
|
1808
|
+
async deleteMemoriesForEntity(entityId) {
|
|
1809
|
+
return runPostgresOperation(() => deleteMemoriesForEntity(handle.db, entityId));
|
|
1667
1810
|
},
|
|
1668
1811
|
async prune(input) {
|
|
1669
|
-
return
|
|
1670
|
-
assertOpen();
|
|
1671
|
-
validatePruneInput(input);
|
|
1672
|
-
await ensureSchemaMatchesEmbedder();
|
|
1812
|
+
return runPostgresOperation(async () => {
|
|
1673
1813
|
const filter = toPruneFilter(input);
|
|
1674
1814
|
if (input.dryRun === true) return {
|
|
1675
1815
|
matchedCount: await countPruneMatches(handle.db, filter),
|
|
@@ -1677,25 +1817,19 @@ function createPostgresClient(config, handle) {
|
|
|
1677
1817
|
dryRun: true
|
|
1678
1818
|
};
|
|
1679
1819
|
const deletedCount = await pruneMemories(handle.db, filter);
|
|
1680
|
-
if (deletedCount > 0 && input.entityId !== void 0) await recordAudit(input.entityId, "memory.pruned", { count: deletedCount });
|
|
1681
1820
|
return {
|
|
1682
1821
|
matchedCount: deletedCount,
|
|
1683
1822
|
deletedCount,
|
|
1684
1823
|
dryRun: false
|
|
1685
1824
|
};
|
|
1686
|
-
}
|
|
1825
|
+
});
|
|
1687
1826
|
},
|
|
1688
|
-
async
|
|
1689
|
-
return
|
|
1690
|
-
assertOpen();
|
|
1691
|
-
validateFindDuplicatesInput(input);
|
|
1692
|
-
await ensureSchemaMatchesEmbedder();
|
|
1693
|
-
const threshold = input.threshold ?? .95;
|
|
1694
|
-
const limit = input.limit ?? 100;
|
|
1827
|
+
async findDuplicatePairs(input) {
|
|
1828
|
+
return runPostgresOperation(async () => {
|
|
1695
1829
|
return (await findDuplicatePairs(handle.db, {
|
|
1696
1830
|
entityId: input.entityId,
|
|
1697
|
-
threshold,
|
|
1698
|
-
limit,
|
|
1831
|
+
threshold: input.threshold ?? .95,
|
|
1832
|
+
limit: input.limit ?? 100,
|
|
1699
1833
|
...input.types === void 0 ? {} : { types: input.types },
|
|
1700
1834
|
...input.tags === void 0 ? {} : { tags: input.tags },
|
|
1701
1835
|
...input.includeSuperseded === void 0 ? {} : { includeSuperseded: input.includeSuperseded },
|
|
@@ -1705,108 +1839,91 @@ function createPostgresClient(config, handle) {
|
|
|
1705
1839
|
b: rowToMemory(row.b),
|
|
1706
1840
|
similarity: Math.max(0, Math.min(1, row.similarity))
|
|
1707
1841
|
}));
|
|
1708
|
-
}
|
|
1842
|
+
});
|
|
1843
|
+
},
|
|
1844
|
+
async addAuditEvents(events) {
|
|
1845
|
+
return runPostgresOperation(async () => {
|
|
1846
|
+
for (const event of events) await insertEvent(handle.db, {
|
|
1847
|
+
id: event.id,
|
|
1848
|
+
entityId: event.entityId,
|
|
1849
|
+
description: event.description,
|
|
1850
|
+
metadata: cloneJsonObject(event.metadata),
|
|
1851
|
+
timestamp: event.timestamp
|
|
1852
|
+
});
|
|
1853
|
+
});
|
|
1709
1854
|
},
|
|
1710
1855
|
async listAuditLog(input) {
|
|
1711
|
-
return
|
|
1712
|
-
assertOpen();
|
|
1713
|
-
validateListAuditLogInput(input);
|
|
1714
|
-
await ensureSchemaMatchesEmbedder();
|
|
1856
|
+
return runPostgresOperation(async () => {
|
|
1715
1857
|
return (await listEvents(handle.db, {
|
|
1716
1858
|
entityId: input.entityId,
|
|
1717
1859
|
limit: input.limit ?? 50,
|
|
1718
1860
|
...input.before === void 0 ? {} : { before: input.before },
|
|
1719
1861
|
...input.after === void 0 ? {} : { after: input.after }
|
|
1720
1862
|
})).map(rowToAuditEvent);
|
|
1721
|
-
}, (result) => ({ count: result.length }));
|
|
1722
|
-
},
|
|
1723
|
-
async stats(input) {
|
|
1724
|
-
return observe(config, "postgres", "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
|
|
1725
|
-
assertOpen();
|
|
1726
|
-
await ensureSchemaMatchesEmbedder();
|
|
1727
|
-
const now = /* @__PURE__ */ new Date();
|
|
1728
|
-
if (input?.entityId) {
|
|
1729
|
-
const counts = await getEntityMemoryStatsCounts(handle.db, input.entityId, now);
|
|
1730
|
-
return {
|
|
1731
|
-
entityId: input.entityId,
|
|
1732
|
-
...counts
|
|
1733
|
-
};
|
|
1734
|
-
}
|
|
1735
|
-
return await getGlobalMemoryStatsCounts(handle.db, now);
|
|
1736
1863
|
});
|
|
1737
1864
|
},
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
await handle.close();
|
|
1865
|
+
async getMemory(entityId, memoryId) {
|
|
1866
|
+
return runPostgresOperation(async () => {
|
|
1867
|
+
const row = await getMemoryById(handle.db, entityId, memoryId);
|
|
1868
|
+
return row ? rowToMemory(row) : null;
|
|
1743
1869
|
});
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
return
|
|
1750
|
-
|
|
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) => {
|
|
1870
|
+
},
|
|
1871
|
+
async loadConsolidationTargets(entityId, ids) {
|
|
1872
|
+
return runPostgresOperation(() => loadConsolidationTargets(handle.db, entityId, ids));
|
|
1873
|
+
},
|
|
1874
|
+
async consolidate(input) {
|
|
1875
|
+
return runPostgresOperation(async () => {
|
|
1876
|
+
return { supersededIds: await handle.db.transaction(async (tx) => {
|
|
1770
1877
|
const updated = await markMemoriesSuperseded(tx, {
|
|
1771
|
-
survivorId:
|
|
1878
|
+
survivorId: input.survivorId,
|
|
1772
1879
|
entityId: input.entityId,
|
|
1773
|
-
ids:
|
|
1774
|
-
now
|
|
1880
|
+
ids: input.supersededIds,
|
|
1881
|
+
now: input.now
|
|
1775
1882
|
});
|
|
1776
|
-
const
|
|
1777
|
-
if (
|
|
1883
|
+
const ids = updated.map((row) => row.id);
|
|
1884
|
+
if (input.auditEnabled) for (const id of ids) await insertEvent(tx, {
|
|
1778
1885
|
id: createEventId(),
|
|
1779
1886
|
entityId: input.entityId,
|
|
1780
1887
|
description: "memory.superseded",
|
|
1781
1888
|
metadata: {
|
|
1782
1889
|
memoryId: id,
|
|
1783
|
-
supersededBy:
|
|
1890
|
+
supersededBy: input.survivorId
|
|
1784
1891
|
},
|
|
1785
|
-
timestamp: now
|
|
1892
|
+
timestamp: input.now
|
|
1786
1893
|
});
|
|
1787
|
-
if (input.mergeTags
|
|
1788
|
-
const mergedTags = new Set(
|
|
1894
|
+
if (input.mergeTags && updated.length > 0) {
|
|
1895
|
+
const mergedTags = new Set(input.survivorTags);
|
|
1789
1896
|
for (const row of updated) for (const tag of row.tags) mergedTags.add(tag);
|
|
1790
|
-
if (mergedTags.size !==
|
|
1897
|
+
if (mergedTags.size !== input.survivorTags.length) await setMemoryTags(tx, {
|
|
1791
1898
|
entityId: input.entityId,
|
|
1792
|
-
memoryId:
|
|
1899
|
+
memoryId: input.survivorId,
|
|
1793
1900
|
tags: [...mergedTags],
|
|
1794
|
-
now
|
|
1901
|
+
now: input.now
|
|
1795
1902
|
});
|
|
1796
1903
|
}
|
|
1904
|
+
return ids;
|
|
1905
|
+
}) };
|
|
1906
|
+
});
|
|
1907
|
+
},
|
|
1908
|
+
async stats(input, now) {
|
|
1909
|
+
return runPostgresOperation(async () => {
|
|
1910
|
+
if (input?.entityId) {
|
|
1911
|
+
const counts = await getEntityMemoryStatsCounts(handle.db, input.entityId, now);
|
|
1797
1912
|
return {
|
|
1798
|
-
|
|
1799
|
-
|
|
1913
|
+
entityId: input.entityId,
|
|
1914
|
+
...counts
|
|
1800
1915
|
};
|
|
1801
|
-
}
|
|
1802
|
-
return
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1916
|
+
}
|
|
1917
|
+
return await getGlobalMemoryStatsCounts(handle.db, now);
|
|
1918
|
+
});
|
|
1919
|
+
},
|
|
1920
|
+
async close() {
|
|
1921
|
+
return runPostgresOperation(() => handle.close());
|
|
1922
|
+
}
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
function createPostgresClient(config, handle) {
|
|
1926
|
+
return createMemoryClient(config, createPostgresStore(handle));
|
|
1810
1927
|
}
|
|
1811
1928
|
//#endregion
|
|
1812
1929
|
//#region src/client.ts
|
|
@@ -1824,8 +1941,9 @@ function createPostgresClient(config, handle) {
|
|
|
1824
1941
|
*
|
|
1825
1942
|
* @param config - Client configuration. {@link MnemocyteConfig.embedder} is required.
|
|
1826
1943
|
* @returns A {@link MnemocyteClient} instance.
|
|
1827
|
-
* @throws {MnemocyteError} With code `"CONFIG"` if `embedder
|
|
1828
|
-
*
|
|
1944
|
+
* @throws {MnemocyteError} With code `"CONFIG"` if `embedder`, retrieval
|
|
1945
|
+
* tuning, or the database URL is malformed, or `"VALIDATION"` if
|
|
1946
|
+
* `databaseUrl` is explicitly empty.
|
|
1829
1947
|
*
|
|
1830
1948
|
* @example Postgres-backed client
|
|
1831
1949
|
* ```ts
|
|
@@ -1837,7 +1955,8 @@ function createPostgresClient(config, handle) {
|
|
|
1837
1955
|
*/
|
|
1838
1956
|
function createMnemocyte(config) {
|
|
1839
1957
|
assertEmbedder(config.embedder);
|
|
1840
|
-
|
|
1958
|
+
validateRetrievalConfig(config.retrieval);
|
|
1959
|
+
if (config.databaseUrl !== void 0) {
|
|
1841
1960
|
assertNonEmptyString(config.databaseUrl, "databaseUrl");
|
|
1842
1961
|
return createPostgresClient(config, createDatabase(config.databaseUrl));
|
|
1843
1962
|
}
|