mnemocyte 0.2.0 → 0.4.0

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