mnemocyte 0.1.5 → 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 +188 -14
- 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 +9 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +964 -806
- package/dist/index.mjs.map +1 -1
- package/dist/{types-QK_43oKJ.d.mts → types-gOPLre4V.d.mts} +63 -28
- package/dist/types-gOPLre4V.d.mts.map +1 -0
- package/migrations/0000_initial.sql.template +42 -0
- package/migrations/0001_add_mnemocyte_meta.sql +6 -0
- package/migrations/meta/0001_snapshot.json +309 -0
- package/migrations/meta/_journal.json +7 -0
- package/migrations/render-initial.mjs +36 -0
- package/package.json +5 -3
- package/dist/types-QK_43oKJ.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,26 +15,29 @@ 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,
|
|
21
|
-
memoriesTable: () => memoriesTable
|
|
27
|
+
memoriesTable: () => memoriesTable,
|
|
28
|
+
metaTable: () => metaTable
|
|
22
29
|
});
|
|
23
30
|
const vector = customType({
|
|
24
31
|
dataType(config) {
|
|
25
|
-
return `vector(${
|
|
32
|
+
return `vector(${config?.dimensions ?? 1536})`;
|
|
26
33
|
},
|
|
27
34
|
toDriver(value) {
|
|
28
|
-
return `[${value.map(formatVectorComponent
|
|
35
|
+
return `[${value.map(formatVectorComponent).join(",")}]`;
|
|
29
36
|
},
|
|
30
37
|
fromDriver(value) {
|
|
31
38
|
return value.slice(1, -1).split(",").filter(Boolean).map(Number);
|
|
32
39
|
}
|
|
33
40
|
});
|
|
34
|
-
function formatVectorComponent$1(value) {
|
|
35
|
-
if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
|
|
36
|
-
return Object.is(value, -0) ? "0" : value.toFixed(17);
|
|
37
|
-
}
|
|
38
41
|
const memoriesTable = pgTable("mnemocyte_memories", {
|
|
39
42
|
id: text("id").primaryKey(),
|
|
40
43
|
entityId: text("entity_id").notNull(),
|
|
@@ -43,7 +46,7 @@ const memoriesTable = pgTable("mnemocyte_memories", {
|
|
|
43
46
|
importance: text("importance").notNull().default("normal"),
|
|
44
47
|
tags: text("tags").array().notNull().default(sql`'{}'::text[]`),
|
|
45
48
|
source: text("source"),
|
|
46
|
-
metadata: jsonb("metadata").notNull().default({}),
|
|
49
|
+
metadata: jsonb("metadata").$type().notNull().default({}),
|
|
47
50
|
confidence: real("confidence").notNull().default(1),
|
|
48
51
|
embedding: vector("embedding", { dimensions: 1536 }),
|
|
49
52
|
embeddingModel: text("embedding_model").notNull(),
|
|
@@ -64,9 +67,13 @@ const eventsTable = pgTable("mnemocyte_events", {
|
|
|
64
67
|
id: text("id").primaryKey(),
|
|
65
68
|
entityId: text("entity_id").notNull(),
|
|
66
69
|
description: text("description").notNull(),
|
|
67
|
-
metadata: jsonb("metadata").notNull().default({}),
|
|
70
|
+
metadata: jsonb("metadata").$type().notNull().default({}),
|
|
68
71
|
timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow()
|
|
69
72
|
}, (table) => [index("mnemocyte_events_entity_time_idx").on(table.entityId, table.timestamp)]);
|
|
73
|
+
const metaTable = pgTable("mnemocyte_meta", {
|
|
74
|
+
key: text("key").primaryKey(),
|
|
75
|
+
embeddingDimensions: integer("embedding_dimensions").notNull()
|
|
76
|
+
});
|
|
70
77
|
//#endregion
|
|
71
78
|
//#region src/db/index.ts
|
|
72
79
|
function parseSslFromUrl(url) {
|
|
@@ -76,7 +83,12 @@ function parseSslFromUrl(url) {
|
|
|
76
83
|
if (mode === "verify-full") return "verify-full";
|
|
77
84
|
}
|
|
78
85
|
function createDatabase(databaseUrl) {
|
|
79
|
-
|
|
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
|
+
}
|
|
80
92
|
const ssl = parseSslFromUrl(url);
|
|
81
93
|
const isPooler = url.port === "6543" || url.searchParams.get("pgbouncer") === "true";
|
|
82
94
|
const client = postgres(databaseUrl, {
|
|
@@ -94,6 +106,203 @@ function createDatabase(databaseUrl) {
|
|
|
94
106
|
};
|
|
95
107
|
}
|
|
96
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
|
|
97
306
|
//#region src/context/formatter.ts
|
|
98
307
|
function escapeXml(value) {
|
|
99
308
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
@@ -245,6 +454,17 @@ async function observe(config, backend, operation, metadata, action, successMeta
|
|
|
245
454
|
throw error;
|
|
246
455
|
}
|
|
247
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
|
+
};
|
|
248
468
|
//#endregion
|
|
249
469
|
//#region src/resilience.ts
|
|
250
470
|
/**
|
|
@@ -331,37 +551,44 @@ function delay(ms, signal) {
|
|
|
331
551
|
}
|
|
332
552
|
});
|
|
333
553
|
}
|
|
554
|
+
function timeoutError(timeoutMs) {
|
|
555
|
+
return new MnemocyteError(`Provider call timed out after ${timeoutMs}ms.`, "TIMEOUT");
|
|
556
|
+
}
|
|
334
557
|
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
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`.
|
|
338
561
|
*/
|
|
339
|
-
function
|
|
340
|
-
if (timeoutMs <= 0 && !signal) return
|
|
562
|
+
function runAttempt(action, timeoutMs, signal) {
|
|
563
|
+
if (timeoutMs <= 0 && !signal) return action(void 0);
|
|
341
564
|
return new Promise((resolve, reject) => {
|
|
342
565
|
let settled = false;
|
|
566
|
+
const controller = new AbortController();
|
|
343
567
|
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
344
568
|
if (settled) return;
|
|
345
569
|
settled = true;
|
|
346
570
|
signal?.removeEventListener("abort", onAbort);
|
|
347
|
-
|
|
571
|
+
controller.abort(timeoutError(timeoutMs));
|
|
572
|
+
reject(timeoutError(timeoutMs));
|
|
348
573
|
}, timeoutMs) : void 0;
|
|
349
574
|
const onAbort = () => {
|
|
350
575
|
if (settled) return;
|
|
351
576
|
settled = true;
|
|
352
577
|
if (timer) clearTimeout(timer);
|
|
578
|
+
controller.abort(signal?.reason);
|
|
353
579
|
reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal?.reason));
|
|
354
580
|
};
|
|
355
581
|
if (signal) {
|
|
356
582
|
if (signal.aborted) {
|
|
357
583
|
if (timer) clearTimeout(timer);
|
|
358
584
|
settled = true;
|
|
585
|
+
controller.abort(signal.reason);
|
|
359
586
|
reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason));
|
|
360
587
|
return;
|
|
361
588
|
}
|
|
362
589
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
363
590
|
}
|
|
364
|
-
|
|
591
|
+
action(controller.signal).then((value) => {
|
|
365
592
|
if (settled) return;
|
|
366
593
|
settled = true;
|
|
367
594
|
if (timer) clearTimeout(timer);
|
|
@@ -400,7 +627,7 @@ async function withResilience(action, options = {}) {
|
|
|
400
627
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
401
628
|
throwIfAborted(externalSignal);
|
|
402
629
|
try {
|
|
403
|
-
return await
|
|
630
|
+
return await runAttempt(action, timeoutMs, externalSignal);
|
|
404
631
|
} catch (error) {
|
|
405
632
|
lastError = error;
|
|
406
633
|
if (error instanceof MnemocyteError && error.code === "ABORTED") throw error;
|
|
@@ -411,84 +638,11 @@ async function withResilience(action, options = {}) {
|
|
|
411
638
|
}
|
|
412
639
|
throw lastError;
|
|
413
640
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const IMPORTANCE_RANK = {
|
|
420
|
-
low: 0,
|
|
421
|
-
normal: 1,
|
|
422
|
-
high: 2,
|
|
423
|
-
critical: 3
|
|
424
|
-
};
|
|
425
|
-
function assertNonEmptyString(value, field) {
|
|
426
|
-
if (value.trim().length === 0) throw new MnemocyteError(`${field} must be a non-empty string.`, "VALIDATION");
|
|
427
|
-
}
|
|
428
|
-
function assertLimit(value) {
|
|
429
|
-
if (!Number.isInteger(value) || value < 1) throw new MnemocyteError("limit must be a positive integer.", "VALIDATION");
|
|
430
|
-
}
|
|
431
|
-
function assertMinScore(value) {
|
|
432
|
-
if (!Number.isFinite(value) || value < 0 || value > 1) throw new MnemocyteError("minScore must be a number between 0 and 1.", "VALIDATION");
|
|
433
|
-
}
|
|
434
|
-
function assertEmbedder(embedder) {
|
|
435
|
-
if (!embedder || typeof embedder.embed !== "function") throw new MnemocyteError("embedder with an embed function is required.", "CONFIG");
|
|
436
|
-
assertNonEmptyString(embedder.model, "embedder.model");
|
|
437
|
-
if (!Number.isInteger(embedder.dimensions) || embedder.dimensions < 1) throw new MnemocyteError("embedder.dimensions must be a positive integer.", "CONFIG");
|
|
438
|
-
}
|
|
439
|
-
function normalizeTags(tags) {
|
|
440
|
-
if (!tags) return [];
|
|
441
|
-
return [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))];
|
|
442
|
-
}
|
|
443
|
-
function createId() {
|
|
444
|
-
return `mem_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
445
|
-
}
|
|
446
|
-
/** Generate a unique audit-event id (`evt_*`). */
|
|
447
|
-
function createEventId() {
|
|
448
|
-
return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
449
|
-
}
|
|
450
|
-
function isExpired(memory, now) {
|
|
451
|
-
return memory.expiresAt !== null && memory.expiresAt.getTime() <= now.getTime();
|
|
452
|
-
}
|
|
453
|
-
function cloneMemory(memory) {
|
|
454
|
-
return {
|
|
455
|
-
...memory,
|
|
456
|
-
tags: [...memory.tags],
|
|
457
|
-
metadata: { ...memory.metadata },
|
|
458
|
-
supersededAt: memory.supersededAt ? new Date(memory.supersededAt) : null,
|
|
459
|
-
expiresAt: memory.expiresAt ? new Date(memory.expiresAt) : null,
|
|
460
|
-
lastAccessedAt: memory.lastAccessedAt ? new Date(memory.lastAccessedAt) : null,
|
|
461
|
-
createdAt: new Date(memory.createdAt),
|
|
462
|
-
updatedAt: new Date(memory.updatedAt)
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
function toDate(value) {
|
|
466
|
-
return value instanceof Date ? value : new Date(value);
|
|
467
|
-
}
|
|
468
|
-
function toNullableDate(value) {
|
|
469
|
-
return value === null ? null : toDate(value);
|
|
470
|
-
}
|
|
471
|
-
function rowToMemory(row) {
|
|
472
|
-
return {
|
|
473
|
-
id: row.id,
|
|
474
|
-
entityId: row.entityId,
|
|
475
|
-
content: row.content,
|
|
476
|
-
type: row.type,
|
|
477
|
-
importance: row.importance,
|
|
478
|
-
tags: row.tags,
|
|
479
|
-
source: row.source,
|
|
480
|
-
metadata: row.metadata,
|
|
481
|
-
confidence: row.confidence,
|
|
482
|
-
embeddingModel: row.embeddingModel,
|
|
483
|
-
embeddingDimensions: row.embeddingDimensions,
|
|
484
|
-
supersededBy: row.supersededBy,
|
|
485
|
-
supersededAt: toNullableDate(row.supersededAt),
|
|
486
|
-
expiresAt: toNullableDate(row.expiresAt),
|
|
487
|
-
lastAccessedAt: toNullableDate(row.lastAccessedAt),
|
|
488
|
-
accessCount: row.accessCount,
|
|
489
|
-
createdAt: toDate(row.createdAt),
|
|
490
|
-
updatedAt: toDate(row.updatedAt)
|
|
491
|
-
};
|
|
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");
|
|
492
646
|
}
|
|
493
647
|
async function embedOne(embedder, text, options = {}) {
|
|
494
648
|
let embeddings;
|
|
@@ -503,7 +657,7 @@ async function embedOne(embedder, text, options = {}) {
|
|
|
503
657
|
}
|
|
504
658
|
const embedding = embeddings[0];
|
|
505
659
|
if (!embedding) throw new MnemocyteError("Embedder returned no embedding.", "EMBEDDING");
|
|
506
|
-
|
|
660
|
+
validateEmbedding(embedding, embedder.dimensions);
|
|
507
661
|
return embedding;
|
|
508
662
|
}
|
|
509
663
|
async function embedMany(embedder, texts, options = {}) {
|
|
@@ -519,23 +673,67 @@ async function embedMany(embedder, texts, options = {}) {
|
|
|
519
673
|
throw new MnemocyteError("Failed to embed texts.", "EMBEDDING", error);
|
|
520
674
|
}
|
|
521
675
|
if (embeddings.length !== texts.length) throw new MnemocyteError(`Embedder returned ${embeddings.length} embeddings for ${texts.length} texts.`, "EMBEDDING");
|
|
522
|
-
for (const embedding of embeddings)
|
|
676
|
+
for (const embedding of embeddings) validateEmbedding(embedding, embedder.dimensions);
|
|
523
677
|
return embeddings;
|
|
524
678
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if (
|
|
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);
|
|
529
683
|
}
|
|
530
|
-
function
|
|
531
|
-
|
|
532
|
-
assertNonEmptyString(input.query, "query");
|
|
533
|
-
if (input.limit !== void 0) assertLimit(input.limit);
|
|
534
|
-
if (input.minScore !== void 0) assertMinScore(input.minScore);
|
|
684
|
+
function assertLimit(value) {
|
|
685
|
+
if (!Number.isInteger(value) || value < 1) throw new MnemocyteError("limit must be a positive integer.", "VALIDATION");
|
|
535
686
|
}
|
|
536
|
-
function
|
|
537
|
-
|
|
538
|
-
|
|
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
|
+
}
|
|
722
|
+
function validateRememberInput(input) {
|
|
723
|
+
assertNonEmptyString(input.entityId, "entityId");
|
|
724
|
+
assertNonEmptyString(input.content, "content");
|
|
725
|
+
if (input.metadata !== void 0) validateJsonObject(input.metadata);
|
|
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");
|
|
727
|
+
}
|
|
728
|
+
function validateRecallInput(input) {
|
|
729
|
+
assertNonEmptyString(input.entityId, "entityId");
|
|
730
|
+
assertNonEmptyString(input.query, "query");
|
|
731
|
+
if (input.limit !== void 0) assertLimit(input.limit);
|
|
732
|
+
if (input.minScore !== void 0) assertMinScore(input.minScore);
|
|
733
|
+
}
|
|
734
|
+
function contextInputToRecallInput(input) {
|
|
735
|
+
return {
|
|
736
|
+
entityId: input.entityId,
|
|
539
737
|
query: input.query,
|
|
540
738
|
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
541
739
|
...input.minScore === void 0 ? {} : { minScore: input.minScore },
|
|
@@ -555,27 +753,6 @@ function validatePruneInput(input) {
|
|
|
555
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");
|
|
556
754
|
}
|
|
557
755
|
/**
|
|
558
|
-
* Test whether `memory` matches a {@link PruneInput}. All specified
|
|
559
|
-
* selectors are AND-combined; unspecified selectors do not restrict.
|
|
560
|
-
*
|
|
561
|
-
* `now` is supplied by the caller so callers can share a consistent
|
|
562
|
-
* timestamp across a batch.
|
|
563
|
-
*/
|
|
564
|
-
function matchesPruneFilter(memory, input, now) {
|
|
565
|
-
if (input.entityId !== void 0 && memory.entityId !== input.entityId) return false;
|
|
566
|
-
if (input.expired === true && !isExpired(memory, now)) return false;
|
|
567
|
-
if (input.superseded === true && memory.supersededBy === null) return false;
|
|
568
|
-
if (input.createdBefore !== void 0 && memory.createdAt.getTime() >= input.createdBefore.getTime()) return false;
|
|
569
|
-
if (input.notAccessedSince !== void 0) {
|
|
570
|
-
const last = memory.lastAccessedAt;
|
|
571
|
-
if (last !== null && last.getTime() >= input.notAccessedSince.getTime()) return false;
|
|
572
|
-
}
|
|
573
|
-
if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
|
|
574
|
-
if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
|
|
575
|
-
if (input.maxImportance !== void 0 && IMPORTANCE_RANK[memory.importance] > IMPORTANCE_RANK[input.maxImportance]) return false;
|
|
576
|
-
return true;
|
|
577
|
-
}
|
|
578
|
-
/**
|
|
579
756
|
* Validate a {@link FindDuplicatesInput} and throw a `"VALIDATION"`
|
|
580
757
|
* {@link MnemocyteError} for malformed `threshold` / `limit` values.
|
|
581
758
|
*/
|
|
@@ -587,18 +764,6 @@ function validateFindDuplicatesInput(input) {
|
|
|
587
764
|
if (input.limit !== void 0) assertLimit(input.limit);
|
|
588
765
|
}
|
|
589
766
|
/**
|
|
590
|
-
* Return `true` when a memory should participate in a duplicate scan,
|
|
591
|
-
* given the filters on `input` and a shared `now` timestamp.
|
|
592
|
-
*/
|
|
593
|
-
function matchesDuplicateFilter(memory, input, now) {
|
|
594
|
-
if (memory.entityId !== input.entityId) return false;
|
|
595
|
-
if (input.includeSuperseded !== true && memory.supersededBy !== null) return false;
|
|
596
|
-
if (input.includeExpired !== true && isExpired(memory, now)) return false;
|
|
597
|
-
if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
|
|
598
|
-
if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
|
|
599
|
-
return true;
|
|
600
|
-
}
|
|
601
|
-
/**
|
|
602
767
|
* Validate a {@link ConsolidateInput} and throw a `"VALIDATION"`
|
|
603
768
|
* {@link MnemocyteError} for malformed inputs (empty `supersededIds`
|
|
604
769
|
* or a `survivorId` appearing in `supersededIds`).
|
|
@@ -618,274 +783,178 @@ function validateListAuditLogInput(input) {
|
|
|
618
783
|
assertNonEmptyString(input.entityId, "entityId");
|
|
619
784
|
if (input.limit !== void 0) assertLimit(input.limit);
|
|
620
785
|
}
|
|
621
|
-
function matchesRecallFilter(memory, input, now) {
|
|
622
|
-
if (memory.entityId !== input.entityId) return false;
|
|
623
|
-
if (!input.includeSuperseded && memory.supersededBy !== null) return false;
|
|
624
|
-
if (!input.includeExpired && isExpired(memory, now)) return false;
|
|
625
|
-
if (input.types && !input.types.includes(memory.type)) return false;
|
|
626
|
-
if (input.tags && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
|
|
627
|
-
if (input.before && memory.createdAt >= input.before) return false;
|
|
628
|
-
if (input.after && memory.createdAt <= input.after) return false;
|
|
629
|
-
return true;
|
|
630
|
-
}
|
|
631
786
|
//#endregion
|
|
632
|
-
//#region src/
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
normal: .5,
|
|
636
|
-
high: .8,
|
|
637
|
-
critical: 1
|
|
638
|
-
};
|
|
639
|
-
const DEFAULT_WEIGHTS = {
|
|
640
|
-
vector: .55,
|
|
641
|
-
lexical: .2,
|
|
642
|
-
recency: .1,
|
|
643
|
-
confidence: .05,
|
|
644
|
-
access: .05,
|
|
645
|
-
importance: .05
|
|
646
|
-
};
|
|
647
|
-
const DEFAULT_RECENCY_HALF_LIFE_DAYS = 90;
|
|
648
|
-
const DEFAULT_ACCESS_SATURATION = 10;
|
|
649
|
-
function cosineSimilarity(a, b) {
|
|
650
|
-
let dot = 0;
|
|
651
|
-
let aMagnitude = 0;
|
|
652
|
-
let bMagnitude = 0;
|
|
653
|
-
for (let i = 0; i < a.length; i += 1) {
|
|
654
|
-
const av = a[i] ?? 0;
|
|
655
|
-
const bv = b[i] ?? 0;
|
|
656
|
-
dot += av * bv;
|
|
657
|
-
aMagnitude += av * av;
|
|
658
|
-
bMagnitude += bv * bv;
|
|
659
|
-
}
|
|
660
|
-
if (aMagnitude === 0 || bMagnitude === 0) return 0;
|
|
661
|
-
return dot / (Math.sqrt(aMagnitude) * Math.sqrt(bMagnitude));
|
|
662
|
-
}
|
|
663
|
-
function tokenizeQuery(query) {
|
|
664
|
-
return query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
787
|
+
//#region src/memory/client-core.ts
|
|
788
|
+
function isPositionalRememberManyInput(input) {
|
|
789
|
+
return Array.isArray(input);
|
|
665
790
|
}
|
|
666
|
-
function
|
|
667
|
-
if (terms.length === 0) return 0;
|
|
668
|
-
const lowerContent = content.toLowerCase();
|
|
669
|
-
return terms.filter((term) => lowerContent.includes(term)).length / terms.length;
|
|
670
|
-
}
|
|
671
|
-
function createLexicalScorer(query) {
|
|
672
|
-
const terms = tokenizeQuery(query);
|
|
673
|
-
return (content) => lexicalScoreWithTerms(content, terms);
|
|
674
|
-
}
|
|
675
|
-
function clampScore(value) {
|
|
676
|
-
if (!Number.isFinite(value)) return 0;
|
|
677
|
-
return Math.max(0, Math.min(1, value));
|
|
678
|
-
}
|
|
679
|
-
function normalizeWeights(weights) {
|
|
680
|
-
const merged = {
|
|
681
|
-
...DEFAULT_WEIGHTS,
|
|
682
|
-
...weights
|
|
683
|
-
};
|
|
684
|
-
const total = merged.vector + merged.lexical + merged.recency + merged.confidence + merged.access + merged.importance;
|
|
685
|
-
if (total <= 0) return DEFAULT_WEIGHTS;
|
|
791
|
+
function providerOptions(config, signal) {
|
|
686
792
|
return {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
recency: merged.recency / total,
|
|
690
|
-
confidence: merged.confidence / total,
|
|
691
|
-
access: merged.access / total,
|
|
692
|
-
importance: merged.importance / total
|
|
793
|
+
...signal === void 0 ? {} : { signal },
|
|
794
|
+
...config.provider === void 0 ? {} : { resilience: config.provider }
|
|
693
795
|
};
|
|
694
796
|
}
|
|
695
|
-
function
|
|
797
|
+
function createStoredMemory(config, input, embedding, now) {
|
|
696
798
|
return {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
|
700
818
|
};
|
|
701
819
|
}
|
|
702
|
-
function
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
+
};
|
|
710
828
|
}
|
|
711
|
-
function
|
|
712
|
-
const { weights } = config;
|
|
713
|
-
const recency = recencyScore(memory.createdAt, config.recencyHalfLifeDays);
|
|
714
|
-
const confidence = clampScore(memory.confidence);
|
|
715
|
-
const access = accessScore(memory.accessCount, config.accessSaturation);
|
|
716
|
-
const importance = IMPORTANCE_SCORES[memory.importance];
|
|
717
|
-
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) {
|
|
718
830
|
return {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
vector: clampScore(vector),
|
|
723
|
-
lexical: clampScore(lexical),
|
|
724
|
-
recency,
|
|
725
|
-
confidence,
|
|
726
|
-
access,
|
|
727
|
-
importance
|
|
728
|
-
},
|
|
729
|
-
explanation: input.explain ? {
|
|
730
|
-
vectorScore: clampScore(vector),
|
|
731
|
-
lexicalScore: clampScore(lexical),
|
|
732
|
-
recencyScore: recency,
|
|
733
|
-
confidenceScore: confidence,
|
|
734
|
-
accessScore: access,
|
|
735
|
-
importanceScore: importance,
|
|
736
|
-
importanceBoost: importance * weights.importance,
|
|
737
|
-
weights,
|
|
738
|
-
finalScore: score
|
|
739
|
-
} : null
|
|
831
|
+
a: cloneMemory(pair.a),
|
|
832
|
+
b: cloneMemory(pair.b),
|
|
833
|
+
similarity: Math.max(0, Math.min(1, pair.similarity))
|
|
740
834
|
};
|
|
741
835
|
}
|
|
742
|
-
|
|
743
|
-
//#region src/memory/in-memory.ts
|
|
744
|
-
function createInMemoryClient(config) {
|
|
745
|
-
const memories = /* @__PURE__ */ new Map();
|
|
746
|
-
const auditLog = [];
|
|
836
|
+
function createMemoryClient(config, store) {
|
|
747
837
|
let closed = false;
|
|
748
838
|
function assertOpen() {
|
|
749
839
|
if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
|
|
750
840
|
}
|
|
751
|
-
function
|
|
752
|
-
|
|
753
|
-
auditLog.push({
|
|
754
|
-
id: createEventId(),
|
|
755
|
-
entityId,
|
|
756
|
-
description,
|
|
757
|
-
metadata,
|
|
758
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
759
|
-
});
|
|
841
|
+
async function ensureSchema() {
|
|
842
|
+
await store.ensureSchema();
|
|
760
843
|
}
|
|
761
|
-
function
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
}
|
|
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 {}
|
|
769
852
|
}
|
|
770
853
|
async function remember(input) {
|
|
771
|
-
|
|
854
|
+
const preparedInput = {
|
|
855
|
+
...input,
|
|
856
|
+
metadata: cloneJsonObject(input.metadata ?? {})
|
|
857
|
+
};
|
|
858
|
+
return observe(config, store.backend, "remember", { entityId: preparedInput.entityId }, async () => {
|
|
772
859
|
assertOpen();
|
|
773
|
-
validateRememberInput(
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
const memory = {
|
|
780
|
-
id: createId(),
|
|
781
|
-
entityId: input.entityId,
|
|
782
|
-
content: input.content,
|
|
783
|
-
type: input.type ?? "fact",
|
|
784
|
-
importance: input.importance ?? "normal",
|
|
785
|
-
tags: normalizeTags(input.tags),
|
|
786
|
-
source: input.source ?? null,
|
|
787
|
-
metadata: input.metadata ?? {},
|
|
788
|
-
confidence: input.confidence ?? 1,
|
|
789
|
-
embeddingModel: config.embedder.model,
|
|
790
|
-
embeddingDimensions: config.embedder.dimensions,
|
|
791
|
-
supersededBy: null,
|
|
792
|
-
supersededAt: null,
|
|
793
|
-
expiresAt: input.expiresAt ?? null,
|
|
794
|
-
lastAccessedAt: null,
|
|
795
|
-
accessCount: 0,
|
|
796
|
-
createdAt: now,
|
|
797
|
-
updatedAt: now,
|
|
798
|
-
embedding
|
|
799
|
-
};
|
|
800
|
-
memories.set(memory.id, memory);
|
|
801
|
-
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", {
|
|
802
866
|
memoryId: memory.id,
|
|
803
867
|
type: memory.type,
|
|
804
868
|
importance: memory.importance
|
|
805
|
-
});
|
|
869
|
+
})]);
|
|
806
870
|
return cloneMemory(memory);
|
|
807
871
|
}, (memory) => ({
|
|
808
872
|
memoryId: memory.id,
|
|
809
873
|
count: 1
|
|
810
874
|
}));
|
|
811
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
|
+
}
|
|
812
901
|
const client = {
|
|
813
902
|
remember,
|
|
814
|
-
|
|
815
|
-
return observe(config, "in-memory", "rememberMany", { count: inputs.length }, async () => {
|
|
816
|
-
assertOpen();
|
|
817
|
-
if (inputs.length === 0) return [];
|
|
818
|
-
for (const input of inputs) validateRememberInput(input);
|
|
819
|
-
const embeddings = await embedMany(config.embedder, inputs.map((i) => i.content), {
|
|
820
|
-
...inputs[0]?.signal === void 0 ? {} : { signal: inputs[0].signal },
|
|
821
|
-
...config.provider === void 0 ? {} : { resilience: config.provider }
|
|
822
|
-
});
|
|
823
|
-
const now = /* @__PURE__ */ new Date();
|
|
824
|
-
const result = [];
|
|
825
|
-
for (let i = 0; i < inputs.length; i++) {
|
|
826
|
-
const input = inputs[i];
|
|
827
|
-
const memory = {
|
|
828
|
-
id: createId(),
|
|
829
|
-
entityId: input.entityId,
|
|
830
|
-
content: input.content,
|
|
831
|
-
type: input.type ?? "fact",
|
|
832
|
-
importance: input.importance ?? "normal",
|
|
833
|
-
tags: normalizeTags(input.tags),
|
|
834
|
-
source: input.source ?? null,
|
|
835
|
-
metadata: input.metadata ?? {},
|
|
836
|
-
confidence: input.confidence ?? 1,
|
|
837
|
-
embeddingModel: config.embedder.model,
|
|
838
|
-
embeddingDimensions: config.embedder.dimensions,
|
|
839
|
-
supersededBy: null,
|
|
840
|
-
supersededAt: null,
|
|
841
|
-
expiresAt: input.expiresAt ?? null,
|
|
842
|
-
lastAccessedAt: null,
|
|
843
|
-
accessCount: 0,
|
|
844
|
-
createdAt: now,
|
|
845
|
-
updatedAt: now,
|
|
846
|
-
embedding: embeddings[i]
|
|
847
|
-
};
|
|
848
|
-
memories.set(memory.id, memory);
|
|
849
|
-
recordAudit(memory.entityId, "memory.created", {
|
|
850
|
-
memoryId: memory.id,
|
|
851
|
-
type: memory.type,
|
|
852
|
-
importance: memory.importance
|
|
853
|
-
});
|
|
854
|
-
result.push(cloneMemory(memory));
|
|
855
|
-
}
|
|
856
|
-
return result;
|
|
857
|
-
}, (result) => ({ count: result.length }));
|
|
858
|
-
},
|
|
903
|
+
rememberMany,
|
|
859
904
|
async recall(input) {
|
|
860
|
-
return observe(config,
|
|
905
|
+
return observe(config, store.backend, "recall", { entityId: input.entityId }, async () => {
|
|
861
906
|
assertOpen();
|
|
862
907
|
validateRecallInput(input);
|
|
863
908
|
const limit = input.limit ?? config.defaults?.limit ?? 10;
|
|
864
909
|
const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
|
|
865
910
|
assertLimit(limit);
|
|
866
911
|
assertMinScore(minScore);
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
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);
|
|
872
916
|
const scoreLexical = createLexicalScorer(input.query);
|
|
873
917
|
const scoringConfig = createScoringConfig(config.retrieval);
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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;
|
|
881
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
|
+
});
|
|
882
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));
|
|
883
951
|
return scored;
|
|
884
952
|
}, (result) => ({ count: result.length }));
|
|
885
953
|
},
|
|
886
954
|
async buildContext(input) {
|
|
887
|
-
return observe(config,
|
|
955
|
+
return observe(config, store.backend, "buildContext", { entityId: input.entityId }, async () => {
|
|
888
956
|
assertOpen();
|
|
957
|
+
validateBuildContextInput(input);
|
|
889
958
|
return buildContext({
|
|
890
959
|
input,
|
|
891
960
|
recall: (contextInput) => {
|
|
@@ -899,178 +968,375 @@ function createInMemoryClient(config) {
|
|
|
899
968
|
});
|
|
900
969
|
},
|
|
901
970
|
async forget(input) {
|
|
902
|
-
return observe(config,
|
|
971
|
+
return observe(config, store.backend, "forget", {
|
|
903
972
|
entityId: input.entityId,
|
|
904
973
|
memoryId: input.memoryId
|
|
905
974
|
}, async () => {
|
|
906
975
|
assertOpen();
|
|
907
976
|
assertNonEmptyString(input.entityId, "entityId");
|
|
908
977
|
assertNonEmptyString(input.memoryId, "memoryId");
|
|
909
|
-
|
|
910
|
-
if (!
|
|
911
|
-
|
|
912
|
-
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 })]);
|
|
913
981
|
});
|
|
914
982
|
},
|
|
915
983
|
async forgetAll(input) {
|
|
916
|
-
return observe(config,
|
|
984
|
+
return observe(config, store.backend, "forgetAll", { entityId: input.entityId }, async () => {
|
|
917
985
|
assertOpen();
|
|
918
986
|
assertNonEmptyString(input.entityId, "entityId");
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
}
|
|
924
|
-
recordAudit(input.entityId, "entity.cleared", { count: deleted });
|
|
925
|
-
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;
|
|
926
991
|
}, (count) => ({ count })).then(() => void 0);
|
|
927
992
|
},
|
|
928
993
|
async prune(input) {
|
|
929
|
-
return observe(config,
|
|
994
|
+
return observe(config, store.backend, "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
|
|
930
995
|
assertOpen();
|
|
931
996
|
validatePruneInput(input);
|
|
932
|
-
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
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
|
+
});
|
|
940
1061
|
return {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1062
|
+
survivorId: survivor.id,
|
|
1063
|
+
supersededCount: result.supersededIds.length,
|
|
1064
|
+
supersededIds: result.supersededIds
|
|
944
1065
|
};
|
|
945
|
-
}, (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;
|
|
946
1207
|
},
|
|
947
|
-
async
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
+
});
|
|
969
1238
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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));
|
|
973
1245
|
},
|
|
974
1246
|
async listAuditLog(input) {
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
const dt = b.event.timestamp.getTime() - a.event.timestamp.getTime();
|
|
989
|
-
if (dt !== 0) return dt;
|
|
990
|
-
return b.idx - a.idx;
|
|
991
|
-
}).slice(0, limit).map(({ event }) => cloneAuditEvent(event));
|
|
992
|
-
}, (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));
|
|
993
1260
|
},
|
|
994
|
-
async
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
const now = /* @__PURE__ */ new Date();
|
|
998
|
-
const allMemories = Array.from(memories.values());
|
|
999
|
-
const selected = input?.entityId ? allMemories.filter((memory) => memory.entityId === input.entityId) : allMemories;
|
|
1000
|
-
const activeMemoryCount = selected.filter((memory) => !isExpired(memory, now) && memory.supersededBy === null).length;
|
|
1001
|
-
const expiredMemoryCount = selected.filter((memory) => isExpired(memory, now)).length;
|
|
1002
|
-
const supersededMemoryCount = selected.filter((memory) => memory.supersededBy !== null).length;
|
|
1003
|
-
if (input?.entityId) return {
|
|
1004
|
-
entityId: input.entityId,
|
|
1005
|
-
memoryCount: selected.length,
|
|
1006
|
-
activeMemoryCount,
|
|
1007
|
-
expiredMemoryCount,
|
|
1008
|
-
supersededMemoryCount
|
|
1009
|
-
};
|
|
1010
|
-
return {
|
|
1011
|
-
entityCount: new Set(allMemories.map((memory) => memory.entityId)).size,
|
|
1012
|
-
memoryCount: selected.length,
|
|
1013
|
-
activeMemoryCount,
|
|
1014
|
-
expiredMemoryCount,
|
|
1015
|
-
supersededMemoryCount
|
|
1016
|
-
};
|
|
1017
|
-
});
|
|
1261
|
+
async getMemory(entityId, memoryId) {
|
|
1262
|
+
const memory = memories.get(memoryId);
|
|
1263
|
+
return memory && memory.entityId === entityId ? cloneMemory(memory) : null;
|
|
1018
1264
|
},
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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
|
+
}];
|
|
1024
1274
|
});
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
entityId
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
const
|
|
1040
|
-
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) {
|
|
1041
1291
|
const memory = memories.get(id);
|
|
1042
|
-
if (
|
|
1043
|
-
if (memory.supersededBy !== null) continue;
|
|
1044
|
-
losers.push(memory);
|
|
1292
|
+
if (memory) for (const tag of memory.tags) mergedTags.add(tag);
|
|
1045
1293
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
loser.supersededBy = survivor.id;
|
|
1050
|
-
loser.supersededAt = now;
|
|
1051
|
-
loser.updatedAt = now;
|
|
1052
|
-
newSupersededIds.push(loser.id);
|
|
1053
|
-
recordAudit(input.entityId, "memory.superseded", {
|
|
1054
|
-
memoryId: loser.id,
|
|
1055
|
-
supersededBy: survivor.id
|
|
1056
|
-
});
|
|
1057
|
-
}
|
|
1058
|
-
if (input.mergeTags !== false && losers.length > 0) {
|
|
1059
|
-
const mergedTags = new Set(survivor.tags);
|
|
1060
|
-
for (const loser of losers) for (const tag of loser.tags) mergedTags.add(tag);
|
|
1061
|
-
if (mergedTags.size !== survivor.tags.length) {
|
|
1062
|
-
survivor.tags = [...mergedTags];
|
|
1063
|
-
survivor.updatedAt = now;
|
|
1064
|
-
}
|
|
1294
|
+
if (mergedTags.size !== survivor.tags.length) {
|
|
1295
|
+
survivor.tags = [...mergedTags];
|
|
1296
|
+
survivor.updatedAt = input.now;
|
|
1065
1297
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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());
|
|
1074
1340
|
}
|
|
1075
1341
|
//#endregion
|
|
1076
1342
|
//#region src/db/queries/events.ts
|
|
@@ -1087,10 +1353,6 @@ async function listEvents(db, filter) {
|
|
|
1087
1353
|
function vectorLiteral(embedding) {
|
|
1088
1354
|
return `[${embedding.map(formatVectorComponent).join(",")}]`;
|
|
1089
1355
|
}
|
|
1090
|
-
function formatVectorComponent(value) {
|
|
1091
|
-
if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
|
|
1092
|
-
return Object.is(value, -0) ? "0" : value.toFixed(17);
|
|
1093
|
-
}
|
|
1094
1356
|
function timestampParam(value) {
|
|
1095
1357
|
return sql`${value.toISOString()}::timestamptz`;
|
|
1096
1358
|
}
|
|
@@ -1132,11 +1394,6 @@ function emptyMemoryStatsCounts() {
|
|
|
1132
1394
|
supersededMemoryCount: 0
|
|
1133
1395
|
};
|
|
1134
1396
|
}
|
|
1135
|
-
async function insertMemory(db, row) {
|
|
1136
|
-
const inserted = (await db.insert(memoriesTable).values(row).returning())[0];
|
|
1137
|
-
if (!inserted) throw new MnemocyteError("Memory insert returned no rows.", "DB");
|
|
1138
|
-
return inserted;
|
|
1139
|
-
}
|
|
1140
1397
|
async function insertMemories(db, rows) {
|
|
1141
1398
|
if (rows.length === 0) return [];
|
|
1142
1399
|
return db.insert(memoriesTable).values(rows).returning();
|
|
@@ -1301,7 +1558,7 @@ async function setMemoryTags(db, params) {
|
|
|
1301
1558
|
updatedAt: params.now
|
|
1302
1559
|
}).where(and(eq(memoriesTable.entityId, params.entityId), eq(memoriesTable.id, params.memoryId)));
|
|
1303
1560
|
}
|
|
1304
|
-
async function
|
|
1561
|
+
async function markMemoriesAccessed(db, memoryIds) {
|
|
1305
1562
|
const now = /* @__PURE__ */ new Date();
|
|
1306
1563
|
if (memoryIds.length === 0) return;
|
|
1307
1564
|
await db.update(memoriesTable).set({
|
|
@@ -1393,71 +1650,84 @@ async function lexicalSearch(db, input) {
|
|
|
1393
1650
|
LIMIT ${input.limit}
|
|
1394
1651
|
`);
|
|
1395
1652
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
async function hybridRecall(input) {
|
|
1399
|
-
const queryEmbedding = await embedOne(input.embedder, input.input.query, {
|
|
1400
|
-
...input.signal === void 0 ? {} : { signal: input.signal },
|
|
1401
|
-
...input.resilience === void 0 ? {} : { resilience: input.resilience }
|
|
1402
|
-
});
|
|
1403
|
-
const candidateMultiplier = input.retrieval?.candidateMultiplier ?? 3;
|
|
1404
|
-
const candidateLimit = Math.max(input.limit, input.limit * candidateMultiplier);
|
|
1405
|
-
const scoreLexical = createLexicalScorer(input.input.query);
|
|
1406
|
-
const scoringConfig = createScoringConfig(input.retrieval);
|
|
1407
|
-
const [vectorRows, lexicalRows] = await Promise.all([vectorSearch(input.db, {
|
|
1408
|
-
...input.input,
|
|
1409
|
-
embedding: queryEmbedding,
|
|
1410
|
-
limit: candidateLimit,
|
|
1411
|
-
minScore: 0
|
|
1412
|
-
}), lexicalSearch(input.db, {
|
|
1413
|
-
...input.input,
|
|
1414
|
-
limit: candidateLimit
|
|
1415
|
-
})]);
|
|
1416
|
-
const merged = /* @__PURE__ */ new Map();
|
|
1417
|
-
for (const row of vectorRows) merged.set(row.id, {
|
|
1418
|
-
row,
|
|
1419
|
-
vectorScore: row.vectorScore,
|
|
1420
|
-
lexicalScore: scoreLexical(row.content)
|
|
1421
|
-
});
|
|
1422
|
-
const lexicalOnlyRows = lexicalRows.filter((row) => !merged.has(row.id));
|
|
1423
|
-
const lexicalOnlyEmbeddings = await getMemoryEmbeddings(input.db, lexicalOnlyRows.map((row) => row.id));
|
|
1424
|
-
for (const row of lexicalRows) {
|
|
1425
|
-
const existing = merged.get(row.id);
|
|
1426
|
-
if (existing) existing.lexicalScore = row.lexicalScore;
|
|
1427
|
-
else {
|
|
1428
|
-
const embedding = lexicalOnlyEmbeddings.get(row.id);
|
|
1429
|
-
const sim = embedding ? cosineSimilarity(embedding, queryEmbedding) : 0;
|
|
1430
|
-
merged.set(row.id, {
|
|
1431
|
-
row,
|
|
1432
|
-
vectorScore: Math.max(0, sim),
|
|
1433
|
-
lexicalScore: row.lexicalScore
|
|
1434
|
-
});
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
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);
|
|
1438
|
-
await markMemoryAccessed(input.db, scored.map((memory) => memory.id));
|
|
1439
|
-
return scored;
|
|
1653
|
+
async function getInstallationMeta(db) {
|
|
1654
|
+
return (await db.select().from(metaTable).where(eq(metaTable.key, "installation")).limit(1))[0] ?? null;
|
|
1440
1655
|
}
|
|
1441
1656
|
//#endregion
|
|
1442
|
-
//#region src/memory/postgres.ts
|
|
1443
|
-
function
|
|
1657
|
+
//#region src/memory/postgres-records.ts
|
|
1658
|
+
function toDate(value) {
|
|
1659
|
+
return value instanceof Date ? value : new Date(value);
|
|
1660
|
+
}
|
|
1661
|
+
function toNullableDate(value) {
|
|
1662
|
+
return value === null ? null : toDate(value);
|
|
1663
|
+
}
|
|
1664
|
+
function rowToMemory(row) {
|
|
1444
1665
|
return {
|
|
1445
1666
|
id: row.id,
|
|
1446
1667
|
entityId: row.entityId,
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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)
|
|
1450
1684
|
};
|
|
1451
1685
|
}
|
|
1686
|
+
//#endregion
|
|
1687
|
+
//#region src/memory/postgres.ts
|
|
1452
1688
|
const IMPORTANCE_LEVELS = [
|
|
1453
1689
|
"low",
|
|
1454
1690
|
"normal",
|
|
1455
1691
|
"high",
|
|
1456
1692
|
"critical"
|
|
1457
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
|
+
}
|
|
1458
1709
|
function importanceCeilingLevels(max) {
|
|
1459
1710
|
return IMPORTANCE_LEVELS.filter((level) => IMPORTANCE_RANK[level] <= IMPORTANCE_RANK[max]);
|
|
1460
1711
|
}
|
|
1712
|
+
function hasPostgresErrorCode(error, code) {
|
|
1713
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
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
|
+
}
|
|
1461
1731
|
function toPruneFilter(input) {
|
|
1462
1732
|
const filter = {};
|
|
1463
1733
|
if (input.entityId !== void 0) filter.entityId = input.entityId;
|
|
@@ -1470,166 +1740,76 @@ function toPruneFilter(input) {
|
|
|
1470
1740
|
if (input.maxImportance !== void 0) filter.maxImportanceLevels = importanceCeilingLevels(input.maxImportance);
|
|
1471
1741
|
return filter;
|
|
1472
1742
|
}
|
|
1473
|
-
function
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
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) {
|
|
1752
|
+
let schemaValidated = false;
|
|
1753
|
+
let schemaValidationPromise;
|
|
1754
|
+
async function ensureEmbeddingCompatibility(embedder) {
|
|
1755
|
+
if (schemaValidated) return;
|
|
1756
|
+
if (!schemaValidationPromise) schemaValidationPromise = validateEmbeddingCompatibility(embedder).catch((error) => {
|
|
1757
|
+
schemaValidationPromise = void 0;
|
|
1758
|
+
throw error;
|
|
1759
|
+
});
|
|
1760
|
+
await schemaValidationPromise;
|
|
1477
1761
|
}
|
|
1478
|
-
async function
|
|
1479
|
-
|
|
1762
|
+
async function validateEmbeddingCompatibility(embedder) {
|
|
1763
|
+
let meta;
|
|
1480
1764
|
try {
|
|
1481
|
-
await
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1765
|
+
meta = await getInstallationMeta(handle.db);
|
|
1766
|
+
} catch (error) {
|
|
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);
|
|
1768
|
+
throw normalizePostgresError(error);
|
|
1769
|
+
}
|
|
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");
|
|
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");
|
|
1772
|
+
schemaValidated = true;
|
|
1489
1773
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
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);
|
|
1497
1781
|
});
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
tags: normalizeTags(input.tags),
|
|
1506
|
-
source: input.source ?? null,
|
|
1507
|
-
metadata: input.metadata ?? {},
|
|
1508
|
-
confidence: input.confidence ?? 1,
|
|
1509
|
-
embedding,
|
|
1510
|
-
embeddingModel: config.embedder.model,
|
|
1511
|
-
embeddingDimensions: config.embedder.dimensions,
|
|
1512
|
-
supersededBy: null,
|
|
1513
|
-
supersededAt: null,
|
|
1514
|
-
expiresAt: input.expiresAt ?? null,
|
|
1515
|
-
lastAccessedAt: null,
|
|
1516
|
-
accessCount: 0,
|
|
1517
|
-
createdAt: now,
|
|
1518
|
-
updatedAt: now
|
|
1519
|
-
}));
|
|
1520
|
-
await recordAudit(memory.entityId, "memory.created", {
|
|
1521
|
-
memoryId: memory.id,
|
|
1522
|
-
type: memory.type,
|
|
1523
|
-
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
|
+
}));
|
|
1524
1789
|
});
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
const client = {
|
|
1532
|
-
remember,
|
|
1533
|
-
async rememberMany(inputs) {
|
|
1534
|
-
return observe(config, "postgres", "rememberMany", { count: inputs.length }, async () => {
|
|
1535
|
-
assertOpen();
|
|
1536
|
-
if (inputs.length === 0) return [];
|
|
1537
|
-
for (const input of inputs) validateRememberInput(input);
|
|
1538
|
-
const embeddings = await embedMany(config.embedder, inputs.map((i) => i.content), {
|
|
1539
|
-
...inputs[0]?.signal === void 0 ? {} : { signal: inputs[0].signal },
|
|
1540
|
-
...config.provider === void 0 ? {} : { resilience: config.provider }
|
|
1541
|
-
});
|
|
1542
|
-
const now = /* @__PURE__ */ new Date();
|
|
1543
|
-
const rows = inputs.map((input, idx) => ({
|
|
1544
|
-
id: createId(),
|
|
1545
|
-
entityId: input.entityId,
|
|
1546
|
-
content: input.content,
|
|
1547
|
-
type: input.type ?? "fact",
|
|
1548
|
-
importance: input.importance ?? "normal",
|
|
1549
|
-
tags: normalizeTags(input.tags),
|
|
1550
|
-
source: input.source ?? null,
|
|
1551
|
-
metadata: input.metadata ?? {},
|
|
1552
|
-
confidence: input.confidence ?? 1,
|
|
1553
|
-
embedding: embeddings[idx],
|
|
1554
|
-
embeddingModel: config.embedder.model,
|
|
1555
|
-
embeddingDimensions: config.embedder.dimensions,
|
|
1556
|
-
supersededBy: null,
|
|
1557
|
-
supersededAt: null,
|
|
1558
|
-
expiresAt: input.expiresAt ?? null,
|
|
1559
|
-
lastAccessedAt: null,
|
|
1560
|
-
accessCount: 0,
|
|
1561
|
-
createdAt: now,
|
|
1562
|
-
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
|
|
1563
1796
|
}));
|
|
1564
|
-
|
|
1565
|
-
for (const memory of memories) recordAudit(memory.entityId, "memory.created", {
|
|
1566
|
-
memoryId: memory.id,
|
|
1567
|
-
type: memory.type,
|
|
1568
|
-
importance: memory.importance
|
|
1569
|
-
});
|
|
1570
|
-
return memories;
|
|
1571
|
-
}, (result) => ({ count: result.length }));
|
|
1797
|
+
});
|
|
1572
1798
|
},
|
|
1573
|
-
async
|
|
1574
|
-
return
|
|
1575
|
-
assertOpen();
|
|
1576
|
-
validateRecallInput(input);
|
|
1577
|
-
const limit = input.limit ?? config.defaults?.limit ?? 10;
|
|
1578
|
-
const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
|
|
1579
|
-
assertLimit(limit);
|
|
1580
|
-
assertMinScore(minScore);
|
|
1581
|
-
return hybridRecall({
|
|
1582
|
-
db: handle.db,
|
|
1583
|
-
embedder: config.embedder,
|
|
1584
|
-
input,
|
|
1585
|
-
limit,
|
|
1586
|
-
minScore,
|
|
1587
|
-
retrieval: config.retrieval,
|
|
1588
|
-
...input.signal === void 0 ? {} : { signal: input.signal },
|
|
1589
|
-
...config.provider === void 0 ? {} : { resilience: config.provider }
|
|
1590
|
-
});
|
|
1591
|
-
}, (result) => ({ count: result.length }));
|
|
1799
|
+
async getMemoryEmbeddings(memoryIds) {
|
|
1800
|
+
return runPostgresOperation(() => getMemoryEmbeddings(handle.db, memoryIds));
|
|
1592
1801
|
},
|
|
1593
|
-
async
|
|
1594
|
-
return
|
|
1595
|
-
assertOpen();
|
|
1596
|
-
return buildContext({
|
|
1597
|
-
input,
|
|
1598
|
-
recall: (contextInput) => {
|
|
1599
|
-
const recallInput = contextInputToRecallInput(contextInput);
|
|
1600
|
-
return client.recall(input.signal === void 0 ? recallInput : {
|
|
1601
|
-
...recallInput,
|
|
1602
|
-
signal: input.signal
|
|
1603
|
-
});
|
|
1604
|
-
}
|
|
1605
|
-
});
|
|
1606
|
-
});
|
|
1802
|
+
async markMemoriesAccessed(memoryIds) {
|
|
1803
|
+
return runPostgresOperation(() => markMemoriesAccessed(handle.db, memoryIds));
|
|
1607
1804
|
},
|
|
1608
|
-
async
|
|
1609
|
-
return
|
|
1610
|
-
entityId: input.entityId,
|
|
1611
|
-
memoryId: input.memoryId
|
|
1612
|
-
}, async () => {
|
|
1613
|
-
assertOpen();
|
|
1614
|
-
assertNonEmptyString(input.entityId, "entityId");
|
|
1615
|
-
assertNonEmptyString(input.memoryId, "memoryId");
|
|
1616
|
-
if (!await deleteMemory(handle.db, input.entityId, input.memoryId)) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
|
|
1617
|
-
await recordAudit(input.entityId, "memory.deleted", { memoryId: input.memoryId });
|
|
1618
|
-
});
|
|
1805
|
+
async deleteMemory(entityId, memoryId) {
|
|
1806
|
+
return runPostgresOperation(() => deleteMemory(handle.db, entityId, memoryId));
|
|
1619
1807
|
},
|
|
1620
|
-
async
|
|
1621
|
-
return
|
|
1622
|
-
assertOpen();
|
|
1623
|
-
assertNonEmptyString(input.entityId, "entityId");
|
|
1624
|
-
const deletedCount = await deleteMemoriesForEntity(handle.db, input.entityId);
|
|
1625
|
-
await recordAudit(input.entityId, "entity.cleared", { count: deletedCount });
|
|
1626
|
-
return deletedCount;
|
|
1627
|
-
}, (count) => ({ count })).then(() => void 0);
|
|
1808
|
+
async deleteMemoriesForEntity(entityId) {
|
|
1809
|
+
return runPostgresOperation(() => deleteMemoriesForEntity(handle.db, entityId));
|
|
1628
1810
|
},
|
|
1629
1811
|
async prune(input) {
|
|
1630
|
-
return
|
|
1631
|
-
assertOpen();
|
|
1632
|
-
validatePruneInput(input);
|
|
1812
|
+
return runPostgresOperation(async () => {
|
|
1633
1813
|
const filter = toPruneFilter(input);
|
|
1634
1814
|
if (input.dryRun === true) return {
|
|
1635
1815
|
matchedCount: await countPruneMatches(handle.db, filter),
|
|
@@ -1637,24 +1817,19 @@ function createPostgresClient(config, handle) {
|
|
|
1637
1817
|
dryRun: true
|
|
1638
1818
|
};
|
|
1639
1819
|
const deletedCount = await pruneMemories(handle.db, filter);
|
|
1640
|
-
if (deletedCount > 0 && input.entityId !== void 0) await recordAudit(input.entityId, "memory.pruned", { count: deletedCount });
|
|
1641
1820
|
return {
|
|
1642
1821
|
matchedCount: deletedCount,
|
|
1643
1822
|
deletedCount,
|
|
1644
1823
|
dryRun: false
|
|
1645
1824
|
};
|
|
1646
|
-
}
|
|
1825
|
+
});
|
|
1647
1826
|
},
|
|
1648
|
-
async
|
|
1649
|
-
return
|
|
1650
|
-
assertOpen();
|
|
1651
|
-
validateFindDuplicatesInput(input);
|
|
1652
|
-
const threshold = input.threshold ?? .95;
|
|
1653
|
-
const limit = input.limit ?? 100;
|
|
1827
|
+
async findDuplicatePairs(input) {
|
|
1828
|
+
return runPostgresOperation(async () => {
|
|
1654
1829
|
return (await findDuplicatePairs(handle.db, {
|
|
1655
1830
|
entityId: input.entityId,
|
|
1656
|
-
threshold,
|
|
1657
|
-
limit,
|
|
1831
|
+
threshold: input.threshold ?? .95,
|
|
1832
|
+
limit: input.limit ?? 100,
|
|
1658
1833
|
...input.types === void 0 ? {} : { types: input.types },
|
|
1659
1834
|
...input.tags === void 0 ? {} : { tags: input.tags },
|
|
1660
1835
|
...input.includeSuperseded === void 0 ? {} : { includeSuperseded: input.includeSuperseded },
|
|
@@ -1664,110 +1839,94 @@ function createPostgresClient(config, handle) {
|
|
|
1664
1839
|
b: rowToMemory(row.b),
|
|
1665
1840
|
similarity: Math.max(0, Math.min(1, row.similarity))
|
|
1666
1841
|
}));
|
|
1667
|
-
}
|
|
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
|
+
});
|
|
1668
1854
|
},
|
|
1669
1855
|
async listAuditLog(input) {
|
|
1670
|
-
return
|
|
1671
|
-
assertOpen();
|
|
1672
|
-
validateListAuditLogInput(input);
|
|
1856
|
+
return runPostgresOperation(async () => {
|
|
1673
1857
|
return (await listEvents(handle.db, {
|
|
1674
1858
|
entityId: input.entityId,
|
|
1675
1859
|
limit: input.limit ?? 50,
|
|
1676
1860
|
...input.before === void 0 ? {} : { before: input.before },
|
|
1677
1861
|
...input.after === void 0 ? {} : { after: input.after }
|
|
1678
1862
|
})).map(rowToAuditEvent);
|
|
1679
|
-
}, (result) => ({ count: result.length }));
|
|
1680
|
-
},
|
|
1681
|
-
async stats(input) {
|
|
1682
|
-
return observe(config, "postgres", "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
|
|
1683
|
-
assertOpen();
|
|
1684
|
-
const now = /* @__PURE__ */ new Date();
|
|
1685
|
-
if (input?.entityId) {
|
|
1686
|
-
const counts = await getEntityMemoryStatsCounts(handle.db, input.entityId, now);
|
|
1687
|
-
return {
|
|
1688
|
-
entityId: input.entityId,
|
|
1689
|
-
...counts
|
|
1690
|
-
};
|
|
1691
|
-
}
|
|
1692
|
-
return await getGlobalMemoryStatsCounts(handle.db, now);
|
|
1693
1863
|
});
|
|
1694
1864
|
},
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
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;
|
|
1700
1869
|
});
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
return
|
|
1707
|
-
|
|
1708
|
-
memoryId: input.survivorId
|
|
1709
|
-
}, async () => {
|
|
1710
|
-
assertOpen();
|
|
1711
|
-
validateConsolidateInput(input);
|
|
1712
|
-
const survivor = await getMemoryById(handle.db, input.entityId, input.survivorId);
|
|
1713
|
-
if (!survivor) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
|
|
1714
|
-
if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
|
|
1715
|
-
const targets = await loadConsolidationTargets(handle.db, input.entityId, input.supersededIds);
|
|
1716
|
-
const foundIds = new Set(targets.map((t) => t.id));
|
|
1717
|
-
for (const id of input.supersededIds) if (!foundIds.has(id)) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
|
|
1718
|
-
const losers = targets.filter((target) => target.supersededBy === null);
|
|
1719
|
-
if (losers.length === 0) return {
|
|
1720
|
-
survivorId: survivor.id,
|
|
1721
|
-
supersededCount: 0,
|
|
1722
|
-
supersededIds: []
|
|
1723
|
-
};
|
|
1724
|
-
const now = /* @__PURE__ */ new Date();
|
|
1725
|
-
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) => {
|
|
1726
1877
|
const updated = await markMemoriesSuperseded(tx, {
|
|
1727
|
-
survivorId:
|
|
1878
|
+
survivorId: input.survivorId,
|
|
1728
1879
|
entityId: input.entityId,
|
|
1729
|
-
ids:
|
|
1730
|
-
now
|
|
1880
|
+
ids: input.supersededIds,
|
|
1881
|
+
now: input.now
|
|
1731
1882
|
});
|
|
1732
|
-
const
|
|
1733
|
-
if (
|
|
1883
|
+
const ids = updated.map((row) => row.id);
|
|
1884
|
+
if (input.auditEnabled) for (const id of ids) await insertEvent(tx, {
|
|
1734
1885
|
id: createEventId(),
|
|
1735
1886
|
entityId: input.entityId,
|
|
1736
1887
|
description: "memory.superseded",
|
|
1737
1888
|
metadata: {
|
|
1738
1889
|
memoryId: id,
|
|
1739
|
-
supersededBy:
|
|
1890
|
+
supersededBy: input.survivorId
|
|
1740
1891
|
},
|
|
1741
|
-
timestamp: now
|
|
1892
|
+
timestamp: input.now
|
|
1742
1893
|
});
|
|
1743
|
-
if (input.mergeTags
|
|
1744
|
-
const mergedTags = new Set(
|
|
1894
|
+
if (input.mergeTags && updated.length > 0) {
|
|
1895
|
+
const mergedTags = new Set(input.survivorTags);
|
|
1745
1896
|
for (const row of updated) for (const tag of row.tags) mergedTags.add(tag);
|
|
1746
|
-
if (mergedTags.size !==
|
|
1897
|
+
if (mergedTags.size !== input.survivorTags.length) await setMemoryTags(tx, {
|
|
1747
1898
|
entityId: input.entityId,
|
|
1748
|
-
memoryId:
|
|
1899
|
+
memoryId: input.survivorId,
|
|
1749
1900
|
tags: [...mergedTags],
|
|
1750
|
-
now
|
|
1901
|
+
now: input.now
|
|
1751
1902
|
});
|
|
1752
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);
|
|
1753
1912
|
return {
|
|
1754
|
-
|
|
1755
|
-
|
|
1913
|
+
entityId: input.entityId,
|
|
1914
|
+
...counts
|
|
1756
1915
|
};
|
|
1757
|
-
}
|
|
1758
|
-
return
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
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));
|
|
1766
1927
|
}
|
|
1767
1928
|
//#endregion
|
|
1768
1929
|
//#region src/client.ts
|
|
1769
|
-
/** Embedding dimensionality pinned by `migrations/0000_initial.sql`. */
|
|
1770
|
-
const POSTGRES_EMBEDDING_DIMENSIONS = 1536;
|
|
1771
1930
|
/**
|
|
1772
1931
|
* Create a Mnemocyte client.
|
|
1773
1932
|
*
|
|
@@ -1782,24 +1941,23 @@ const POSTGRES_EMBEDDING_DIMENSIONS = 1536;
|
|
|
1782
1941
|
*
|
|
1783
1942
|
* @param config - Client configuration. {@link MnemocyteConfig.embedder} is required.
|
|
1784
1943
|
* @returns A {@link MnemocyteClient} instance.
|
|
1785
|
-
* @throws {MnemocyteError} With code `"CONFIG"` if `embedder
|
|
1786
|
-
*
|
|
1787
|
-
* is
|
|
1788
|
-
* `"VALIDATION"` if `databaseUrl` is provided but empty.
|
|
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.
|
|
1789
1947
|
*
|
|
1790
1948
|
* @example Postgres-backed client
|
|
1791
1949
|
* ```ts
|
|
1792
1950
|
* const client = createMnemocyte({
|
|
1793
1951
|
* databaseUrl: process.env.DATABASE_URL,
|
|
1794
|
-
* embedder, // embedder.dimensions
|
|
1952
|
+
* embedder, // embedder.dimensions must match mnemocyte_meta
|
|
1795
1953
|
* });
|
|
1796
1954
|
* ```
|
|
1797
1955
|
*/
|
|
1798
1956
|
function createMnemocyte(config) {
|
|
1799
1957
|
assertEmbedder(config.embedder);
|
|
1800
|
-
|
|
1958
|
+
validateRetrievalConfig(config.retrieval);
|
|
1959
|
+
if (config.databaseUrl !== void 0) {
|
|
1801
1960
|
assertNonEmptyString(config.databaseUrl, "databaseUrl");
|
|
1802
|
-
if (config.embedder.dimensions !== POSTGRES_EMBEDDING_DIMENSIONS) throw new MnemocyteError(`embedder.dimensions must be ${POSTGRES_EMBEDDING_DIMENSIONS} for the Postgres backend (got ${config.embedder.dimensions}). The bundled migration pins the vector column to vector(${POSTGRES_EMBEDDING_DIMENSIONS}).`, "CONFIG");
|
|
1803
1961
|
return createPostgresClient(config, createDatabase(config.databaseUrl));
|
|
1804
1962
|
}
|
|
1805
1963
|
return createInMemoryClient(config);
|