mnemocyte 0.1.1 → 0.1.3

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, desc, eq, gt, inArray, isNotNull, isNull, lt, or, sql } from "drizzle-orm";
3
+ import { and, count, countDistinct, 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;
@@ -54,7 +54,11 @@ const memoriesTable = pgTable("mnemocyte_memories", {
54
54
  accessCount: integer("access_count").notNull().default(0),
55
55
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
56
56
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
57
- }, (table) => [index("mnemocyte_memories_entity_idx").on(table.entityId), index("mnemocyte_memories_entity_type_idx").on(table.entityId, table.type)]);
57
+ }, (table) => [
58
+ index("mnemocyte_memories_entity_idx").on(table.entityId),
59
+ index("mnemocyte_memories_entity_type_idx").on(table.entityId, table.type),
60
+ index("mnemocyte_memories_embedding_hnsw_idx").using("hnsw", table.embedding.op("vector_cosine_ops"))
61
+ ]);
58
62
  const eventsTable = pgTable("mnemocyte_events", {
59
63
  id: text("id").primaryKey(),
60
64
  entityId: text("entity_id").notNull(),
@@ -64,11 +68,22 @@ const eventsTable = pgTable("mnemocyte_events", {
64
68
  }, (table) => [index("mnemocyte_events_entity_time_idx").on(table.entityId, table.timestamp)]);
65
69
  //#endregion
66
70
  //#region src/db/index.ts
71
+ function parseSslFromUrl(url) {
72
+ const mode = url.searchParams.get("sslmode");
73
+ if (!mode || mode === "disable") return void 0;
74
+ if (mode === "require" || mode === "prefer" || mode === "allow") return { rejectUnauthorized: false };
75
+ if (mode === "verify-full") return "verify-full";
76
+ }
67
77
  function createDatabase(databaseUrl) {
78
+ const url = new URL(databaseUrl);
79
+ const ssl = parseSslFromUrl(url);
80
+ const isPooler = url.port === "6543" || url.searchParams.get("pgbouncer") === "true";
68
81
  const client = postgres(databaseUrl, {
69
82
  max: 10,
70
83
  idle_timeout: 20,
71
- connect_timeout: 10
84
+ connect_timeout: 10,
85
+ ...ssl ? { ssl } : {},
86
+ ...isPooler ? { prepare: false } : {}
72
87
  });
73
88
  return {
74
89
  db: drizzle(client, { schema: schema_exports }),
@@ -214,11 +229,19 @@ async function buildContext(options) {
214
229
  const maxTokens = options.input.maxTokens ?? 1200;
215
230
  const tokenCounter = options.input.tokenCounter ?? heuristicTokenCounter;
216
231
  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;
232
+ let low = 0;
233
+ let high = memories.length;
234
+ let bestContext = "";
235
+ while (low <= high) {
236
+ const mid = Math.floor((low + high) / 2);
237
+ const omittedCount = memories.length - mid;
238
+ const context = formatContext(memories.slice(0, mid), options.input.query, format, omittedCount);
239
+ if (tokenCounter.count(context) <= maxTokens) {
240
+ bestContext = context;
241
+ low = mid + 1;
242
+ } else high = mid - 1;
221
243
  }
244
+ if (bestContext || memories.length === 0) return bestContext;
222
245
  return trimToTokenBudget(formatContext([], options.input.query, format, memories.length), maxTokens, tokenCounter);
223
246
  }
224
247
  //#endregion
@@ -481,6 +504,12 @@ function cloneMemory(memory) {
481
504
  updatedAt: new Date(memory.updatedAt)
482
505
  };
483
506
  }
507
+ function toDate(value) {
508
+ return value instanceof Date ? value : new Date(value);
509
+ }
510
+ function toNullableDate(value) {
511
+ return value === null ? null : toDate(value);
512
+ }
484
513
  function rowToMemory(row) {
485
514
  return {
486
515
  id: row.id,
@@ -495,12 +524,12 @@ function rowToMemory(row) {
495
524
  embeddingModel: row.embeddingModel,
496
525
  embeddingDimensions: row.embeddingDimensions,
497
526
  supersededBy: row.supersededBy,
498
- supersededAt: row.supersededAt,
499
- expiresAt: row.expiresAt,
500
- lastAccessedAt: row.lastAccessedAt,
527
+ supersededAt: toNullableDate(row.supersededAt),
528
+ expiresAt: toNullableDate(row.expiresAt),
529
+ lastAccessedAt: toNullableDate(row.lastAccessedAt),
501
530
  accessCount: row.accessCount,
502
- createdAt: row.createdAt,
503
- updatedAt: row.updatedAt
531
+ createdAt: toDate(row.createdAt),
532
+ updatedAt: toDate(row.updatedAt)
504
533
  };
505
534
  }
506
535
  async function embedOne(embedder, text, options = {}) {
@@ -519,6 +548,22 @@ async function embedOne(embedder, text, options = {}) {
519
548
  if (embedding.length !== embedder.dimensions) throw new MnemocyteError("Embedder returned an embedding with unexpected dimensions.", "EMBEDDING");
520
549
  return embedding;
521
550
  }
551
+ async function embedMany(embedder, texts, options = {}) {
552
+ if (texts.length === 0) return [];
553
+ let embeddings;
554
+ try {
555
+ embeddings = await withResilience((signal) => signal === void 0 ? embedder.embed(texts) : embedder.embed(texts, { signal }), {
556
+ ...options.signal === void 0 ? {} : { signal: options.signal },
557
+ ...options.resilience === void 0 ? {} : { resilience: options.resilience }
558
+ });
559
+ } catch (error) {
560
+ if (error instanceof MnemocyteError && (error.code === "TIMEOUT" || error.code === "ABORTED")) throw error;
561
+ throw new MnemocyteError("Failed to embed texts.", "EMBEDDING", error);
562
+ }
563
+ if (embeddings.length !== texts.length) throw new MnemocyteError(`Embedder returned ${embeddings.length} embeddings for ${texts.length} texts.`, "EMBEDDING");
564
+ for (const embedding of embeddings) if (embedding.length !== embedder.dimensions) throw new MnemocyteError("Embedder returned an embedding with unexpected dimensions.", "EMBEDDING");
565
+ return embeddings;
566
+ }
522
567
  function validateRememberInput(input) {
523
568
  assertNonEmptyString(input.entityId, "entityId");
524
569
  assertNonEmptyString(input.content, "content");
@@ -657,12 +702,18 @@ function cosineSimilarity(a, b) {
657
702
  if (aMagnitude === 0 || bMagnitude === 0) return 0;
658
703
  return dot / (Math.sqrt(aMagnitude) * Math.sqrt(bMagnitude));
659
704
  }
660
- function lexicalScore(content, query) {
661
- const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
705
+ function tokenizeQuery(query) {
706
+ return query.toLowerCase().split(/\s+/).filter(Boolean);
707
+ }
708
+ function lexicalScoreWithTerms(content, terms) {
662
709
  if (terms.length === 0) return 0;
663
710
  const lowerContent = content.toLowerCase();
664
711
  return terms.filter((term) => lowerContent.includes(term)).length / terms.length;
665
712
  }
713
+ function createLexicalScorer(query) {
714
+ const terms = tokenizeQuery(query);
715
+ return (content) => lexicalScoreWithTerms(content, terms);
716
+ }
666
717
  function clampScore(value) {
667
718
  if (!Number.isFinite(value)) return 0;
668
719
  return Math.max(0, Math.min(1, value));
@@ -683,6 +734,13 @@ function normalizeWeights(weights) {
683
734
  importance: merged.importance / total
684
735
  };
685
736
  }
737
+ function createScoringConfig(config) {
738
+ return {
739
+ weights: normalizeWeights(config?.weights),
740
+ recencyHalfLifeDays: config?.recencyHalfLifeDays ?? DEFAULT_RECENCY_HALF_LIFE_DAYS,
741
+ accessSaturation: config?.accessSaturation ?? DEFAULT_ACCESS_SATURATION
742
+ };
743
+ }
686
744
  function recencyScore(createdAt, halfLifeDays) {
687
745
  const ageMs = Date.now() - createdAt.getTime();
688
746
  const ageDays = Math.max(0, ageMs / 864e5);
@@ -692,11 +750,11 @@ function accessScore(accessCount, saturation) {
692
750
  if (accessCount <= 0) return 0;
693
751
  return Math.min(1, Math.log1p(accessCount) / Math.log1p(saturation));
694
752
  }
695
- function toScoredMemory(memory, vector, lexical, input, config) {
696
- const weights = normalizeWeights(config?.weights);
697
- const recency = recencyScore(memory.createdAt, config?.recencyHalfLifeDays ?? DEFAULT_RECENCY_HALF_LIFE_DAYS);
753
+ function toScoredMemoryWithConfig(memory, vector, lexical, input, config) {
754
+ const { weights } = config;
755
+ const recency = recencyScore(memory.createdAt, config.recencyHalfLifeDays);
698
756
  const confidence = clampScore(memory.confidence);
699
- const access = accessScore(memory.accessCount, config?.accessSaturation ?? DEFAULT_ACCESS_SATURATION);
757
+ const access = accessScore(memory.accessCount, config.accessSaturation);
700
758
  const importance = IMPORTANCE_SCORES[memory.importance];
701
759
  const score = Math.max(0, Math.min(1, clampScore(vector) * weights.vector + clampScore(lexical) * weights.lexical + recency * weights.recency + confidence * weights.confidence + access * weights.access + importance * weights.importance));
702
760
  return {
@@ -793,13 +851,50 @@ function createInMemoryClient(config) {
793
851
  count: 1
794
852
  }));
795
853
  }
796
- return {
854
+ const client = {
797
855
  remember,
798
856
  async rememberMany(inputs) {
799
857
  return observe(config, "in-memory", "rememberMany", { count: inputs.length }, async () => {
800
858
  assertOpen();
859
+ if (inputs.length === 0) return [];
860
+ for (const input of inputs) validateRememberInput(input);
861
+ const embeddings = await embedMany(config.embedder, inputs.map((i) => i.content), {
862
+ ...inputs[0]?.signal === void 0 ? {} : { signal: inputs[0].signal },
863
+ ...config.provider === void 0 ? {} : { resilience: config.provider }
864
+ });
865
+ const now = /* @__PURE__ */ new Date();
801
866
  const result = [];
802
- for (const input of inputs) result.push(await remember(input));
867
+ for (let i = 0; i < inputs.length; i++) {
868
+ const input = inputs[i];
869
+ const memory = {
870
+ id: createId(),
871
+ entityId: input.entityId,
872
+ content: input.content,
873
+ type: input.type ?? "fact",
874
+ importance: input.importance ?? "normal",
875
+ tags: normalizeTags(input.tags),
876
+ source: input.source ?? null,
877
+ metadata: input.metadata ?? {},
878
+ confidence: input.confidence ?? 1,
879
+ embeddingModel: config.embedder.model,
880
+ embeddingDimensions: config.embedder.dimensions,
881
+ supersededBy: null,
882
+ supersededAt: null,
883
+ expiresAt: input.expiresAt ?? null,
884
+ lastAccessedAt: null,
885
+ accessCount: 0,
886
+ createdAt: now,
887
+ updatedAt: now,
888
+ embedding: embeddings[i]
889
+ };
890
+ memories.set(memory.id, memory);
891
+ recordAudit(memory.entityId, "memory.created", {
892
+ memoryId: memory.id,
893
+ type: memory.type,
894
+ importance: memory.importance
895
+ });
896
+ result.push(cloneMemory(memory));
897
+ }
803
898
  return result;
804
899
  }, (result) => ({ count: result.length }));
805
900
  },
@@ -816,7 +911,9 @@ function createInMemoryClient(config) {
816
911
  ...config.provider === void 0 ? {} : { resilience: config.provider }
817
912
  });
818
913
  const now = /* @__PURE__ */ new Date();
819
- const scored = Array.from(memories.values()).filter((memory) => matchesRecallFilter(memory, input, now)).map((memory) => toScoredMemory(memory, Math.max(0, cosineSimilarity(memory.embedding, queryEmbedding)), lexicalScore(memory.content, input.query), input, config.retrieval)).filter((memory) => memory.score >= minScore).sort((a, b) => b.score - a.score).slice(0, limit);
914
+ const scoreLexical = createLexicalScorer(input.query);
915
+ const scoringConfig = createScoringConfig(config.retrieval);
916
+ const scored = Array.from(memories.values()).filter((memory) => matchesRecallFilter(memory, input, now)).map((memory) => toScoredMemoryWithConfig(memory, Math.max(0, cosineSimilarity(memory.embedding, queryEmbedding)), scoreLexical(memory.content), input, scoringConfig)).filter((memory) => memory.score >= minScore).sort((a, b) => b.score - a.score).slice(0, limit);
820
917
  for (const memory of scored) {
821
918
  const stored = memories.get(memory.id);
822
919
  if (stored) {
@@ -835,7 +932,7 @@ function createInMemoryClient(config) {
835
932
  input,
836
933
  recall: (contextInput) => {
837
934
  const recallInput = contextInputToRecallInput(contextInput);
838
- return this.recall(input.signal === void 0 ? recallInput : {
935
+ return client.recall(input.signal === void 0 ? recallInput : {
839
936
  ...recallInput,
840
937
  signal: input.signal
841
938
  });
@@ -969,6 +1066,7 @@ function createInMemoryClient(config) {
969
1066
  });
970
1067
  }
971
1068
  };
1069
+ return client;
972
1070
  function createExperimental() {
973
1071
  return { async consolidate(input) {
974
1072
  return observe(config, "in-memory", "consolidate", {
@@ -1020,7 +1118,7 @@ function createInMemoryClient(config) {
1020
1118
  //#region src/db/queries/events.ts
1021
1119
  async function insertEvent(db, row) {
1022
1120
  const inserted = (await db.insert(eventsTable).values(row).returning())[0];
1023
- if (!inserted) throw new Error("Event insert returned no rows.");
1121
+ if (!inserted) throw new MnemocyteError("Event insert returned no rows.", "DB");
1024
1122
  return inserted;
1025
1123
  }
1026
1124
  async function listEvents(db, filter) {
@@ -1035,19 +1133,70 @@ function formatVectorComponent(value) {
1035
1133
  if (!Number.isFinite(value)) throw new Error("Vector values must be finite numbers.");
1036
1134
  return Object.is(value, -0) ? "0" : value.toFixed(17);
1037
1135
  }
1038
- function filterConditions(filter) {
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);
1136
+ function timestampParam(value) {
1137
+ return sql`${value.toISOString()}::timestamptz`;
1138
+ }
1139
+ function textArrayLiteral(values) {
1140
+ return sql`ARRAY[${sql.join(values.map((value) => sql`${value}`), sql`, `)}]::text[]`;
1141
+ }
1142
+ function tagsContainAll(tags) {
1143
+ return tags && tags.length > 0 ? sql`${memoriesTable.tags} @> ${textArrayLiteral(tags)}` : void 0;
1144
+ }
1145
+ function rawTagsFilter(tags) {
1146
+ return tags && tags.length > 0 ? sql`AND tags @> ${textArrayLiteral(tags)}` : sql``;
1147
+ }
1148
+ function duplicateTagsFilter(tags) {
1149
+ return tags && tags.length > 0 ? sql`AND a.tags @> ${textArrayLiteral(tags)} AND b.tags @> ${textArrayLiteral(tags)}` : sql``;
1150
+ }
1151
+ function memoryStatsCountFields(now) {
1152
+ return {
1153
+ memoryCount: count(),
1154
+ activeMemoryCount: sql`
1155
+ (count(*) FILTER (
1156
+ WHERE ${memoriesTable.supersededBy} IS NULL
1157
+ AND (${memoriesTable.expiresAt} IS NULL OR ${memoriesTable.expiresAt} > ${timestampParam(now)})
1158
+ ))::int
1159
+ `.mapWith(Number),
1160
+ expiredMemoryCount: sql`
1161
+ (count(*) FILTER (
1162
+ WHERE ${memoriesTable.expiresAt} IS NOT NULL
1163
+ AND ${memoriesTable.expiresAt} <= ${timestampParam(now)}
1164
+ ))::int
1165
+ `.mapWith(Number),
1166
+ supersededMemoryCount: sql`(count(*) FILTER (WHERE ${memoriesTable.supersededBy} IS NOT NULL))::int`.mapWith(Number)
1167
+ };
1168
+ }
1169
+ function emptyMemoryStatsCounts() {
1170
+ return {
1171
+ memoryCount: 0,
1172
+ activeMemoryCount: 0,
1173
+ expiredMemoryCount: 0,
1174
+ supersededMemoryCount: 0
1175
+ };
1040
1176
  }
1041
1177
  async function insertMemory(db, row) {
1042
1178
  const inserted = (await db.insert(memoriesTable).values(row).returning())[0];
1043
- if (!inserted) throw new Error("Memory insert returned no rows.");
1179
+ if (!inserted) throw new MnemocyteError("Memory insert returned no rows.", "DB");
1044
1180
  return inserted;
1045
1181
  }
1182
+ async function insertMemories(db, rows) {
1183
+ if (rows.length === 0) return [];
1184
+ return db.insert(memoriesTable).values(rows).returning();
1185
+ }
1046
1186
  async function getMemoryById(db, entityId, memoryId) {
1047
1187
  return (await db.select().from(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), eq(memoriesTable.id, memoryId))).limit(1))[0] ?? null;
1048
1188
  }
1049
- async function listMemories(db, filter) {
1050
- return db.select().from(memoriesTable).where(filterConditions(filter));
1189
+ async function getEntityMemoryStatsCounts(db, entityId, now) {
1190
+ return (await db.select(memoryStatsCountFields(now)).from(memoriesTable).where(eq(memoriesTable.entityId, entityId)))[0] ?? emptyMemoryStatsCounts();
1191
+ }
1192
+ async function getGlobalMemoryStatsCounts(db, now) {
1193
+ return (await db.select({
1194
+ entityCount: countDistinct(memoriesTable.entityId),
1195
+ ...memoryStatsCountFields(now)
1196
+ }).from(memoriesTable))[0] ?? {
1197
+ entityCount: 0,
1198
+ ...emptyMemoryStatsCounts()
1199
+ };
1051
1200
  }
1052
1201
  async function deleteMemory(db, entityId, memoryId) {
1053
1202
  return (await db.delete(memoriesTable).where(and(eq(memoriesTable.entityId, entityId), eq(memoriesTable.id, memoryId))).returning({ id: memoriesTable.id })).length > 0;
@@ -1056,10 +1205,10 @@ async function deleteMemoriesForEntity(db, entityId) {
1056
1205
  return (await db.delete(memoriesTable).where(eq(memoriesTable.entityId, entityId)).returning({ id: memoriesTable.id })).length;
1057
1206
  }
1058
1207
  function pruneConditions(filter) {
1059
- 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, filter.tags && filter.tags.length > 0 ? sql`${memoriesTable.tags} @> ${filter.tags}` : void 0, filter.maxImportanceLevels && filter.maxImportanceLevels.length > 0 ? inArray(memoriesTable.importance, [...filter.maxImportanceLevels]) : void 0);
1208
+ 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);
1060
1209
  }
1061
1210
  async function countPruneMatches(db, filter) {
1062
- return (await db.select({ count: sql`count(*)::int` }).from(memoriesTable).where(pruneConditions(filter)))[0]?.count ?? 0;
1211
+ return db.$count(memoriesTable, pruneConditions(filter));
1063
1212
  }
1064
1213
  async function pruneMemories(db, filter) {
1065
1214
  return (await db.delete(memoriesTable).where(pruneConditions(filter)).returning({ id: memoriesTable.id })).length;
@@ -1117,7 +1266,7 @@ async function findDuplicatePairs(db, input) {
1117
1266
  ${input.includeSuperseded ? sql`` : sql`AND a.superseded_by IS NULL AND b.superseded_by IS NULL`}
1118
1267
  ${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
1268
  ${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``}
1269
+ ${duplicateTagsFilter(input.tags)}
1121
1270
  AND 1 - (a.embedding <=> b.embedding) >= ${input.threshold}
1122
1271
  ORDER BY "similarity" DESC
1123
1272
  LIMIT ${input.limit}
@@ -1195,13 +1344,22 @@ async function setMemoryTags(db, params) {
1195
1344
  }).where(and(eq(memoriesTable.entityId, params.entityId), eq(memoriesTable.id, params.memoryId)));
1196
1345
  }
1197
1346
  async function markMemoryAccessed(db, memoryIds) {
1347
+ const now = /* @__PURE__ */ new Date();
1198
1348
  if (memoryIds.length === 0) return;
1199
1349
  await db.update(memoriesTable).set({
1200
- lastAccessedAt: /* @__PURE__ */ new Date(),
1350
+ lastAccessedAt: now,
1201
1351
  accessCount: sql`${memoriesTable.accessCount} + 1`,
1202
- updatedAt: /* @__PURE__ */ new Date()
1352
+ updatedAt: now
1203
1353
  }).where(inArray(memoriesTable.id, [...memoryIds]));
1204
1354
  }
1355
+ async function getMemoryEmbeddings(db, memoryIds) {
1356
+ if (memoryIds.length === 0) return /* @__PURE__ */ new Map();
1357
+ const rows = await db.select({
1358
+ id: memoriesTable.id,
1359
+ embedding: memoriesTable.embedding
1360
+ }).from(memoriesTable).where(inArray(memoriesTable.id, [...memoryIds]));
1361
+ return new Map(rows.flatMap((row) => row.embedding ? [[row.id, row.embedding]] : []));
1362
+ }
1205
1363
  async function vectorSearch(db, input) {
1206
1364
  const embedding = vectorLiteral(input.embedding);
1207
1365
  const minScore = input.minScore ?? 0;
@@ -1216,7 +1374,6 @@ async function vectorSearch(db, input) {
1216
1374
  source,
1217
1375
  metadata,
1218
1376
  confidence,
1219
- embedding,
1220
1377
  embedding_model AS "embeddingModel",
1221
1378
  embedding_dimensions AS "embeddingDimensions",
1222
1379
  superseded_by AS "supersededBy",
@@ -1234,9 +1391,9 @@ async function vectorSearch(db, input) {
1234
1391
  ${input.includeSuperseded ? sql`` : sql`AND superseded_by IS NULL`}
1235
1392
  ${input.includeExpired ? sql`` : sql`AND (expires_at IS NULL OR expires_at > now())`}
1236
1393
  ${input.types && input.types.length > 0 ? sql`AND type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)})` : sql``}
1237
- ${input.tags && input.tags.length > 0 ? sql`AND tags @> ${input.tags}` : sql``}
1238
- ${input.before ? sql`AND created_at < ${input.before}` : sql``}
1239
- ${input.after ? sql`AND created_at > ${input.after}` : sql``}
1394
+ ${rawTagsFilter(input.tags)}
1395
+ ${input.before ? sql`AND created_at < ${timestampParam(input.before)}` : sql``}
1396
+ ${input.after ? sql`AND created_at > ${timestampParam(input.after)}` : sql``}
1240
1397
  AND 1 - (embedding <=> ${embedding}::vector) >= ${minScore}
1241
1398
  ORDER BY embedding <=> ${embedding}::vector
1242
1399
  LIMIT ${input.limit}
@@ -1254,7 +1411,6 @@ async function lexicalSearch(db, input) {
1254
1411
  source,
1255
1412
  metadata,
1256
1413
  confidence,
1257
- embedding,
1258
1414
  embedding_model AS "embeddingModel",
1259
1415
  embedding_dimensions AS "embeddingDimensions",
1260
1416
  superseded_by AS "supersededBy",
@@ -1271,9 +1427,9 @@ async function lexicalSearch(db, input) {
1271
1427
  ${input.includeSuperseded ? sql`` : sql`AND superseded_by IS NULL`}
1272
1428
  ${input.includeExpired ? sql`` : sql`AND (expires_at IS NULL OR expires_at > now())`}
1273
1429
  ${input.types && input.types.length > 0 ? sql`AND type IN (${sql.join(input.types.map((type) => sql`${type}`), sql`, `)})` : sql``}
1274
- ${input.tags && input.tags.length > 0 ? sql`AND tags @> ${input.tags}` : sql``}
1275
- ${input.before ? sql`AND created_at < ${input.before}` : sql``}
1276
- ${input.after ? sql`AND created_at > ${input.after}` : sql``}
1430
+ ${rawTagsFilter(input.tags)}
1431
+ ${input.before ? sql`AND created_at < ${timestampParam(input.before)}` : sql``}
1432
+ ${input.after ? sql`AND created_at > ${timestampParam(input.after)}` : sql``}
1277
1433
  AND to_tsvector('english', content) @@ websearch_to_tsquery('english', ${input.query})
1278
1434
  ORDER BY "lexicalScore" DESC
1279
1435
  LIMIT ${input.limit}
@@ -1288,6 +1444,8 @@ async function hybridRecall(input) {
1288
1444
  });
1289
1445
  const candidateMultiplier = input.retrieval?.candidateMultiplier ?? 3;
1290
1446
  const candidateLimit = Math.max(input.limit, input.limit * candidateMultiplier);
1447
+ const scoreLexical = createLexicalScorer(input.input.query);
1448
+ const scoringConfig = createScoringConfig(input.retrieval);
1291
1449
  const [vectorRows, lexicalRows] = await Promise.all([vectorSearch(input.db, {
1292
1450
  ...input.input,
1293
1451
  embedding: queryEmbedding,
@@ -1301,18 +1459,24 @@ async function hybridRecall(input) {
1301
1459
  for (const row of vectorRows) merged.set(row.id, {
1302
1460
  row,
1303
1461
  vectorScore: row.vectorScore,
1304
- lexicalScore: 0
1462
+ lexicalScore: scoreLexical(row.content)
1305
1463
  });
1464
+ const lexicalOnlyRows = lexicalRows.filter((row) => !merged.has(row.id));
1465
+ const lexicalOnlyEmbeddings = await getMemoryEmbeddings(input.db, lexicalOnlyRows.map((row) => row.id));
1306
1466
  for (const row of lexicalRows) {
1307
1467
  const existing = merged.get(row.id);
1308
1468
  if (existing) existing.lexicalScore = row.lexicalScore;
1309
- else merged.set(row.id, {
1310
- row,
1311
- vectorScore: 0,
1312
- lexicalScore: row.lexicalScore
1313
- });
1469
+ else {
1470
+ const embedding = lexicalOnlyEmbeddings.get(row.id);
1471
+ const sim = embedding ? cosineSimilarity(embedding, queryEmbedding) : 0;
1472
+ merged.set(row.id, {
1473
+ row,
1474
+ vectorScore: Math.max(0, sim),
1475
+ lexicalScore: row.lexicalScore
1476
+ });
1477
+ }
1314
1478
  }
1315
- const scored = Array.from(merged.values()).map((entry) => toScoredMemory(rowToMemory(entry.row), Math.max(0, entry.vectorScore), Math.max(0, entry.lexicalScore), input.input, input.retrieval)).filter((memory) => memory.score >= input.minScore).sort((a, b) => b.score - a.score).slice(0, input.limit);
1479
+ const scored = Array.from(merged.values()).map((entry) => toScoredMemoryWithConfig(rowToMemory(entry.row), Math.max(0, entry.vectorScore), Math.max(0, entry.lexicalScore), input.input, scoringConfig)).filter((memory) => memory.score >= input.minScore).sort((a, b) => b.score - a.score).slice(0, input.limit);
1316
1480
  await markMemoryAccessed(input.db, scored.map((memory) => memory.id));
1317
1481
  return scored;
1318
1482
  }
@@ -1406,14 +1570,46 @@ function createPostgresClient(config, handle) {
1406
1570
  count: 1
1407
1571
  }));
1408
1572
  }
1409
- return {
1573
+ const client = {
1410
1574
  remember,
1411
1575
  async rememberMany(inputs) {
1412
1576
  return observe(config, "postgres", "rememberMany", { count: inputs.length }, async () => {
1413
1577
  assertOpen();
1414
- const result = [];
1415
- for (const input of inputs) result.push(await remember(input));
1416
- return result;
1578
+ if (inputs.length === 0) return [];
1579
+ for (const input of inputs) validateRememberInput(input);
1580
+ const embeddings = await embedMany(config.embedder, inputs.map((i) => i.content), {
1581
+ ...inputs[0]?.signal === void 0 ? {} : { signal: inputs[0].signal },
1582
+ ...config.provider === void 0 ? {} : { resilience: config.provider }
1583
+ });
1584
+ const now = /* @__PURE__ */ new Date();
1585
+ const rows = inputs.map((input, idx) => ({
1586
+ id: createId(),
1587
+ entityId: input.entityId,
1588
+ content: input.content,
1589
+ type: input.type ?? "fact",
1590
+ importance: input.importance ?? "normal",
1591
+ tags: normalizeTags(input.tags),
1592
+ source: input.source ?? null,
1593
+ metadata: input.metadata ?? {},
1594
+ confidence: input.confidence ?? 1,
1595
+ embedding: embeddings[idx],
1596
+ embeddingModel: config.embedder.model,
1597
+ embeddingDimensions: config.embedder.dimensions,
1598
+ supersededBy: null,
1599
+ supersededAt: null,
1600
+ expiresAt: input.expiresAt ?? null,
1601
+ lastAccessedAt: null,
1602
+ accessCount: 0,
1603
+ createdAt: now,
1604
+ updatedAt: now
1605
+ }));
1606
+ const memories = (await insertMemories(handle.db, rows)).map(rowToMemory);
1607
+ for (const memory of memories) recordAudit(memory.entityId, "memory.created", {
1608
+ memoryId: memory.id,
1609
+ type: memory.type,
1610
+ importance: memory.importance
1611
+ });
1612
+ return memories;
1417
1613
  }, (result) => ({ count: result.length }));
1418
1614
  },
1419
1615
  async recall(input) {
@@ -1443,7 +1639,7 @@ function createPostgresClient(config, handle) {
1443
1639
  input,
1444
1640
  recall: (contextInput) => {
1445
1641
  const recallInput = contextInputToRecallInput(contextInput);
1446
- return this.recall(input.signal === void 0 ? recallInput : {
1642
+ return client.recall(input.signal === void 0 ? recallInput : {
1447
1643
  ...recallInput,
1448
1644
  signal: input.signal
1449
1645
  });
@@ -1529,27 +1725,13 @@ function createPostgresClient(config, handle) {
1529
1725
  assertOpen();
1530
1726
  const now = /* @__PURE__ */ new Date();
1531
1727
  if (input?.entityId) {
1532
- const selected = await listMemories(handle.db, {
1533
- entityId: input.entityId,
1534
- includeExpired: true,
1535
- includeSuperseded: true
1536
- });
1728
+ const counts = await getEntityMemoryStatsCounts(handle.db, input.entityId, now);
1537
1729
  return {
1538
1730
  entityId: input.entityId,
1539
- memoryCount: selected.length,
1540
- activeMemoryCount: selected.filter((memory) => !isExpired(rowToMemory(memory), now) && memory.supersededBy === null).length,
1541
- expiredMemoryCount: selected.filter((memory) => isExpired(rowToMemory(memory), now)).length,
1542
- supersededMemoryCount: selected.filter((memory) => memory.supersededBy !== null).length
1731
+ ...counts
1543
1732
  };
1544
1733
  }
1545
- const allMemories = await handle.db.select().from(memoriesTable);
1546
- return {
1547
- entityCount: new Set(allMemories.map((memory) => memory.entityId)).size,
1548
- memoryCount: allMemories.length,
1549
- activeMemoryCount: allMemories.filter((memory) => !isExpired(rowToMemory(memory), now) && memory.supersededBy === null).length,
1550
- expiredMemoryCount: allMemories.filter((memory) => isExpired(rowToMemory(memory), now)).length,
1551
- supersededMemoryCount: allMemories.filter((memory) => memory.supersededBy !== null).length
1552
- };
1734
+ return await getGlobalMemoryStatsCounts(handle.db, now);
1553
1735
  });
1554
1736
  },
1555
1737
  experimental: createExperimental(),
@@ -1560,6 +1742,7 @@ function createPostgresClient(config, handle) {
1560
1742
  });
1561
1743
  }
1562
1744
  };
1745
+ return client;
1563
1746
  function createExperimental() {
1564
1747
  return { async consolidate(input) {
1565
1748
  return observe(config, "postgres", "consolidate", {
@@ -1581,27 +1764,39 @@ function createPostgresClient(config, handle) {
1581
1764
  supersededIds: []
1582
1765
  };
1583
1766
  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, {
1767
+ const { newSupersededIds } = await handle.db.transaction(async (tx) => {
1768
+ const updated = await markMemoriesSuperseded(tx, {
1769
+ survivorId: survivor.id,
1599
1770
  entityId: input.entityId,
1600
- memoryId: survivor.id,
1601
- tags: [...mergedTags],
1771
+ ids: losers.map((loser) => loser.id),
1602
1772
  now
1603
1773
  });
1604
- }
1774
+ const newSupersededIds = updated.map((row) => row.id);
1775
+ if (config.audit?.enabled === true) for (const id of newSupersededIds) await insertEvent(tx, {
1776
+ id: createEventId(),
1777
+ entityId: input.entityId,
1778
+ description: "memory.superseded",
1779
+ metadata: {
1780
+ memoryId: id,
1781
+ supersededBy: survivor.id
1782
+ },
1783
+ timestamp: now
1784
+ });
1785
+ if (input.mergeTags !== false && updated.length > 0) {
1786
+ const mergedTags = new Set(survivor.tags);
1787
+ for (const row of updated) for (const tag of row.tags) mergedTags.add(tag);
1788
+ if (mergedTags.size !== survivor.tags.length) await setMemoryTags(tx, {
1789
+ entityId: input.entityId,
1790
+ memoryId: survivor.id,
1791
+ tags: [...mergedTags],
1792
+ now
1793
+ });
1794
+ }
1795
+ return {
1796
+ updated,
1797
+ newSupersededIds
1798
+ };
1799
+ });
1605
1800
  return {
1606
1801
  survivorId: survivor.id,
1607
1802
  supersededCount: newSupersededIds.length,