mnemocyte 0.0.8 → 0.1.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,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;
@@ -74,6 +74,56 @@ function createDatabase(databaseUrl) {
74
74
  };
75
75
  }
76
76
  //#endregion
77
+ //#region src/errors.ts
78
+ /**
79
+ * Error type thrown by every public Mnemocyte API.
80
+ *
81
+ * Use the {@link MnemocyteError.code} property — not the message — to branch
82
+ * on failure modes, or call {@link isMnemocyteError} as a type guard when
83
+ * working with `unknown` errors.
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * try {
88
+ * await client.recall({ entityId: "u1", query: "hi" });
89
+ * } catch (err) {
90
+ * if (isMnemocyteError(err) && err.code === "DB") {
91
+ * // handle database failure
92
+ * }
93
+ * throw err;
94
+ * }
95
+ * ```
96
+ */
97
+ var MnemocyteError = class extends Error {
98
+ /** Stable, machine-readable failure code. See {@link MnemocyteErrorCode}. */
99
+ code;
100
+ /** Underlying cause, if any (e.g. an original driver or embedder error). */
101
+ cause;
102
+ /**
103
+ * @param message - Human-readable error message.
104
+ * @param code - Stable failure code; see {@link MnemocyteErrorCode}.
105
+ * @param cause - Optional underlying cause (driver error, network error, etc.).
106
+ */
107
+ constructor(message, code, cause) {
108
+ super(message);
109
+ this.name = "MnemocyteError";
110
+ this.code = code;
111
+ this.cause = cause;
112
+ }
113
+ };
114
+ /**
115
+ * Type guard for {@link MnemocyteError}.
116
+ *
117
+ * Prefer this over `instanceof` when working across module boundaries or
118
+ * bundlers where multiple copies of the class may exist.
119
+ *
120
+ * @param error - Value to test (typically a caught `unknown`).
121
+ * @returns `true` if `error` is a {@link MnemocyteError}.
122
+ */
123
+ function isMnemocyteError(error) {
124
+ return error instanceof MnemocyteError;
125
+ }
126
+ //#endregion
77
127
  //#region src/context/formatter.ts
78
128
  function escapeXml(value) {
79
129
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
@@ -152,56 +202,6 @@ async function buildContext(options) {
152
202
  return trimToTokenBudget(formatContext(await options.recall(options.input), options.input.query, format), maxTokens, tokenCounter);
153
203
  }
154
204
  //#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;
190
- }
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;
203
- }
204
- //#endregion
205
205
  //#region src/observability.ts
206
206
  function withMetadata(event, metadata) {
207
207
  return {
@@ -442,6 +442,10 @@ function normalizeTags(tags) {
442
442
  function createId() {
443
443
  return `mem_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
444
444
  }
445
+ /** Generate a unique audit-event id (`evt_*`). */
446
+ function createEventId() {
447
+ return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
448
+ }
445
449
  function isExpired(memory, now) {
446
450
  return memory.expiresAt !== null && memory.expiresAt.getTime() <= now.getTime();
447
451
  }
@@ -450,6 +454,7 @@ function cloneMemory(memory) {
450
454
  ...memory,
451
455
  tags: [...memory.tags],
452
456
  metadata: { ...memory.metadata },
457
+ supersededAt: memory.supersededAt ? new Date(memory.supersededAt) : null,
453
458
  expiresAt: memory.expiresAt ? new Date(memory.expiresAt) : null,
454
459
  lastAccessedAt: memory.lastAccessedAt ? new Date(memory.lastAccessedAt) : null,
455
460
  createdAt: new Date(memory.createdAt),
@@ -470,6 +475,7 @@ function rowToMemory(row) {
470
475
  embeddingModel: row.embeddingModel,
471
476
  embeddingDimensions: row.embeddingDimensions,
472
477
  supersededBy: row.supersededBy,
478
+ supersededAt: row.supersededAt,
473
479
  expiresAt: row.expiresAt,
474
480
  lastAccessedAt: row.lastAccessedAt,
475
481
  accessCount: row.accessCount,
@@ -546,6 +552,49 @@ function matchesPruneFilter(memory, input, now) {
546
552
  if (input.maxImportance !== void 0 && IMPORTANCE_RANK[memory.importance] > IMPORTANCE_RANK[input.maxImportance]) return false;
547
553
  return true;
548
554
  }
555
+ /**
556
+ * Validate a {@link FindDuplicatesInput} and throw a `"VALIDATION"`
557
+ * {@link MnemocyteError} for malformed `threshold` / `limit` values.
558
+ */
559
+ function validateFindDuplicatesInput(input) {
560
+ assertNonEmptyString(input.entityId, "entityId");
561
+ if (input.threshold !== void 0) {
562
+ if (!Number.isFinite(input.threshold) || input.threshold < 0 || input.threshold > 1) throw new MnemocyteError("threshold must be a number between 0 and 1.", "VALIDATION");
563
+ }
564
+ if (input.limit !== void 0) assertLimit(input.limit);
565
+ }
566
+ /**
567
+ * Return `true` when a memory should participate in a duplicate scan,
568
+ * given the filters on `input` and a shared `now` timestamp.
569
+ */
570
+ function matchesDuplicateFilter(memory, input, now) {
571
+ if (memory.entityId !== input.entityId) return false;
572
+ if (input.includeSuperseded !== true && memory.supersededBy !== null) return false;
573
+ if (input.includeExpired !== true && isExpired(memory, now)) return false;
574
+ if (input.types !== void 0 && !input.types.includes(memory.type)) return false;
575
+ if (input.tags !== void 0 && input.tags.length > 0 && !input.tags.every((tag) => memory.tags.includes(tag))) return false;
576
+ return true;
577
+ }
578
+ /**
579
+ * Validate a {@link ConsolidateInput} and throw a `"VALIDATION"`
580
+ * {@link MnemocyteError} for malformed inputs (empty `supersededIds`
581
+ * or a `survivorId` appearing in `supersededIds`).
582
+ */
583
+ function validateConsolidateInput(input) {
584
+ assertNonEmptyString(input.entityId, "entityId");
585
+ assertNonEmptyString(input.survivorId, "survivorId");
586
+ if (input.supersededIds.length === 0) throw new MnemocyteError("supersededIds must contain at least one memory id.", "VALIDATION");
587
+ if (input.supersededIds.includes(input.survivorId)) throw new MnemocyteError("survivorId must not appear in supersededIds.", "VALIDATION");
588
+ for (const id of input.supersededIds) assertNonEmptyString(id, "supersededIds[*]");
589
+ }
590
+ /**
591
+ * Validate a {@link ListAuditLogInput} and throw a `"VALIDATION"`
592
+ * {@link MnemocyteError} when malformed.
593
+ */
594
+ function validateListAuditLogInput(input) {
595
+ assertNonEmptyString(input.entityId, "entityId");
596
+ if (input.limit !== void 0) assertLimit(input.limit);
597
+ }
549
598
  function matchesRecallFilter(memory, input, now) {
550
599
  if (memory.entityId !== input.entityId) return false;
551
600
  if (!input.includeSuperseded && memory.supersededBy !== null) return false;
@@ -658,10 +707,30 @@ function toScoredMemory(memory, vector, lexical, input, config) {
658
707
  //#region src/memory/in-memory.ts
659
708
  function createInMemoryClient(config) {
660
709
  const memories = /* @__PURE__ */ new Map();
710
+ const auditLog = [];
661
711
  let closed = false;
662
712
  function assertOpen() {
663
713
  if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
664
714
  }
715
+ function recordAudit(entityId, description, metadata) {
716
+ if (config.audit?.enabled !== true) return;
717
+ auditLog.push({
718
+ id: createEventId(),
719
+ entityId,
720
+ description,
721
+ metadata,
722
+ timestamp: /* @__PURE__ */ new Date()
723
+ });
724
+ }
725
+ function cloneAuditEvent(event) {
726
+ return {
727
+ id: event.id,
728
+ entityId: event.entityId,
729
+ description: event.description,
730
+ metadata: { ...event.metadata },
731
+ timestamp: new Date(event.timestamp)
732
+ };
733
+ }
665
734
  async function remember(input) {
666
735
  return observe(config, "in-memory", "remember", { entityId: input.entityId }, async () => {
667
736
  assertOpen();
@@ -684,6 +753,7 @@ function createInMemoryClient(config) {
684
753
  embeddingModel: config.embedder.model,
685
754
  embeddingDimensions: config.embedder.dimensions,
686
755
  supersededBy: null,
756
+ supersededAt: null,
687
757
  expiresAt: input.expiresAt ?? null,
688
758
  lastAccessedAt: null,
689
759
  accessCount: 0,
@@ -692,6 +762,11 @@ function createInMemoryClient(config) {
692
762
  embedding
693
763
  };
694
764
  memories.set(memory.id, memory);
765
+ recordAudit(memory.entityId, "memory.created", {
766
+ memoryId: memory.id,
767
+ type: memory.type,
768
+ importance: memory.importance
769
+ });
695
770
  return cloneMemory(memory);
696
771
  }, (memory) => ({
697
772
  memoryId: memory.id,
@@ -759,6 +834,7 @@ function createInMemoryClient(config) {
759
834
  const memory = memories.get(input.memoryId);
760
835
  if (!memory || memory.entityId !== input.entityId) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
761
836
  memories.delete(input.memoryId);
837
+ recordAudit(memory.entityId, "memory.deleted", { memoryId: memory.id });
762
838
  });
763
839
  },
764
840
  async forgetAll(input) {
@@ -770,6 +846,7 @@ function createInMemoryClient(config) {
770
846
  memories.delete(id);
771
847
  deleted += 1;
772
848
  }
849
+ recordAudit(input.entityId, "entity.cleared", { count: deleted });
773
850
  return deleted;
774
851
  }, (count) => ({ count })).then(() => void 0);
775
852
  },
@@ -781,7 +858,10 @@ function createInMemoryClient(config) {
781
858
  const matched = [];
782
859
  for (const memory of memories.values()) if (matchesPruneFilter(memory, input, now)) matched.push(memory.id);
783
860
  const dryRun = input.dryRun === true;
784
- if (!dryRun) for (const id of matched) memories.delete(id);
861
+ if (!dryRun) {
862
+ for (const id of matched) memories.delete(id);
863
+ if (matched.length > 0 && input.entityId !== void 0) recordAudit(input.entityId, "memory.pruned", { count: matched.length });
864
+ }
785
865
  return {
786
866
  matchedCount: matched.length,
787
867
  deletedCount: dryRun ? 0 : matched.length,
@@ -789,6 +869,53 @@ function createInMemoryClient(config) {
789
869
  };
790
870
  }, (result) => ({ count: result.deletedCount }));
791
871
  },
872
+ async findDuplicates(input) {
873
+ return observe(config, "in-memory", "findDuplicates", { entityId: input.entityId }, async () => {
874
+ assertOpen();
875
+ validateFindDuplicatesInput(input);
876
+ const threshold = input.threshold ?? .95;
877
+ const limit = input.limit ?? 100;
878
+ const now = /* @__PURE__ */ new Date();
879
+ const candidates = Array.from(memories.values()).filter((memory) => matchesDuplicateFilter(memory, input, now));
880
+ const pairs = [];
881
+ for (let i = 0; i < candidates.length; i += 1) {
882
+ const a = candidates[i];
883
+ if (a === void 0) continue;
884
+ for (let j = i + 1; j < candidates.length; j += 1) {
885
+ const b = candidates[j];
886
+ if (b === void 0) continue;
887
+ const similarity = Math.max(0, Math.min(1, cosineSimilarity(a.embedding, b.embedding)));
888
+ if (similarity >= threshold) pairs.push({
889
+ a: cloneMemory(a),
890
+ b: cloneMemory(b),
891
+ similarity
892
+ });
893
+ }
894
+ }
895
+ pairs.sort((x, y) => y.similarity - x.similarity);
896
+ return pairs.slice(0, limit);
897
+ }, (result) => ({ count: result.length }));
898
+ },
899
+ async listAuditLog(input) {
900
+ return observe(config, "in-memory", "listAuditLog", { entityId: input.entityId }, async () => {
901
+ assertOpen();
902
+ validateListAuditLogInput(input);
903
+ const limit = input.limit ?? 50;
904
+ return auditLog.map((event, idx) => ({
905
+ event,
906
+ idx
907
+ })).filter(({ event }) => {
908
+ if (event.entityId !== input.entityId) return false;
909
+ if (input.before !== void 0 && event.timestamp.getTime() >= input.before.getTime()) return false;
910
+ if (input.after !== void 0 && event.timestamp.getTime() <= input.after.getTime()) return false;
911
+ return true;
912
+ }).sort((a, b) => {
913
+ const dt = b.event.timestamp.getTime() - a.event.timestamp.getTime();
914
+ if (dt !== 0) return dt;
915
+ return b.idx - a.idx;
916
+ }).slice(0, limit).map(({ event }) => cloneAuditEvent(event));
917
+ }, (result) => ({ count: result.length }));
918
+ },
792
919
  async stats(input) {
793
920
  return observe(config, "in-memory", "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
794
921
  assertOpen();
@@ -814,6 +941,7 @@ function createInMemoryClient(config) {
814
941
  };
815
942
  });
816
943
  },
944
+ experimental: createExperimental(),
817
945
  async close() {
818
946
  return observe(config, "in-memory", "close", {}, async () => {
819
947
  closed = true;
@@ -821,11 +949,62 @@ function createInMemoryClient(config) {
821
949
  });
822
950
  }
823
951
  };
952
+ function createExperimental() {
953
+ return { async consolidate(input) {
954
+ return observe(config, "in-memory", "consolidate", {
955
+ entityId: input.entityId,
956
+ memoryId: input.survivorId
957
+ }, async () => {
958
+ assertOpen();
959
+ validateConsolidateInput(input);
960
+ const survivor = memories.get(input.survivorId);
961
+ if (!survivor || survivor.entityId !== input.entityId) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
962
+ if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
963
+ const losers = [];
964
+ for (const id of input.supersededIds) {
965
+ const memory = memories.get(id);
966
+ if (!memory || memory.entityId !== input.entityId) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
967
+ if (memory.supersededBy !== null) continue;
968
+ losers.push(memory);
969
+ }
970
+ const now = /* @__PURE__ */ new Date();
971
+ const newSupersededIds = [];
972
+ for (const loser of losers) {
973
+ loser.supersededBy = survivor.id;
974
+ loser.supersededAt = now;
975
+ loser.updatedAt = now;
976
+ newSupersededIds.push(loser.id);
977
+ recordAudit(input.entityId, "memory.superseded", {
978
+ memoryId: loser.id,
979
+ supersededBy: survivor.id
980
+ });
981
+ }
982
+ if (input.mergeTags !== false && losers.length > 0) {
983
+ const mergedTags = new Set(survivor.tags);
984
+ for (const loser of losers) for (const tag of loser.tags) mergedTags.add(tag);
985
+ if (mergedTags.size !== survivor.tags.length) {
986
+ survivor.tags = [...mergedTags];
987
+ survivor.updatedAt = now;
988
+ }
989
+ }
990
+ return {
991
+ survivorId: survivor.id,
992
+ supersededCount: newSupersededIds.length,
993
+ supersededIds: newSupersededIds
994
+ };
995
+ }, (result) => ({ count: result.supersededCount }));
996
+ } };
997
+ }
824
998
  }
825
999
  //#endregion
826
1000
  //#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;
1001
+ async function insertEvent(db, row) {
1002
+ const inserted = (await db.insert(eventsTable).values(row).returning())[0];
1003
+ if (!inserted) throw new Error("Event insert returned no rows.");
1004
+ return inserted;
1005
+ }
1006
+ async function listEvents(db, filter) {
1007
+ 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
1008
  }
830
1009
  //#endregion
831
1010
  //#region src/db/queries/memories.ts
@@ -840,6 +1019,9 @@ async function insertMemory(db, row) {
840
1019
  if (!inserted) throw new Error("Memory insert returned no rows.");
841
1020
  return inserted;
842
1021
  }
1022
+ async function getMemoryById(db, entityId, memoryId) {
1023
+ return (await db.select().from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), eq(memoriesTable.id, memoryId))).limit(1))[0] ?? null;
1024
+ }
843
1025
  async function listMemories(db, filter) {
844
1026
  return db.select().from(memoriesTable).where(filterConditions(filter));
845
1027
  }
@@ -858,6 +1040,136 @@ async function countPruneMatches(db, filter) {
858
1040
  async function pruneMemories(db, filter) {
859
1041
  return (await db.delete(memoriesTable).where(pruneConditions(filter)).returning({ id: memoriesTable.id })).length;
860
1042
  }
1043
+ async function findDuplicatePairs(db, input) {
1044
+ return (await db.execute(sql`
1045
+ SELECT
1046
+ a.id AS "aId",
1047
+ a.entity_id AS "aEntityId",
1048
+ a.content AS "aContent",
1049
+ a.type AS "aType",
1050
+ a.importance AS "aImportance",
1051
+ a.tags AS "aTags",
1052
+ a.source AS "aSource",
1053
+ a.metadata AS "aMetadata",
1054
+ a.confidence AS "aConfidence",
1055
+ a.embedding AS "aEmbedding",
1056
+ a.embedding_model AS "aEmbeddingModel",
1057
+ a.embedding_dimensions AS "aEmbeddingDimensions",
1058
+ a.superseded_by AS "aSupersededBy",
1059
+ a.superseded_at AS "aSupersededAt",
1060
+ a.expires_at AS "aExpiresAt",
1061
+ a.last_accessed_at AS "aLastAccessedAt",
1062
+ a.access_count AS "aAccessCount",
1063
+ a.created_at AS "aCreatedAt",
1064
+ a.updated_at AS "aUpdatedAt",
1065
+ b.id AS "bId",
1066
+ b.entity_id AS "bEntityId",
1067
+ b.content AS "bContent",
1068
+ b.type AS "bType",
1069
+ b.importance AS "bImportance",
1070
+ b.tags AS "bTags",
1071
+ b.source AS "bSource",
1072
+ b.metadata AS "bMetadata",
1073
+ b.confidence AS "bConfidence",
1074
+ b.embedding AS "bEmbedding",
1075
+ b.embedding_model AS "bEmbeddingModel",
1076
+ b.embedding_dimensions AS "bEmbeddingDimensions",
1077
+ b.superseded_by AS "bSupersededBy",
1078
+ b.superseded_at AS "bSupersededAt",
1079
+ b.expires_at AS "bExpiresAt",
1080
+ b.last_accessed_at AS "bLastAccessedAt",
1081
+ b.access_count AS "bAccessCount",
1082
+ b.created_at AS "bCreatedAt",
1083
+ b.updated_at AS "bUpdatedAt",
1084
+ 1 - (a.embedding <=> b.embedding) AS "similarity"
1085
+ FROM mnemocyte_memories a
1086
+ JOIN mnemocyte_memories b
1087
+ ON b.entity_id = a.entity_id
1088
+ AND a.id < b.id
1089
+ AND a.embedding IS NOT NULL
1090
+ AND b.embedding IS NOT NULL
1091
+ WHERE
1092
+ a.entity_id = ${input.entityId}
1093
+ ${input.includeSuperseded ? sql`` : sql`AND a.superseded_by IS NULL AND b.superseded_by IS NULL`}
1094
+ ${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())`}
1095
+ ${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``}
1096
+ ${input.tags && input.tags.length > 0 ? sql`AND a.tags @> ${input.tags} AND b.tags @> ${input.tags}` : sql``}
1097
+ AND 1 - (a.embedding <=> b.embedding) >= ${input.threshold}
1098
+ ORDER BY "similarity" DESC
1099
+ LIMIT ${input.limit}
1100
+ `)).map((row) => {
1101
+ return {
1102
+ a: {
1103
+ id: row.aId,
1104
+ entityId: row.aEntityId,
1105
+ content: row.aContent,
1106
+ type: row.aType,
1107
+ importance: row.aImportance,
1108
+ tags: row.aTags,
1109
+ source: row.aSource,
1110
+ metadata: row.aMetadata,
1111
+ confidence: row.aConfidence,
1112
+ embedding: row.aEmbedding,
1113
+ embeddingModel: row.aEmbeddingModel,
1114
+ embeddingDimensions: row.aEmbeddingDimensions,
1115
+ supersededBy: row.aSupersededBy,
1116
+ supersededAt: row.aSupersededAt,
1117
+ expiresAt: row.aExpiresAt,
1118
+ lastAccessedAt: row.aLastAccessedAt,
1119
+ accessCount: row.aAccessCount,
1120
+ createdAt: row.aCreatedAt,
1121
+ updatedAt: row.aUpdatedAt
1122
+ },
1123
+ b: {
1124
+ id: row.bId,
1125
+ entityId: row.bEntityId,
1126
+ content: row.bContent,
1127
+ type: row.bType,
1128
+ importance: row.bImportance,
1129
+ tags: row.bTags,
1130
+ source: row.bSource,
1131
+ metadata: row.bMetadata,
1132
+ confidence: row.bConfidence,
1133
+ embedding: row.bEmbedding,
1134
+ embeddingModel: row.bEmbeddingModel,
1135
+ embeddingDimensions: row.bEmbeddingDimensions,
1136
+ supersededBy: row.bSupersededBy,
1137
+ supersededAt: row.bSupersededAt,
1138
+ expiresAt: row.bExpiresAt,
1139
+ lastAccessedAt: row.bLastAccessedAt,
1140
+ accessCount: row.bAccessCount,
1141
+ createdAt: row.bCreatedAt,
1142
+ updatedAt: row.bUpdatedAt
1143
+ },
1144
+ similarity: Number(row.similarity) || 0
1145
+ };
1146
+ });
1147
+ }
1148
+ async function loadConsolidationTargets(db, entityId, ids) {
1149
+ if (ids.length === 0) return [];
1150
+ return await db.select({
1151
+ id: memoriesTable.id,
1152
+ tags: memoriesTable.tags,
1153
+ supersededBy: memoriesTable.supersededBy
1154
+ }).from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), inArray(memoriesTable.id, [...ids])));
1155
+ }
1156
+ async function markMemoriesSuperseded(db, params) {
1157
+ if (params.ids.length === 0) return [];
1158
+ return await db.update(memoriesTable).set({
1159
+ supersededBy: params.survivorId,
1160
+ supersededAt: params.now,
1161
+ updatedAt: params.now
1162
+ }).where(and(eq(memoriesTable.entityId, params.entityId), inArray(memoriesTable.id, [...params.ids]), isNull(memoriesTable.supersededBy))).returning({
1163
+ id: memoriesTable.id,
1164
+ tags: memoriesTable.tags
1165
+ });
1166
+ }
1167
+ async function setMemoryTags(db, params) {
1168
+ await db.update(memoriesTable).set({
1169
+ tags: [...params.tags],
1170
+ updatedAt: params.now
1171
+ }).where(and(eq(memoriesTable.entityId, params.entityId), eq(memoriesTable.id, params.memoryId)));
1172
+ }
861
1173
  async function markMemoryAccessed(db, memoryIds) {
862
1174
  if (memoryIds.length === 0) return;
863
1175
  await db.update(memoriesTable).set({
@@ -982,6 +1294,15 @@ async function hybridRecall(input) {
982
1294
  }
983
1295
  //#endregion
984
1296
  //#region src/memory/postgres.ts
1297
+ function rowToAuditEvent(row) {
1298
+ return {
1299
+ id: row.id,
1300
+ entityId: row.entityId,
1301
+ description: row.description,
1302
+ metadata: row.metadata,
1303
+ timestamp: row.timestamp
1304
+ };
1305
+ }
985
1306
  const IMPORTANCE_LEVELS = [
986
1307
  "low",
987
1308
  "normal",
@@ -1008,6 +1329,18 @@ function createPostgresClient(config, handle) {
1008
1329
  function assertOpen() {
1009
1330
  if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
1010
1331
  }
1332
+ async function recordAudit(entityId, description, metadata) {
1333
+ if (config.audit?.enabled !== true) return;
1334
+ try {
1335
+ await insertEvent(handle.db, {
1336
+ id: createEventId(),
1337
+ entityId,
1338
+ description,
1339
+ metadata,
1340
+ timestamp: /* @__PURE__ */ new Date()
1341
+ });
1342
+ } catch {}
1343
+ }
1011
1344
  async function remember(input) {
1012
1345
  return observe(config, "postgres", "remember", { entityId: input.entityId }, async () => {
1013
1346
  assertOpen();
@@ -1017,7 +1350,7 @@ function createPostgresClient(config, handle) {
1017
1350
  ...config.provider === void 0 ? {} : { resilience: config.provider }
1018
1351
  });
1019
1352
  const now = /* @__PURE__ */ new Date();
1020
- return rowToMemory(await insertMemory(handle.db, {
1353
+ const memory = rowToMemory(await insertMemory(handle.db, {
1021
1354
  id: createId(),
1022
1355
  entityId: input.entityId,
1023
1356
  content: input.content,
@@ -1038,6 +1371,12 @@ function createPostgresClient(config, handle) {
1038
1371
  createdAt: now,
1039
1372
  updatedAt: now
1040
1373
  }));
1374
+ await recordAudit(memory.entityId, "memory.created", {
1375
+ memoryId: memory.id,
1376
+ type: memory.type,
1377
+ importance: memory.importance
1378
+ });
1379
+ return memory;
1041
1380
  }, (memory) => ({
1042
1381
  memoryId: memory.id,
1043
1382
  count: 1
@@ -1097,14 +1436,17 @@ function createPostgresClient(config, handle) {
1097
1436
  assertNonEmptyString(input.entityId, "entityId");
1098
1437
  assertNonEmptyString(input.memoryId, "memoryId");
1099
1438
  if (!await deleteMemory(handle.db, input.entityId, input.memoryId)) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
1439
+ await recordAudit(input.entityId, "memory.deleted", { memoryId: input.memoryId });
1100
1440
  });
1101
1441
  },
1102
1442
  async forgetAll(input) {
1103
1443
  return observe(config, "postgres", "forgetAll", { entityId: input.entityId }, async () => {
1104
1444
  assertOpen();
1105
1445
  assertNonEmptyString(input.entityId, "entityId");
1106
- await Promise.all([deleteMemoriesForEntity(handle.db, input.entityId), deleteEventsForEntity(handle.db, input.entityId)]);
1107
- });
1446
+ const deletedCount = await deleteMemoriesForEntity(handle.db, input.entityId);
1447
+ await recordAudit(input.entityId, "entity.cleared", { count: deletedCount });
1448
+ return deletedCount;
1449
+ }, (count) => ({ count })).then(() => void 0);
1108
1450
  },
1109
1451
  async prune(input) {
1110
1452
  return observe(config, "postgres", "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
@@ -1117,6 +1459,7 @@ function createPostgresClient(config, handle) {
1117
1459
  dryRun: true
1118
1460
  };
1119
1461
  const deletedCount = await pruneMemories(handle.db, filter);
1462
+ if (deletedCount > 0 && input.entityId !== void 0) await recordAudit(input.entityId, "memory.pruned", { count: deletedCount });
1120
1463
  return {
1121
1464
  matchedCount: deletedCount,
1122
1465
  deletedCount,
@@ -1124,6 +1467,39 @@ function createPostgresClient(config, handle) {
1124
1467
  };
1125
1468
  }, (result) => ({ count: result.deletedCount }));
1126
1469
  },
1470
+ async findDuplicates(input) {
1471
+ return observe(config, "postgres", "findDuplicates", { entityId: input.entityId }, async () => {
1472
+ assertOpen();
1473
+ validateFindDuplicatesInput(input);
1474
+ const threshold = input.threshold ?? .95;
1475
+ const limit = input.limit ?? 100;
1476
+ return (await findDuplicatePairs(handle.db, {
1477
+ entityId: input.entityId,
1478
+ threshold,
1479
+ limit,
1480
+ ...input.types === void 0 ? {} : { types: input.types },
1481
+ ...input.tags === void 0 ? {} : { tags: input.tags },
1482
+ ...input.includeSuperseded === void 0 ? {} : { includeSuperseded: input.includeSuperseded },
1483
+ ...input.includeExpired === void 0 ? {} : { includeExpired: input.includeExpired }
1484
+ })).map((row) => ({
1485
+ a: rowToMemory(row.a),
1486
+ b: rowToMemory(row.b),
1487
+ similarity: Math.max(0, Math.min(1, row.similarity))
1488
+ }));
1489
+ }, (result) => ({ count: result.length }));
1490
+ },
1491
+ async listAuditLog(input) {
1492
+ return observe(config, "postgres", "listAuditLog", { entityId: input.entityId }, async () => {
1493
+ assertOpen();
1494
+ validateListAuditLogInput(input);
1495
+ return (await listEvents(handle.db, {
1496
+ entityId: input.entityId,
1497
+ limit: input.limit ?? 50,
1498
+ ...input.before === void 0 ? {} : { before: input.before },
1499
+ ...input.after === void 0 ? {} : { after: input.after }
1500
+ })).map(rowToAuditEvent);
1501
+ }, (result) => ({ count: result.length }));
1502
+ },
1127
1503
  async stats(input) {
1128
1504
  return observe(config, "postgres", "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
1129
1505
  assertOpen();
@@ -1152,6 +1528,7 @@ function createPostgresClient(config, handle) {
1152
1528
  };
1153
1529
  });
1154
1530
  },
1531
+ experimental: createExperimental(),
1155
1532
  async close() {
1156
1533
  return observe(config, "postgres", "close", {}, async () => {
1157
1534
  closed = true;
@@ -1159,9 +1536,61 @@ function createPostgresClient(config, handle) {
1159
1536
  });
1160
1537
  }
1161
1538
  };
1539
+ function createExperimental() {
1540
+ return { async consolidate(input) {
1541
+ return observe(config, "postgres", "consolidate", {
1542
+ entityId: input.entityId,
1543
+ memoryId: input.survivorId
1544
+ }, async () => {
1545
+ assertOpen();
1546
+ validateConsolidateInput(input);
1547
+ const survivor = await getMemoryById(handle.db, input.entityId, input.survivorId);
1548
+ if (!survivor) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
1549
+ if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
1550
+ const targets = await loadConsolidationTargets(handle.db, input.entityId, input.supersededIds);
1551
+ const foundIds = new Set(targets.map((t) => t.id));
1552
+ for (const id of input.supersededIds) if (!foundIds.has(id)) throw new MnemocyteError("Superseded memory was not found.", "NOT_FOUND");
1553
+ const losers = targets.filter((target) => target.supersededBy === null);
1554
+ if (losers.length === 0) return {
1555
+ survivorId: survivor.id,
1556
+ supersededCount: 0,
1557
+ supersededIds: []
1558
+ };
1559
+ const now = /* @__PURE__ */ new Date();
1560
+ const updated = await markMemoriesSuperseded(handle.db, {
1561
+ survivorId: survivor.id,
1562
+ entityId: input.entityId,
1563
+ ids: losers.map((loser) => loser.id),
1564
+ now
1565
+ });
1566
+ const newSupersededIds = updated.map((row) => row.id);
1567
+ for (const id of newSupersededIds) await recordAudit(input.entityId, "memory.superseded", {
1568
+ memoryId: id,
1569
+ supersededBy: survivor.id
1570
+ });
1571
+ if (input.mergeTags !== false && updated.length > 0) {
1572
+ const mergedTags = new Set(survivor.tags);
1573
+ for (const row of updated) for (const tag of row.tags) mergedTags.add(tag);
1574
+ if (mergedTags.size !== survivor.tags.length) await setMemoryTags(handle.db, {
1575
+ entityId: input.entityId,
1576
+ memoryId: survivor.id,
1577
+ tags: [...mergedTags],
1578
+ now
1579
+ });
1580
+ }
1581
+ return {
1582
+ survivorId: survivor.id,
1583
+ supersededCount: newSupersededIds.length,
1584
+ supersededIds: newSupersededIds
1585
+ };
1586
+ }, (result) => ({ count: result.supersededCount }));
1587
+ } };
1588
+ }
1162
1589
  }
1163
1590
  //#endregion
1164
1591
  //#region src/client.ts
1592
+ /** Embedding dimensionality pinned by `migrations/0000_initial.sql`. */
1593
+ const POSTGRES_EMBEDDING_DIMENSIONS = 1536;
1165
1594
  /**
1166
1595
  * Create a Mnemocyte client.
1167
1596
  *
@@ -1176,14 +1605,16 @@ function createPostgresClient(config, handle) {
1176
1605
  *
1177
1606
  * @param config - Client configuration. {@link MnemocyteConfig.embedder} is required.
1178
1607
  * @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.
1608
+ * @throws {MnemocyteError} With code `"CONFIG"` if `embedder` is missing or
1609
+ * invalid (including when `databaseUrl` is set and `embedder.dimensions`
1610
+ * is not 1536 — the dimensionality pinned by the bundled migration), or
1611
+ * `"VALIDATION"` if `databaseUrl` is provided but empty.
1181
1612
  *
1182
1613
  * @example Postgres-backed client
1183
1614
  * ```ts
1184
1615
  * const client = createMnemocyte({
1185
1616
  * databaseUrl: process.env.DATABASE_URL,
1186
- * embedder,
1617
+ * embedder, // embedder.dimensions MUST be 1536 for the Postgres backend
1187
1618
  * });
1188
1619
  * ```
1189
1620
  */
@@ -1191,6 +1622,7 @@ function createMnemocyte(config) {
1191
1622
  assertEmbedder(config.embedder);
1192
1623
  if (config.databaseUrl) {
1193
1624
  assertNonEmptyString(config.databaseUrl, "databaseUrl");
1625
+ 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
1626
  return createPostgresClient(config, createDatabase(config.databaseUrl));
1195
1627
  }
1196
1628
  return createInMemoryClient(config);