mnemocyte 0.3.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
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) => {
@@ -18,7 +18,7 @@ var __exportAll = (all, no_symbols) => {
18
18
  //#region src/db/vector.ts
19
19
  function formatVectorComponent(value) {
20
20
  if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
21
- return Object.is(value, -0) ? "0" : value.toFixed(17);
21
+ return Object.is(value, -0) ? "0" : value.toString();
22
22
  }
23
23
  //#endregion
24
24
  //#region src/db/schema.ts
@@ -72,7 +72,8 @@ const eventsTable = pgTable("mnemocyte_events", {
72
72
  }, (table) => [index("mnemocyte_events_entity_time_idx").on(table.entityId, table.timestamp)]);
73
73
  const metaTable = pgTable("mnemocyte_meta", {
74
74
  key: text("key").primaryKey(),
75
- embeddingDimensions: integer("embedding_dimensions").notNull()
75
+ embeddingDimensions: integer("embedding_dimensions").notNull(),
76
+ embeddingModel: text("embedding_model")
76
77
  });
77
78
  //#endregion
78
79
  //#region src/db/index.ts
@@ -89,6 +90,7 @@ function createDatabase(databaseUrl) {
89
90
  } catch (error) {
90
91
  throw new MnemocyteError("databaseUrl must be a valid Postgres connection URL.", "CONFIG", error);
91
92
  }
93
+ if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") throw new MnemocyteError("databaseUrl must use the postgres: or postgresql: protocol.", "CONFIG");
92
94
  const ssl = parseSslFromUrl(url);
93
95
  const isPooler = url.port === "6543" || url.searchParams.get("pgbouncer") === "true";
94
96
  const client = postgres(databaseUrl, {
@@ -106,6 +108,179 @@ function createDatabase(databaseUrl) {
106
108
  };
107
109
  }
108
110
  //#endregion
111
+ //#region src/resilience.ts
112
+ /**
113
+ * Default per-provider-call timeout. Disabled by default so that callers
114
+ * who do not configure {@link ProviderResilienceConfig.timeoutMs} are
115
+ * unaffected.
116
+ */
117
+ const DEFAULT_TIMEOUT_MS = 0;
118
+ /** Default number of retry attempts on transient provider failures. */
119
+ const DEFAULT_RETRIES = 0;
120
+ /** Default base backoff delay used between retry attempts. */
121
+ const DEFAULT_BASE_DELAY_MS = 100;
122
+ /** Default ceiling for retry backoff delay. */
123
+ const DEFAULT_MAX_DELAY_MS = 2e3;
124
+ /**
125
+ * Throw a {@link MnemocyteError} with code `"ABORTED"` if `signal` is
126
+ * already aborted. Safe to call with `undefined`.
127
+ */
128
+ function throwIfAborted(signal) {
129
+ if (signal?.aborted) throw new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason);
130
+ }
131
+ /**
132
+ * Heuristic default for whether an error should be retried.
133
+ *
134
+ * Conservative by design: only retries on common transient failure
135
+ * indicators (network errors, 5xx-ish messages, `ECONN*` etc.). Never
136
+ * retries on validation, configuration, or {@link MnemocyteError} of
137
+ * code `"VALIDATION"` / `"CONFIG"` / `"CONFLICT"` / `"ABORTED"`.
138
+ */
139
+ function defaultShouldRetry(error) {
140
+ if (error instanceof MnemocyteError) {
141
+ if (error.code === "VALIDATION" || error.code === "CONFIG" || error.code === "CONFLICT" || error.code === "ABORTED") return false;
142
+ return true;
143
+ }
144
+ if (typeof error === "object" && error !== null && "status" in error && typeof error.status === "number" && [
145
+ 429,
146
+ 500,
147
+ 502,
148
+ 503,
149
+ 504
150
+ ].includes(error.status)) return true;
151
+ if (error instanceof Error) {
152
+ const message = error.message.toLowerCase();
153
+ if (message.includes("econn") || message.includes("etimedout") || message.includes("network") || message.includes("timeout") || message.includes("temporarily") || message.includes("rate limit") || message.includes("503") || message.includes("502") || message.includes("504") || message.includes("500")) return true;
154
+ }
155
+ return false;
156
+ }
157
+ /**
158
+ * Compute the backoff delay for the given retry attempt, using exponential
159
+ * growth capped at {@link ProviderResilienceConfig.maxDelayMs}.
160
+ *
161
+ * `attempt` is 1-indexed: the delay before the first retry uses
162
+ * `attempt = 1`, before the second retry `attempt = 2`, etc.
163
+ */
164
+ function computeDelayMs(attempt, baseDelayMs, maxDelayMs) {
165
+ const exponential = baseDelayMs * 2 ** (attempt - 1);
166
+ return Math.max(0, Math.min(maxDelayMs, exponential));
167
+ }
168
+ /**
169
+ * Promise-based `setTimeout` that rejects with an `"ABORTED"`
170
+ * {@link MnemocyteError} if `signal` aborts while waiting.
171
+ */
172
+ function delay(ms, signal) {
173
+ if (ms <= 0) {
174
+ throwIfAborted(signal);
175
+ return Promise.resolve();
176
+ }
177
+ return new Promise((resolve, reject) => {
178
+ const timer = setTimeout(() => {
179
+ signal?.removeEventListener("abort", onAbort);
180
+ resolve();
181
+ }, ms);
182
+ const onAbort = () => {
183
+ clearTimeout(timer);
184
+ reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal?.reason));
185
+ };
186
+ if (signal) {
187
+ if (signal.aborted) {
188
+ clearTimeout(timer);
189
+ reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason));
190
+ return;
191
+ }
192
+ signal.addEventListener("abort", onAbort, { once: true });
193
+ }
194
+ });
195
+ }
196
+ function timeoutError(timeoutMs) {
197
+ return new MnemocyteError(`Provider call timed out after ${timeoutMs}ms.`, "TIMEOUT");
198
+ }
199
+ /**
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`.
203
+ */
204
+ function runAttempt(action, timeoutMs, signal) {
205
+ if (timeoutMs <= 0 && !signal) return action(void 0);
206
+ return new Promise((resolve, reject) => {
207
+ let settled = false;
208
+ const controller = new AbortController();
209
+ const timer = timeoutMs > 0 ? setTimeout(() => {
210
+ if (settled) return;
211
+ settled = true;
212
+ signal?.removeEventListener("abort", onAbort);
213
+ controller.abort(timeoutError(timeoutMs));
214
+ reject(timeoutError(timeoutMs));
215
+ }, timeoutMs) : void 0;
216
+ const onAbort = () => {
217
+ if (settled) return;
218
+ settled = true;
219
+ if (timer) clearTimeout(timer);
220
+ controller.abort(signal?.reason);
221
+ reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal?.reason));
222
+ };
223
+ if (signal) {
224
+ if (signal.aborted) {
225
+ if (timer) clearTimeout(timer);
226
+ settled = true;
227
+ controller.abort(signal.reason);
228
+ reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason));
229
+ return;
230
+ }
231
+ signal.addEventListener("abort", onAbort, { once: true });
232
+ }
233
+ action(controller.signal).then((value) => {
234
+ if (settled) return;
235
+ settled = true;
236
+ if (timer) clearTimeout(timer);
237
+ signal?.removeEventListener("abort", onAbort);
238
+ resolve(value);
239
+ }, (error) => {
240
+ if (settled) return;
241
+ settled = true;
242
+ if (timer) clearTimeout(timer);
243
+ signal?.removeEventListener("abort", onAbort);
244
+ reject(error);
245
+ });
246
+ });
247
+ }
248
+ /**
249
+ * Run `action` with timeout, retry, and abort handling derived from
250
+ * `options.resilience` and `options.signal`. Each retry attempt receives
251
+ * a fresh `AbortSignal` linked to the caller's signal — so callers may
252
+ * abort mid-retry and the in-flight attempt will cancel promptly if the
253
+ * underlying provider honours signals.
254
+ *
255
+ * Errors from {@link MnemocyteError} with code `"VALIDATION"` /
256
+ * `"CONFIG"` / `"CONFLICT"` / `"ABORTED"` are never retried.
257
+ */
258
+ async function withResilience(action, options = {}) {
259
+ const config = options.resilience ?? {};
260
+ const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
261
+ const retries = Math.max(0, config.retries ?? DEFAULT_RETRIES);
262
+ const baseDelayMs = Math.max(0, config.baseDelayMs ?? DEFAULT_BASE_DELAY_MS);
263
+ const maxDelayMs = Math.max(baseDelayMs, config.maxDelayMs ?? DEFAULT_MAX_DELAY_MS);
264
+ const shouldRetry = config.shouldRetry ?? defaultShouldRetry;
265
+ const externalSignal = options.signal;
266
+ throwIfAborted(externalSignal);
267
+ const maxAttempts = retries + 1;
268
+ let lastError;
269
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
270
+ throwIfAborted(externalSignal);
271
+ try {
272
+ return await runAttempt(action, timeoutMs, externalSignal);
273
+ } catch (error) {
274
+ lastError = error;
275
+ if (error instanceof MnemocyteError && error.code === "ABORTED") throw error;
276
+ if (attempt >= maxAttempts) throw error;
277
+ if (!shouldRetry(error, attempt)) throw error;
278
+ await delay(computeDelayMs(attempt, baseDelayMs, maxDelayMs), externalSignal);
279
+ }
280
+ }
281
+ throw lastError;
282
+ }
283
+ //#endregion
109
284
  //#region src/memory/json.ts
110
285
  function invalidJsonValue(path, cause) {
111
286
  return new MnemocyteError(`${path} must contain only JSON-compatible value data.`, "VALIDATION", cause);
@@ -152,10 +327,6 @@ function cloneJsonObject(value, path = "metadata") {
152
327
  throw invalidJsonValue(path, error);
153
328
  }
154
329
  }
155
- /** Assert that a runtime value is a JSON-compatible object. */
156
- function validateJsonObject(value, path = "metadata") {
157
- cloneJsonObject(value, path);
158
- }
159
330
  //#endregion
160
331
  //#region src/memory/records.ts
161
332
  function normalizeTags(tags) {
@@ -239,6 +410,9 @@ function clampScore(value) {
239
410
  if (!Number.isFinite(value)) return 0;
240
411
  return Math.max(0, Math.min(1, value));
241
412
  }
413
+ function toVectorScore(cosineSimilarity) {
414
+ return clampScore(cosineSimilarity);
415
+ }
242
416
  function normalizeWeights(weights) {
243
417
  const merged = {
244
418
  ...DEFAULT_RETRIEVAL_WEIGHTS,
@@ -321,6 +495,10 @@ function markdownFence(value) {
321
495
  const longestBacktickRun = Math.max(0, ...Array.from(value.matchAll(/`+/g), (match) => match[0].length));
322
496
  return "`".repeat(Math.max(3, longestBacktickRun + 1));
323
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
+ }
324
502
  function formatMarkdown(memories, query, omittedCount) {
325
503
  const lines = [
326
504
  "# Memory Context",
@@ -337,6 +515,11 @@ function formatMarkdown(memories, query, omittedCount) {
337
515
  return lines.join("\n");
338
516
  }
339
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])]);
340
523
  const lines = [
341
524
  "MEMORY CONTEXT",
342
525
  "",
@@ -344,7 +527,7 @@ function formatPlain(memories, query, omittedCount) {
344
527
  "",
345
528
  "RELEVANT MEMORIES"
346
529
  ];
347
- for (const [index, memory] of memories.entries()) lines.push("", `--- MEMORY ${index + 1} START ---`, formatMetadata(memory), memory.content, `--- MEMORY ${index + 1} END ---`);
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}`);
348
531
  if (omittedCount > 0) lines.push("", `[${omittedCount} memories omitted to fit token budget]`);
349
532
  return lines.join("\n");
350
533
  }
@@ -364,6 +547,7 @@ function formatContext(memories, query, format, omittedCount = 0) {
364
547
  }
365
548
  //#endregion
366
549
  //#region src/context/tokens.ts
550
+ const TRUNCATION_MARKER = "[truncated to fit token budget]";
367
551
  const heuristicTokenCounter = { count(text) {
368
552
  return Math.ceil(text.length / 4);
369
553
  } };
@@ -374,13 +558,18 @@ function trimToTokenBudget(text, maxTokens, tokenCounter) {
374
558
  let best = "";
375
559
  while (low <= high) {
376
560
  const mid = Math.floor((low + high) / 2);
377
- const output = `${text.slice(0, mid).trimEnd()}\n[truncated to fit token budget]`;
561
+ const output = `${text.slice(0, mid).trimEnd()}\n${TRUNCATION_MARKER}`;
378
562
  if (tokenCounter.count(output) <= maxTokens) {
379
563
  best = output;
380
564
  low = mid + 1;
381
565
  } else high = mid - 1;
382
566
  }
383
- return best || "[truncated to fit token budget]";
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 "";
384
573
  }
385
574
  //#endregion
386
575
  //#region src/context/builder.ts
@@ -419,230 +608,71 @@ async function emitObservation(config, event) {
419
608
  await config.observability?.onEvent?.(event);
420
609
  } catch {
421
610
  return;
422
- }
423
- }
424
- async function observe(config, backend, operation, metadata, action, successMetadata) {
425
- const startedAt = Date.now();
426
- await emitObservation(config, withMetadata({
427
- operation,
428
- phase: "start",
429
- backend,
430
- timestamp: new Date(startedAt)
431
- }, metadata));
432
- try {
433
- const result = await action();
434
- await emitObservation(config, withMetadata({
435
- operation,
436
- phase: "success",
437
- backend,
438
- timestamp: /* @__PURE__ */ new Date(),
439
- durationMs: Date.now() - startedAt
440
- }, {
441
- ...metadata,
442
- ...successMetadata?.(result)
443
- }));
444
- return result;
445
- } catch (error) {
446
- await emitObservation(config, withMetadata({
447
- operation,
448
- phase: "error",
449
- backend,
450
- timestamp: /* @__PURE__ */ new Date(),
451
- durationMs: Date.now() - startedAt,
452
- error
453
- }, metadata));
454
- throw error;
455
- }
456
- }
457
- /**
458
- * Ordering of {@link ImportanceLevel} from least to most important. Used
459
- * by `prune({ maxImportance })` to compare a memory's importance against
460
- * the caller-supplied ceiling.
461
- */
462
- const IMPORTANCE_RANK = {
463
- low: 0,
464
- normal: 1,
465
- high: 2,
466
- critical: 3
467
- };
468
- //#endregion
469
- //#region src/resilience.ts
470
- /**
471
- * Default per-provider-call timeout. Disabled by default so that callers
472
- * who do not configure {@link ProviderResilienceConfig.timeoutMs} are
473
- * unaffected.
474
- */
475
- const DEFAULT_TIMEOUT_MS = 0;
476
- /** Default number of retry attempts on transient provider failures. */
477
- const DEFAULT_RETRIES = 0;
478
- /** Default base backoff delay used between retry attempts. */
479
- const DEFAULT_BASE_DELAY_MS = 100;
480
- /** Default ceiling for retry backoff delay. */
481
- const DEFAULT_MAX_DELAY_MS = 2e3;
482
- /**
483
- * Throw a {@link MnemocyteError} with code `"ABORTED"` if `signal` is
484
- * already aborted. Safe to call with `undefined`.
485
- */
486
- function throwIfAborted(signal) {
487
- if (signal?.aborted) throw new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason);
488
- }
489
- /**
490
- * Heuristic default for whether an error should be retried.
491
- *
492
- * Conservative by design: only retries on common transient failure
493
- * indicators (network errors, 5xx-ish messages, `ECONN*` etc.). Never
494
- * retries on validation, configuration, or {@link MnemocyteError} of
495
- * code `"VALIDATION"` / `"CONFIG"` / `"ABORTED"`.
496
- */
497
- function defaultShouldRetry(error) {
498
- if (error instanceof MnemocyteError) {
499
- if (error.code === "VALIDATION" || error.code === "CONFIG" || error.code === "ABORTED") return false;
500
- return true;
501
- }
502
- if (typeof error === "object" && error !== null && "status" in error && typeof error.status === "number" && [
503
- 429,
504
- 500,
505
- 502,
506
- 503,
507
- 504
508
- ].includes(error.status)) return true;
509
- if (error instanceof Error) {
510
- const message = error.message.toLowerCase();
511
- if (message.includes("econn") || message.includes("etimedout") || message.includes("network") || message.includes("timeout") || message.includes("temporarily") || message.includes("rate limit") || message.includes("503") || message.includes("502") || message.includes("504") || message.includes("500")) return true;
512
- }
513
- return false;
514
- }
515
- /**
516
- * Compute the backoff delay for the given retry attempt, using exponential
517
- * growth capped at {@link ProviderResilienceConfig.maxDelayMs}.
518
- *
519
- * `attempt` is 1-indexed: the delay before the first retry uses
520
- * `attempt = 1`, before the second retry `attempt = 2`, etc.
521
- */
522
- function computeDelayMs(attempt, baseDelayMs, maxDelayMs) {
523
- const exponential = baseDelayMs * 2 ** (attempt - 1);
524
- return Math.max(0, Math.min(maxDelayMs, exponential));
525
- }
526
- /**
527
- * Promise-based `setTimeout` that rejects with an `"ABORTED"`
528
- * {@link MnemocyteError} if `signal` aborts while waiting.
529
- */
530
- function delay(ms, signal) {
531
- if (ms <= 0) {
532
- throwIfAborted(signal);
533
- return Promise.resolve();
534
- }
535
- return new Promise((resolve, reject) => {
536
- const timer = setTimeout(() => {
537
- signal?.removeEventListener("abort", onAbort);
538
- resolve();
539
- }, ms);
540
- const onAbort = () => {
541
- clearTimeout(timer);
542
- reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal?.reason));
543
- };
544
- if (signal) {
545
- if (signal.aborted) {
546
- clearTimeout(timer);
547
- reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason));
548
- return;
549
- }
550
- signal.addEventListener("abort", onAbort, { once: true });
551
- }
552
- });
553
- }
554
- function timeoutError(timeoutMs) {
555
- return new MnemocyteError(`Provider call timed out after ${timeoutMs}ms.`, "TIMEOUT");
556
- }
557
- /**
558
- * Run one provider attempt with a signal linked to the caller's signal and
559
- * Mnemocyte's configured timeout. Timeout aborts are actively propagated to
560
- * providers that honor `AbortSignal`.
561
- */
562
- function runAttempt(action, timeoutMs, signal) {
563
- if (timeoutMs <= 0 && !signal) return action(void 0);
564
- return new Promise((resolve, reject) => {
565
- let settled = false;
566
- const controller = new AbortController();
567
- const timer = timeoutMs > 0 ? setTimeout(() => {
568
- if (settled) return;
569
- settled = true;
570
- signal?.removeEventListener("abort", onAbort);
571
- controller.abort(timeoutError(timeoutMs));
572
- reject(timeoutError(timeoutMs));
573
- }, timeoutMs) : void 0;
574
- const onAbort = () => {
575
- if (settled) return;
576
- settled = true;
577
- if (timer) clearTimeout(timer);
578
- controller.abort(signal?.reason);
579
- reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal?.reason));
580
- };
581
- if (signal) {
582
- if (signal.aborted) {
583
- if (timer) clearTimeout(timer);
584
- settled = true;
585
- controller.abort(signal.reason);
586
- reject(new MnemocyteError("Operation was aborted.", "ABORTED", signal.reason));
587
- return;
588
- }
589
- signal.addEventListener("abort", onAbort, { once: true });
590
- }
591
- action(controller.signal).then((value) => {
592
- if (settled) return;
593
- settled = true;
594
- if (timer) clearTimeout(timer);
595
- signal?.removeEventListener("abort", onAbort);
596
- resolve(value);
597
- }, (error) => {
598
- if (settled) return;
599
- settled = true;
600
- if (timer) clearTimeout(timer);
601
- signal?.removeEventListener("abort", onAbort);
602
- reject(error);
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;
603
656
  });
604
- });
657
+ }
605
658
  }
606
659
  /**
607
- * Run `action` with timeout, retry, and abort handling derived from
608
- * `options.resilience` and `options.signal`. Each retry attempt receives
609
- * a fresh `AbortSignal` linked to the caller's signal — so callers may
610
- * abort mid-retry and the in-flight attempt will cancel promptly if the
611
- * underlying provider honours signals.
612
- *
613
- * Errors from {@link MnemocyteError} with code `"VALIDATION"` /
614
- * `"CONFIG"` / `"ABORTED"` are never retried.
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.
615
663
  */
616
- async function withResilience(action, options = {}) {
617
- const config = options.resilience ?? {};
618
- const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
619
- const retries = Math.max(0, config.retries ?? DEFAULT_RETRIES);
620
- const baseDelayMs = Math.max(0, config.baseDelayMs ?? DEFAULT_BASE_DELAY_MS);
621
- const maxDelayMs = Math.max(baseDelayMs, config.maxDelayMs ?? DEFAULT_MAX_DELAY_MS);
622
- const shouldRetry = config.shouldRetry ?? defaultShouldRetry;
623
- const externalSignal = options.signal;
624
- throwIfAborted(externalSignal);
625
- const maxAttempts = retries + 1;
626
- let lastError;
627
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
628
- throwIfAborted(externalSignal);
629
- try {
630
- return await runAttempt(action, timeoutMs, externalSignal);
631
- } catch (error) {
632
- lastError = error;
633
- if (error instanceof MnemocyteError && error.code === "ABORTED") throw error;
634
- if (attempt >= maxAttempts) throw error;
635
- if (!shouldRetry(error, attempt)) throw error;
636
- await delay(computeDelayMs(attempt, baseDelayMs, maxDelayMs), externalSignal);
637
- }
638
- }
639
- throw lastError;
640
- }
664
+ const IMPORTANCE_RANK = {
665
+ low: 0,
666
+ normal: 1,
667
+ high: 2,
668
+ critical: 3
669
+ };
641
670
  //#endregion
642
671
  //#region src/memory/embeddings.ts
643
672
  function validateEmbedding(embedding, dimensions) {
644
673
  if (embedding.length !== dimensions) throw new MnemocyteError("Embedder returned an embedding with unexpected dimensions.", "EMBEDDING");
645
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");
646
676
  }
647
677
  async function embedOne(embedder, text, options = {}) {
648
678
  let embeddings;
@@ -677,7 +707,48 @@ async function embedMany(embedder, texts, options = {}) {
677
707
  return embeddings;
678
708
  }
679
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
680
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
+ }
681
752
  function assertNonEmptyString(value, field, code = "VALIDATION") {
682
753
  if (typeof value !== "string" || value.trim().length === 0) throw new MnemocyteError(`${field} must be a non-empty string.`, code);
683
754
  }
@@ -716,20 +787,39 @@ function validateRetrievalConfig(config) {
716
787
  if (config.accessSaturation !== void 0) assertPositiveFiniteConfigValue(config.accessSaturation, "retrieval.accessSaturation");
717
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");
718
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
+ }
719
801
  function validateBuildContextInput(input) {
720
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");
721
803
  }
722
804
  function validateRememberInput(input) {
723
805
  assertNonEmptyString(input.entityId, "entityId");
724
806
  assertNonEmptyString(input.content, "content");
725
- if (input.metadata !== void 0) validateJsonObject(input.metadata);
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");
726
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");
727
816
  }
728
817
  function validateRecallInput(input) {
729
818
  assertNonEmptyString(input.entityId, "entityId");
730
819
  assertNonEmptyString(input.query, "query");
731
820
  if (input.limit !== void 0) assertLimit(input.limit);
732
821
  if (input.minScore !== void 0) assertMinScore(input.minScore);
822
+ if (input.types !== void 0) assertMemoryTypeArray(input.types, "types");
733
823
  }
734
824
  function contextInputToRecallInput(input) {
735
825
  return {
@@ -744,13 +834,49 @@ function contextInputToRecallInput(input) {
744
834
  };
745
835
  }
746
836
  /**
747
- * Validate a {@link PruneInput} and throw a `"VALIDATION"`
748
- * {@link MnemocyteError} when no selector is supplied. Mnemocyte
749
- * deliberately rejects `prune({})` to avoid surprise full-table deletes.
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.
750
840
  */
751
841
  function validatePruneInput(input) {
752
842
  if (input.entityId !== void 0) assertNonEmptyString(input.entityId, "entityId");
753
- if (!(input.entityId !== void 0 || input.expired === true || input.superseded === true || input.createdBefore !== void 0 || input.notAccessedSince !== void 0 || input.types !== void 0 && input.types.length > 0 || input.tags !== void 0 && input.tags.length > 0 || input.maxImportance !== void 0)) throw new MnemocyteError("prune requires at least one selector (entityId, expired, superseded, createdBefore, notAccessedSince, types, tags, or maxImportance).", "VALIDATION");
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;
754
880
  }
755
881
  /**
756
882
  * Validate a {@link FindDuplicatesInput} and throw a `"VALIDATION"`
@@ -762,6 +888,7 @@ function validateFindDuplicatesInput(input) {
762
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");
763
889
  }
764
890
  if (input.limit !== void 0) assertLimit(input.limit);
891
+ if (input.types !== void 0) assertMemoryTypeArray(input.types, "types");
765
892
  }
766
893
  /**
767
894
  * Validate a {@link ConsolidateInput} and throw a `"VALIDATION"`
@@ -782,6 +909,16 @@ function validateConsolidateInput(input) {
782
909
  function validateListAuditLogInput(input) {
783
910
  assertNonEmptyString(input.entityId, "entityId");
784
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`);
785
922
  }
786
923
  //#endregion
787
924
  //#region src/memory/client-core.ts
@@ -794,6 +931,17 @@ function providerOptions(config, signal) {
794
931
  ...config.provider === void 0 ? {} : { resilience: config.provider }
795
932
  };
796
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
+ }
797
945
  function createStoredMemory(config, input, embedding, now) {
798
946
  return {
799
947
  id: createId(),
@@ -803,7 +951,7 @@ function createStoredMemory(config, input, embedding, now) {
803
951
  importance: input.importance ?? "normal",
804
952
  tags: normalizeTags(input.tags),
805
953
  source: input.source ?? null,
806
- metadata: cloneJsonObject(input.metadata ?? {}),
954
+ metadata: input.metadata,
807
955
  confidence: input.confidence ?? 1,
808
956
  embeddingModel: config.embedder.model,
809
957
  embeddingDimensions: config.embedder.dimensions,
@@ -822,10 +970,35 @@ function auditEvent(entityId, description, metadata, timestamp = /* @__PURE__ */
822
970
  id: createEventId(),
823
971
  entityId,
824
972
  description,
825
- metadata: cloneJsonObject(metadata),
973
+ metadata,
826
974
  timestamp
827
975
  };
828
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
+ }
829
1002
  function toDuplicatePair(pair) {
830
1003
  return {
831
1004
  a: cloneMemory(pair.a),
@@ -834,9 +1007,58 @@ function toDuplicatePair(pair) {
834
1007
  };
835
1008
  }
836
1009
  function createMemoryClient(config, store) {
837
- let closed = false;
1010
+ let state = "open";
1011
+ let activeOperations = 0;
1012
+ let operationsDrainedPromise;
1013
+ let resolveOperationsDrained;
1014
+ let closePromise;
838
1015
  function assertOpen() {
839
- if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
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;
840
1062
  }
841
1063
  async function ensureSchema() {
842
1064
  await store.ensureSchema();
@@ -850,38 +1072,34 @@ function createMemoryClient(config, store) {
850
1072
  await store.addAuditEvents(events);
851
1073
  } catch {}
852
1074
  }
853
- async function remember(input) {
854
- const preparedInput = {
855
- ...input,
856
- metadata: cloneJsonObject(input.metadata ?? {})
857
- };
858
- return observe(config, store.backend, "remember", { entityId: preparedInput.entityId }, async () => {
1075
+ function remember(input) {
1076
+ return runOperation("remember", { entityId: input.entityId }, () => observePrepared(config, store.backend, "remember", { entityId: input.entityId }, () => snapshotRememberInput(input), async (preparedInput) => {
859
1077
  assertOpen();
860
1078
  validateRememberInput(preparedInput);
861
1079
  await ensureEmbeddingCompatibility();
862
- const embedding = await embedOne(config.embedder, preparedInput.content, providerOptions(config, preparedInput.signal));
863
- const [memory] = await store.insertMemories([createStoredMemory(config, preparedInput, embedding, /* @__PURE__ */ new Date())]);
864
- if (!memory) throw new MnemocyteError("Memory insert returned no rows.", "DB");
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");
865
1083
  await recordAudit([auditEvent(memory.entityId, "memory.created", {
866
1084
  memoryId: memory.id,
867
1085
  type: memory.type,
868
1086
  importance: memory.importance
869
1087
  })]);
870
- return cloneMemory(memory);
1088
+ return memory;
871
1089
  }, (memory) => ({
872
1090
  memoryId: memory.id,
873
1091
  count: 1
874
- }));
1092
+ })));
875
1093
  }
876
- async function rememberMany(input) {
877
- const positional = isPositionalRememberManyInput(input);
878
- const inputs = positional ? input : input.inputs;
879
- const signal = positional ? input[0]?.signal : input.signal;
880
- const preparedInputs = inputs.map((item) => ({
881
- ...item,
882
- metadata: cloneJsonObject(item.metadata ?? {})
883
- }));
884
- return observe(config, store.backend, "rememberMany", { count: preparedInputs.length }, async () => {
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 }) => {
885
1103
  assertOpen();
886
1104
  if (preparedInputs.length === 0) return [];
887
1105
  for (const item of preparedInputs) validateRememberInput(item);
@@ -889,86 +1107,101 @@ function createMemoryClient(config, store) {
889
1107
  const embeddings = await embedMany(config.embedder, preparedInputs.map((item) => item.content), providerOptions(config, signal));
890
1108
  const now = /* @__PURE__ */ new Date();
891
1109
  const stored = preparedInputs.map((item, idx) => createStoredMemory(config, item, embeddings[idx], now));
892
- const memories = await store.insertMemories(stored);
1110
+ const memories = normalizeStoreRows(stored, await store.insertMemories(stored), "Memory insert");
893
1111
  await recordAudit(memories.map((memory) => auditEvent(memory.entityId, "memory.created", {
894
1112
  memoryId: memory.id,
895
1113
  type: memory.type,
896
1114
  importance: memory.importance
897
1115
  })));
898
- return memories.map(cloneMemory);
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;
1155
+ }
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
1162
+ });
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
+ });
899
1176
  }, (result) => ({ count: result.length }));
900
1177
  }
901
- const client = {
1178
+ return {
902
1179
  remember,
903
1180
  rememberMany,
904
- async recall(input) {
905
- return observe(config, store.backend, "recall", { entityId: input.entityId }, async () => {
906
- assertOpen();
907
- validateRecallInput(input);
908
- const limit = input.limit ?? config.defaults?.limit ?? 10;
909
- const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
910
- assertLimit(limit);
911
- assertMinScore(minScore);
912
- await ensureEmbeddingCompatibility();
913
- const queryEmbedding = await embedOne(config.embedder, input.query, providerOptions(config, input.signal));
914
- const candidateMultiplier = config.retrieval?.candidateMultiplier ?? 3;
915
- const candidateLimit = Math.max(limit, limit * candidateMultiplier);
916
- const scoreLexical = createLexicalScorer(input.query);
917
- const scoringConfig = createScoringConfig(config.retrieval);
918
- const [vectorCandidates, lexicalCandidates] = await Promise.all([store.vectorSearch({
919
- ...input,
920
- embedding: queryEmbedding,
921
- limit: candidateLimit,
922
- minScore: 0
923
- }), store.lexicalSearch({
924
- ...input,
925
- limit: candidateLimit
926
- })]);
927
- const merged = /* @__PURE__ */ new Map();
928
- for (const candidate of vectorCandidates) merged.set(candidate.memory.id, {
929
- memory: candidate.memory,
930
- vectorScore: candidate.vectorScore,
931
- lexicalScore: scoreLexical(candidate.memory.content)
932
- });
933
- const lexicalOnly = lexicalCandidates.filter((candidate) => !merged.has(candidate.memory.id));
934
- const lexicalOnlyEmbeddings = await store.getMemoryEmbeddings(lexicalOnly.map((candidate) => candidate.memory.id));
935
- for (const candidate of lexicalCandidates) {
936
- const existing = merged.get(candidate.memory.id);
937
- if (existing) {
938
- existing.lexicalScore = candidate.lexicalScore;
939
- continue;
940
- }
941
- const embedding = lexicalOnlyEmbeddings.get(candidate.memory.id);
942
- const similarity = embedding ? cosineSimilarity(embedding, queryEmbedding) : 0;
943
- merged.set(candidate.memory.id, {
944
- memory: candidate.memory,
945
- vectorScore: Math.max(0, similarity),
946
- lexicalScore: candidate.lexicalScore
947
- });
948
- }
949
- const scored = Array.from(merged.values()).map((entry) => toScoredMemoryWithConfig(entry.memory, Math.max(0, entry.vectorScore), Math.max(0, entry.lexicalScore), input, scoringConfig)).filter((memory) => memory.score >= minScore).sort((a, b) => b.score - a.score).slice(0, limit);
950
- await store.markMemoriesAccessed(scored.map((memory) => memory.id));
951
- return scored;
952
- }, (result) => ({ count: result.length }));
1181
+ recall(input) {
1182
+ return runOperation("recall", { entityId: input.entityId }, () => recall(input));
953
1183
  },
954
- async buildContext(input) {
955
- return observe(config, store.backend, "buildContext", { entityId: input.entityId }, async () => {
1184
+ buildContext(input) {
1185
+ return runOperation("buildContext", { entityId: input.entityId }, () => observe(config, store.backend, "buildContext", { entityId: input.entityId }, async () => {
956
1186
  assertOpen();
957
1187
  validateBuildContextInput(input);
958
1188
  return buildContext({
959
1189
  input,
960
1190
  recall: (contextInput) => {
961
1191
  const recallInput = contextInputToRecallInput(contextInput);
962
- return client.recall(input.signal === void 0 ? recallInput : {
1192
+ return recall(input.signal === void 0 ? recallInput : {
963
1193
  ...recallInput,
964
1194
  signal: input.signal
965
1195
  });
966
1196
  }
967
1197
  });
968
- });
1198
+ }));
969
1199
  },
970
- async forget(input) {
971
- return observe(config, store.backend, "forget", {
1200
+ forget(input) {
1201
+ return runOperation("forget", {
1202
+ entityId: input.entityId,
1203
+ memoryId: input.memoryId
1204
+ }, () => observe(config, store.backend, "forget", {
972
1205
  entityId: input.entityId,
973
1206
  memoryId: input.memoryId
974
1207
  }, async () => {
@@ -978,73 +1211,81 @@ function createMemoryClient(config, store) {
978
1211
  await ensureSchema();
979
1212
  if (!await store.deleteMemory(input.entityId, input.memoryId)) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
980
1213
  await recordAudit([auditEvent(input.entityId, "memory.deleted", { memoryId: input.memoryId })]);
981
- });
1214
+ }));
982
1215
  },
983
- async forgetAll(input) {
984
- return observe(config, store.backend, "forgetAll", { entityId: input.entityId }, async () => {
1216
+ forgetAll(input) {
1217
+ return runOperation("forgetAll", { entityId: input.entityId }, () => observe(config, store.backend, "forgetAll", { entityId: input.entityId }, async () => {
985
1218
  assertOpen();
986
1219
  assertNonEmptyString(input.entityId, "entityId");
987
1220
  await ensureSchema();
988
1221
  const deletedCount = await store.deleteMemoriesForEntity(input.entityId);
989
1222
  await recordAudit([auditEvent(input.entityId, "entity.cleared", { count: deletedCount })]);
990
1223
  return deletedCount;
991
- }, (count) => ({ count })).then(() => void 0);
1224
+ }, (count) => ({ count })).then(() => void 0));
992
1225
  },
993
- async prune(input) {
994
- return observe(config, store.backend, "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
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 () => {
995
1228
  assertOpen();
996
- validatePruneInput(input);
1229
+ throwIfAborted(input.signal);
1230
+ const filter = validatePruneInput(input);
997
1231
  await ensureSchema();
998
- const result = await store.prune(input);
999
- if (result.deletedCount > 0 && input.entityId !== void 0 && result.dryRun === false) await recordAudit([auditEvent(input.entityId, "memory.pruned", { count: result.deletedCount })]);
1000
- return result;
1001
- }, (result) => ({ count: result.deletedCount }));
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 })));
1002
1241
  },
1003
- async findDuplicates(input) {
1004
- return observe(config, store.backend, "findDuplicates", { entityId: input.entityId }, async () => {
1242
+ findDuplicates(input) {
1243
+ return runOperation("findDuplicates", { entityId: input.entityId }, () => observe(config, store.backend, "findDuplicates", { entityId: input.entityId }, async () => {
1005
1244
  assertOpen();
1245
+ throwIfAborted(input.signal);
1006
1246
  validateFindDuplicatesInput(input);
1007
1247
  await ensureEmbeddingCompatibility();
1008
- return (await store.findDuplicatePairs(input)).map(toDuplicatePair);
1009
- }, (result) => ({ count: result.length }));
1248
+ return (await store.findDuplicatePairs(input, storeOptions(input.signal))).map(toDuplicatePair);
1249
+ }, (result) => ({ count: result.length })));
1010
1250
  },
1011
- async listAuditLog(input) {
1012
- return observe(config, store.backend, "listAuditLog", { entityId: input.entityId }, async () => {
1251
+ listAuditLog(input) {
1252
+ return runOperation("listAuditLog", { entityId: input.entityId }, () => observe(config, store.backend, "listAuditLog", { entityId: input.entityId }, async () => {
1013
1253
  assertOpen();
1254
+ throwIfAborted(input.signal);
1014
1255
  validateListAuditLogInput(input);
1015
1256
  await ensureSchema();
1016
- return (await store.listAuditLog(input)).map((event) => ({
1017
- id: event.id,
1018
- entityId: event.entityId,
1019
- description: event.description,
1020
- metadata: cloneJsonObject(event.metadata),
1021
- timestamp: new Date(event.timestamp)
1022
- }));
1023
- }, (result) => ({ count: result.length }));
1257
+ return await store.listAuditLog(input, storeOptions(input.signal));
1258
+ }, (result) => ({ count: result.length })));
1024
1259
  },
1025
- async stats(input) {
1026
- return observe(config, store.backend, "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
1260
+ stats(input) {
1261
+ return runOperation("stats", input?.entityId ? { entityId: input.entityId } : {}, () => observe(config, store.backend, "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
1027
1262
  assertOpen();
1028
1263
  await ensureSchema();
1029
1264
  return store.stats(input, /* @__PURE__ */ new Date());
1030
- });
1265
+ }));
1031
1266
  },
1032
- experimental: { async consolidate(input) {
1033
- return observe(config, store.backend, "consolidate", {
1267
+ experimental: { consolidate(input) {
1268
+ return runOperation("consolidate", {
1269
+ entityId: input.entityId,
1270
+ memoryId: input.survivorId
1271
+ }, () => observe(config, store.backend, "consolidate", {
1034
1272
  entityId: input.entityId,
1035
1273
  memoryId: input.survivorId
1036
1274
  }, async () => {
1037
1275
  assertOpen();
1276
+ throwIfAborted(input.signal);
1038
1277
  validateConsolidateInput(input);
1039
1278
  await ensureSchema();
1040
- const survivor = await store.getMemory(input.entityId, input.survivorId);
1279
+ const survivor = await store.getMemory(input.entityId, input.survivorId, storeOptions(input.signal));
1280
+ throwIfAborted(input.signal);
1041
1281
  if (!survivor) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
1042
1282
  if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
1043
- const targets = await store.loadConsolidationTargets(input.entityId, input.supersededIds);
1283
+ const targets = await store.loadConsolidationTargets(input.entityId, input.supersededIds, storeOptions(input.signal));
1284
+ throwIfAborted(input.signal);
1044
1285
  const foundIds = new Set(targets.map((target) => target.id));
1045
1286
  for (const id of input.supersededIds) if (!foundIds.has(id)) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
1046
- const losers = targets.filter((target) => target.supersededBy === null);
1047
- if (losers.length === 0) return {
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 {
1048
1289
  survivorId: survivor.id,
1049
1290
  supersededCount: 0,
1050
1291
  supersededIds: []
@@ -1053,28 +1294,40 @@ function createMemoryClient(config, store) {
1053
1294
  entityId: input.entityId,
1054
1295
  survivorId: survivor.id,
1055
1296
  survivorTags: survivor.tags,
1056
- supersededIds: losers.map((loser) => loser.id),
1297
+ supersededIds: input.supersededIds,
1057
1298
  mergeTags: input.mergeTags !== false,
1058
1299
  now: /* @__PURE__ */ new Date(),
1059
1300
  auditEnabled: config.audit?.enabled === true
1060
- });
1301
+ }, storeOptions(input.signal));
1061
1302
  return {
1062
1303
  survivorId: survivor.id,
1063
1304
  supersededCount: result.supersededIds.length,
1064
1305
  supersededIds: result.supersededIds
1065
1306
  };
1066
- }, (result) => ({ count: result.supersededCount }));
1307
+ }, (result) => ({ count: result.supersededCount })));
1067
1308
  } },
1068
- async close() {
1069
- return observe(config, store.backend, "close", {}, async () => {
1070
- if (!closed) {
1071
- closed = true;
1309
+ close() {
1310
+ if (closePromise) return closePromise;
1311
+ state = "closing";
1312
+ closePromise = observe(config, store.backend, "close", {}, async () => {
1313
+ await waitForActiveOperations();
1314
+ try {
1072
1315
  await store.close();
1316
+ state = "closed";
1317
+ } catch (error) {
1318
+ state = "open";
1319
+ closePromise = void 0;
1320
+ throw error;
1073
1321
  }
1074
1322
  });
1323
+ return closePromise;
1075
1324
  }
1076
1325
  };
1077
- return client;
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);
1078
1331
  }
1079
1332
  //#endregion
1080
1333
  //#region src/memory/filters.ts
@@ -1082,7 +1335,7 @@ function isExpired(memory, now) {
1082
1335
  return memory.expiresAt !== null && memory.expiresAt.getTime() <= now.getTime();
1083
1336
  }
1084
1337
  /**
1085
- * Test whether `memory` matches a {@link PruneInput}. All specified
1338
+ * Test whether `memory` matches a validated prune filter. All specified
1086
1339
  * selectors are AND-combined; unspecified selectors do not restrict.
1087
1340
  *
1088
1341
  * `now` is supplied by the caller so callers can share a consistent
@@ -1126,12 +1379,6 @@ function matchesRecallFilter(memory, input, now) {
1126
1379
  }
1127
1380
  //#endregion
1128
1381
  //#region src/memory/in-memory.ts
1129
- function cloneStoredMemory(memory) {
1130
- return {
1131
- ...cloneMemory(memory),
1132
- embedding: [...memory.embedding]
1133
- };
1134
- }
1135
1382
  function cloneAuditEvent(event) {
1136
1383
  return {
1137
1384
  id: event.id,
@@ -1141,6 +1388,15 @@ function cloneAuditEvent(event) {
1141
1388
  timestamp: new Date(event.timestamp)
1142
1389
  };
1143
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
+ }
1144
1400
  function createInMemoryStore() {
1145
1401
  const memories = /* @__PURE__ */ new Map();
1146
1402
  const auditLog = [];
@@ -1151,19 +1407,18 @@ function createInMemoryStore() {
1151
1407
  async insertMemories(rows) {
1152
1408
  const inserted = [];
1153
1409
  for (const row of rows) {
1154
- const stored = cloneStoredMemory(row);
1155
- memories.set(stored.id, stored);
1156
- inserted.push(cloneMemory(stored));
1410
+ memories.set(row.id, row);
1411
+ inserted.push(cloneMemory(row));
1157
1412
  }
1158
1413
  return inserted;
1159
1414
  },
1160
1415
  async vectorSearch(input) {
1161
1416
  const now = /* @__PURE__ */ new Date();
1162
- const minScore = input.minScore ?? 0;
1417
+ const minVectorScore = input.minVectorScore ?? 0;
1163
1418
  return Array.from(memories.values()).filter((memory) => matchesRecallFilter(memory, input, now)).map((memory) => ({
1164
1419
  memory: cloneMemory(memory),
1165
- vectorScore: Math.max(0, cosineSimilarity(memory.embedding, input.embedding))
1166
- })).filter((candidate) => candidate.vectorScore >= minScore).sort((a, b) => b.vectorScore - a.vectorScore).slice(0, input.limit);
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);
1167
1422
  },
1168
1423
  async lexicalSearch(input) {
1169
1424
  const now = /* @__PURE__ */ new Date();
@@ -1183,50 +1438,82 @@ function createInMemoryStore() {
1183
1438
  },
1184
1439
  async markMemoriesAccessed(memoryIds) {
1185
1440
  const now = /* @__PURE__ */ new Date();
1441
+ const updates = [];
1186
1442
  for (const id of memoryIds) {
1187
1443
  const memory = memories.get(id);
1188
1444
  if (memory) {
1189
1445
  memory.lastAccessedAt = now;
1190
1446
  memory.accessCount += 1;
1191
1447
  memory.updatedAt = now;
1448
+ updates.push({
1449
+ id: memory.id,
1450
+ lastAccessedAt: now,
1451
+ accessCount: memory.accessCount,
1452
+ updatedAt: now
1453
+ });
1192
1454
  }
1193
1455
  }
1456
+ return updates;
1194
1457
  },
1195
1458
  async deleteMemory(entityId, memoryId) {
1196
1459
  const memory = memories.get(memoryId);
1197
1460
  if (!memory || memory.entityId !== entityId) return false;
1461
+ assertNoDependentMemories(memories, new Set([memoryId]));
1198
1462
  return memories.delete(memoryId);
1199
1463
  },
1200
1464
  async deleteMemoriesForEntity(entityId) {
1201
- let deleted = 0;
1202
- for (const [id, memory] of memories) if (memory.entityId === entityId) {
1203
- memories.delete(id);
1204
- deleted += 1;
1205
- }
1206
- return deleted;
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;
1207
1469
  },
1208
- async prune(input) {
1470
+ async prune(input, options) {
1471
+ throwIfAborted(options?.signal);
1472
+ assertPruneFilterHasSelector(input);
1209
1473
  const now = /* @__PURE__ */ new Date();
1210
1474
  const matched = [];
1211
- for (const memory of memories.values()) if (matchesPruneFilter(memory, input, now)) matched.push(memory.id);
1475
+ for (const memory of memories.values()) {
1476
+ throwIfAborted(options?.signal);
1477
+ if (matchesPruneFilter(memory, input, now)) matched.push(memory);
1478
+ }
1212
1479
  const dryRun = input.dryRun === true;
1213
- if (!dryRun) for (const id of matched) memories.delete(id);
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);
1488
+ }
1489
+ deletedByEntity = Array.from(deletedCounts, ([entityId, deletedCount]) => ({
1490
+ entityId,
1491
+ deletedCount
1492
+ })).sort((a, b) => a.entityId.localeCompare(b.entityId));
1493
+ }
1214
1494
  return {
1215
1495
  matchedCount: matched.length,
1216
1496
  deletedCount: dryRun ? 0 : matched.length,
1217
- dryRun
1497
+ dryRun,
1498
+ deletedByEntity
1218
1499
  };
1219
1500
  },
1220
- async findDuplicatePairs(input) {
1501
+ async findDuplicatePairs(input, options) {
1502
+ throwIfAborted(options?.signal);
1221
1503
  const threshold = input.threshold ?? .95;
1222
1504
  const limit = input.limit ?? 100;
1223
1505
  const now = /* @__PURE__ */ new Date();
1224
- const candidates = Array.from(memories.values()).filter((memory) => matchesDuplicateFilter(memory, input, now));
1506
+ const candidates = Array.from(memories.values()).filter((memory) => {
1507
+ throwIfAborted(options?.signal);
1508
+ return matchesDuplicateFilter(memory, input, now);
1509
+ });
1225
1510
  const pairs = [];
1226
1511
  for (let i = 0; i < candidates.length; i += 1) {
1512
+ throwIfAborted(options?.signal);
1227
1513
  const a = candidates[i];
1228
1514
  if (a === void 0) continue;
1229
1515
  for (let j = i + 1; j < candidates.length; j += 1) {
1516
+ throwIfAborted(options?.signal);
1230
1517
  const b = candidates[j];
1231
1518
  if (b === void 0) continue;
1232
1519
  const similarity = Math.max(0, Math.min(1, cosineSimilarity(a.embedding, b.embedding)));
@@ -1243,27 +1530,29 @@ function createInMemoryStore() {
1243
1530
  async addAuditEvents(events) {
1244
1531
  auditLog.push(...events.map(cloneAuditEvent));
1245
1532
  },
1246
- async listAuditLog(input) {
1533
+ async listAuditLog(input, options) {
1534
+ throwIfAborted(options?.signal);
1247
1535
  const limit = input.limit ?? 50;
1248
- return auditLog.map((event, idx) => ({
1249
- event,
1250
- idx
1251
- })).filter(({ event }) => {
1536
+ return auditLog.filter((event) => {
1537
+ throwIfAborted(options?.signal);
1252
1538
  if (event.entityId !== input.entityId) return false;
1253
1539
  if (input.before !== void 0 && event.timestamp.getTime() >= input.before.getTime()) return false;
1254
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;
1255
1543
  return true;
1256
1544
  }).sort((a, b) => {
1257
- const dt = b.event.timestamp.getTime() - a.event.timestamp.getTime();
1258
- return dt === 0 ? b.idx - a.idx : dt;
1259
- }).slice(0, limit).map(({ event }) => cloneAuditEvent(event));
1545
+ return compareAuditPositions(b, a);
1546
+ }).slice(0, limit).map(cloneAuditEvent);
1260
1547
  },
1261
- async getMemory(entityId, memoryId) {
1548
+ async getMemory(entityId, memoryId, options) {
1549
+ throwIfAborted(options?.signal);
1262
1550
  const memory = memories.get(memoryId);
1263
1551
  return memory && memory.entityId === entityId ? cloneMemory(memory) : null;
1264
1552
  },
1265
- async loadConsolidationTargets(entityId, ids) {
1553
+ async loadConsolidationTargets(entityId, ids, options) {
1266
1554
  return ids.flatMap((id) => {
1555
+ throwIfAborted(options?.signal);
1267
1556
  const memory = memories.get(id);
1268
1557
  if (!memory || memory.entityId !== entityId) return [];
1269
1558
  return [{
@@ -1273,40 +1562,50 @@ function createInMemoryStore() {
1273
1562
  }];
1274
1563
  });
1275
1564
  },
1276
- async consolidate(input) {
1565
+ async consolidate(input, options) {
1566
+ throwIfAborted(options?.signal);
1277
1567
  const survivor = memories.get(input.survivorId);
1278
1568
  const newlySuperseded = [];
1569
+ const targets = [];
1279
1570
  for (const id of input.supersededIds) {
1571
+ throwIfAborted(options?.signal);
1280
1572
  const memory = memories.get(id);
1281
1573
  if (!memory || memory.entityId !== input.entityId) continue;
1282
- if (memory.supersededBy !== null) continue;
1283
- memory.supersededBy = input.survivorId;
1284
- memory.supersededAt = input.now;
1285
- memory.updatedAt = input.now;
1286
- newlySuperseded.push(memory.id);
1574
+ targets.push(memory);
1287
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;
1288
1579
  if (survivor && input.mergeTags && newlySuperseded.length > 0) {
1289
1580
  const mergedTags = new Set(input.survivorTags);
1290
- for (const id of newlySuperseded) {
1291
- const memory = memories.get(id);
1292
- if (memory) for (const tag of memory.tags) mergedTags.add(tag);
1293
- }
1294
- if (mergedTags.size !== survivor.tags.length) {
1295
- survivor.tags = [...mergedTags];
1296
- survivor.updatedAt = input.now;
1581
+ for (const memory of newlySuperseded) {
1582
+ throwIfAborted(options?.signal);
1583
+ for (const tag of memory.tags) mergedTags.add(tag);
1297
1584
  }
1585
+ if (mergedTags.size !== survivor.tags.length) mergedSurvivorTags = [...mergedTags];
1298
1586
  }
1299
- if (input.auditEnabled) for (const id of newlySuperseded) auditLog.push({
1587
+ const auditEvents = input.auditEnabled ? newlySuperseded.map((memory) => ({
1300
1588
  id: createEventId(),
1301
1589
  entityId: input.entityId,
1302
1590
  description: "memory.superseded",
1303
1591
  metadata: {
1304
- memoryId: id,
1592
+ memoryId: memory.id,
1305
1593
  supersededBy: input.survivorId
1306
1594
  },
1307
1595
  timestamp: input.now
1308
- });
1309
- return { supersededIds: newlySuperseded };
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) };
1310
1609
  },
1311
1610
  async stats(input, now) {
1312
1611
  const allMemories = Array.from(memories.values());
@@ -1339,20 +1638,78 @@ function createInMemoryClient(config) {
1339
1638
  return createMemoryClient(config, createInMemoryStore());
1340
1639
  }
1341
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);
1657
+ }
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
+ }
1665
+ //#endregion
1342
1666
  //#region src/db/queries/events.ts
1667
+ function timestampParam$1(value) {
1668
+ return sql`${value.toISOString()}::timestamptz`;
1669
+ }
1343
1670
  async function insertEvent(db, row) {
1344
1671
  const inserted = (await db.insert(eventsTable).values(row).returning())[0];
1345
1672
  if (!inserted) throw new MnemocyteError("Event insert returned no rows.", "DB");
1346
1673
  return inserted;
1347
1674
  }
1348
- async function listEvents(db, filter) {
1349
- return db.select().from(eventsTable).where(and(eq(eventsTable.entityId, filter.entityId), filter.before ? lt(eventsTable.timestamp, filter.before) : void 0, filter.after ? gt(eventsTable.timestamp, filter.after) : void 0)).orderBy(desc(eventsTable.timestamp)).limit(filter.limit ?? 50);
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);
1350
1692
  }
1351
1693
  //#endregion
1352
1694
  //#region src/db/queries/memories.ts
1353
1695
  function vectorLiteral(embedding) {
1354
1696
  return `[${embedding.map(formatVectorComponent).join(",")}]`;
1355
1697
  }
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
+ `;
1712
+ }
1356
1713
  function timestampParam(value) {
1357
1714
  return sql`${value.toISOString()}::timestamptz`;
1358
1715
  }
@@ -1414,78 +1771,136 @@ async function getGlobalMemoryStatsCounts(db, now) {
1414
1771
  };
1415
1772
  }
1416
1773
  async function deleteMemory(db, entityId, memoryId) {
1417
- return (await db.delete(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), eq(memoriesTable.id, memoryId))).returning({ id: memoriesTable.id })).length > 0;
1774
+ return deleteMemoriesWithDependentGuard(db, and(eq(memoriesTable.entityId, entityId), eq(memoriesTable.id, memoryId)));
1418
1775
  }
1419
1776
  async function deleteMemoriesForEntity(db, entityId) {
1420
- return (await db.delete(memoriesTable).where(eq(memoriesTable.entityId, entityId)).returning({ id: memoriesTable.id })).length;
1777
+ return deleteMemoriesWithDependentGuard(db, eq(memoriesTable.entityId, entityId));
1421
1778
  }
1422
1779
  function pruneConditions(filter) {
1423
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);
1424
1781
  }
1425
- async function countPruneMatches(db, filter) {
1426
- return db.$count(memoriesTable, pruneConditions(filter));
1427
- }
1428
- async function pruneMemories(db, filter) {
1429
- return (await db.delete(memoriesTable).where(pruneConditions(filter)).returning({ id: memoriesTable.id })).length;
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
+ };
1430
1845
  }
1431
- async function findDuplicatePairs(db, input) {
1432
- return (await db.execute(sql`
1433
- SELECT
1434
- a.id AS "aId",
1435
- a.entity_id AS "aEntityId",
1436
- a.content AS "aContent",
1437
- a.type AS "aType",
1438
- a.importance AS "aImportance",
1439
- a.tags AS "aTags",
1440
- a.source AS "aSource",
1441
- a.metadata AS "aMetadata",
1442
- a.confidence AS "aConfidence",
1443
- a.embedding AS "aEmbedding",
1444
- a.embedding_model AS "aEmbeddingModel",
1445
- a.embedding_dimensions AS "aEmbeddingDimensions",
1446
- a.superseded_by AS "aSupersededBy",
1447
- a.superseded_at AS "aSupersededAt",
1448
- a.expires_at AS "aExpiresAt",
1449
- a.last_accessed_at AS "aLastAccessedAt",
1450
- a.access_count AS "aAccessCount",
1451
- a.created_at AS "aCreatedAt",
1452
- a.updated_at AS "aUpdatedAt",
1453
- b.id AS "bId",
1454
- b.entity_id AS "bEntityId",
1455
- b.content AS "bContent",
1456
- b.type AS "bType",
1457
- b.importance AS "bImportance",
1458
- b.tags AS "bTags",
1459
- b.source AS "bSource",
1460
- b.metadata AS "bMetadata",
1461
- b.confidence AS "bConfidence",
1462
- b.embedding AS "bEmbedding",
1463
- b.embedding_model AS "bEmbeddingModel",
1464
- b.embedding_dimensions AS "bEmbeddingDimensions",
1465
- b.superseded_by AS "bSupersededBy",
1466
- b.superseded_at AS "bSupersededAt",
1467
- b.expires_at AS "bExpiresAt",
1468
- b.last_accessed_at AS "bLastAccessedAt",
1469
- b.access_count AS "bAccessCount",
1470
- b.created_at AS "bCreatedAt",
1471
- b.updated_at AS "bUpdatedAt",
1472
- 1 - (a.embedding <=> b.embedding) AS "similarity"
1473
- FROM mnemocyte_memories a
1474
- JOIN mnemocyte_memories b
1475
- ON b.entity_id = a.entity_id
1476
- AND a.id < b.id
1477
- AND a.embedding IS NOT NULL
1478
- AND b.embedding IS NOT NULL
1479
- WHERE
1480
- a.entity_id = ${input.entityId}
1481
- ${input.includeSuperseded ? sql`` : sql`AND a.superseded_by IS NULL AND b.superseded_by IS NULL`}
1482
- ${input.includeExpired ? sql`` : sql`AND (a.expires_at IS NULL OR a.expires_at > now()) AND (b.expires_at IS NULL OR b.expires_at > now())`}
1483
- ${input.types && input.types.length > 0 ? sql`AND a.type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)}) AND b.type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)})` : sql``}
1484
- ${duplicateTagsFilter(input.tags)}
1485
- AND 1 - (a.embedding <=> b.embedding) >= ${input.threshold}
1486
- ORDER BY "similarity" DESC
1487
- LIMIT ${input.limit}
1488
- `)).map((row) => {
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) => {
1489
1904
  return {
1490
1905
  a: {
1491
1906
  id: row.aId,
@@ -1541,6 +1956,14 @@ async function loadConsolidationTargets(db, entityId, ids) {
1541
1956
  supersededBy: memoriesTable.supersededBy
1542
1957
  }).from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), inArray(memoriesTable.id, [...ids])));
1543
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
+ }
1544
1967
  async function markMemoriesSuperseded(db, params) {
1545
1968
  if (params.ids.length === 0) return [];
1546
1969
  return await db.update(memoriesTable).set({
@@ -1560,12 +1983,17 @@ async function setMemoryTags(db, params) {
1560
1983
  }
1561
1984
  async function markMemoriesAccessed(db, memoryIds) {
1562
1985
  const now = /* @__PURE__ */ new Date();
1563
- if (memoryIds.length === 0) return;
1564
- await db.update(memoriesTable).set({
1986
+ if (memoryIds.length === 0) return [];
1987
+ return db.update(memoriesTable).set({
1565
1988
  lastAccessedAt: now,
1566
1989
  accessCount: sql`${memoriesTable.accessCount} + 1`,
1567
1990
  updatedAt: now
1568
- }).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
+ });
1569
1997
  }
1570
1998
  async function getMemoryEmbeddings(db, memoryIds) {
1571
1999
  if (memoryIds.length === 0) return /* @__PURE__ */ new Map();
@@ -1577,7 +2005,8 @@ async function getMemoryEmbeddings(db, memoryIds) {
1577
2005
  }
1578
2006
  async function vectorSearch(db, input) {
1579
2007
  const embedding = vectorLiteral(input.embedding);
1580
- const minScore = input.minScore ?? 0;
2008
+ const vectorScore = clampedVectorScore(embedding);
2009
+ const minVectorScore = input.minVectorScore ?? 0;
1581
2010
  return await db.execute(sql`
1582
2011
  SELECT
1583
2012
  id,
@@ -1598,7 +2027,7 @@ async function vectorSearch(db, input) {
1598
2027
  access_count AS "accessCount",
1599
2028
  created_at AS "createdAt",
1600
2029
  updated_at AS "updatedAt",
1601
- 1 - (embedding <=> ${embedding}::vector) AS "vectorScore"
2030
+ ${vectorScore} AS "vectorScore"
1602
2031
  FROM mnemocyte_memories
1603
2032
  WHERE
1604
2033
  entity_id = ${input.entityId}
@@ -1609,7 +2038,7 @@ async function vectorSearch(db, input) {
1609
2038
  ${rawTagsFilter(input.tags)}
1610
2039
  ${input.before ? sql`AND created_at < ${timestampParam(input.before)}` : sql``}
1611
2040
  ${input.after ? sql`AND created_at > ${timestampParam(input.after)}` : sql``}
1612
- AND 1 - (embedding <=> ${embedding}::vector) >= ${minScore}
2041
+ AND ${vectorScore} >= ${minVectorScore}
1613
2042
  ORDER BY embedding <=> ${embedding}::vector
1614
2043
  LIMIT ${input.limit}
1615
2044
  `);
@@ -1653,6 +2082,12 @@ async function lexicalSearch(db, input) {
1653
2082
  async function getInstallationMeta(db) {
1654
2083
  return (await db.select().from(metaTable).where(eq(metaTable.key, "installation")).limit(1))[0] ?? null;
1655
2084
  }
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;
2090
+ }
1656
2091
  //#endregion
1657
2092
  //#region src/memory/postgres-records.ts
1658
2093
  function toDate(value) {
@@ -1697,13 +2132,14 @@ const MIGRATION_ERROR_CODES = new Set([
1697
2132
  "42704",
1698
2133
  "42883"
1699
2134
  ]);
2135
+ const CONSOLIDATION_SURVIVOR_FOREIGN_KEY = "mnemocyte_memories_superseded_by_mnemocyte_memories_id_fk";
1700
2136
  function rowToAuditEvent(row) {
1701
2137
  return {
1702
2138
  id: row.id,
1703
2139
  entityId: row.entityId,
1704
2140
  description: row.description,
1705
2141
  metadata: cloneJsonObject(row.metadata),
1706
- timestamp: row.timestamp
2142
+ timestamp: new Date(row.timestamp)
1707
2143
  };
1708
2144
  }
1709
2145
  function importanceCeilingLevels(max) {
@@ -1712,6 +2148,9 @@ function importanceCeilingLevels(max) {
1712
2148
  function hasPostgresErrorCode(error, code) {
1713
2149
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
1714
2150
  }
2151
+ function hasPostgresConstraintName(error, name) {
2152
+ return typeof error === "object" && error !== null && "constraint_name" in error && error.constraint_name === name;
2153
+ }
1715
2154
  function getPostgresErrorCode(error) {
1716
2155
  return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
1717
2156
  }
@@ -1728,6 +2167,15 @@ async function runPostgresOperation(operation) {
1728
2167
  throw normalizePostgresError(error);
1729
2168
  }
1730
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
+ }
1731
2179
  function toPruneFilter(input) {
1732
2180
  const filter = {};
1733
2181
  if (input.entityId !== void 0) filter.entityId = input.entityId;
@@ -1744,7 +2192,6 @@ function toMemoryRow(memory) {
1744
2192
  return {
1745
2193
  ...memory,
1746
2194
  tags: [...memory.tags],
1747
- metadata: cloneJsonObject(memory.metadata),
1748
2195
  embedding: [...memory.embedding]
1749
2196
  };
1750
2197
  }
@@ -1764,11 +2211,26 @@ function createPostgresStore(handle) {
1764
2211
  try {
1765
2212
  meta = await getInstallationMeta(handle.db);
1766
2213
  } catch (error) {
1767
- if (hasPostgresErrorCode(error, "42P01")) throw new MnemocyteError("mnemocyte_meta is missing. Apply the v0.2.0 migration or render a dimension-specific initial migration before using the Postgres backend.", "MIGRATION", error);
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);
1768
2215
  throw normalizePostgresError(error);
1769
2216
  }
1770
- if (!meta) throw new MnemocyteError("mnemocyte_meta is missing the \"installation\" row. Apply the v0.2.0 migration or render a dimension-specific initial migration before using the Postgres backend.", "MIGRATION");
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");
1771
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");
1772
2234
  schemaValidated = true;
1773
2235
  }
1774
2236
  return {
@@ -1800,31 +2262,52 @@ function createPostgresStore(handle) {
1800
2262
  return runPostgresOperation(() => getMemoryEmbeddings(handle.db, memoryIds));
1801
2263
  },
1802
2264
  async markMemoriesAccessed(memoryIds) {
1803
- return runPostgresOperation(() => markMemoriesAccessed(handle.db, 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
+ };
2272
+ });
2273
+ });
1804
2274
  },
1805
2275
  async deleteMemory(entityId, memoryId) {
1806
- return runPostgresOperation(() => deleteMemory(handle.db, 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;
2280
+ });
1807
2281
  },
1808
2282
  async deleteMemoriesForEntity(entityId) {
1809
- return runPostgresOperation(() => deleteMemoriesForEntity(handle.db, entityId));
2283
+ return runPostgresDeleteOperation(async () => {
2284
+ const result = await deleteMemoriesForEntity(handle.db, entityId);
2285
+ if (result.hasDependents) throw memoryHasDependentsError();
2286
+ return result.deletedCount;
2287
+ });
1810
2288
  },
1811
- async prune(input) {
1812
- return runPostgresOperation(async () => {
2289
+ async prune(input, options) {
2290
+ return runPostgresDeleteOperation(async () => {
2291
+ throwIfAborted(options?.signal);
2292
+ assertPruneFilterHasSelector(input);
1813
2293
  const filter = toPruneFilter(input);
1814
2294
  if (input.dryRun === true) return {
1815
- matchedCount: await countPruneMatches(handle.db, filter),
2295
+ matchedCount: await countPruneMatches(handle.db, filter, options?.signal),
1816
2296
  deletedCount: 0,
1817
- dryRun: true
2297
+ dryRun: true,
2298
+ deletedByEntity: []
1818
2299
  };
1819
- const deletedCount = await pruneMemories(handle.db, filter);
2300
+ const result = await pruneMemories(handle.db, filter, options?.signal);
2301
+ if (result.hasDependents) throw memoryHasDependentsError();
1820
2302
  return {
1821
- matchedCount: deletedCount,
1822
- deletedCount,
1823
- dryRun: false
2303
+ matchedCount: result.matchedCount,
2304
+ deletedCount: result.deletedCount,
2305
+ dryRun: false,
2306
+ deletedByEntity: result.deletedByEntity
1824
2307
  };
1825
2308
  });
1826
2309
  },
1827
- async findDuplicatePairs(input) {
2310
+ async findDuplicatePairs(input, options) {
1828
2311
  return runPostgresOperation(async () => {
1829
2312
  return (await findDuplicatePairs(handle.db, {
1830
2313
  entityId: input.entityId,
@@ -1834,7 +2317,7 @@ function createPostgresStore(handle) {
1834
2317
  ...input.tags === void 0 ? {} : { tags: input.tags },
1835
2318
  ...input.includeSuperseded === void 0 ? {} : { includeSuperseded: input.includeSuperseded },
1836
2319
  ...input.includeExpired === void 0 ? {} : { includeExpired: input.includeExpired }
1837
- })).map((row) => ({
2320
+ }, options?.signal)).map((row) => ({
1838
2321
  a: rowToMemory(row.a),
1839
2322
  b: rowToMemory(row.b),
1840
2323
  similarity: Math.max(0, Math.min(1, row.similarity))
@@ -1852,55 +2335,83 @@ function createPostgresStore(handle) {
1852
2335
  });
1853
2336
  });
1854
2337
  },
1855
- async listAuditLog(input) {
2338
+ async listAuditLog(input, options) {
1856
2339
  return runPostgresOperation(async () => {
1857
2340
  return (await listEvents(handle.db, {
1858
2341
  entityId: input.entityId,
1859
2342
  limit: input.limit ?? 50,
1860
2343
  ...input.before === void 0 ? {} : { before: input.before },
1861
- ...input.after === void 0 ? {} : { after: input.after }
1862
- })).map(rowToAuditEvent);
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);
1863
2348
  });
1864
2349
  },
1865
- async getMemory(entityId, memoryId) {
2350
+ async getMemory(entityId, memoryId, options) {
1866
2351
  return runPostgresOperation(async () => {
2352
+ throwIfAborted(options?.signal);
1867
2353
  const row = await getMemoryById(handle.db, entityId, memoryId);
2354
+ throwIfAborted(options?.signal);
1868
2355
  return row ? rowToMemory(row) : null;
1869
2356
  });
1870
2357
  },
1871
- async loadConsolidationTargets(entityId, ids) {
1872
- return runPostgresOperation(() => loadConsolidationTargets(handle.db, entityId, ids));
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
+ });
1873
2365
  },
1874
- async consolidate(input) {
2366
+ async consolidate(input, options) {
1875
2367
  return runPostgresOperation(async () => {
2368
+ throwIfAborted(options?.signal);
1876
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);
1877
2375
  const updated = await markMemoriesSuperseded(tx, {
1878
2376
  survivorId: input.survivorId,
1879
2377
  entityId: input.entityId,
1880
- ids: input.supersededIds,
2378
+ ids: activeIds,
1881
2379
  now: input.now
1882
2380
  });
2381
+ throwIfAborted(options?.signal);
1883
2382
  const ids = updated.map((row) => row.id);
1884
- if (input.auditEnabled) for (const id of ids) await insertEvent(tx, {
1885
- id: createEventId(),
1886
- entityId: input.entityId,
1887
- description: "memory.superseded",
1888
- metadata: {
1889
- memoryId: id,
1890
- supersededBy: input.survivorId
1891
- },
1892
- timestamp: input.now
1893
- });
1894
- if (input.mergeTags && updated.length > 0) {
1895
- const mergedTags = new Set(input.survivorTags);
1896
- for (const row of updated) for (const tag of row.tags) mergedTags.add(tag);
1897
- if (mergedTags.size !== input.survivorTags.length) await setMemoryTags(tx, {
2383
+ if (input.auditEnabled) for (const id of ids) {
2384
+ throwIfAborted(options?.signal);
2385
+ await insertEvent(tx, {
2386
+ id: createEventId(),
1898
2387
  entityId: input.entityId,
1899
- memoryId: input.survivorId,
1900
- tags: [...mergedTags],
1901
- now: input.now
2388
+ description: "memory.superseded",
2389
+ metadata: {
2390
+ memoryId: id,
2391
+ supersededBy: input.survivorId
2392
+ },
2393
+ timestamp: input.now
1902
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
+ }
1903
2413
  }
2414
+ throwIfAborted(options?.signal);
1904
2415
  return ids;
1905
2416
  }) };
1906
2417
  });
@@ -1942,7 +2453,8 @@ function createPostgresClient(config, handle) {
1942
2453
  * @param config - Client configuration. {@link MnemocyteConfig.embedder} is required.
1943
2454
  * @returns A {@link MnemocyteClient} instance.
1944
2455
  * @throws {MnemocyteError} With code `"CONFIG"` if `embedder`, retrieval
1945
- * tuning, or the database URL is malformed, or `"VALIDATION"` if
2456
+ * tuning, provider resilience, or the database URL is malformed or does not
2457
+ * use the `postgres:` / `postgresql:` protocol, or `"VALIDATION"` if
1946
2458
  * `databaseUrl` is explicitly empty.
1947
2459
  *
1948
2460
  * @example Postgres-backed client
@@ -1956,6 +2468,7 @@ function createPostgresClient(config, handle) {
1956
2468
  function createMnemocyte(config) {
1957
2469
  assertEmbedder(config.embedder);
1958
2470
  validateRetrievalConfig(config.retrieval);
2471
+ validateProviderResilienceConfig(config.provider);
1959
2472
  if (config.databaseUrl !== void 0) {
1960
2473
  assertNonEmptyString(config.databaseUrl, "databaseUrl");
1961
2474
  return createPostgresClient(config, createDatabase(config.databaseUrl));