mnemocyte 0.0.8 → 0.1.1

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,6 +1,6 @@
1
1
  import { drizzle } from "drizzle-orm/postgres-js";
2
2
  import postgres from "postgres";
3
- import { and, eq, gt, inArray, isNotNull, isNull, lt, or, sql } from "drizzle-orm";
3
+ import { and, desc, eq, gt, inArray, isNotNull, isNull, lt, or, sql } from "drizzle-orm";
4
4
  import { customType, index, integer, jsonb, pgTable, real, text, timestamp } from "drizzle-orm/pg-core";
5
5
  //#region \0rolldown/runtime.js
6
6
  var __defProp = Object.defineProperty;
@@ -24,12 +24,16 @@ const vector = customType({
24
24
  return `vector(${typeof config === "number" ? config : 1536})`;
25
25
  },
26
26
  toDriver(value) {
27
- return `[${value.join(",")}]`;
27
+ return `[${value.map(formatVectorComponent$1).join(",")}]`;
28
28
  },
29
29
  fromDriver(value) {
30
30
  return value.slice(1, -1).split(",").filter(Boolean).map(Number);
31
31
  }
32
32
  });
33
+ function formatVectorComponent$1(value) {
34
+ if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
35
+ return Object.is(value, -0) ? "0" : value.toFixed(17);
36
+ }
33
37
  const memoriesTable = pgTable("mnemocyte_memories", {
34
38
  id: text("id").primaryKey(),
35
39
  entityId: text("entity_id").notNull(),
@@ -43,7 +47,7 @@ const memoriesTable = pgTable("mnemocyte_memories", {
43
47
  embedding: vector("embedding", { dimensions: 1536 }),
44
48
  embeddingModel: text("embedding_model").notNull(),
45
49
  embeddingDimensions: integer("embedding_dimensions").notNull(),
46
- supersededBy: text("superseded_by"),
50
+ supersededBy: text("superseded_by").references(() => memoriesTable.id),
47
51
  supersededAt: timestamp("superseded_at", { withTimezone: true }),
48
52
  expiresAt: timestamp("expires_at", { withTimezone: true }),
49
53
  lastAccessedAt: timestamp("last_accessed_at", { withTimezone: true }),
@@ -74,6 +78,56 @@ function createDatabase(databaseUrl) {
74
78
  };
75
79
  }
76
80
  //#endregion
81
+ //#region src/errors.ts
82
+ /**
83
+ * Error type thrown by every public Mnemocyte API.
84
+ *
85
+ * Use the {@link MnemocyteError.code} property — not the message — to branch
86
+ * on failure modes, or call {@link isMnemocyteError} as a type guard when
87
+ * working with `unknown` errors.
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * try {
92
+ * await client.recall({ entityId: "u1", query: "hi" });
93
+ * } catch (err) {
94
+ * if (isMnemocyteError(err) && err.code === "DB") {
95
+ * // handle database failure
96
+ * }
97
+ * throw err;
98
+ * }
99
+ * ```
100
+ */
101
+ var MnemocyteError = class extends Error {
102
+ /** Stable, machine-readable failure code. See {@link MnemocyteErrorCode}. */
103
+ code;
104
+ /** Underlying cause, if any (e.g. an original driver or embedder error). */
105
+ cause;
106
+ /**
107
+ * @param message - Human-readable error message.
108
+ * @param code - Stable failure code; see {@link MnemocyteErrorCode}.
109
+ * @param cause - Optional underlying cause (driver error, network error, etc.).
110
+ */
111
+ constructor(message, code, cause) {
112
+ super(message);
113
+ this.name = "MnemocyteError";
114
+ this.code = code;
115
+ this.cause = cause;
116
+ }
117
+ };
118
+ /**
119
+ * Type guard for {@link MnemocyteError}.
120
+ *
121
+ * Prefer this over `instanceof` when working across module boundaries or
122
+ * bundlers where multiple copies of the class may exist.
123
+ *
124
+ * @param error - Value to test (typically a caught `unknown`).
125
+ * @returns `true` if `error` is a {@link MnemocyteError}.
126
+ */
127
+ function isMnemocyteError(error) {
128
+ return error instanceof MnemocyteError;
129
+ }
130
+ //#endregion
77
131
  //#region src/context/formatter.ts
78
132
  function escapeXml(value) {
79
133
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
@@ -88,7 +142,11 @@ function formatMetadata(memory) {
88
142
  if (memory.tags.length > 0) parts.push(`tags=${memory.tags.join(",")}`);
89
143
  return parts.join("; ");
90
144
  }
91
- function formatMarkdown(memories, query) {
145
+ function markdownFence(value) {
146
+ const longestBacktickRun = Math.max(0, ...Array.from(value.matchAll(/`+/g), (match) => match[0].length));
147
+ return "`".repeat(Math.max(3, longestBacktickRun + 1));
148
+ }
149
+ function formatMarkdown(memories, query, omittedCount) {
92
150
  const lines = [
93
151
  "# Memory Context",
94
152
  "",
@@ -96,10 +154,14 @@ function formatMarkdown(memories, query) {
96
154
  "",
97
155
  "## Relevant Memories"
98
156
  ];
99
- for (const memory of memories) lines.push("", `- ${memory.content}`, ` - ${formatMetadata(memory)}`);
157
+ for (const memory of memories) {
158
+ const fence = markdownFence(memory.content);
159
+ lines.push("", `- ${formatMetadata(memory)}`, ` ${fence}text`, ...memory.content.split("\n").map((line) => ` ${line}`), ` ${fence}`);
160
+ }
161
+ if (omittedCount > 0) lines.push("", `[${omittedCount} memories omitted to fit token budget]`);
100
162
  return lines.join("\n");
101
163
  }
102
- function formatPlain(memories, query) {
164
+ function formatPlain(memories, query, omittedCount) {
103
165
  const lines = [
104
166
  "MEMORY CONTEXT",
105
167
  "",
@@ -107,20 +169,22 @@ function formatPlain(memories, query) {
107
169
  "",
108
170
  "RELEVANT MEMORIES"
109
171
  ];
110
- for (const memory of memories) lines.push("", memory.content, formatMetadata(memory));
172
+ for (const [index, memory] of memories.entries()) lines.push("", `--- MEMORY ${index + 1} START ---`, formatMetadata(memory), memory.content, `--- MEMORY ${index + 1} END ---`);
173
+ if (omittedCount > 0) lines.push("", `[${omittedCount} memories omitted to fit token budget]`);
111
174
  return lines.join("\n");
112
175
  }
113
- function formatXml(memories, query) {
176
+ function formatXml(memories, query, omittedCount) {
114
177
  const lines = [`<memory_context query="${escapeXml(query)}">`];
115
178
  for (const memory of memories) lines.push(` <memory id="${escapeXml(memory.id)}" type="${escapeXml(memory.type)}" importance="${escapeXml(memory.importance)}" score="${memory.score.toFixed(3)}">`, ` <content>${escapeXml(memory.content)}</content>`, ` <metadata>${escapeXml(formatMetadata(memory))}</metadata>`, " </memory>");
179
+ if (omittedCount > 0) lines.push(` <omitted count="${omittedCount}" reason="token_budget" />`);
116
180
  lines.push("</memory_context>");
117
181
  return lines.join("\n");
118
182
  }
119
- function formatContext(memories, query, format) {
183
+ function formatContext(memories, query, format, omittedCount = 0) {
120
184
  switch (format) {
121
- case "markdown": return formatMarkdown(memories, query);
122
- case "plain": return formatPlain(memories, query);
123
- case "xml": return formatXml(memories, query);
185
+ case "markdown": return formatMarkdown(memories, query, omittedCount);
186
+ case "plain": return formatPlain(memories, query, omittedCount);
187
+ case "xml": return formatXml(memories, query, omittedCount);
124
188
  }
125
189
  }
126
190
  //#endregion
@@ -149,57 +213,13 @@ async function buildContext(options) {
149
213
  const format = options.input.format ?? "markdown";
150
214
  const maxTokens = options.input.maxTokens ?? 1200;
151
215
  const tokenCounter = options.input.tokenCounter ?? heuristicTokenCounter;
152
- return trimToTokenBudget(formatContext(await options.recall(options.input), options.input.query, format), maxTokens, tokenCounter);
153
- }
154
- //#endregion
155
- //#region src/errors.ts
156
- /**
157
- * Error type thrown by every public Mnemocyte API.
158
- *
159
- * Use the {@link MnemocyteError.code} property — not the message — to branch
160
- * on failure modes, or call {@link isMnemocyteError} as a type guard when
161
- * working with `unknown` errors.
162
- *
163
- * @example
164
- * ```ts
165
- * try {
166
- * await client.recall({ entityId: "u1", query: "hi" });
167
- * } catch (err) {
168
- * if (isMnemocyteError(err) && err.code === "DB") {
169
- * // handle database failure
170
- * }
171
- * throw err;
172
- * }
173
- * ```
174
- */
175
- var MnemocyteError = class extends Error {
176
- /** Stable, machine-readable failure code. See {@link MnemocyteErrorCode}. */
177
- code;
178
- /** Underlying cause, if any (e.g. an original driver or embedder error). */
179
- cause;
180
- /**
181
- * @param message - Human-readable error message.
182
- * @param code - Stable failure code; see {@link MnemocyteErrorCode}.
183
- * @param cause - Optional underlying cause (driver error, network error, etc.).
184
- */
185
- constructor(message, code, cause) {
186
- super(message);
187
- this.name = "MnemocyteError";
188
- this.code = code;
189
- this.cause = cause;
216
+ const memories = await options.recall(options.input);
217
+ for (let count = memories.length; count >= 0; count -= 1) {
218
+ const omittedCount = memories.length - count;
219
+ const context = formatContext(memories.slice(0, count), options.input.query, format, omittedCount);
220
+ if (tokenCounter.count(context) <= maxTokens) return context;
190
221
  }
191
- };
192
- /**
193
- * Type guard for {@link MnemocyteError}.
194
- *
195
- * Prefer this over `instanceof` when working across module boundaries or
196
- * bundlers where multiple copies of the class may exist.
197
- *
198
- * @param error - Value to test (typically a caught `unknown`).
199
- * @returns `true` if `error` is a {@link MnemocyteError}.
200
- */
201
- function isMnemocyteError(error) {
202
- return error instanceof MnemocyteError;
222
+ return trimToTokenBudget(formatContext([], options.input.query, format, memories.length), maxTokens, tokenCounter);
203
223
  }
204
224
  //#endregion
205
225
  //#region src/observability.ts
@@ -442,6 +462,10 @@ function normalizeTags(tags) {
442
462
  function createId() {
443
463
  return `mem_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
444
464
  }
465
+ /** Generate a unique audit-event id (`evt_*`). */
466
+ function createEventId() {
467
+ return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
468
+ }
445
469
  function isExpired(memory, now) {
446
470
  return memory.expiresAt !== null && memory.expiresAt.getTime() <= now.getTime();
447
471
  }
@@ -450,6 +474,7 @@ function cloneMemory(memory) {
450
474
  ...memory,
451
475
  tags: [...memory.tags],
452
476
  metadata: { ...memory.metadata },
477
+ supersededAt: memory.supersededAt ? new Date(memory.supersededAt) : null,
453
478
  expiresAt: memory.expiresAt ? new Date(memory.expiresAt) : null,
454
479
  lastAccessedAt: memory.lastAccessedAt ? new Date(memory.lastAccessedAt) : null,
455
480
  createdAt: new Date(memory.createdAt),
@@ -470,6 +495,7 @@ function rowToMemory(row) {
470
495
  embeddingModel: row.embeddingModel,
471
496
  embeddingDimensions: row.embeddingDimensions,
472
497
  supersededBy: row.supersededBy,
498
+ supersededAt: row.supersededAt,
473
499
  expiresAt: row.expiresAt,
474
500
  lastAccessedAt: row.lastAccessedAt,
475
501
  accessCount: row.accessCount,
@@ -546,6 +572,49 @@ function matchesPruneFilter(memory, input, now) {
546
572
  if (input.maxImportance !== void 0 && IMPORTANCE_RANK[memory.importance] > IMPORTANCE_RANK[input.maxImportance]) return false;
547
573
  return true;
548
574
  }
575
+ /**
576
+ * Validate a {@link FindDuplicatesInput} and throw a `"VALIDATION"`
577
+ * {@link MnemocyteError} for malformed `threshold` / `limit` values.
578
+ */
579
+ function validateFindDuplicatesInput(input) {
580
+ assertNonEmptyString(input.entityId, "entityId");
581
+ if (input.threshold !== void 0) {
582
+ if (!Number.isFinite(input.threshold) || input.threshold < 0 || input.threshold > 1) throw new MnemocyteError("threshold must be a number between 0 and 1.", "VALIDATION");
583
+ }
584
+ if (input.limit !== void 0) assertLimit(input.limit);
585
+ }
586
+ /**
587
+ * Return `true` when a memory should participate in a duplicate scan,
588
+ * given the filters on `input` and a shared `now` timestamp.
589
+ */
590
+ function matchesDuplicateFilter(memory, input, now) {
591
+ if (memory.entityId !== input.entityId) return false;
592
+ if (input.includeSuperseded !== true && memory.supersededBy !== null) return false;
593
+ if (input.includeExpired !== true && isExpired(memory, now)) return false;
594
+ if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
595
+ if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
596
+ return true;
597
+ }
598
+ /**
599
+ * Validate a {@link ConsolidateInput} and throw a `"VALIDATION"`
600
+ * {@link MnemocyteError} for malformed inputs (empty `supersededIds`
601
+ * or a `survivorId` appearing in `supersededIds`).
602
+ */
603
+ function validateConsolidateInput(input) {
604
+ assertNonEmptyString(input.entityId, "entityId");
605
+ assertNonEmptyString(input.survivorId, "survivorId");
606
+ if (input.supersededIds.length === 0) throw new MnemocyteError("supersededIds must contain at least one memory id.", "VALIDATION");
607
+ if (input.supersededIds.includes(input.survivorId)) throw new MnemocyteError("survivorId must not appear in supersededIds.", "VALIDATION");
608
+ for (const id of input.supersededIds) assertNonEmptyString(id, "supersededIds[*]");
609
+ }
610
+ /**
611
+ * Validate a {@link ListAuditLogInput} and throw a `"VALIDATION"`
612
+ * {@link MnemocyteError} when malformed.
613
+ */
614
+ function validateListAuditLogInput(input) {
615
+ assertNonEmptyString(input.entityId, "entityId");
616
+ if (input.limit !== void 0) assertLimit(input.limit);
617
+ }
549
618
  function matchesRecallFilter(memory, input, now) {
550
619
  if (memory.entityId !== input.entityId) return false;
551
620
  if (!input.includeSuperseded && memory.supersededBy !== null) return false;
@@ -658,10 +727,30 @@ function toScoredMemory(memory, vector, lexical, input, config) {
658
727
  //#region src/memory/in-memory.ts
659
728
  function createInMemoryClient(config) {
660
729
  const memories = /* @__PURE__ */ new Map();
730
+ const auditLog = [];
661
731
  let closed = false;
662
732
  function assertOpen() {
663
733
  if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
664
734
  }
735
+ function recordAudit(entityId, description, metadata) {
736
+ if (config.audit?.enabled !== true) return;
737
+ auditLog.push({
738
+ id: createEventId(),
739
+ entityId,
740
+ description,
741
+ metadata,
742
+ timestamp: /* @__PURE__ */ new Date()
743
+ });
744
+ }
745
+ function cloneAuditEvent(event) {
746
+ return {
747
+ id: event.id,
748
+ entityId: event.entityId,
749
+ description: event.description,
750
+ metadata: { ...event.metadata },
751
+ timestamp: new Date(event.timestamp)
752
+ };
753
+ }
665
754
  async function remember(input) {
666
755
  return observe(config, "in-memory", "remember", { entityId: input.entityId }, async () => {
667
756
  assertOpen();
@@ -684,6 +773,7 @@ function createInMemoryClient(config) {
684
773
  embeddingModel: config.embedder.model,
685
774
  embeddingDimensions: config.embedder.dimensions,
686
775
  supersededBy: null,
776
+ supersededAt: null,
687
777
  expiresAt: input.expiresAt ?? null,
688
778
  lastAccessedAt: null,
689
779
  accessCount: 0,
@@ -692,6 +782,11 @@ function createInMemoryClient(config) {
692
782
  embedding
693
783
  };
694
784
  memories.set(memory.id, memory);
785
+ recordAudit(memory.entityId, "memory.created", {
786
+ memoryId: memory.id,
787
+ type: memory.type,
788
+ importance: memory.importance
789
+ });
695
790
  return cloneMemory(memory);
696
791
  }, (memory) => ({
697
792
  memoryId: memory.id,
@@ -759,6 +854,7 @@ function createInMemoryClient(config) {
759
854
  const memory = memories.get(input.memoryId);
760
855
  if (!memory || memory.entityId !== input.entityId) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
761
856
  memories.delete(input.memoryId);
857
+ recordAudit(memory.entityId, "memory.deleted", { memoryId: memory.id });
762
858
  });
763
859
  },
764
860
  async forgetAll(input) {
@@ -770,6 +866,7 @@ function createInMemoryClient(config) {
770
866
  memories.delete(id);
771
867
  deleted += 1;
772
868
  }
869
+ recordAudit(input.entityId, "entity.cleared", { count: deleted });
773
870
  return deleted;
774
871
  }, (count) => ({ count })).then(() => void 0);
775
872
  },
@@ -781,7 +878,10 @@ function createInMemoryClient(config) {
781
878
  const matched = [];
782
879
  for (const memory of memories.values()) if (matchesPruneFilter(memory, input, now)) matched.push(memory.id);
783
880
  const dryRun = input.dryRun === true;
784
- if (!dryRun) for (const id of matched) memories.delete(id);
881
+ if (!dryRun) {
882
+ for (const id of matched) memories.delete(id);
883
+ if (matched.length > 0 && input.entityId !== void 0) recordAudit(input.entityId, "memory.pruned", { count: matched.length });
884
+ }
785
885
  return {
786
886
  matchedCount: matched.length,
787
887
  deletedCount: dryRun ? 0 : matched.length,
@@ -789,6 +889,53 @@ function createInMemoryClient(config) {
789
889
  };
790
890
  }, (result) => ({ count: result.deletedCount }));
791
891
  },
892
+ async findDuplicates(input) {
893
+ return observe(config, "in-memory", "findDuplicates", { entityId: input.entityId }, async () => {
894
+ assertOpen();
895
+ validateFindDuplicatesInput(input);
896
+ const threshold = input.threshold ?? .95;
897
+ const limit = input.limit ?? 100;
898
+ const now = /* @__PURE__ */ new Date();
899
+ const candidates = Array.from(memories.values()).filter((memory) => matchesDuplicateFilter(memory, input, now));
900
+ const pairs = [];
901
+ for (let i = 0; i < candidates.length; i += 1) {
902
+ const a = candidates[i];
903
+ if (a === void 0) continue;
904
+ for (let j = i + 1; j < candidates.length; j += 1) {
905
+ const b = candidates[j];
906
+ if (b === void 0) continue;
907
+ const similarity = Math.max(0, Math.min(1, cosineSimilarity(a.embedding, b.embedding)));
908
+ if (similarity >= threshold) pairs.push({
909
+ a: cloneMemory(a),
910
+ b: cloneMemory(b),
911
+ similarity
912
+ });
913
+ }
914
+ }
915
+ pairs.sort((x, y) => y.similarity - x.similarity);
916
+ return pairs.slice(0, limit);
917
+ }, (result) => ({ count: result.length }));
918
+ },
919
+ async listAuditLog(input) {
920
+ return observe(config, "in-memory", "listAuditLog", { entityId: input.entityId }, async () => {
921
+ assertOpen();
922
+ validateListAuditLogInput(input);
923
+ const limit = input.limit ?? 50;
924
+ return auditLog.map((event, idx) => ({
925
+ event,
926
+ idx
927
+ })).filter(({ event }) => {
928
+ if (event.entityId !== input.entityId) return false;
929
+ if (input.before !== void 0 && event.timestamp.getTime() >= input.before.getTime()) return false;
930
+ if (input.after !== void 0 && event.timestamp.getTime() <= input.after.getTime()) return false;
931
+ return true;
932
+ }).sort((a, b) => {
933
+ const dt = b.event.timestamp.getTime() - a.event.timestamp.getTime();
934
+ if (dt !== 0) return dt;
935
+ return b.idx - a.idx;
936
+ }).slice(0, limit).map(({ event }) => cloneAuditEvent(event));
937
+ }, (result) => ({ count: result.length }));
938
+ },
792
939
  async stats(input) {
793
940
  return observe(config, "in-memory", "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
794
941
  assertOpen();
@@ -814,6 +961,7 @@ function createInMemoryClient(config) {
814
961
  };
815
962
  });
816
963
  },
964
+ experimental: createExperimental(),
817
965
  async close() {
818
966
  return observe(config, "in-memory", "close", {}, async () => {
819
967
  closed = true;
@@ -821,25 +969,83 @@ function createInMemoryClient(config) {
821
969
  });
822
970
  }
823
971
  };
972
+ function createExperimental() {
973
+ return { async consolidate(input) {
974
+ return observe(config, "in-memory", "consolidate", {
975
+ entityId: input.entityId,
976
+ memoryId: input.survivorId
977
+ }, async () => {
978
+ assertOpen();
979
+ validateConsolidateInput(input);
980
+ const survivor = memories.get(input.survivorId);
981
+ if (!survivor || survivor.entityId !== input.entityId) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
982
+ if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
983
+ const losers = [];
984
+ for (const id of input.supersededIds) {
985
+ const memory = memories.get(id);
986
+ if (!memory || memory.entityId !== input.entityId) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
987
+ if (memory.supersededBy !== null) continue;
988
+ losers.push(memory);
989
+ }
990
+ const now = /* @__PURE__ */ new Date();
991
+ const newSupersededIds = [];
992
+ for (const loser of losers) {
993
+ loser.supersededBy = survivor.id;
994
+ loser.supersededAt = now;
995
+ loser.updatedAt = now;
996
+ newSupersededIds.push(loser.id);
997
+ recordAudit(input.entityId, "memory.superseded", {
998
+ memoryId: loser.id,
999
+ supersededBy: survivor.id
1000
+ });
1001
+ }
1002
+ if (input.mergeTags !== false && losers.length > 0) {
1003
+ const mergedTags = new Set(survivor.tags);
1004
+ for (const loser of losers) for (const tag of loser.tags) mergedTags.add(tag);
1005
+ if (mergedTags.size !== survivor.tags.length) {
1006
+ survivor.tags = [...mergedTags];
1007
+ survivor.updatedAt = now;
1008
+ }
1009
+ }
1010
+ return {
1011
+ survivorId: survivor.id,
1012
+ supersededCount: newSupersededIds.length,
1013
+ supersededIds: newSupersededIds
1014
+ };
1015
+ }, (result) => ({ count: result.supersededCount }));
1016
+ } };
1017
+ }
824
1018
  }
825
1019
  //#endregion
826
1020
  //#region src/db/queries/events.ts
827
- async function deleteEventsForEntity(db, entityId) {
828
- return (await db.delete(eventsTable).where(eq(eventsTable.entityId, entityId)).returning({ id: eventsTable.id })).length;
1021
+ async function insertEvent(db, row) {
1022
+ const inserted = (await db.insert(eventsTable).values(row).returning())[0];
1023
+ if (!inserted) throw new Error("Event insert returned no rows.");
1024
+ return inserted;
1025
+ }
1026
+ async function listEvents(db, filter) {
1027
+ 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);
829
1028
  }
830
1029
  //#endregion
831
1030
  //#region src/db/queries/memories.ts
832
1031
  function vectorLiteral(embedding) {
833
- return `[${embedding.join(",")}]`;
1032
+ return `[${embedding.map(formatVectorComponent).join(",")}]`;
1033
+ }
1034
+ function formatVectorComponent(value) {
1035
+ if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
1036
+ return Object.is(value, -0) ? "0" : value.toFixed(17);
834
1037
  }
835
1038
  function filterConditions(filter) {
836
- return and(eq(memoriesTable.entityId, filter.entityId), filter.includeSuperseded ? void 0 : isNull(memoriesTable.supersededBy), filter.includeExpired ? void 0 : sql`(${memoriesTable.expiresAt} IS NULL OR ${memoriesTable.expiresAt} > now())`, filter.types && filter.types.length > 0 ? inArray(memoriesTable.type, [...filter.types]) : void 0, filter.tags && filter.tags.length > 0 ? sql`${memoriesTable.tags} && ${filter.tags}` : void 0, filter.before ? lt(memoriesTable.createdAt, filter.before) : void 0, filter.after ? gt(memoriesTable.createdAt, filter.after) : void 0);
1039
+ return and(eq(memoriesTable.entityId, filter.entityId), filter.includeSuperseded ? void 0 : isNull(memoriesTable.supersededBy), filter.includeExpired ? void 0 : sql`(${memoriesTable.expiresAt} IS NULL OR ${memoriesTable.expiresAt} > now())`, filter.types && filter.types.length > 0 ? inArray(memoriesTable.type, [...filter.types]) : void 0, filter.tags && filter.tags.length > 0 ? sql`${memoriesTable.tags} @> ${filter.tags}` : void 0, filter.before ? lt(memoriesTable.createdAt, filter.before) : void 0, filter.after ? gt(memoriesTable.createdAt, filter.after) : void 0);
837
1040
  }
838
1041
  async function insertMemory(db, row) {
839
1042
  const inserted = (await db.insert(memoriesTable).values(row).returning())[0];
840
1043
  if (!inserted) throw new Error("Memory insert returned no rows.");
841
1044
  return inserted;
842
1045
  }
1046
+ async function getMemoryById(db, entityId, memoryId) {
1047
+ return (await db.select().from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), eq(memoriesTable.id, memoryId))).limit(1))[0] ?? null;
1048
+ }
843
1049
  async function listMemories(db, filter) {
844
1050
  return db.select().from(memoriesTable).where(filterConditions(filter));
845
1051
  }
@@ -858,6 +1064,136 @@ async function countPruneMatches(db, filter) {
858
1064
  async function pruneMemories(db, filter) {
859
1065
  return (await db.delete(memoriesTable).where(pruneConditions(filter)).returning({ id: memoriesTable.id })).length;
860
1066
  }
1067
+ async function findDuplicatePairs(db, input) {
1068
+ return (await db.execute(sql`
1069
+ SELECT
1070
+ a.id AS "aId",
1071
+ a.entity_id AS "aEntityId",
1072
+ a.content AS "aContent",
1073
+ a.type AS "aType",
1074
+ a.importance AS "aImportance",
1075
+ a.tags AS "aTags",
1076
+ a.source AS "aSource",
1077
+ a.metadata AS "aMetadata",
1078
+ a.confidence AS "aConfidence",
1079
+ a.embedding AS "aEmbedding",
1080
+ a.embedding_model AS "aEmbeddingModel",
1081
+ a.embedding_dimensions AS "aEmbeddingDimensions",
1082
+ a.superseded_by AS "aSupersededBy",
1083
+ a.superseded_at AS "aSupersededAt",
1084
+ a.expires_at AS "aExpiresAt",
1085
+ a.last_accessed_at AS "aLastAccessedAt",
1086
+ a.access_count AS "aAccessCount",
1087
+ a.created_at AS "aCreatedAt",
1088
+ a.updated_at AS "aUpdatedAt",
1089
+ b.id AS "bId",
1090
+ b.entity_id AS "bEntityId",
1091
+ b.content AS "bContent",
1092
+ b.type AS "bType",
1093
+ b.importance AS "bImportance",
1094
+ b.tags AS "bTags",
1095
+ b.source AS "bSource",
1096
+ b.metadata AS "bMetadata",
1097
+ b.confidence AS "bConfidence",
1098
+ b.embedding AS "bEmbedding",
1099
+ b.embedding_model AS "bEmbeddingModel",
1100
+ b.embedding_dimensions AS "bEmbeddingDimensions",
1101
+ b.superseded_by AS "bSupersededBy",
1102
+ b.superseded_at AS "bSupersededAt",
1103
+ b.expires_at AS "bExpiresAt",
1104
+ b.last_accessed_at AS "bLastAccessedAt",
1105
+ b.access_count AS "bAccessCount",
1106
+ b.created_at AS "bCreatedAt",
1107
+ b.updated_at AS "bUpdatedAt",
1108
+ 1 - (a.embedding <=> b.embedding) AS "similarity"
1109
+ FROM mnemocyte_memories a
1110
+ JOIN mnemocyte_memories b
1111
+ ON b.entity_id = a.entity_id
1112
+ AND a.id < b.id
1113
+ AND a.embedding IS NOT NULL
1114
+ AND b.embedding IS NOT NULL
1115
+ WHERE
1116
+ a.entity_id = ${input.entityId}
1117
+ ${input.includeSuperseded ? sql`` : sql`AND a.superseded_by IS NULL AND b.superseded_by IS NULL`}
1118
+ ${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())`}
1119
+ ${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``}
1120
+ ${input.tags && input.tags.length > 0 ? sql`AND a.tags @> ${input.tags} AND b.tags @> ${input.tags}` : sql``}
1121
+ AND 1 - (a.embedding <=> b.embedding) >= ${input.threshold}
1122
+ ORDER BY "similarity" DESC
1123
+ LIMIT ${input.limit}
1124
+ `)).map((row) => {
1125
+ return {
1126
+ a: {
1127
+ id: row.aId,
1128
+ entityId: row.aEntityId,
1129
+ content: row.aContent,
1130
+ type: row.aType,
1131
+ importance: row.aImportance,
1132
+ tags: row.aTags,
1133
+ source: row.aSource,
1134
+ metadata: row.aMetadata,
1135
+ confidence: row.aConfidence,
1136
+ embedding: row.aEmbedding,
1137
+ embeddingModel: row.aEmbeddingModel,
1138
+ embeddingDimensions: row.aEmbeddingDimensions,
1139
+ supersededBy: row.aSupersededBy,
1140
+ supersededAt: row.aSupersededAt,
1141
+ expiresAt: row.aExpiresAt,
1142
+ lastAccessedAt: row.aLastAccessedAt,
1143
+ accessCount: row.aAccessCount,
1144
+ createdAt: row.aCreatedAt,
1145
+ updatedAt: row.aUpdatedAt
1146
+ },
1147
+ b: {
1148
+ id: row.bId,
1149
+ entityId: row.bEntityId,
1150
+ content: row.bContent,
1151
+ type: row.bType,
1152
+ importance: row.bImportance,
1153
+ tags: row.bTags,
1154
+ source: row.bSource,
1155
+ metadata: row.bMetadata,
1156
+ confidence: row.bConfidence,
1157
+ embedding: row.bEmbedding,
1158
+ embeddingModel: row.bEmbeddingModel,
1159
+ embeddingDimensions: row.bEmbeddingDimensions,
1160
+ supersededBy: row.bSupersededBy,
1161
+ supersededAt: row.bSupersededAt,
1162
+ expiresAt: row.bExpiresAt,
1163
+ lastAccessedAt: row.bLastAccessedAt,
1164
+ accessCount: row.bAccessCount,
1165
+ createdAt: row.bCreatedAt,
1166
+ updatedAt: row.bUpdatedAt
1167
+ },
1168
+ similarity: Number(row.similarity) || 0
1169
+ };
1170
+ });
1171
+ }
1172
+ async function loadConsolidationTargets(db, entityId, ids) {
1173
+ if (ids.length === 0) return [];
1174
+ return await db.select({
1175
+ id: memoriesTable.id,
1176
+ tags: memoriesTable.tags,
1177
+ supersededBy: memoriesTable.supersededBy
1178
+ }).from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), inArray(memoriesTable.id, [...ids])));
1179
+ }
1180
+ async function markMemoriesSuperseded(db, params) {
1181
+ if (params.ids.length === 0) return [];
1182
+ return await db.update(memoriesTable).set({
1183
+ supersededBy: params.survivorId,
1184
+ supersededAt: params.now,
1185
+ updatedAt: params.now
1186
+ }).where(and(eq(memoriesTable.entityId, params.entityId), inArray(memoriesTable.id, [...params.ids]), isNull(memoriesTable.supersededBy))).returning({
1187
+ id: memoriesTable.id,
1188
+ tags: memoriesTable.tags
1189
+ });
1190
+ }
1191
+ async function setMemoryTags(db, params) {
1192
+ await db.update(memoriesTable).set({
1193
+ tags: [...params.tags],
1194
+ updatedAt: params.now
1195
+ }).where(and(eq(memoriesTable.entityId, params.entityId), eq(memoriesTable.id, params.memoryId)));
1196
+ }
861
1197
  async function markMemoryAccessed(db, memoryIds) {
862
1198
  if (memoryIds.length === 0) return;
863
1199
  await db.update(memoriesTable).set({
@@ -898,7 +1234,7 @@ async function vectorSearch(db, input) {
898
1234
  ${input.includeSuperseded ? sql`` : sql`AND superseded_by IS NULL`}
899
1235
  ${input.includeExpired ? sql`` : sql`AND (expires_at IS NULL OR expires_at > now())`}
900
1236
  ${input.types && input.types.length > 0 ? sql`AND type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)})` : sql``}
901
- ${input.tags && input.tags.length > 0 ? sql`AND tags && ${input.tags}` : sql``}
1237
+ ${input.tags && input.tags.length > 0 ? sql`AND tags @> ${input.tags}` : sql``}
902
1238
  ${input.before ? sql`AND created_at < ${input.before}` : sql``}
903
1239
  ${input.after ? sql`AND created_at > ${input.after}` : sql``}
904
1240
  AND 1 - (embedding <=> ${embedding}::vector) >= ${minScore}
@@ -935,7 +1271,7 @@ async function lexicalSearch(db, input) {
935
1271
  ${input.includeSuperseded ? sql`` : sql`AND superseded_by IS NULL`}
936
1272
  ${input.includeExpired ? sql`` : sql`AND (expires_at IS NULL OR expires_at > now())`}
937
1273
  ${input.types && input.types.length > 0 ? sql`AND type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)})` : sql``}
938
- ${input.tags && input.tags.length > 0 ? sql`AND tags && ${input.tags}` : sql``}
1274
+ ${input.tags && input.tags.length > 0 ? sql`AND tags @> ${input.tags}` : sql``}
939
1275
  ${input.before ? sql`AND created_at < ${input.before}` : sql``}
940
1276
  ${input.after ? sql`AND created_at > ${input.after}` : sql``}
941
1277
  AND to_tsvector('english', content) @@ websearch_to_tsquery('english', ${input.query})
@@ -982,6 +1318,15 @@ async function hybridRecall(input) {
982
1318
  }
983
1319
  //#endregion
984
1320
  //#region src/memory/postgres.ts
1321
+ function rowToAuditEvent(row) {
1322
+ return {
1323
+ id: row.id,
1324
+ entityId: row.entityId,
1325
+ description: row.description,
1326
+ metadata: row.metadata,
1327
+ timestamp: row.timestamp
1328
+ };
1329
+ }
985
1330
  const IMPORTANCE_LEVELS = [
986
1331
  "low",
987
1332
  "normal",
@@ -1008,6 +1353,18 @@ function createPostgresClient(config, handle) {
1008
1353
  function assertOpen() {
1009
1354
  if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
1010
1355
  }
1356
+ async function recordAudit(entityId, description, metadata) {
1357
+ if (config.audit?.enabled !== true) return;
1358
+ try {
1359
+ await insertEvent(handle.db, {
1360
+ id: createEventId(),
1361
+ entityId,
1362
+ description,
1363
+ metadata,
1364
+ timestamp: /* @__PURE__ */ new Date()
1365
+ });
1366
+ } catch {}
1367
+ }
1011
1368
  async function remember(input) {
1012
1369
  return observe(config, "postgres", "remember", { entityId: input.entityId }, async () => {
1013
1370
  assertOpen();
@@ -1017,7 +1374,7 @@ function createPostgresClient(config, handle) {
1017
1374
  ...config.provider === void 0 ? {} : { resilience: config.provider }
1018
1375
  });
1019
1376
  const now = /* @__PURE__ */ new Date();
1020
- return rowToMemory(await insertMemory(handle.db, {
1377
+ const memory = rowToMemory(await insertMemory(handle.db, {
1021
1378
  id: createId(),
1022
1379
  entityId: input.entityId,
1023
1380
  content: input.content,
@@ -1038,6 +1395,12 @@ function createPostgresClient(config, handle) {
1038
1395
  createdAt: now,
1039
1396
  updatedAt: now
1040
1397
  }));
1398
+ await recordAudit(memory.entityId, "memory.created", {
1399
+ memoryId: memory.id,
1400
+ type: memory.type,
1401
+ importance: memory.importance
1402
+ });
1403
+ return memory;
1041
1404
  }, (memory) => ({
1042
1405
  memoryId: memory.id,
1043
1406
  count: 1
@@ -1097,14 +1460,17 @@ function createPostgresClient(config, handle) {
1097
1460
  assertNonEmptyString(input.entityId, "entityId");
1098
1461
  assertNonEmptyString(input.memoryId, "memoryId");
1099
1462
  if (!await deleteMemory(handle.db, input.entityId, input.memoryId)) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
1463
+ await recordAudit(input.entityId, "memory.deleted", { memoryId: input.memoryId });
1100
1464
  });
1101
1465
  },
1102
1466
  async forgetAll(input) {
1103
1467
  return observe(config, "postgres", "forgetAll", { entityId: input.entityId }, async () => {
1104
1468
  assertOpen();
1105
1469
  assertNonEmptyString(input.entityId, "entityId");
1106
- await Promise.all([deleteMemoriesForEntity(handle.db, input.entityId), deleteEventsForEntity(handle.db, input.entityId)]);
1107
- });
1470
+ const deletedCount = await deleteMemoriesForEntity(handle.db, input.entityId);
1471
+ await recordAudit(input.entityId, "entity.cleared", { count: deletedCount });
1472
+ return deletedCount;
1473
+ }, (count) => ({ count })).then(() => void 0);
1108
1474
  },
1109
1475
  async prune(input) {
1110
1476
  return observe(config, "postgres", "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
@@ -1117,6 +1483,7 @@ function createPostgresClient(config, handle) {
1117
1483
  dryRun: true
1118
1484
  };
1119
1485
  const deletedCount = await pruneMemories(handle.db, filter);
1486
+ if (deletedCount > 0 && input.entityId !== void 0) await recordAudit(input.entityId, "memory.pruned", { count: deletedCount });
1120
1487
  return {
1121
1488
  matchedCount: deletedCount,
1122
1489
  deletedCount,
@@ -1124,6 +1491,39 @@ function createPostgresClient(config, handle) {
1124
1491
  };
1125
1492
  }, (result) => ({ count: result.deletedCount }));
1126
1493
  },
1494
+ async findDuplicates(input) {
1495
+ return observe(config, "postgres", "findDuplicates", { entityId: input.entityId }, async () => {
1496
+ assertOpen();
1497
+ validateFindDuplicatesInput(input);
1498
+ const threshold = input.threshold ?? .95;
1499
+ const limit = input.limit ?? 100;
1500
+ return (await findDuplicatePairs(handle.db, {
1501
+ entityId: input.entityId,
1502
+ threshold,
1503
+ limit,
1504
+ ...input.types === void 0 ? {} : { types: input.types },
1505
+ ...input.tags === void 0 ? {} : { tags: input.tags },
1506
+ ...input.includeSuperseded === void 0 ? {} : { includeSuperseded: input.includeSuperseded },
1507
+ ...input.includeExpired === void 0 ? {} : { includeExpired: input.includeExpired }
1508
+ })).map((row) => ({
1509
+ a: rowToMemory(row.a),
1510
+ b: rowToMemory(row.b),
1511
+ similarity: Math.max(0, Math.min(1, row.similarity))
1512
+ }));
1513
+ }, (result) => ({ count: result.length }));
1514
+ },
1515
+ async listAuditLog(input) {
1516
+ return observe(config, "postgres", "listAuditLog", { entityId: input.entityId }, async () => {
1517
+ assertOpen();
1518
+ validateListAuditLogInput(input);
1519
+ return (await listEvents(handle.db, {
1520
+ entityId: input.entityId,
1521
+ limit: input.limit ?? 50,
1522
+ ...input.before === void 0 ? {} : { before: input.before },
1523
+ ...input.after === void 0 ? {} : { after: input.after }
1524
+ })).map(rowToAuditEvent);
1525
+ }, (result) => ({ count: result.length }));
1526
+ },
1127
1527
  async stats(input) {
1128
1528
  return observe(config, "postgres", "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
1129
1529
  assertOpen();
@@ -1152,6 +1552,7 @@ function createPostgresClient(config, handle) {
1152
1552
  };
1153
1553
  });
1154
1554
  },
1555
+ experimental: createExperimental(),
1155
1556
  async close() {
1156
1557
  return observe(config, "postgres", "close", {}, async () => {
1157
1558
  closed = true;
@@ -1159,9 +1560,61 @@ function createPostgresClient(config, handle) {
1159
1560
  });
1160
1561
  }
1161
1562
  };
1563
+ function createExperimental() {
1564
+ return { async consolidate(input) {
1565
+ return observe(config, "postgres", "consolidate", {
1566
+ entityId: input.entityId,
1567
+ memoryId: input.survivorId
1568
+ }, async () => {
1569
+ assertOpen();
1570
+ validateConsolidateInput(input);
1571
+ const survivor = await getMemoryById(handle.db, input.entityId, input.survivorId);
1572
+ if (!survivor) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
1573
+ if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
1574
+ const targets = await loadConsolidationTargets(handle.db, input.entityId, input.supersededIds);
1575
+ const foundIds = new Set(targets.map((t) => t.id));
1576
+ for (const id of input.supersededIds) if (!foundIds.has(id)) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
1577
+ const losers = targets.filter((target) => target.supersededBy === null);
1578
+ if (losers.length === 0) return {
1579
+ survivorId: survivor.id,
1580
+ supersededCount: 0,
1581
+ supersededIds: []
1582
+ };
1583
+ const now = /* @__PURE__ */ new Date();
1584
+ const updated = await markMemoriesSuperseded(handle.db, {
1585
+ survivorId: survivor.id,
1586
+ entityId: input.entityId,
1587
+ ids: losers.map((loser) => loser.id),
1588
+ now
1589
+ });
1590
+ const newSupersededIds = updated.map((row) => row.id);
1591
+ for (const id of newSupersededIds) await recordAudit(input.entityId, "memory.superseded", {
1592
+ memoryId: id,
1593
+ supersededBy: survivor.id
1594
+ });
1595
+ if (input.mergeTags !== false && updated.length > 0) {
1596
+ const mergedTags = new Set(survivor.tags);
1597
+ for (const row of updated) for (const tag of row.tags) mergedTags.add(tag);
1598
+ if (mergedTags.size !== survivor.tags.length) await setMemoryTags(handle.db, {
1599
+ entityId: input.entityId,
1600
+ memoryId: survivor.id,
1601
+ tags: [...mergedTags],
1602
+ now
1603
+ });
1604
+ }
1605
+ return {
1606
+ survivorId: survivor.id,
1607
+ supersededCount: newSupersededIds.length,
1608
+ supersededIds: newSupersededIds
1609
+ };
1610
+ }, (result) => ({ count: result.supersededCount }));
1611
+ } };
1612
+ }
1162
1613
  }
1163
1614
  //#endregion
1164
1615
  //#region src/client.ts
1616
+ /** Embedding dimensionality pinned by `migrations/0000_initial.sql`. */
1617
+ const POSTGRES_EMBEDDING_DIMENSIONS = 1536;
1165
1618
  /**
1166
1619
  * Create a Mnemocyte client.
1167
1620
  *
@@ -1176,14 +1629,16 @@ function createPostgresClient(config, handle) {
1176
1629
  *
1177
1630
  * @param config - Client configuration. {@link MnemocyteConfig.embedder} is required.
1178
1631
  * @returns A {@link MnemocyteClient} instance.
1179
- * @throws {MnemocyteError} With code `"CONFIG"` if `embedder` is missing or invalid,
1180
- * or `"VALIDATION"` if `databaseUrl` is provided but empty.
1632
+ * @throws {MnemocyteError} With code `"CONFIG"` if `embedder` is missing or
1633
+ * invalid (including when `databaseUrl` is set and `embedder.dimensions`
1634
+ * is not 1536 — the dimensionality pinned by the bundled migration), or
1635
+ * `"VALIDATION"` if `databaseUrl` is provided but empty.
1181
1636
  *
1182
1637
  * @example Postgres-backed client
1183
1638
  * ```ts
1184
1639
  * const client = createMnemocyte({
1185
1640
  * databaseUrl: process.env.DATABASE_URL,
1186
- * embedder,
1641
+ * embedder, // embedder.dimensions MUST be 1536 for the Postgres backend
1187
1642
  * });
1188
1643
  * ```
1189
1644
  */
@@ -1191,6 +1646,7 @@ function createMnemocyte(config) {
1191
1646
  assertEmbedder(config.embedder);
1192
1647
  if (config.databaseUrl) {
1193
1648
  assertNonEmptyString(config.databaseUrl, "databaseUrl");
1649
+ if (config.embedder.dimensions !== POSTGRES_EMBEDDING_DIMENSIONS) throw new MnemocyteError(`embedder.dimensions must be ${POSTGRES_EMBEDDING_DIMENSIONS} for the Postgres backend (got ${config.embedder.dimensions}). The bundled migration pins the vector column to vector(${POSTGRES_EMBEDDING_DIMENSIONS}).`, "CONFIG");
1194
1650
  return createPostgresClient(config, createDatabase(config.databaseUrl));
1195
1651
  }
1196
1652
  return createInMemoryClient(config);