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/README.md +137 -103
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +283 -88
- package/dist/index.mjs.map +1 -1
- package/migrations/0000_initial.sql +33 -44
- package/migrations/meta/0000_snapshot.json +284 -0
- package/migrations/meta/_journal.json +13 -0
- package/package.json +23 -19
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) => [
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
661
|
-
|
|
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
|
|
696
|
-
const weights =
|
|
697
|
-
const recency = recencyScore(memory.createdAt, config
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1039
|
-
return
|
|
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
|
|
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
|
|
1050
|
-
return db.select().from(memoriesTable).where(
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
1350
|
+
lastAccessedAt: now,
|
|
1201
1351
|
accessCount: sql`${memoriesTable.accessCount} + 1`,
|
|
1202
|
-
updatedAt:
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
1310
|
-
row
|
|
1311
|
-
|
|
1312
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
1415
|
-
for (const input of inputs)
|
|
1416
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1585
|
-
|
|
1586
|
-
|
|
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
|
-
|
|
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,
|