mnemocyte 0.3.0 → 0.5.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 +266 -22
- package/dist/embedders/openai.d.mts +1 -1
- package/dist/embedders/openai.d.mts.map +1 -1
- package/dist/embedders/openai.mjs +24 -9
- package/dist/embedders/openai.mjs.map +1 -1
- package/dist/{errors-B_7ZdjEO.mjs → errors-DIPysVWl.mjs} +1 -1
- package/dist/errors-DIPysVWl.mjs.map +1 -0
- package/dist/index.d.mts +16 -12
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +155 -1699
- package/dist/index.mjs.map +1 -1
- package/dist/postgres-BHQ1pj16.mjs +913 -0
- package/dist/postgres-BHQ1pj16.mjs.map +1 -0
- package/dist/postgres-runtime-DxJxns07.mjs +9 -0
- package/dist/postgres-runtime-DxJxns07.mjs.map +1 -0
- package/dist/store-config-BFFTm1sj.mjs +1255 -0
- package/dist/store-config-BFFTm1sj.mjs.map +1 -0
- package/dist/stores/drizzle.d.mts +18 -0
- package/dist/stores/drizzle.d.mts.map +1 -0
- package/dist/stores/drizzle.mjs +20 -0
- package/dist/stores/drizzle.mjs.map +1 -0
- package/dist/{types-gOPLre4V.d.mts → types-XUWYDoXM.d.mts} +136 -34
- package/dist/types-XUWYDoXM.d.mts.map +1 -0
- package/migrations/0000_initial.sql.template +2 -1
- package/migrations/0002_add_embedding_model.sql +15 -0
- package/migrations/meta/0002_snapshot.json +315 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +7 -3
- package/dist/errors-B_7ZdjEO.mjs.map +0 -1
- package/dist/types-gOPLre4V.d.mts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,1088 +1,11 @@
|
|
|
1
|
-
import { n as isMnemocyteError, t as MnemocyteError } from "./errors-
|
|
2
|
-
import {
|
|
3
|
-
import postgres from "postgres";
|
|
4
|
-
import { and, count, countDistinct, desc, eq, gt, inArray, isNotNull, isNull, lt, or, sql } from "drizzle-orm";
|
|
5
|
-
import { customType, index, integer, jsonb, pgTable, real, text, timestamp } from "drizzle-orm/pg-core";
|
|
6
|
-
//#region \0rolldown/runtime.js
|
|
7
|
-
var __defProp = Object.defineProperty;
|
|
8
|
-
var __exportAll = (all, no_symbols) => {
|
|
9
|
-
let target = {};
|
|
10
|
-
for (var name in all) __defProp(target, name, {
|
|
11
|
-
get: all[name],
|
|
12
|
-
enumerable: true
|
|
13
|
-
});
|
|
14
|
-
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
15
|
-
return target;
|
|
16
|
-
};
|
|
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
|
|
24
|
-
//#region src/db/schema.ts
|
|
25
|
-
var schema_exports = /* @__PURE__ */ __exportAll({
|
|
26
|
-
eventsTable: () => eventsTable,
|
|
27
|
-
memoriesTable: () => memoriesTable,
|
|
28
|
-
metaTable: () => metaTable
|
|
29
|
-
});
|
|
30
|
-
const vector = customType({
|
|
31
|
-
dataType(config) {
|
|
32
|
-
return `vector(${config?.dimensions ?? 1536})`;
|
|
33
|
-
},
|
|
34
|
-
toDriver(value) {
|
|
35
|
-
return `[${value.map(formatVectorComponent).join(",")}]`;
|
|
36
|
-
},
|
|
37
|
-
fromDriver(value) {
|
|
38
|
-
return value.slice(1, -1).split(",").filter(Boolean).map(Number);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
const memoriesTable = pgTable("mnemocyte_memories", {
|
|
42
|
-
id: text("id").primaryKey(),
|
|
43
|
-
entityId: text("entity_id").notNull(),
|
|
44
|
-
content: text("content").notNull(),
|
|
45
|
-
type: text("type").notNull().default("fact"),
|
|
46
|
-
importance: text("importance").notNull().default("normal"),
|
|
47
|
-
tags: text("tags").array().notNull().default(sql`'{}'::text[]`),
|
|
48
|
-
source: text("source"),
|
|
49
|
-
metadata: jsonb("metadata").$type().notNull().default({}),
|
|
50
|
-
confidence: real("confidence").notNull().default(1),
|
|
51
|
-
embedding: vector("embedding", { dimensions: 1536 }),
|
|
52
|
-
embeddingModel: text("embedding_model").notNull(),
|
|
53
|
-
embeddingDimensions: integer("embedding_dimensions").notNull(),
|
|
54
|
-
supersededBy: text("superseded_by").references(() => memoriesTable.id),
|
|
55
|
-
supersededAt: timestamp("superseded_at", { withTimezone: true }),
|
|
56
|
-
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
|
57
|
-
lastAccessedAt: timestamp("last_accessed_at", { withTimezone: true }),
|
|
58
|
-
accessCount: integer("access_count").notNull().default(0),
|
|
59
|
-
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
60
|
-
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
61
|
-
}, (table) => [
|
|
62
|
-
index("mnemocyte_memories_entity_idx").on(table.entityId),
|
|
63
|
-
index("mnemocyte_memories_entity_type_idx").on(table.entityId, table.type),
|
|
64
|
-
index("mnemocyte_memories_embedding_hnsw_idx").using("hnsw", table.embedding.op("vector_cosine_ops"))
|
|
65
|
-
]);
|
|
66
|
-
const eventsTable = pgTable("mnemocyte_events", {
|
|
67
|
-
id: text("id").primaryKey(),
|
|
68
|
-
entityId: text("entity_id").notNull(),
|
|
69
|
-
description: text("description").notNull(),
|
|
70
|
-
metadata: jsonb("metadata").$type().notNull().default({}),
|
|
71
|
-
timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow()
|
|
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
|
-
});
|
|
77
|
-
//#endregion
|
|
78
|
-
//#region src/db/index.ts
|
|
79
|
-
function parseSslFromUrl(url) {
|
|
80
|
-
const mode = url.searchParams.get("sslmode");
|
|
81
|
-
if (!mode || mode === "disable") return void 0;
|
|
82
|
-
if (mode === "require" || mode === "prefer" || mode === "allow") return { rejectUnauthorized: false };
|
|
83
|
-
if (mode === "verify-full") return "verify-full";
|
|
84
|
-
}
|
|
85
|
-
function createDatabase(databaseUrl) {
|
|
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
|
-
}
|
|
92
|
-
const ssl = parseSslFromUrl(url);
|
|
93
|
-
const isPooler = url.port === "6543" || url.searchParams.get("pgbouncer") === "true";
|
|
94
|
-
const client = postgres(databaseUrl, {
|
|
95
|
-
max: 10,
|
|
96
|
-
idle_timeout: 20,
|
|
97
|
-
connect_timeout: 10,
|
|
98
|
-
...ssl ? { ssl } : {},
|
|
99
|
-
...isPooler ? { prepare: false } : {}
|
|
100
|
-
});
|
|
101
|
-
return {
|
|
102
|
-
db: drizzle(client, { schema: schema_exports }),
|
|
103
|
-
async close() {
|
|
104
|
-
await client.end();
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
}
|
|
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
|
|
306
|
-
//#region src/context/formatter.ts
|
|
307
|
-
function escapeXml(value) {
|
|
308
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
309
|
-
}
|
|
310
|
-
function formatMetadata(memory) {
|
|
311
|
-
const parts = [
|
|
312
|
-
`type=${memory.type}`,
|
|
313
|
-
`importance=${memory.importance}`,
|
|
314
|
-
`score=${memory.score.toFixed(3)}`
|
|
315
|
-
];
|
|
316
|
-
if (memory.source) parts.push(`source=${memory.source}`);
|
|
317
|
-
if (memory.tags.length > 0) parts.push(`tags=${memory.tags.join(",")}`);
|
|
318
|
-
return parts.join("; ");
|
|
319
|
-
}
|
|
320
|
-
function markdownFence(value) {
|
|
321
|
-
const longestBacktickRun = Math.max(0, ...Array.from(value.matchAll(/`+/g), (match) => match[0].length));
|
|
322
|
-
return "`".repeat(Math.max(3, longestBacktickRun + 1));
|
|
323
|
-
}
|
|
324
|
-
function formatMarkdown(memories, query, omittedCount) {
|
|
325
|
-
const lines = [
|
|
326
|
-
"# Memory Context",
|
|
327
|
-
"",
|
|
328
|
-
`Query: ${query}`,
|
|
329
|
-
"",
|
|
330
|
-
"## Relevant Memories"
|
|
331
|
-
];
|
|
332
|
-
for (const memory of memories) {
|
|
333
|
-
const fence = markdownFence(memory.content);
|
|
334
|
-
lines.push("", `- ${formatMetadata(memory)}`, ` ${fence}text`, ...memory.content.split("\n").map((line) => ` ${line}`), ` ${fence}`);
|
|
335
|
-
}
|
|
336
|
-
if (omittedCount > 0) lines.push("", `[${omittedCount} memories omitted to fit token budget]`);
|
|
337
|
-
return lines.join("\n");
|
|
338
|
-
}
|
|
339
|
-
function formatPlain(memories, query, omittedCount) {
|
|
340
|
-
const lines = [
|
|
341
|
-
"MEMORY CONTEXT",
|
|
342
|
-
"",
|
|
343
|
-
`QUERY: ${query}`,
|
|
344
|
-
"",
|
|
345
|
-
"RELEVANT MEMORIES"
|
|
346
|
-
];
|
|
347
|
-
for (const [index, memory] of memories.entries()) lines.push("", `--- MEMORY ${index + 1} START ---`, formatMetadata(memory), memory.content, `--- MEMORY ${index + 1} END ---`);
|
|
348
|
-
if (omittedCount > 0) lines.push("", `[${omittedCount} memories omitted to fit token budget]`);
|
|
349
|
-
return lines.join("\n");
|
|
350
|
-
}
|
|
351
|
-
function formatXml(memories, query, omittedCount) {
|
|
352
|
-
const lines = [`<memory_context query="${escapeXml(query)}">`];
|
|
353
|
-
for (const memory of memories) lines.push(` <memory id="${escapeXml(memory.id)}" type="${escapeXml(memory.type)}" importance="${escapeXml(memory.importance)}" score="${memory.score.toFixed(3)}">`, ` <content>${escapeXml(memory.content)}</content>`, ` <metadata>${escapeXml(formatMetadata(memory))}</metadata>`, " </memory>");
|
|
354
|
-
if (omittedCount > 0) lines.push(` <omitted count="${omittedCount}" reason="token_budget" />`);
|
|
355
|
-
lines.push("</memory_context>");
|
|
356
|
-
return lines.join("\n");
|
|
357
|
-
}
|
|
358
|
-
function formatContext(memories, query, format, omittedCount = 0) {
|
|
359
|
-
switch (format) {
|
|
360
|
-
case "markdown": return formatMarkdown(memories, query, omittedCount);
|
|
361
|
-
case "plain": return formatPlain(memories, query, omittedCount);
|
|
362
|
-
case "xml": return formatXml(memories, query, omittedCount);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
//#endregion
|
|
366
|
-
//#region src/context/tokens.ts
|
|
367
|
-
const heuristicTokenCounter = { count(text) {
|
|
368
|
-
return Math.ceil(text.length / 4);
|
|
369
|
-
} };
|
|
370
|
-
function trimToTokenBudget(text, maxTokens, tokenCounter) {
|
|
371
|
-
if (maxTokens < 1 || tokenCounter.count(text) <= maxTokens) return text;
|
|
372
|
-
let low = 0;
|
|
373
|
-
let high = text.length;
|
|
374
|
-
let best = "";
|
|
375
|
-
while (low <= high) {
|
|
376
|
-
const mid = Math.floor((low + high) / 2);
|
|
377
|
-
const output = `${text.slice(0, mid).trimEnd()}\n[truncated to fit token budget]`;
|
|
378
|
-
if (tokenCounter.count(output) <= maxTokens) {
|
|
379
|
-
best = output;
|
|
380
|
-
low = mid + 1;
|
|
381
|
-
} else high = mid - 1;
|
|
382
|
-
}
|
|
383
|
-
return best || "[truncated to fit token budget]";
|
|
384
|
-
}
|
|
385
|
-
//#endregion
|
|
386
|
-
//#region src/context/builder.ts
|
|
387
|
-
async function buildContext(options) {
|
|
388
|
-
const format = options.input.format ?? "markdown";
|
|
389
|
-
const maxTokens = options.input.maxTokens ?? 1200;
|
|
390
|
-
const tokenCounter = options.input.tokenCounter ?? heuristicTokenCounter;
|
|
391
|
-
const memories = await options.recall(options.input);
|
|
392
|
-
let low = 0;
|
|
393
|
-
let high = memories.length;
|
|
394
|
-
let bestContext = "";
|
|
395
|
-
while (low <= high) {
|
|
396
|
-
const mid = Math.floor((low + high) / 2);
|
|
397
|
-
const omittedCount = memories.length - mid;
|
|
398
|
-
const context = formatContext(memories.slice(0, mid), options.input.query, format, omittedCount);
|
|
399
|
-
if (tokenCounter.count(context) <= maxTokens) {
|
|
400
|
-
bestContext = context;
|
|
401
|
-
low = mid + 1;
|
|
402
|
-
} else high = mid - 1;
|
|
403
|
-
}
|
|
404
|
-
if (bestContext || memories.length === 0) return bestContext;
|
|
405
|
-
return trimToTokenBudget(formatContext([], options.input.query, format, memories.length), maxTokens, tokenCounter);
|
|
406
|
-
}
|
|
407
|
-
//#endregion
|
|
408
|
-
//#region src/observability.ts
|
|
409
|
-
function withMetadata(event, metadata) {
|
|
410
|
-
return {
|
|
411
|
-
...event,
|
|
412
|
-
...metadata.entityId === void 0 ? {} : { entityId: metadata.entityId },
|
|
413
|
-
...metadata.memoryId === void 0 ? {} : { memoryId: metadata.memoryId },
|
|
414
|
-
...metadata.count === void 0 ? {} : { count: metadata.count }
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
async function emitObservation(config, event) {
|
|
418
|
-
try {
|
|
419
|
-
await config.observability?.onEvent?.(event);
|
|
420
|
-
} catch {
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
async function observe(config, backend, operation, metadata, action, successMetadata) {
|
|
425
|
-
const startedAt = Date.now();
|
|
426
|
-
await emitObservation(config, withMetadata({
|
|
427
|
-
operation,
|
|
428
|
-
phase: "start",
|
|
429
|
-
backend,
|
|
430
|
-
timestamp: new Date(startedAt)
|
|
431
|
-
}, metadata));
|
|
432
|
-
try {
|
|
433
|
-
const result = await action();
|
|
434
|
-
await emitObservation(config, withMetadata({
|
|
435
|
-
operation,
|
|
436
|
-
phase: "success",
|
|
437
|
-
backend,
|
|
438
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
439
|
-
durationMs: Date.now() - startedAt
|
|
440
|
-
}, {
|
|
441
|
-
...metadata,
|
|
442
|
-
...successMetadata?.(result)
|
|
443
|
-
}));
|
|
444
|
-
return result;
|
|
445
|
-
} catch (error) {
|
|
446
|
-
await emitObservation(config, withMetadata({
|
|
447
|
-
operation,
|
|
448
|
-
phase: "error",
|
|
449
|
-
backend,
|
|
450
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
451
|
-
durationMs: Date.now() - startedAt,
|
|
452
|
-
error
|
|
453
|
-
}, metadata));
|
|
454
|
-
throw error;
|
|
455
|
-
}
|
|
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
|
-
};
|
|
468
|
-
//#endregion
|
|
469
|
-
//#region src/resilience.ts
|
|
470
|
-
/**
|
|
471
|
-
* Default per-provider-call timeout. Disabled by default so that callers
|
|
472
|
-
* who do not configure {@link ProviderResilienceConfig.timeoutMs} are
|
|
473
|
-
* unaffected.
|
|
474
|
-
*/
|
|
475
|
-
const DEFAULT_TIMEOUT_MS = 0;
|
|
476
|
-
/** Default number of retry attempts on transient provider failures. */
|
|
477
|
-
const DEFAULT_RETRIES = 0;
|
|
478
|
-
/** Default base backoff delay used between retry attempts. */
|
|
479
|
-
const DEFAULT_BASE_DELAY_MS = 100;
|
|
480
|
-
/** Default ceiling for retry backoff delay. */
|
|
481
|
-
const DEFAULT_MAX_DELAY_MS = 2e3;
|
|
482
|
-
/**
|
|
483
|
-
* Throw a {@link MnemocyteError} with code `"ABORTED"` if `signal` is
|
|
484
|
-
* already aborted. Safe to call with `undefined`.
|
|
485
|
-
*/
|
|
486
|
-
function throwIfAborted(signal) {
|
|
487
|
-
if (signal?.aborted) throw new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason);
|
|
488
|
-
}
|
|
489
|
-
/**
|
|
490
|
-
* Heuristic default for whether an error should be retried.
|
|
491
|
-
*
|
|
492
|
-
* Conservative by design: only retries on common transient failure
|
|
493
|
-
* indicators (network errors, 5xx-ish messages, `ECONN*` etc.). Never
|
|
494
|
-
* retries on validation, configuration, or {@link MnemocyteError} of
|
|
495
|
-
* code `"VALIDATION"` / `"CONFIG"` / `"ABORTED"`.
|
|
496
|
-
*/
|
|
497
|
-
function defaultShouldRetry(error) {
|
|
498
|
-
if (error instanceof MnemocyteError) {
|
|
499
|
-
if (error.code === "VALIDATION" || error.code === "CONFIG" || error.code === "ABORTED") return false;
|
|
500
|
-
return true;
|
|
501
|
-
}
|
|
502
|
-
if (typeof error === "object" && error !== null && "status" in error && typeof error.status === "number" && [
|
|
503
|
-
429,
|
|
504
|
-
500,
|
|
505
|
-
502,
|
|
506
|
-
503,
|
|
507
|
-
504
|
|
508
|
-
].includes(error.status)) return true;
|
|
509
|
-
if (error instanceof Error) {
|
|
510
|
-
const message = error.message.toLowerCase();
|
|
511
|
-
if (message.includes("econn") || message.includes("etimedout") || message.includes("network") || message.includes("timeout") || message.includes("temporarily") || message.includes("rate limit") || message.includes("503") || message.includes("502") || message.includes("504") || message.includes("500")) return true;
|
|
512
|
-
}
|
|
513
|
-
return false;
|
|
514
|
-
}
|
|
515
|
-
/**
|
|
516
|
-
* Compute the backoff delay for the given retry attempt, using exponential
|
|
517
|
-
* growth capped at {@link ProviderResilienceConfig.maxDelayMs}.
|
|
518
|
-
*
|
|
519
|
-
* `attempt` is 1-indexed: the delay before the first retry uses
|
|
520
|
-
* `attempt = 1`, before the second retry `attempt = 2`, etc.
|
|
521
|
-
*/
|
|
522
|
-
function computeDelayMs(attempt, baseDelayMs, maxDelayMs) {
|
|
523
|
-
const exponential = baseDelayMs * 2 ** (attempt - 1);
|
|
524
|
-
return Math.max(0, Math.min(maxDelayMs, exponential));
|
|
525
|
-
}
|
|
526
|
-
/**
|
|
527
|
-
* Promise-based `setTimeout` that rejects with an `"ABORTED"`
|
|
528
|
-
* {@link MnemocyteError} if `signal` aborts while waiting.
|
|
529
|
-
*/
|
|
530
|
-
function delay(ms, signal) {
|
|
531
|
-
if (ms <= 0) {
|
|
532
|
-
throwIfAborted(signal);
|
|
533
|
-
return Promise.resolve();
|
|
534
|
-
}
|
|
535
|
-
return new Promise((resolve, reject) => {
|
|
536
|
-
const timer = setTimeout(() => {
|
|
537
|
-
signal?.removeEventListener("abort", onAbort);
|
|
538
|
-
resolve();
|
|
539
|
-
}, ms);
|
|
540
|
-
const onAbort = () => {
|
|
541
|
-
clearTimeout(timer);
|
|
542
|
-
reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal?.reason));
|
|
543
|
-
};
|
|
544
|
-
if (signal) {
|
|
545
|
-
if (signal.aborted) {
|
|
546
|
-
clearTimeout(timer);
|
|
547
|
-
reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason));
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
551
|
-
}
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
function timeoutError(timeoutMs) {
|
|
555
|
-
return new MnemocyteError(`Provider call timed out after ${timeoutMs}ms.`, "TIMEOUT");
|
|
556
|
-
}
|
|
557
|
-
/**
|
|
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`.
|
|
561
|
-
*/
|
|
562
|
-
function runAttempt(action, timeoutMs, signal) {
|
|
563
|
-
if (timeoutMs <= 0 && !signal) return action(void 0);
|
|
564
|
-
return new Promise((resolve, reject) => {
|
|
565
|
-
let settled = false;
|
|
566
|
-
const controller = new AbortController();
|
|
567
|
-
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
568
|
-
if (settled) return;
|
|
569
|
-
settled = true;
|
|
570
|
-
signal?.removeEventListener("abort", onAbort);
|
|
571
|
-
controller.abort(timeoutError(timeoutMs));
|
|
572
|
-
reject(timeoutError(timeoutMs));
|
|
573
|
-
}, timeoutMs) : void 0;
|
|
574
|
-
const onAbort = () => {
|
|
575
|
-
if (settled) return;
|
|
576
|
-
settled = true;
|
|
577
|
-
if (timer) clearTimeout(timer);
|
|
578
|
-
controller.abort(signal?.reason);
|
|
579
|
-
reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal?.reason));
|
|
580
|
-
};
|
|
581
|
-
if (signal) {
|
|
582
|
-
if (signal.aborted) {
|
|
583
|
-
if (timer) clearTimeout(timer);
|
|
584
|
-
settled = true;
|
|
585
|
-
controller.abort(signal.reason);
|
|
586
|
-
reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason));
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
590
|
-
}
|
|
591
|
-
action(controller.signal).then((value) => {
|
|
592
|
-
if (settled) return;
|
|
593
|
-
settled = true;
|
|
594
|
-
if (timer) clearTimeout(timer);
|
|
595
|
-
signal?.removeEventListener("abort", onAbort);
|
|
596
|
-
resolve(value);
|
|
597
|
-
}, (error) => {
|
|
598
|
-
if (settled) return;
|
|
599
|
-
settled = true;
|
|
600
|
-
if (timer) clearTimeout(timer);
|
|
601
|
-
signal?.removeEventListener("abort", onAbort);
|
|
602
|
-
reject(error);
|
|
603
|
-
});
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
/**
|
|
607
|
-
* Run `action` with timeout, retry, and abort handling derived from
|
|
608
|
-
* `options.resilience` and `options.signal`. Each retry attempt receives
|
|
609
|
-
* a fresh `AbortSignal` linked to the caller's signal — so callers may
|
|
610
|
-
* abort mid-retry and the in-flight attempt will cancel promptly if the
|
|
611
|
-
* underlying provider honours signals.
|
|
612
|
-
*
|
|
613
|
-
* Errors from {@link MnemocyteError} with code `"VALIDATION"` /
|
|
614
|
-
* `"CONFIG"` / `"ABORTED"` are never retried.
|
|
615
|
-
*/
|
|
616
|
-
async function withResilience(action, options = {}) {
|
|
617
|
-
const config = options.resilience ?? {};
|
|
618
|
-
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
619
|
-
const retries = Math.max(0, config.retries ?? DEFAULT_RETRIES);
|
|
620
|
-
const baseDelayMs = Math.max(0, config.baseDelayMs ?? DEFAULT_BASE_DELAY_MS);
|
|
621
|
-
const maxDelayMs = Math.max(baseDelayMs, config.maxDelayMs ?? DEFAULT_MAX_DELAY_MS);
|
|
622
|
-
const shouldRetry = config.shouldRetry ?? defaultShouldRetry;
|
|
623
|
-
const externalSignal = options.signal;
|
|
624
|
-
throwIfAborted(externalSignal);
|
|
625
|
-
const maxAttempts = retries + 1;
|
|
626
|
-
let lastError;
|
|
627
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
628
|
-
throwIfAborted(externalSignal);
|
|
629
|
-
try {
|
|
630
|
-
return await runAttempt(action, timeoutMs, externalSignal);
|
|
631
|
-
} catch (error) {
|
|
632
|
-
lastError = error;
|
|
633
|
-
if (error instanceof MnemocyteError && error.code === "ABORTED") throw error;
|
|
634
|
-
if (attempt >= maxAttempts) throw error;
|
|
635
|
-
if (!shouldRetry(error, attempt)) throw error;
|
|
636
|
-
await delay(computeDelayMs(attempt, baseDelayMs, maxDelayMs), externalSignal);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
throw lastError;
|
|
640
|
-
}
|
|
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");
|
|
646
|
-
}
|
|
647
|
-
async function embedOne(embedder, text, options = {}) {
|
|
648
|
-
let embeddings;
|
|
649
|
-
try {
|
|
650
|
-
embeddings = await withResilience((signal) => signal === void 0 ? embedder.embed([text]) : embedder.embed([text], { signal }), {
|
|
651
|
-
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
652
|
-
...options.resilience === void 0 ? {} : { resilience: options.resilience }
|
|
653
|
-
});
|
|
654
|
-
} catch (error) {
|
|
655
|
-
if (error instanceof MnemocyteError && (error.code === "TIMEOUT" || error.code === "ABORTED")) throw error;
|
|
656
|
-
throw new MnemocyteError("Failed to embed text.", "EMBEDDING", error);
|
|
657
|
-
}
|
|
658
|
-
const embedding = embeddings[0];
|
|
659
|
-
if (!embedding) throw new MnemocyteError("Embedder returned no embedding.", "EMBEDDING");
|
|
660
|
-
validateEmbedding(embedding, embedder.dimensions);
|
|
661
|
-
return embedding;
|
|
662
|
-
}
|
|
663
|
-
async function embedMany(embedder, texts, options = {}) {
|
|
664
|
-
if (texts.length === 0) return [];
|
|
665
|
-
let embeddings;
|
|
666
|
-
try {
|
|
667
|
-
embeddings = await withResilience((signal) => signal === void 0 ? embedder.embed(texts) : embedder.embed(texts, { signal }), {
|
|
668
|
-
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
669
|
-
...options.resilience === void 0 ? {} : { resilience: options.resilience }
|
|
670
|
-
});
|
|
671
|
-
} catch (error) {
|
|
672
|
-
if (error instanceof MnemocyteError && (error.code === "TIMEOUT" || error.code === "ABORTED")) throw error;
|
|
673
|
-
throw new MnemocyteError("Failed to embed texts.", "EMBEDDING", error);
|
|
674
|
-
}
|
|
675
|
-
if (embeddings.length !== texts.length) throw new MnemocyteError(`Embedder returned ${embeddings.length} embeddings for ${texts.length} texts.`, "EMBEDDING");
|
|
676
|
-
for (const embedding of embeddings) validateEmbedding(embedding, embedder.dimensions);
|
|
677
|
-
return embeddings;
|
|
678
|
-
}
|
|
679
|
-
//#endregion
|
|
680
|
-
//#region src/memory/validation.ts
|
|
681
|
-
function assertNonEmptyString(value, field, code = "VALIDATION") {
|
|
682
|
-
if (typeof value !== "string" || value.trim().length === 0) throw new MnemocyteError(`${field} must be a non-empty string.`, code);
|
|
683
|
-
}
|
|
684
|
-
function assertLimit(value) {
|
|
685
|
-
if (!Number.isInteger(value) || value < 1) throw new MnemocyteError("limit must be a positive integer.", "VALIDATION");
|
|
686
|
-
}
|
|
687
|
-
function assertMinScore(value) {
|
|
688
|
-
if (!Number.isFinite(value) || value < 0 || value > 1) throw new MnemocyteError("minScore must be a number between 0 and 1.", "VALIDATION");
|
|
689
|
-
}
|
|
690
|
-
function assertEmbedder(embedder) {
|
|
691
|
-
if (!embedder || typeof embedder.embed !== "function") throw new MnemocyteError("embedder with an embed function is required.", "CONFIG");
|
|
692
|
-
assertNonEmptyString(embedder.model, "embedder.model", "CONFIG");
|
|
693
|
-
if (!Number.isInteger(embedder.dimensions) || embedder.dimensions < 1) throw new MnemocyteError("embedder.dimensions must be a positive integer.", "CONFIG");
|
|
694
|
-
}
|
|
695
|
-
const RETRIEVAL_WEIGHT_KEYS = [
|
|
696
|
-
"vector",
|
|
697
|
-
"lexical",
|
|
698
|
-
"recency",
|
|
699
|
-
"confidence",
|
|
700
|
-
"access",
|
|
701
|
-
"importance"
|
|
702
|
-
];
|
|
703
|
-
function assertPositiveFiniteConfigValue(value, field) {
|
|
704
|
-
if (!Number.isFinite(value) || value <= 0) throw new MnemocyteError(`${field} must be a positive finite number.`, "CONFIG");
|
|
705
|
-
}
|
|
706
|
-
function validateRetrievalConfig(config) {
|
|
707
|
-
if (config === void 0) return;
|
|
708
|
-
let weightTotal = 0;
|
|
709
|
-
for (const key of RETRIEVAL_WEIGHT_KEYS) {
|
|
710
|
-
const weight = config.weights?.[key] ?? DEFAULT_RETRIEVAL_WEIGHTS[key];
|
|
711
|
-
if (!Number.isFinite(weight) || weight < 0) throw new MnemocyteError(`retrieval.weights.${key} must be a non-negative finite number.`, "CONFIG");
|
|
712
|
-
weightTotal += weight;
|
|
713
|
-
}
|
|
714
|
-
if (weightTotal === 0) throw new MnemocyteError("retrieval.weights must have an effective total greater than zero.", "CONFIG");
|
|
715
|
-
if (config.recencyHalfLifeDays !== void 0) assertPositiveFiniteConfigValue(config.recencyHalfLifeDays, "retrieval.recencyHalfLifeDays");
|
|
716
|
-
if (config.accessSaturation !== void 0) assertPositiveFiniteConfigValue(config.accessSaturation, "retrieval.accessSaturation");
|
|
717
|
-
if (config.candidateMultiplier !== void 0 && (!Number.isInteger(config.candidateMultiplier) || config.candidateMultiplier < 1)) throw new MnemocyteError("retrieval.candidateMultiplier must be an integer greater than or equal to 1.", "CONFIG");
|
|
718
|
-
}
|
|
719
|
-
function validateBuildContextInput(input) {
|
|
720
|
-
if (input.maxTokens !== void 0 && (!Number.isInteger(input.maxTokens) || input.maxTokens < 1)) throw new MnemocyteError("maxTokens must be a positive integer when supplied.", "VALIDATION");
|
|
721
|
-
}
|
|
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,
|
|
737
|
-
query: input.query,
|
|
738
|
-
...input.limit === void 0 ? {} : { limit: input.limit },
|
|
739
|
-
...input.minScore === void 0 ? {} : { minScore: input.minScore },
|
|
740
|
-
...input.types === void 0 ? {} : { types: input.types },
|
|
741
|
-
...input.tags === void 0 ? {} : { tags: input.tags },
|
|
742
|
-
...input.includeSuperseded === void 0 ? {} : { includeSuperseded: input.includeSuperseded },
|
|
743
|
-
...input.includeExpired === void 0 ? {} : { includeExpired: input.includeExpired }
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
/**
|
|
747
|
-
* Validate a {@link PruneInput} and throw a `"VALIDATION"`
|
|
748
|
-
* {@link MnemocyteError} when no selector is supplied. Mnemocyte
|
|
749
|
-
* deliberately rejects `prune({})` to avoid surprise full-table deletes.
|
|
750
|
-
*/
|
|
751
|
-
function validatePruneInput(input) {
|
|
752
|
-
if (input.entityId !== void 0) assertNonEmptyString(input.entityId, "entityId");
|
|
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");
|
|
754
|
-
}
|
|
755
|
-
/**
|
|
756
|
-
* Validate a {@link FindDuplicatesInput} and throw a `"VALIDATION"`
|
|
757
|
-
* {@link MnemocyteError} for malformed `threshold` / `limit` values.
|
|
758
|
-
*/
|
|
759
|
-
function validateFindDuplicatesInput(input) {
|
|
760
|
-
assertNonEmptyString(input.entityId, "entityId");
|
|
761
|
-
if (input.threshold !== void 0) {
|
|
762
|
-
if (!Number.isFinite(input.threshold) || input.threshold < 0 || input.threshold > 1) throw new MnemocyteError("threshold must be a number between 0 and 1.", "VALIDATION");
|
|
763
|
-
}
|
|
764
|
-
if (input.limit !== void 0) assertLimit(input.limit);
|
|
765
|
-
}
|
|
766
|
-
/**
|
|
767
|
-
* Validate a {@link ConsolidateInput} and throw a `"VALIDATION"`
|
|
768
|
-
* {@link MnemocyteError} for malformed inputs (empty `supersededIds`
|
|
769
|
-
* or a `survivorId` appearing in `supersededIds`).
|
|
770
|
-
*/
|
|
771
|
-
function validateConsolidateInput(input) {
|
|
772
|
-
assertNonEmptyString(input.entityId, "entityId");
|
|
773
|
-
assertNonEmptyString(input.survivorId, "survivorId");
|
|
774
|
-
if (input.supersededIds.length === 0) throw new MnemocyteError("supersededIds must contain at least one memory id.", "VALIDATION");
|
|
775
|
-
if (input.supersededIds.includes(input.survivorId)) throw new MnemocyteError("survivorId must not appear in supersededIds.", "VALIDATION");
|
|
776
|
-
for (const id of input.supersededIds) assertNonEmptyString(id, "supersededIds[*]");
|
|
777
|
-
}
|
|
778
|
-
/**
|
|
779
|
-
* Validate a {@link ListAuditLogInput} and throw a `"VALIDATION"`
|
|
780
|
-
* {@link MnemocyteError} when malformed.
|
|
781
|
-
*/
|
|
782
|
-
function validateListAuditLogInput(input) {
|
|
783
|
-
assertNonEmptyString(input.entityId, "entityId");
|
|
784
|
-
if (input.limit !== void 0) assertLimit(input.limit);
|
|
785
|
-
}
|
|
786
|
-
//#endregion
|
|
787
|
-
//#region src/memory/client-core.ts
|
|
788
|
-
function isPositionalRememberManyInput(input) {
|
|
789
|
-
return Array.isArray(input);
|
|
790
|
-
}
|
|
791
|
-
function providerOptions(config, signal) {
|
|
792
|
-
return {
|
|
793
|
-
...signal === void 0 ? {} : { signal },
|
|
794
|
-
...config.provider === void 0 ? {} : { resilience: config.provider }
|
|
795
|
-
};
|
|
796
|
-
}
|
|
797
|
-
function createStoredMemory(config, input, embedding, now) {
|
|
798
|
-
return {
|
|
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
|
|
818
|
-
};
|
|
819
|
-
}
|
|
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
|
-
};
|
|
828
|
-
}
|
|
829
|
-
function toDuplicatePair(pair) {
|
|
830
|
-
return {
|
|
831
|
-
a: cloneMemory(pair.a),
|
|
832
|
-
b: cloneMemory(pair.b),
|
|
833
|
-
similarity: Math.max(0, Math.min(1, pair.similarity))
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
function createMemoryClient(config, store) {
|
|
837
|
-
let closed = false;
|
|
838
|
-
function assertOpen() {
|
|
839
|
-
if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
|
|
840
|
-
}
|
|
841
|
-
async function ensureSchema() {
|
|
842
|
-
await store.ensureSchema();
|
|
843
|
-
}
|
|
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 {}
|
|
852
|
-
}
|
|
853
|
-
async function remember(input) {
|
|
854
|
-
const preparedInput = {
|
|
855
|
-
...input,
|
|
856
|
-
metadata: cloneJsonObject(input.metadata ?? {})
|
|
857
|
-
};
|
|
858
|
-
return observe(config, store.backend, "remember", { entityId: preparedInput.entityId }, async () => {
|
|
859
|
-
assertOpen();
|
|
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", {
|
|
866
|
-
memoryId: memory.id,
|
|
867
|
-
type: memory.type,
|
|
868
|
-
importance: memory.importance
|
|
869
|
-
})]);
|
|
870
|
-
return cloneMemory(memory);
|
|
871
|
-
}, (memory) => ({
|
|
872
|
-
memoryId: memory.id,
|
|
873
|
-
count: 1
|
|
874
|
-
}));
|
|
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
|
-
}
|
|
901
|
-
const client = {
|
|
902
|
-
remember,
|
|
903
|
-
rememberMany,
|
|
904
|
-
async recall(input) {
|
|
905
|
-
return observe(config, store.backend, "recall", { entityId: input.entityId }, async () => {
|
|
906
|
-
assertOpen();
|
|
907
|
-
validateRecallInput(input);
|
|
908
|
-
const limit = input.limit ?? config.defaults?.limit ?? 10;
|
|
909
|
-
const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
|
|
910
|
-
assertLimit(limit);
|
|
911
|
-
assertMinScore(minScore);
|
|
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);
|
|
916
|
-
const scoreLexical = createLexicalScorer(input.query);
|
|
917
|
-
const scoringConfig = createScoringConfig(config.retrieval);
|
|
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;
|
|
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
|
-
});
|
|
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));
|
|
951
|
-
return scored;
|
|
952
|
-
}, (result) => ({ count: result.length }));
|
|
953
|
-
},
|
|
954
|
-
async buildContext(input) {
|
|
955
|
-
return observe(config, store.backend, "buildContext", { entityId: input.entityId }, async () => {
|
|
956
|
-
assertOpen();
|
|
957
|
-
validateBuildContextInput(input);
|
|
958
|
-
return buildContext({
|
|
959
|
-
input,
|
|
960
|
-
recall: (contextInput) => {
|
|
961
|
-
const recallInput = contextInputToRecallInput(contextInput);
|
|
962
|
-
return client.recall(input.signal === void 0 ? recallInput : {
|
|
963
|
-
...recallInput,
|
|
964
|
-
signal: input.signal
|
|
965
|
-
});
|
|
966
|
-
}
|
|
967
|
-
});
|
|
968
|
-
});
|
|
969
|
-
},
|
|
970
|
-
async forget(input) {
|
|
971
|
-
return observe(config, store.backend, "forget", {
|
|
972
|
-
entityId: input.entityId,
|
|
973
|
-
memoryId: input.memoryId
|
|
974
|
-
}, async () => {
|
|
975
|
-
assertOpen();
|
|
976
|
-
assertNonEmptyString(input.entityId, "entityId");
|
|
977
|
-
assertNonEmptyString(input.memoryId, "memoryId");
|
|
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 })]);
|
|
981
|
-
});
|
|
982
|
-
},
|
|
983
|
-
async forgetAll(input) {
|
|
984
|
-
return observe(config, store.backend, "forgetAll", { entityId: input.entityId }, async () => {
|
|
985
|
-
assertOpen();
|
|
986
|
-
assertNonEmptyString(input.entityId, "entityId");
|
|
987
|
-
await ensureSchema();
|
|
988
|
-
const deletedCount = await store.deleteMemoriesForEntity(input.entityId);
|
|
989
|
-
await recordAudit([auditEvent(input.entityId, "entity.cleared", { count: deletedCount })]);
|
|
990
|
-
return deletedCount;
|
|
991
|
-
}, (count) => ({ count })).then(() => void 0);
|
|
992
|
-
},
|
|
993
|
-
async prune(input) {
|
|
994
|
-
return observe(config, store.backend, "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
|
|
995
|
-
assertOpen();
|
|
996
|
-
validatePruneInput(input);
|
|
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
|
-
});
|
|
1061
|
-
return {
|
|
1062
|
-
survivorId: survivor.id,
|
|
1063
|
-
supersededCount: result.supersededIds.length,
|
|
1064
|
-
supersededIds: result.supersededIds
|
|
1065
|
-
};
|
|
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
|
|
1
|
+
import { n as isMnemocyteError, t as MnemocyteError } from "./errors-DIPysVWl.mjs";
|
|
2
|
+
import { _ as cloneJsonObject, a as assertEmbedder, c as validateRetrievalConfig, d as IMPORTANCE_RANK, f as cosineSimilarity, g as createEventId, h as cloneMemory, i as createMemoryClient, l as assertPruneFilterHasSelector, m as toVectorScore, n as unwrapMnemocyteStoreConfig, o as assertNonEmptyString, p as createLexicalScorer, r as memoryHasDependentsError, s as validateProviderResilienceConfig, v as throwIfAborted, y as parsePostgresDatabaseUrl } from "./store-config-BFFTm1sj.mjs";
|
|
1080
3
|
//#region src/memory/filters.ts
|
|
1081
4
|
function isExpired(memory, now) {
|
|
1082
5
|
return memory.expiresAt !== null && memory.expiresAt.getTime() <= now.getTime();
|
|
1083
6
|
}
|
|
1084
7
|
/**
|
|
1085
|
-
* Test whether `memory` matches a
|
|
8
|
+
* Test whether `memory` matches a validated prune filter. All specified
|
|
1086
9
|
* selectors are AND-combined; unspecified selectors do not restrict.
|
|
1087
10
|
*
|
|
1088
11
|
* `now` is supplied by the caller so callers can share a consistent
|
|
@@ -1126,12 +49,6 @@ function matchesRecallFilter(memory, input, now) {
|
|
|
1126
49
|
}
|
|
1127
50
|
//#endregion
|
|
1128
51
|
//#region src/memory/in-memory.ts
|
|
1129
|
-
function cloneStoredMemory(memory) {
|
|
1130
|
-
return {
|
|
1131
|
-
...cloneMemory(memory),
|
|
1132
|
-
embedding: [...memory.embedding]
|
|
1133
|
-
};
|
|
1134
|
-
}
|
|
1135
52
|
function cloneAuditEvent(event) {
|
|
1136
53
|
return {
|
|
1137
54
|
id: event.id,
|
|
@@ -1141,6 +58,15 @@ function cloneAuditEvent(event) {
|
|
|
1141
58
|
timestamp: new Date(event.timestamp)
|
|
1142
59
|
};
|
|
1143
60
|
}
|
|
61
|
+
function compareAuditPositions(a, b) {
|
|
62
|
+
const timestampDifference = a.timestamp.getTime() - b.timestamp.getTime();
|
|
63
|
+
if (timestampDifference !== 0) return timestampDifference;
|
|
64
|
+
if (a.id === b.id) return 0;
|
|
65
|
+
return a.id < b.id ? -1 : 1;
|
|
66
|
+
}
|
|
67
|
+
function assertNoDependentMemories(memories, targetIds) {
|
|
68
|
+
for (const memory of memories.values()) if (memory.supersededBy !== null && targetIds.has(memory.supersededBy)) throw memoryHasDependentsError();
|
|
69
|
+
}
|
|
1144
70
|
function createInMemoryStore() {
|
|
1145
71
|
const memories = /* @__PURE__ */ new Map();
|
|
1146
72
|
const auditLog = [];
|
|
@@ -1151,19 +77,18 @@ function createInMemoryStore() {
|
|
|
1151
77
|
async insertMemories(rows) {
|
|
1152
78
|
const inserted = [];
|
|
1153
79
|
for (const row of rows) {
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
inserted.push(cloneMemory(stored));
|
|
80
|
+
memories.set(row.id, row);
|
|
81
|
+
inserted.push(cloneMemory(row));
|
|
1157
82
|
}
|
|
1158
83
|
return inserted;
|
|
1159
84
|
},
|
|
1160
85
|
async vectorSearch(input) {
|
|
1161
86
|
const now = /* @__PURE__ */ new Date();
|
|
1162
|
-
const
|
|
87
|
+
const minVectorScore = input.minVectorScore ?? 0;
|
|
1163
88
|
return Array.from(memories.values()).filter((memory) => matchesRecallFilter(memory, input, now)).map((memory) => ({
|
|
1164
89
|
memory: cloneMemory(memory),
|
|
1165
|
-
vectorScore:
|
|
1166
|
-
})).filter((candidate) => candidate.vectorScore >=
|
|
90
|
+
vectorScore: toVectorScore(cosineSimilarity(memory.embedding, input.embedding))
|
|
91
|
+
})).filter((candidate) => candidate.vectorScore >= minVectorScore).sort((a, b) => b.vectorScore - a.vectorScore).slice(0, input.limit);
|
|
1167
92
|
},
|
|
1168
93
|
async lexicalSearch(input) {
|
|
1169
94
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -1183,50 +108,82 @@ function createInMemoryStore() {
|
|
|
1183
108
|
},
|
|
1184
109
|
async markMemoriesAccessed(memoryIds) {
|
|
1185
110
|
const now = /* @__PURE__ */ new Date();
|
|
111
|
+
const updates = [];
|
|
1186
112
|
for (const id of memoryIds) {
|
|
1187
113
|
const memory = memories.get(id);
|
|
1188
114
|
if (memory) {
|
|
1189
115
|
memory.lastAccessedAt = now;
|
|
1190
116
|
memory.accessCount += 1;
|
|
1191
117
|
memory.updatedAt = now;
|
|
118
|
+
updates.push({
|
|
119
|
+
id: memory.id,
|
|
120
|
+
lastAccessedAt: now,
|
|
121
|
+
accessCount: memory.accessCount,
|
|
122
|
+
updatedAt: now
|
|
123
|
+
});
|
|
1192
124
|
}
|
|
1193
125
|
}
|
|
126
|
+
return updates;
|
|
1194
127
|
},
|
|
1195
128
|
async deleteMemory(entityId, memoryId) {
|
|
1196
129
|
const memory = memories.get(memoryId);
|
|
1197
130
|
if (!memory || memory.entityId !== entityId) return false;
|
|
131
|
+
assertNoDependentMemories(memories, new Set([memoryId]));
|
|
1198
132
|
return memories.delete(memoryId);
|
|
1199
133
|
},
|
|
1200
134
|
async deleteMemoriesForEntity(entityId) {
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
135
|
+
const targetIds = new Set(Array.from(memories.values()).filter((memory) => memory.entityId === entityId).map((memory) => memory.id));
|
|
136
|
+
assertNoDependentMemories(memories, targetIds);
|
|
137
|
+
for (const id of targetIds) memories.delete(id);
|
|
138
|
+
return targetIds.size;
|
|
139
|
+
},
|
|
140
|
+
async prune(input, options) {
|
|
141
|
+
throwIfAborted(options?.signal);
|
|
142
|
+
assertPruneFilterHasSelector(input);
|
|
1209
143
|
const now = /* @__PURE__ */ new Date();
|
|
1210
144
|
const matched = [];
|
|
1211
|
-
for (const memory of memories.values())
|
|
145
|
+
for (const memory of memories.values()) {
|
|
146
|
+
throwIfAborted(options?.signal);
|
|
147
|
+
if (matchesPruneFilter(memory, input, now)) matched.push(memory);
|
|
148
|
+
}
|
|
1212
149
|
const dryRun = input.dryRun === true;
|
|
1213
|
-
|
|
150
|
+
let deletedByEntity = [];
|
|
151
|
+
if (!dryRun) {
|
|
152
|
+
throwIfAborted(options?.signal);
|
|
153
|
+
assertNoDependentMemories(memories, new Set(matched.map((memory) => memory.id)));
|
|
154
|
+
const deletedCounts = /* @__PURE__ */ new Map();
|
|
155
|
+
for (const memory of matched) {
|
|
156
|
+
memories.delete(memory.id);
|
|
157
|
+
deletedCounts.set(memory.entityId, (deletedCounts.get(memory.entityId) ?? 0) + 1);
|
|
158
|
+
}
|
|
159
|
+
deletedByEntity = Array.from(deletedCounts, ([entityId, deletedCount]) => ({
|
|
160
|
+
entityId,
|
|
161
|
+
deletedCount
|
|
162
|
+
})).sort((a, b) => a.entityId.localeCompare(b.entityId));
|
|
163
|
+
}
|
|
1214
164
|
return {
|
|
1215
165
|
matchedCount: matched.length,
|
|
1216
166
|
deletedCount: dryRun ? 0 : matched.length,
|
|
1217
|
-
dryRun
|
|
167
|
+
dryRun,
|
|
168
|
+
deletedByEntity
|
|
1218
169
|
};
|
|
1219
170
|
},
|
|
1220
|
-
async findDuplicatePairs(input) {
|
|
171
|
+
async findDuplicatePairs(input, options) {
|
|
172
|
+
throwIfAborted(options?.signal);
|
|
1221
173
|
const threshold = input.threshold ?? .95;
|
|
1222
174
|
const limit = input.limit ?? 100;
|
|
1223
175
|
const now = /* @__PURE__ */ new Date();
|
|
1224
|
-
const candidates = Array.from(memories.values()).filter((memory) =>
|
|
176
|
+
const candidates = Array.from(memories.values()).filter((memory) => {
|
|
177
|
+
throwIfAborted(options?.signal);
|
|
178
|
+
return matchesDuplicateFilter(memory, input, now);
|
|
179
|
+
});
|
|
1225
180
|
const pairs = [];
|
|
1226
181
|
for (let i = 0; i < candidates.length; i += 1) {
|
|
182
|
+
throwIfAborted(options?.signal);
|
|
1227
183
|
const a = candidates[i];
|
|
1228
184
|
if (a === void 0) continue;
|
|
1229
185
|
for (let j = i + 1; j < candidates.length; j += 1) {
|
|
186
|
+
throwIfAborted(options?.signal);
|
|
1230
187
|
const b = candidates[j];
|
|
1231
188
|
if (b === void 0) continue;
|
|
1232
189
|
const similarity = Math.max(0, Math.min(1, cosineSimilarity(a.embedding, b.embedding)));
|
|
@@ -1243,27 +200,29 @@ function createInMemoryStore() {
|
|
|
1243
200
|
async addAuditEvents(events) {
|
|
1244
201
|
auditLog.push(...events.map(cloneAuditEvent));
|
|
1245
202
|
},
|
|
1246
|
-
async listAuditLog(input) {
|
|
203
|
+
async listAuditLog(input, options) {
|
|
204
|
+
throwIfAborted(options?.signal);
|
|
1247
205
|
const limit = input.limit ?? 50;
|
|
1248
|
-
return auditLog.
|
|
1249
|
-
|
|
1250
|
-
idx
|
|
1251
|
-
})).filter(({ event }) => {
|
|
206
|
+
return auditLog.filter((event) => {
|
|
207
|
+
throwIfAborted(options?.signal);
|
|
1252
208
|
if (event.entityId !== input.entityId) return false;
|
|
1253
209
|
if (input.before !== void 0 && event.timestamp.getTime() >= input.before.getTime()) return false;
|
|
1254
210
|
if (input.after !== void 0 && event.timestamp.getTime() <= input.after.getTime()) return false;
|
|
211
|
+
if (input.beforeCursor !== void 0 && compareAuditPositions(event, input.beforeCursor) >= 0) return false;
|
|
212
|
+
if (input.afterCursor !== void 0 && compareAuditPositions(event, input.afterCursor) <= 0) return false;
|
|
1255
213
|
return true;
|
|
1256
214
|
}).sort((a, b) => {
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
}).slice(0, limit).map(({ event }) => cloneAuditEvent(event));
|
|
215
|
+
return compareAuditPositions(b, a);
|
|
216
|
+
}).slice(0, limit).map(cloneAuditEvent);
|
|
1260
217
|
},
|
|
1261
|
-
async getMemory(entityId, memoryId) {
|
|
218
|
+
async getMemory(entityId, memoryId, options) {
|
|
219
|
+
throwIfAborted(options?.signal);
|
|
1262
220
|
const memory = memories.get(memoryId);
|
|
1263
221
|
return memory && memory.entityId === entityId ? cloneMemory(memory) : null;
|
|
1264
222
|
},
|
|
1265
|
-
async loadConsolidationTargets(entityId, ids) {
|
|
223
|
+
async loadConsolidationTargets(entityId, ids, options) {
|
|
1266
224
|
return ids.flatMap((id) => {
|
|
225
|
+
throwIfAborted(options?.signal);
|
|
1267
226
|
const memory = memories.get(id);
|
|
1268
227
|
if (!memory || memory.entityId !== entityId) return [];
|
|
1269
228
|
return [{
|
|
@@ -1273,40 +232,51 @@ function createInMemoryStore() {
|
|
|
1273
232
|
}];
|
|
1274
233
|
});
|
|
1275
234
|
},
|
|
1276
|
-
async consolidate(input) {
|
|
235
|
+
async consolidate(input, options) {
|
|
236
|
+
throwIfAborted(options?.signal);
|
|
1277
237
|
const survivor = memories.get(input.survivorId);
|
|
238
|
+
if (!survivor || survivor.entityId !== input.entityId || survivor.supersededBy !== null) throw new MnemocyteError("Consolidation survivor changed before mutation.", "CONFLICT");
|
|
1278
239
|
const newlySuperseded = [];
|
|
240
|
+
const targets = [];
|
|
1279
241
|
for (const id of input.supersededIds) {
|
|
242
|
+
throwIfAborted(options?.signal);
|
|
1280
243
|
const memory = memories.get(id);
|
|
1281
244
|
if (!memory || memory.entityId !== input.entityId) continue;
|
|
1282
|
-
|
|
1283
|
-
memory.supersededBy = input.survivorId;
|
|
1284
|
-
memory.supersededAt = input.now;
|
|
1285
|
-
memory.updatedAt = input.now;
|
|
1286
|
-
newlySuperseded.push(memory.id);
|
|
245
|
+
targets.push(memory);
|
|
1287
246
|
}
|
|
1288
|
-
if (
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
survivor.updatedAt = input.now;
|
|
247
|
+
if (targets.some((memory) => memory.supersededBy !== null && memory.supersededBy !== input.survivorId)) throw new MnemocyteError("Superseded memory already belongs to a different survivor.", "CONFLICT");
|
|
248
|
+
for (const memory of targets) if (memory.supersededBy === null) newlySuperseded.push(memory);
|
|
249
|
+
let mergedSurvivorTags;
|
|
250
|
+
if (input.mergeTags && newlySuperseded.length > 0) {
|
|
251
|
+
const mergedTags = new Set(survivor.tags);
|
|
252
|
+
for (const memory of newlySuperseded) {
|
|
253
|
+
throwIfAborted(options?.signal);
|
|
254
|
+
for (const tag of memory.tags) mergedTags.add(tag);
|
|
1297
255
|
}
|
|
256
|
+
if (mergedTags.size !== survivor.tags.length) mergedSurvivorTags = [...mergedTags];
|
|
1298
257
|
}
|
|
1299
|
-
|
|
258
|
+
const auditEvents = input.auditEnabled ? newlySuperseded.map((memory) => ({
|
|
1300
259
|
id: createEventId(),
|
|
1301
260
|
entityId: input.entityId,
|
|
1302
261
|
description: "memory.superseded",
|
|
1303
262
|
metadata: {
|
|
1304
|
-
memoryId: id,
|
|
263
|
+
memoryId: memory.id,
|
|
1305
264
|
supersededBy: input.survivorId
|
|
1306
265
|
},
|
|
1307
266
|
timestamp: input.now
|
|
1308
|
-
});
|
|
1309
|
-
|
|
267
|
+
})) : [];
|
|
268
|
+
throwIfAborted(options?.signal);
|
|
269
|
+
for (const memory of newlySuperseded) {
|
|
270
|
+
memory.supersededBy = input.survivorId;
|
|
271
|
+
memory.supersededAt = input.now;
|
|
272
|
+
memory.updatedAt = input.now;
|
|
273
|
+
}
|
|
274
|
+
if (mergedSurvivorTags) {
|
|
275
|
+
survivor.tags = mergedSurvivorTags;
|
|
276
|
+
survivor.updatedAt = input.now;
|
|
277
|
+
}
|
|
278
|
+
if (input.auditEnabled) auditLog.push(...auditEvents);
|
|
279
|
+
return { supersededIds: newlySuperseded.map((memory) => memory.id) };
|
|
1310
280
|
},
|
|
1311
281
|
async stats(input, now) {
|
|
1312
282
|
const allMemories = Array.from(memories.values());
|
|
@@ -1339,611 +309,93 @@ function createInMemoryClient(config) {
|
|
|
1339
309
|
return createMemoryClient(config, createInMemoryStore());
|
|
1340
310
|
}
|
|
1341
311
|
//#endregion
|
|
1342
|
-
//#region src/
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
async function listEvents(db, filter) {
|
|
1349
|
-
return db.select().from(eventsTable).where(and(eq(eventsTable.entityId, filter.entityId), filter.before ? lt(eventsTable.timestamp, filter.before) : void 0, filter.after ? gt(eventsTable.timestamp, filter.after) : void 0)).orderBy(desc(eventsTable.timestamp)).limit(filter.limit ?? 50);
|
|
1350
|
-
}
|
|
1351
|
-
//#endregion
|
|
1352
|
-
//#region src/db/queries/memories.ts
|
|
1353
|
-
function vectorLiteral(embedding) {
|
|
1354
|
-
return `[${embedding.map(formatVectorComponent).join(",")}]`;
|
|
1355
|
-
}
|
|
1356
|
-
function timestampParam(value) {
|
|
1357
|
-
return sql`${value.toISOString()}::timestamptz`;
|
|
1358
|
-
}
|
|
1359
|
-
function textArrayLiteral(values) {
|
|
1360
|
-
return sql`ARRAY[${sql.join(values.map((value) => sql`${value}`), sql`, `)}]::text[]`;
|
|
1361
|
-
}
|
|
1362
|
-
function tagsContainAll(tags) {
|
|
1363
|
-
return tags && tags.length > 0 ? sql`${memoriesTable.tags} @> ${textArrayLiteral(tags)}` : void 0;
|
|
1364
|
-
}
|
|
1365
|
-
function rawTagsFilter(tags) {
|
|
1366
|
-
return tags && tags.length > 0 ? sql`AND tags @> ${textArrayLiteral(tags)}` : sql``;
|
|
1367
|
-
}
|
|
1368
|
-
function duplicateTagsFilter(tags) {
|
|
1369
|
-
return tags && tags.length > 0 ? sql`AND a.tags @> ${textArrayLiteral(tags)} AND b.tags @> ${textArrayLiteral(tags)}` : sql``;
|
|
1370
|
-
}
|
|
1371
|
-
function memoryStatsCountFields(now) {
|
|
1372
|
-
return {
|
|
1373
|
-
memoryCount: count(),
|
|
1374
|
-
activeMemoryCount: sql`
|
|
1375
|
-
(count(*) FILTER (
|
|
1376
|
-
WHERE ${memoriesTable.supersededBy} IS NULL
|
|
1377
|
-
AND (${memoriesTable.expiresAt} IS NULL OR ${memoriesTable.expiresAt} > ${timestampParam(now)})
|
|
1378
|
-
))::int
|
|
1379
|
-
`.mapWith(Number),
|
|
1380
|
-
expiredMemoryCount: sql`
|
|
1381
|
-
(count(*) FILTER (
|
|
1382
|
-
WHERE ${memoriesTable.expiresAt} IS NOT NULL
|
|
1383
|
-
AND ${memoriesTable.expiresAt} <= ${timestampParam(now)}
|
|
1384
|
-
))::int
|
|
1385
|
-
`.mapWith(Number),
|
|
1386
|
-
supersededMemoryCount: sql`(count(*) FILTER (WHERE ${memoriesTable.supersededBy} IS NOT NULL))::int`.mapWith(Number)
|
|
1387
|
-
};
|
|
1388
|
-
}
|
|
1389
|
-
function emptyMemoryStatsCounts() {
|
|
1390
|
-
return {
|
|
1391
|
-
memoryCount: 0,
|
|
1392
|
-
activeMemoryCount: 0,
|
|
1393
|
-
expiredMemoryCount: 0,
|
|
1394
|
-
supersededMemoryCount: 0
|
|
1395
|
-
};
|
|
1396
|
-
}
|
|
1397
|
-
async function insertMemories(db, rows) {
|
|
1398
|
-
if (rows.length === 0) return [];
|
|
1399
|
-
return db.insert(memoriesTable).values(rows).returning();
|
|
1400
|
-
}
|
|
1401
|
-
async function getMemoryById(db, entityId, memoryId) {
|
|
1402
|
-
return (await db.select().from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), eq(memoriesTable.id, memoryId))).limit(1))[0] ?? null;
|
|
1403
|
-
}
|
|
1404
|
-
async function getEntityMemoryStatsCounts(db, entityId, now) {
|
|
1405
|
-
return (await db.select(memoryStatsCountFields(now)).from(memoriesTable).where(eq(memoriesTable.entityId, entityId)))[0] ?? emptyMemoryStatsCounts();
|
|
1406
|
-
}
|
|
1407
|
-
async function getGlobalMemoryStatsCounts(db, now) {
|
|
1408
|
-
return (await db.select({
|
|
1409
|
-
entityCount: countDistinct(memoriesTable.entityId),
|
|
1410
|
-
...memoryStatsCountFields(now)
|
|
1411
|
-
}).from(memoriesTable))[0] ?? {
|
|
1412
|
-
entityCount: 0,
|
|
1413
|
-
...emptyMemoryStatsCounts()
|
|
1414
|
-
};
|
|
1415
|
-
}
|
|
1416
|
-
async function deleteMemory(db, entityId, memoryId) {
|
|
1417
|
-
return (await db.delete(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), eq(memoriesTable.id, memoryId))).returning({ id: memoriesTable.id })).length > 0;
|
|
1418
|
-
}
|
|
1419
|
-
async function deleteMemoriesForEntity(db, entityId) {
|
|
1420
|
-
return (await db.delete(memoriesTable).where(eq(memoriesTable.entityId, entityId)).returning({ id: memoriesTable.id })).length;
|
|
1421
|
-
}
|
|
1422
|
-
function pruneConditions(filter) {
|
|
1423
|
-
return and(filter.entityId !== void 0 ? eq(memoriesTable.entityId, filter.entityId) : void 0, filter.expired ? and(isNotNull(memoriesTable.expiresAt), sql`${memoriesTable.expiresAt} <= now()`) : void 0, filter.superseded ? isNotNull(memoriesTable.supersededBy) : void 0, filter.createdBefore ? lt(memoriesTable.createdAt, filter.createdBefore) : void 0, filter.notAccessedSince ? or(isNull(memoriesTable.lastAccessedAt), lt(memoriesTable.lastAccessedAt, filter.notAccessedSince)) : void 0, filter.types && filter.types.length > 0 ? inArray(memoriesTable.type, [...filter.types]) : void 0, tagsContainAll(filter.tags), filter.maxImportanceLevels && filter.maxImportanceLevels.length > 0 ? inArray(memoriesTable.importance, [...filter.maxImportanceLevels]) : void 0);
|
|
1424
|
-
}
|
|
1425
|
-
async function countPruneMatches(db, filter) {
|
|
1426
|
-
return db.$count(memoriesTable, pruneConditions(filter));
|
|
1427
|
-
}
|
|
1428
|
-
async function pruneMemories(db, filter) {
|
|
1429
|
-
return (await db.delete(memoriesTable).where(pruneConditions(filter)).returning({ id: memoriesTable.id })).length;
|
|
1430
|
-
}
|
|
1431
|
-
async function findDuplicatePairs(db, input) {
|
|
1432
|
-
return (await db.execute(sql`
|
|
1433
|
-
SELECT
|
|
1434
|
-
a.id AS "aId",
|
|
1435
|
-
a.entity_id AS "aEntityId",
|
|
1436
|
-
a.content AS "aContent",
|
|
1437
|
-
a.type AS "aType",
|
|
1438
|
-
a.importance AS "aImportance",
|
|
1439
|
-
a.tags AS "aTags",
|
|
1440
|
-
a.source AS "aSource",
|
|
1441
|
-
a.metadata AS "aMetadata",
|
|
1442
|
-
a.confidence AS "aConfidence",
|
|
1443
|
-
a.embedding AS "aEmbedding",
|
|
1444
|
-
a.embedding_model AS "aEmbeddingModel",
|
|
1445
|
-
a.embedding_dimensions AS "aEmbeddingDimensions",
|
|
1446
|
-
a.superseded_by AS "aSupersededBy",
|
|
1447
|
-
a.superseded_at AS "aSupersededAt",
|
|
1448
|
-
a.expires_at AS "aExpiresAt",
|
|
1449
|
-
a.last_accessed_at AS "aLastAccessedAt",
|
|
1450
|
-
a.access_count AS "aAccessCount",
|
|
1451
|
-
a.created_at AS "aCreatedAt",
|
|
1452
|
-
a.updated_at AS "aUpdatedAt",
|
|
1453
|
-
b.id AS "bId",
|
|
1454
|
-
b.entity_id AS "bEntityId",
|
|
1455
|
-
b.content AS "bContent",
|
|
1456
|
-
b.type AS "bType",
|
|
1457
|
-
b.importance AS "bImportance",
|
|
1458
|
-
b.tags AS "bTags",
|
|
1459
|
-
b.source AS "bSource",
|
|
1460
|
-
b.metadata AS "bMetadata",
|
|
1461
|
-
b.confidence AS "bConfidence",
|
|
1462
|
-
b.embedding AS "bEmbedding",
|
|
1463
|
-
b.embedding_model AS "bEmbeddingModel",
|
|
1464
|
-
b.embedding_dimensions AS "bEmbeddingDimensions",
|
|
1465
|
-
b.superseded_by AS "bSupersededBy",
|
|
1466
|
-
b.superseded_at AS "bSupersededAt",
|
|
1467
|
-
b.expires_at AS "bExpiresAt",
|
|
1468
|
-
b.last_accessed_at AS "bLastAccessedAt",
|
|
1469
|
-
b.access_count AS "bAccessCount",
|
|
1470
|
-
b.created_at AS "bCreatedAt",
|
|
1471
|
-
b.updated_at AS "bUpdatedAt",
|
|
1472
|
-
1 - (a.embedding <=> b.embedding) AS "similarity"
|
|
1473
|
-
FROM mnemocyte_memories a
|
|
1474
|
-
JOIN mnemocyte_memories b
|
|
1475
|
-
ON b.entity_id = a.entity_id
|
|
1476
|
-
AND a.id < b.id
|
|
1477
|
-
AND a.embedding IS NOT NULL
|
|
1478
|
-
AND b.embedding IS NOT NULL
|
|
1479
|
-
WHERE
|
|
1480
|
-
a.entity_id = ${input.entityId}
|
|
1481
|
-
${input.includeSuperseded ? sql`` : sql`AND a.superseded_by IS NULL AND b.superseded_by IS NULL`}
|
|
1482
|
-
${input.includeExpired ? sql`` : sql`AND (a.expires_at IS NULL OR a.expires_at > now()) AND (b.expires_at IS NULL OR b.expires_at > now())`}
|
|
1483
|
-
${input.types && input.types.length > 0 ? sql`AND a.type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)}) AND b.type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)})` : sql``}
|
|
1484
|
-
${duplicateTagsFilter(input.tags)}
|
|
1485
|
-
AND 1 - (a.embedding <=> b.embedding) >= ${input.threshold}
|
|
1486
|
-
ORDER BY "similarity" DESC
|
|
1487
|
-
LIMIT ${input.limit}
|
|
1488
|
-
`)).map((row) => {
|
|
1489
|
-
return {
|
|
1490
|
-
a: {
|
|
1491
|
-
id: row.aId,
|
|
1492
|
-
entityId: row.aEntityId,
|
|
1493
|
-
content: row.aContent,
|
|
1494
|
-
type: row.aType,
|
|
1495
|
-
importance: row.aImportance,
|
|
1496
|
-
tags: row.aTags,
|
|
1497
|
-
source: row.aSource,
|
|
1498
|
-
metadata: row.aMetadata,
|
|
1499
|
-
confidence: row.aConfidence,
|
|
1500
|
-
embedding: row.aEmbedding,
|
|
1501
|
-
embeddingModel: row.aEmbeddingModel,
|
|
1502
|
-
embeddingDimensions: row.aEmbeddingDimensions,
|
|
1503
|
-
supersededBy: row.aSupersededBy,
|
|
1504
|
-
supersededAt: row.aSupersededAt,
|
|
1505
|
-
expiresAt: row.aExpiresAt,
|
|
1506
|
-
lastAccessedAt: row.aLastAccessedAt,
|
|
1507
|
-
accessCount: row.aAccessCount,
|
|
1508
|
-
createdAt: row.aCreatedAt,
|
|
1509
|
-
updatedAt: row.aUpdatedAt
|
|
1510
|
-
},
|
|
1511
|
-
b: {
|
|
1512
|
-
id: row.bId,
|
|
1513
|
-
entityId: row.bEntityId,
|
|
1514
|
-
content: row.bContent,
|
|
1515
|
-
type: row.bType,
|
|
1516
|
-
importance: row.bImportance,
|
|
1517
|
-
tags: row.bTags,
|
|
1518
|
-
source: row.bSource,
|
|
1519
|
-
metadata: row.bMetadata,
|
|
1520
|
-
confidence: row.bConfidence,
|
|
1521
|
-
embedding: row.bEmbedding,
|
|
1522
|
-
embeddingModel: row.bEmbeddingModel,
|
|
1523
|
-
embeddingDimensions: row.bEmbeddingDimensions,
|
|
1524
|
-
supersededBy: row.bSupersededBy,
|
|
1525
|
-
supersededAt: row.bSupersededAt,
|
|
1526
|
-
expiresAt: row.bExpiresAt,
|
|
1527
|
-
lastAccessedAt: row.bLastAccessedAt,
|
|
1528
|
-
accessCount: row.bAccessCount,
|
|
1529
|
-
createdAt: row.bCreatedAt,
|
|
1530
|
-
updatedAt: row.bUpdatedAt
|
|
1531
|
-
},
|
|
1532
|
-
similarity: Number(row.similarity) || 0
|
|
1533
|
-
};
|
|
1534
|
-
});
|
|
1535
|
-
}
|
|
1536
|
-
async function loadConsolidationTargets(db, entityId, ids) {
|
|
1537
|
-
if (ids.length === 0) return [];
|
|
1538
|
-
return await db.select({
|
|
1539
|
-
id: memoriesTable.id,
|
|
1540
|
-
tags: memoriesTable.tags,
|
|
1541
|
-
supersededBy: memoriesTable.supersededBy
|
|
1542
|
-
}).from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), inArray(memoriesTable.id, [...ids])));
|
|
1543
|
-
}
|
|
1544
|
-
async function markMemoriesSuperseded(db, params) {
|
|
1545
|
-
if (params.ids.length === 0) return [];
|
|
1546
|
-
return await db.update(memoriesTable).set({
|
|
1547
|
-
supersededBy: params.survivorId,
|
|
1548
|
-
supersededAt: params.now,
|
|
1549
|
-
updatedAt: params.now
|
|
1550
|
-
}).where(and(eq(memoriesTable.entityId, params.entityId), inArray(memoriesTable.id, [...params.ids]), isNull(memoriesTable.supersededBy))).returning({
|
|
1551
|
-
id: memoriesTable.id,
|
|
1552
|
-
tags: memoriesTable.tags
|
|
1553
|
-
});
|
|
1554
|
-
}
|
|
1555
|
-
async function setMemoryTags(db, params) {
|
|
1556
|
-
await db.update(memoriesTable).set({
|
|
1557
|
-
tags: [...params.tags],
|
|
1558
|
-
updatedAt: params.now
|
|
1559
|
-
}).where(and(eq(memoriesTable.entityId, params.entityId), eq(memoriesTable.id, params.memoryId)));
|
|
1560
|
-
}
|
|
1561
|
-
async function markMemoriesAccessed(db, memoryIds) {
|
|
1562
|
-
const now = /* @__PURE__ */ new Date();
|
|
1563
|
-
if (memoryIds.length === 0) return;
|
|
1564
|
-
await db.update(memoriesTable).set({
|
|
1565
|
-
lastAccessedAt: now,
|
|
1566
|
-
accessCount: sql`${memoriesTable.accessCount} + 1`,
|
|
1567
|
-
updatedAt: now
|
|
1568
|
-
}).where(inArray(memoriesTable.id, [...memoryIds]));
|
|
1569
|
-
}
|
|
1570
|
-
async function getMemoryEmbeddings(db, memoryIds) {
|
|
1571
|
-
if (memoryIds.length === 0) return /* @__PURE__ */ new Map();
|
|
1572
|
-
const rows = await db.select({
|
|
1573
|
-
id: memoriesTable.id,
|
|
1574
|
-
embedding: memoriesTable.embedding
|
|
1575
|
-
}).from(memoriesTable).where(inArray(memoriesTable.id, [...memoryIds]));
|
|
1576
|
-
return new Map(rows.flatMap((row) => row.embedding ? [[row.id, row.embedding]] : []));
|
|
1577
|
-
}
|
|
1578
|
-
async function vectorSearch(db, input) {
|
|
1579
|
-
const embedding = vectorLiteral(input.embedding);
|
|
1580
|
-
const minScore = input.minScore ?? 0;
|
|
1581
|
-
return await db.execute(sql`
|
|
1582
|
-
SELECT
|
|
1583
|
-
id,
|
|
1584
|
-
entity_id AS "entityId",
|
|
1585
|
-
content,
|
|
1586
|
-
type,
|
|
1587
|
-
importance,
|
|
1588
|
-
tags,
|
|
1589
|
-
source,
|
|
1590
|
-
metadata,
|
|
1591
|
-
confidence,
|
|
1592
|
-
embedding_model AS "embeddingModel",
|
|
1593
|
-
embedding_dimensions AS "embeddingDimensions",
|
|
1594
|
-
superseded_by AS "supersededBy",
|
|
1595
|
-
superseded_at AS "supersededAt",
|
|
1596
|
-
expires_at AS "expiresAt",
|
|
1597
|
-
last_accessed_at AS "lastAccessedAt",
|
|
1598
|
-
access_count AS "accessCount",
|
|
1599
|
-
created_at AS "createdAt",
|
|
1600
|
-
updated_at AS "updatedAt",
|
|
1601
|
-
1 - (embedding <=> ${embedding}::vector) AS "vectorScore"
|
|
1602
|
-
FROM mnemocyte_memories
|
|
1603
|
-
WHERE
|
|
1604
|
-
entity_id = ${input.entityId}
|
|
1605
|
-
AND embedding IS NOT NULL
|
|
1606
|
-
${input.includeSuperseded ? sql`` : sql`AND superseded_by IS NULL`}
|
|
1607
|
-
${input.includeExpired ? sql`` : sql`AND (expires_at IS NULL OR expires_at > now())`}
|
|
1608
|
-
${input.types && input.types.length > 0 ? sql`AND type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)})` : sql``}
|
|
1609
|
-
${rawTagsFilter(input.tags)}
|
|
1610
|
-
${input.before ? sql`AND created_at < ${timestampParam(input.before)}` : sql``}
|
|
1611
|
-
${input.after ? sql`AND created_at > ${timestampParam(input.after)}` : sql``}
|
|
1612
|
-
AND 1 - (embedding <=> ${embedding}::vector) >= ${minScore}
|
|
1613
|
-
ORDER BY embedding <=> ${embedding}::vector
|
|
1614
|
-
LIMIT ${input.limit}
|
|
1615
|
-
`);
|
|
1616
|
-
}
|
|
1617
|
-
async function lexicalSearch(db, input) {
|
|
1618
|
-
return await db.execute(sql`
|
|
1619
|
-
SELECT
|
|
1620
|
-
id,
|
|
1621
|
-
entity_id AS "entityId",
|
|
1622
|
-
content,
|
|
1623
|
-
type,
|
|
1624
|
-
importance,
|
|
1625
|
-
tags,
|
|
1626
|
-
source,
|
|
1627
|
-
metadata,
|
|
1628
|
-
confidence,
|
|
1629
|
-
embedding_model AS "embeddingModel",
|
|
1630
|
-
embedding_dimensions AS "embeddingDimensions",
|
|
1631
|
-
superseded_by AS "supersededBy",
|
|
1632
|
-
superseded_at AS "supersededAt",
|
|
1633
|
-
expires_at AS "expiresAt",
|
|
1634
|
-
last_accessed_at AS "lastAccessedAt",
|
|
1635
|
-
access_count AS "accessCount",
|
|
1636
|
-
created_at AS "createdAt",
|
|
1637
|
-
updated_at AS "updatedAt",
|
|
1638
|
-
ts_rank(to_tsvector('english', content), websearch_to_tsquery('english', ${input.query})) AS "lexicalScore"
|
|
1639
|
-
FROM mnemocyte_memories
|
|
1640
|
-
WHERE
|
|
1641
|
-
entity_id = ${input.entityId}
|
|
1642
|
-
${input.includeSuperseded ? sql`` : sql`AND superseded_by IS NULL`}
|
|
1643
|
-
${input.includeExpired ? sql`` : sql`AND (expires_at IS NULL OR expires_at > now())`}
|
|
1644
|
-
${input.types && input.types.length > 0 ? sql`AND type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)})` : sql``}
|
|
1645
|
-
${rawTagsFilter(input.tags)}
|
|
1646
|
-
${input.before ? sql`AND created_at < ${timestampParam(input.before)}` : sql``}
|
|
1647
|
-
${input.after ? sql`AND created_at > ${timestampParam(input.after)}` : sql``}
|
|
1648
|
-
AND to_tsvector('english', content) @@ websearch_to_tsquery('english', ${input.query})
|
|
1649
|
-
ORDER BY "lexicalScore" DESC
|
|
1650
|
-
LIMIT ${input.limit}
|
|
1651
|
-
`);
|
|
1652
|
-
}
|
|
1653
|
-
async function getInstallationMeta(db) {
|
|
1654
|
-
return (await db.select().from(metaTable).where(eq(metaTable.key, "installation")).limit(1))[0] ?? null;
|
|
1655
|
-
}
|
|
1656
|
-
//#endregion
|
|
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) {
|
|
1665
|
-
return {
|
|
1666
|
-
id: row.id,
|
|
1667
|
-
entityId: row.entityId,
|
|
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)
|
|
1684
|
-
};
|
|
1685
|
-
}
|
|
1686
|
-
//#endregion
|
|
1687
|
-
//#region src/memory/postgres.ts
|
|
1688
|
-
const IMPORTANCE_LEVELS = [
|
|
1689
|
-
"low",
|
|
1690
|
-
"normal",
|
|
1691
|
-
"high",
|
|
1692
|
-
"critical"
|
|
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
|
-
}
|
|
1709
|
-
function importanceCeilingLevels(max) {
|
|
1710
|
-
return IMPORTANCE_LEVELS.filter((level) => IMPORTANCE_RANK[level] <= IMPORTANCE_RANK[max]);
|
|
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
|
-
}
|
|
1731
|
-
function toPruneFilter(input) {
|
|
1732
|
-
const filter = {};
|
|
1733
|
-
if (input.entityId !== void 0) filter.entityId = input.entityId;
|
|
1734
|
-
if (input.expired === true) filter.expired = true;
|
|
1735
|
-
if (input.superseded === true) filter.superseded = true;
|
|
1736
|
-
if (input.createdBefore !== void 0) filter.createdBefore = input.createdBefore;
|
|
1737
|
-
if (input.notAccessedSince !== void 0) filter.notAccessedSince = input.notAccessedSince;
|
|
1738
|
-
if (input.types !== void 0 && input.types.length > 0) filter.types = input.types;
|
|
1739
|
-
if (input.tags !== void 0 && input.tags.length > 0) filter.tags = input.tags;
|
|
1740
|
-
if (input.maxImportance !== void 0) filter.maxImportanceLevels = importanceCeilingLevels(input.maxImportance);
|
|
1741
|
-
return filter;
|
|
1742
|
-
}
|
|
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;
|
|
1761
|
-
}
|
|
1762
|
-
async function validateEmbeddingCompatibility(embedder) {
|
|
1763
|
-
let meta;
|
|
1764
|
-
try {
|
|
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;
|
|
312
|
+
//#region src/memory/lazy-postgres.ts
|
|
313
|
+
function createLazyPostgresStore(databaseUrl) {
|
|
314
|
+
let storePromise;
|
|
315
|
+
function loadStore() {
|
|
316
|
+
storePromise ??= import("./postgres-runtime-DxJxns07.mjs").then(({ createPostgresStoreFromUrl }) => createPostgresStoreFromUrl(databaseUrl));
|
|
317
|
+
return storePromise;
|
|
1773
318
|
}
|
|
1774
319
|
return {
|
|
1775
320
|
backend: "postgres",
|
|
1776
|
-
async ensureSchema() {
|
|
1777
|
-
|
|
321
|
+
async ensureSchema() {
|
|
322
|
+
return (await loadStore()).ensureSchema();
|
|
323
|
+
},
|
|
324
|
+
async ensureEmbeddingCompatibility(embedder) {
|
|
325
|
+
return (await loadStore()).ensureEmbeddingCompatibility(embedder);
|
|
326
|
+
},
|
|
1778
327
|
async insertMemories(memories) {
|
|
1779
|
-
return
|
|
1780
|
-
return (await insertMemories(handle.db, memories.map(toMemoryRow))).map(rowToMemory);
|
|
1781
|
-
});
|
|
328
|
+
return (await loadStore()).insertMemories(memories);
|
|
1782
329
|
},
|
|
1783
330
|
async vectorSearch(input) {
|
|
1784
|
-
return
|
|
1785
|
-
return (await vectorSearch(handle.db, input)).map((row) => ({
|
|
1786
|
-
memory: rowToMemory(row),
|
|
1787
|
-
vectorScore: row.vectorScore
|
|
1788
|
-
}));
|
|
1789
|
-
});
|
|
331
|
+
return (await loadStore()).vectorSearch(input);
|
|
1790
332
|
},
|
|
1791
333
|
async lexicalSearch(input) {
|
|
1792
|
-
return
|
|
1793
|
-
return (await lexicalSearch(handle.db, input)).map((row) => ({
|
|
1794
|
-
memory: rowToMemory(row),
|
|
1795
|
-
lexicalScore: row.lexicalScore
|
|
1796
|
-
}));
|
|
1797
|
-
});
|
|
334
|
+
return (await loadStore()).lexicalSearch(input);
|
|
1798
335
|
},
|
|
1799
336
|
async getMemoryEmbeddings(memoryIds) {
|
|
1800
|
-
return
|
|
337
|
+
return (await loadStore()).getMemoryEmbeddings(memoryIds);
|
|
1801
338
|
},
|
|
1802
339
|
async markMemoriesAccessed(memoryIds) {
|
|
1803
|
-
return
|
|
340
|
+
return (await loadStore()).markMemoriesAccessed(memoryIds);
|
|
1804
341
|
},
|
|
1805
342
|
async deleteMemory(entityId, memoryId) {
|
|
1806
|
-
return
|
|
343
|
+
return (await loadStore()).deleteMemory(entityId, memoryId);
|
|
1807
344
|
},
|
|
1808
345
|
async deleteMemoriesForEntity(entityId) {
|
|
1809
|
-
return
|
|
346
|
+
return (await loadStore()).deleteMemoriesForEntity(entityId);
|
|
1810
347
|
},
|
|
1811
|
-
async prune(input) {
|
|
1812
|
-
return
|
|
1813
|
-
const filter = toPruneFilter(input);
|
|
1814
|
-
if (input.dryRun === true) return {
|
|
1815
|
-
matchedCount: await countPruneMatches(handle.db, filter),
|
|
1816
|
-
deletedCount: 0,
|
|
1817
|
-
dryRun: true
|
|
1818
|
-
};
|
|
1819
|
-
const deletedCount = await pruneMemories(handle.db, filter);
|
|
1820
|
-
return {
|
|
1821
|
-
matchedCount: deletedCount,
|
|
1822
|
-
deletedCount,
|
|
1823
|
-
dryRun: false
|
|
1824
|
-
};
|
|
1825
|
-
});
|
|
348
|
+
async prune(input, options) {
|
|
349
|
+
return (await loadStore()).prune(input, options);
|
|
1826
350
|
},
|
|
1827
|
-
async findDuplicatePairs(input) {
|
|
1828
|
-
return
|
|
1829
|
-
return (await findDuplicatePairs(handle.db, {
|
|
1830
|
-
entityId: input.entityId,
|
|
1831
|
-
threshold: input.threshold ?? .95,
|
|
1832
|
-
limit: input.limit ?? 100,
|
|
1833
|
-
...input.types === void 0 ? {} : { types: input.types },
|
|
1834
|
-
...input.tags === void 0 ? {} : { tags: input.tags },
|
|
1835
|
-
...input.includeSuperseded === void 0 ? {} : { includeSuperseded: input.includeSuperseded },
|
|
1836
|
-
...input.includeExpired === void 0 ? {} : { includeExpired: input.includeExpired }
|
|
1837
|
-
})).map((row) => ({
|
|
1838
|
-
a: rowToMemory(row.a),
|
|
1839
|
-
b: rowToMemory(row.b),
|
|
1840
|
-
similarity: Math.max(0, Math.min(1, row.similarity))
|
|
1841
|
-
}));
|
|
1842
|
-
});
|
|
351
|
+
async findDuplicatePairs(input, options) {
|
|
352
|
+
return (await loadStore()).findDuplicatePairs(input, options);
|
|
1843
353
|
},
|
|
1844
354
|
async addAuditEvents(events) {
|
|
1845
|
-
return
|
|
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
|
-
});
|
|
355
|
+
return (await loadStore()).addAuditEvents(events);
|
|
1854
356
|
},
|
|
1855
|
-
async listAuditLog(input) {
|
|
1856
|
-
return
|
|
1857
|
-
return (await listEvents(handle.db, {
|
|
1858
|
-
entityId: input.entityId,
|
|
1859
|
-
limit: input.limit ?? 50,
|
|
1860
|
-
...input.before === void 0 ? {} : { before: input.before },
|
|
1861
|
-
...input.after === void 0 ? {} : { after: input.after }
|
|
1862
|
-
})).map(rowToAuditEvent);
|
|
1863
|
-
});
|
|
357
|
+
async listAuditLog(input, options) {
|
|
358
|
+
return (await loadStore()).listAuditLog(input, options);
|
|
1864
359
|
},
|
|
1865
|
-
async getMemory(entityId, memoryId) {
|
|
1866
|
-
return
|
|
1867
|
-
const row = await getMemoryById(handle.db, entityId, memoryId);
|
|
1868
|
-
return row ? rowToMemory(row) : null;
|
|
1869
|
-
});
|
|
360
|
+
async getMemory(entityId, memoryId, options) {
|
|
361
|
+
return (await loadStore()).getMemory(entityId, memoryId, options);
|
|
1870
362
|
},
|
|
1871
|
-
async loadConsolidationTargets(entityId, ids) {
|
|
1872
|
-
return
|
|
363
|
+
async loadConsolidationTargets(entityId, ids, options) {
|
|
364
|
+
return (await loadStore()).loadConsolidationTargets(entityId, ids, options);
|
|
1873
365
|
},
|
|
1874
|
-
async consolidate(input) {
|
|
1875
|
-
return
|
|
1876
|
-
return { supersededIds: await handle.db.transaction(async (tx) => {
|
|
1877
|
-
const updated = await markMemoriesSuperseded(tx, {
|
|
1878
|
-
survivorId: input.survivorId,
|
|
1879
|
-
entityId: input.entityId,
|
|
1880
|
-
ids: input.supersededIds,
|
|
1881
|
-
now: input.now
|
|
1882
|
-
});
|
|
1883
|
-
const ids = updated.map((row) => row.id);
|
|
1884
|
-
if (input.auditEnabled) for (const id of ids) await insertEvent(tx, {
|
|
1885
|
-
id: createEventId(),
|
|
1886
|
-
entityId: input.entityId,
|
|
1887
|
-
description: "memory.superseded",
|
|
1888
|
-
metadata: {
|
|
1889
|
-
memoryId: id,
|
|
1890
|
-
supersededBy: input.survivorId
|
|
1891
|
-
},
|
|
1892
|
-
timestamp: input.now
|
|
1893
|
-
});
|
|
1894
|
-
if (input.mergeTags && updated.length > 0) {
|
|
1895
|
-
const mergedTags = new Set(input.survivorTags);
|
|
1896
|
-
for (const row of updated) for (const tag of row.tags) mergedTags.add(tag);
|
|
1897
|
-
if (mergedTags.size !== input.survivorTags.length) await setMemoryTags(tx, {
|
|
1898
|
-
entityId: input.entityId,
|
|
1899
|
-
memoryId: input.survivorId,
|
|
1900
|
-
tags: [...mergedTags],
|
|
1901
|
-
now: input.now
|
|
1902
|
-
});
|
|
1903
|
-
}
|
|
1904
|
-
return ids;
|
|
1905
|
-
}) };
|
|
1906
|
-
});
|
|
366
|
+
async consolidate(input, options) {
|
|
367
|
+
return (await loadStore()).consolidate(input, options);
|
|
1907
368
|
},
|
|
1908
369
|
async stats(input, now) {
|
|
1909
|
-
return
|
|
1910
|
-
if (input?.entityId) {
|
|
1911
|
-
const counts = await getEntityMemoryStatsCounts(handle.db, input.entityId, now);
|
|
1912
|
-
return {
|
|
1913
|
-
entityId: input.entityId,
|
|
1914
|
-
...counts
|
|
1915
|
-
};
|
|
1916
|
-
}
|
|
1917
|
-
return await getGlobalMemoryStatsCounts(handle.db, now);
|
|
1918
|
-
});
|
|
370
|
+
return (await loadStore()).stats(input, now);
|
|
1919
371
|
},
|
|
1920
372
|
async close() {
|
|
1921
|
-
return
|
|
373
|
+
return (await loadStore()).close();
|
|
1922
374
|
}
|
|
1923
375
|
};
|
|
1924
376
|
}
|
|
1925
|
-
function createPostgresClient(config, handle) {
|
|
1926
|
-
return createMemoryClient(config, createPostgresStore(handle));
|
|
1927
|
-
}
|
|
1928
377
|
//#endregion
|
|
1929
378
|
//#region src/client.ts
|
|
1930
379
|
/**
|
|
1931
380
|
* Create a Mnemocyte client.
|
|
1932
381
|
*
|
|
1933
|
-
* When {@link MnemocyteConfig.
|
|
1934
|
-
*
|
|
1935
|
-
*
|
|
1936
|
-
* for tests, demos,
|
|
382
|
+
* When {@link MnemocyteConfig.store} is provided, that adapter is used. When
|
|
383
|
+
* {@link MnemocyteConfig.databaseUrl} is provided, a URL-owned Postgres client
|
|
384
|
+
* is used (requires the `pgvector` extension and the bundled migrations).
|
|
385
|
+
* When neither is supplied, an in-memory client is returned for tests, demos,
|
|
386
|
+
* and short-lived processes.
|
|
1937
387
|
*
|
|
1938
|
-
*
|
|
1939
|
-
*
|
|
1940
|
-
*
|
|
388
|
+
* Call {@link MnemocyteClient.close} when finished. URL-created database
|
|
389
|
+
* resources are owned and closed by Mnemocyte; resources behind a supplied
|
|
390
|
+
* store retain the ownership policy documented by that adapter.
|
|
1941
391
|
*
|
|
1942
392
|
* @param config - Client configuration. {@link MnemocyteConfig.embedder} is required.
|
|
1943
393
|
* @returns A {@link MnemocyteClient} instance.
|
|
1944
394
|
* @throws {MnemocyteError} With code `"CONFIG"` if `embedder`, retrieval
|
|
1945
|
-
* tuning, or the database URL is malformed
|
|
1946
|
-
* `
|
|
395
|
+
* tuning, provider resilience, or the database URL is malformed or does not
|
|
396
|
+
* use the `postgres:` / `postgresql:` protocol, or if `databaseUrl` and
|
|
397
|
+
* `store` are both provided; or `"VALIDATION"` if `databaseUrl` is explicitly
|
|
398
|
+
* empty.
|
|
1947
399
|
*
|
|
1948
400
|
* @example Postgres-backed client
|
|
1949
401
|
* ```ts
|
|
@@ -1956,10 +408,14 @@ function createPostgresClient(config, handle) {
|
|
|
1956
408
|
function createMnemocyte(config) {
|
|
1957
409
|
assertEmbedder(config.embedder);
|
|
1958
410
|
validateRetrievalConfig(config.retrieval);
|
|
411
|
+
validateProviderResilienceConfig(config.provider);
|
|
412
|
+
if (config.databaseUrl !== void 0 && config.store !== void 0) throw new MnemocyteError("databaseUrl and store cannot be provided together.", "CONFIG");
|
|
1959
413
|
if (config.databaseUrl !== void 0) {
|
|
1960
414
|
assertNonEmptyString(config.databaseUrl, "databaseUrl");
|
|
1961
|
-
|
|
415
|
+
parsePostgresDatabaseUrl(config.databaseUrl);
|
|
416
|
+
return createMemoryClient(config, createLazyPostgresStore(config.databaseUrl));
|
|
1962
417
|
}
|
|
418
|
+
if (config.store !== void 0) return createMemoryClient(config, unwrapMnemocyteStoreConfig(config.store));
|
|
1963
419
|
return createInMemoryClient(config);
|
|
1964
420
|
}
|
|
1965
421
|
//#endregion
|