@memrosetta/core 0.2.18 → 0.2.19
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.d.ts +38 -4
- package/dist/index.js +130 -19
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -160,6 +160,21 @@ declare function rrfMergeWeighted(ftsResults: readonly {
|
|
|
160
160
|
readonly memory: Memory;
|
|
161
161
|
readonly rank: number;
|
|
162
162
|
}[], k?: number, limit?: number, ftsWeight?: number, vecWeight?: number): readonly SearchResult[];
|
|
163
|
+
/**
|
|
164
|
+
* Generative Agents-inspired 3-factor reranking.
|
|
165
|
+
* final_score = w_recency * recency + w_importance * importance + w_relevance * relevance
|
|
166
|
+
*
|
|
167
|
+
* - recency: exponential decay from learnedAt (0.995^hours)
|
|
168
|
+
* - importance: memory.salience (0-1)
|
|
169
|
+
* - relevance: original search score (from FTS/vector/RRF)
|
|
170
|
+
*
|
|
171
|
+
* All three are min-max normalized before combining.
|
|
172
|
+
*/
|
|
173
|
+
declare function applyThreeFactorReranking(results: readonly SearchResult[], weights?: {
|
|
174
|
+
recency?: number;
|
|
175
|
+
importance?: number;
|
|
176
|
+
relevance?: number;
|
|
177
|
+
}): readonly SearchResult[];
|
|
163
178
|
/**
|
|
164
179
|
* Remove duplicate search results based on content identity.
|
|
165
180
|
* Keeps the first (highest-scored) occurrence when duplicates exist.
|
|
@@ -231,6 +246,14 @@ declare class SqliteMemoryEngine implements IMemoryEngine {
|
|
|
231
246
|
* swallowed so that memory storage is never blocked.
|
|
232
247
|
*/
|
|
233
248
|
private checkContradictions;
|
|
249
|
+
/**
|
|
250
|
+
* Check for near-duplicate memories after storing.
|
|
251
|
+
* Uses direct cosine similarity (not the combined search score) to avoid
|
|
252
|
+
* false positives from the multi-factor reranking.
|
|
253
|
+
* If cosine similarity > 0.95, auto-create an 'updates' relation (new supersedes old).
|
|
254
|
+
* Only runs for single store() calls, not storeBatch() (too slow for bulk).
|
|
255
|
+
*/
|
|
256
|
+
private checkDuplicates;
|
|
234
257
|
private ensureInitialized;
|
|
235
258
|
}
|
|
236
259
|
declare function createEngine(options: SqliteEngineOptions): SqliteMemoryEngine;
|
|
@@ -257,19 +280,30 @@ declare function createEngine(options: SqliteEngineOptions): SqliteMemoryEngine;
|
|
|
257
280
|
* @returns activation score in [0, 1]
|
|
258
281
|
*/
|
|
259
282
|
declare function computeActivation(accessTimestamps: readonly string[], salience: number, now?: Date): number;
|
|
283
|
+
/**
|
|
284
|
+
* Ebbinghaus forgetting curve: R = e^(-t/S)
|
|
285
|
+
* R = retention (0-1)
|
|
286
|
+
* t = days since last access
|
|
287
|
+
* S = strength (access_count, minimum 1)
|
|
288
|
+
*
|
|
289
|
+
* Works with existing fields (access_count, last_accessed_at) without
|
|
290
|
+
* needing an access history table like ACT-R does.
|
|
291
|
+
*/
|
|
292
|
+
declare function computeEbbinghaus(accessCount: number, lastAccessedAt: string | null, now?: Date): number;
|
|
260
293
|
|
|
261
294
|
declare const DEFAULT_TIER_CONFIG: TierConfig;
|
|
262
295
|
/**
|
|
263
296
|
* Determine the appropriate tier for a memory based on its properties.
|
|
264
297
|
*
|
|
265
|
-
* - hot: manually promoted memories stay hot
|
|
266
|
-
* - warm: memories within warmDays of creation
|
|
267
|
-
* - cold: older than warmDays
|
|
298
|
+
* - hot: manually promoted memories stay hot, OR auto-promoted via high access count (>= 10)
|
|
299
|
+
* - warm: memories within warmDays of creation, OR old but with high activation
|
|
300
|
+
* - cold: older than warmDays AND low activation
|
|
268
301
|
*/
|
|
269
302
|
declare function determineTier(memory: {
|
|
270
303
|
readonly learnedAt: string;
|
|
271
304
|
readonly activationScore: number;
|
|
272
305
|
readonly tier: string;
|
|
306
|
+
readonly accessCount?: number;
|
|
273
307
|
}, config?: TierConfig, now?: Date): MemoryTier;
|
|
274
308
|
/**
|
|
275
309
|
* Estimate token count from content length.
|
|
@@ -277,4 +311,4 @@ declare function determineTier(memory: {
|
|
|
277
311
|
*/
|
|
278
312
|
declare function estimateTokens(content: string): number;
|
|
279
313
|
|
|
280
|
-
export { DEFAULT_TIER_CONFIG, type MemoryRow, type PreparedStatements, type RelationStatements, type SchemaOptions, type SearchSqlResult, type SqliteEngineOptions, SqliteMemoryEngine, type VectorSearchResult, applyKeywordBoost, bruteForceVectorSearch, buildFtsQuery, buildSearchSql, computeActivation, createEngine, createPreparedStatements, createRelation, createRelationStatements, deduplicateResults, determineTier, ensureSchema, estimateTokens, extractQueryTokens, ftsSearch, generateMemoryId, getRelationsByMemory, keywordsToString, normalizeScores, nowIso, rowToMemory, rrfMerge, rrfMergeWeighted, searchMemories, serializeEmbedding, storeBatchAsync, storeBatchInTransaction, storeMemory, storeMemoryAsync, stringToKeywords, updateAccessTracking, vectorSearch };
|
|
314
|
+
export { DEFAULT_TIER_CONFIG, type MemoryRow, type PreparedStatements, type RelationStatements, type SchemaOptions, type SearchSqlResult, type SqliteEngineOptions, SqliteMemoryEngine, type VectorSearchResult, applyKeywordBoost, applyThreeFactorReranking, bruteForceVectorSearch, buildFtsQuery, buildSearchSql, computeActivation, computeEbbinghaus, createEngine, createPreparedStatements, createRelation, createRelationStatements, deduplicateResults, determineTier, ensureSchema, estimateTokens, extractQueryTokens, ftsSearch, generateMemoryId, getRelationsByMemory, keywordsToString, normalizeScores, nowIso, rowToMemory, rrfMerge, rrfMergeWeighted, searchMemories, serializeEmbedding, storeBatchAsync, storeBatchInTransaction, storeMemory, storeMemoryAsync, stringToKeywords, updateAccessTracking, vectorSearch };
|
package/dist/index.js
CHANGED
|
@@ -871,14 +871,41 @@ function cosineSimilarity(a, b) {
|
|
|
871
871
|
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
872
872
|
return denom === 0 ? 0 : dot / denom;
|
|
873
873
|
}
|
|
874
|
-
function
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
874
|
+
function applyThreeFactorReranking(results, weights) {
|
|
875
|
+
if (results.length === 0) return results;
|
|
876
|
+
const w = {
|
|
877
|
+
recency: weights?.recency ?? 1,
|
|
878
|
+
importance: weights?.importance ?? 1,
|
|
879
|
+
relevance: weights?.relevance ?? 1
|
|
880
|
+
};
|
|
881
|
+
const now = Date.now();
|
|
882
|
+
const scored = results.map((r) => {
|
|
883
|
+
const hoursSince = (now - new Date(r.memory.learnedAt).getTime()) / (1e3 * 60 * 60);
|
|
884
|
+
const recency = Math.pow(0.995, Math.max(0, hoursSince));
|
|
885
|
+
const importance = r.memory.salience ?? 1;
|
|
886
|
+
const relevance = r.score;
|
|
887
|
+
return { ...r, recency, importance, relevance };
|
|
888
|
+
});
|
|
889
|
+
const NORM_EPSILON = 0.01;
|
|
890
|
+
const safeNormalize = (values) => {
|
|
891
|
+
let min = Infinity;
|
|
892
|
+
let max = -Infinity;
|
|
893
|
+
for (const v of values) {
|
|
894
|
+
if (v < min) min = v;
|
|
895
|
+
if (v > max) max = v;
|
|
896
|
+
}
|
|
897
|
+
const range = max - min;
|
|
898
|
+
if (range < NORM_EPSILON) return values.map(() => 1);
|
|
899
|
+
return values.map((v) => (v - min) / range);
|
|
900
|
+
};
|
|
901
|
+
const recencies = safeNormalize(scored.map((s) => s.recency));
|
|
902
|
+
const importances = safeNormalize(scored.map((s) => s.importance));
|
|
903
|
+
const relevances = safeNormalize(scored.map((s) => s.relevance));
|
|
904
|
+
return scored.map((s, i) => ({
|
|
905
|
+
memory: s.memory,
|
|
906
|
+
score: w.recency * recencies[i] + w.importance * importances[i] + w.relevance * relevances[i],
|
|
907
|
+
matchType: s.matchType
|
|
908
|
+
})).sort((a, b) => b.score - a.score);
|
|
882
909
|
}
|
|
883
910
|
function deduplicateResults(results) {
|
|
884
911
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -929,7 +956,7 @@ function searchMemories(db, query, queryVec, useVecTable = true) {
|
|
|
929
956
|
const ftsResults = ftsSearch(db, query);
|
|
930
957
|
let finalResults;
|
|
931
958
|
if (!queryVec) {
|
|
932
|
-
const weighted =
|
|
959
|
+
const weighted = applyThreeFactorReranking(ftsResults);
|
|
933
960
|
const boosted = applyKeywordBoost(weighted, queryTokens);
|
|
934
961
|
finalResults = deduplicateResults(boosted);
|
|
935
962
|
updateAccessTracking(db, finalResults.map((r) => r.memory.memoryId));
|
|
@@ -955,11 +982,11 @@ function searchMemories(db, query, queryVec, useVecTable = true) {
|
|
|
955
982
|
// Convert distance back to similarity
|
|
956
983
|
matchType: "vector"
|
|
957
984
|
}));
|
|
958
|
-
const weighted =
|
|
985
|
+
const weighted = applyThreeFactorReranking(vecOnly);
|
|
959
986
|
const boosted = applyKeywordBoost(weighted, queryTokens);
|
|
960
987
|
finalResults = deduplicateResults(boosted);
|
|
961
988
|
} else if (vecResults.length === 0) {
|
|
962
|
-
const weighted =
|
|
989
|
+
const weighted = applyThreeFactorReranking(ftsResults);
|
|
963
990
|
const boosted = applyKeywordBoost(weighted, queryTokens);
|
|
964
991
|
finalResults = deduplicateResults(boosted);
|
|
965
992
|
} else {
|
|
@@ -972,7 +999,7 @@ function searchMemories(db, query, queryVec, useVecTable = true) {
|
|
|
972
999
|
score: r.score * (vecIds.has(r.memory.memoryId) ? 1.3 : 1)
|
|
973
1000
|
}));
|
|
974
1001
|
reranked.sort((a, b) => b.score - a.score);
|
|
975
|
-
const weighted =
|
|
1002
|
+
const weighted = applyThreeFactorReranking(reranked);
|
|
976
1003
|
const boosted = applyKeywordBoost(weighted, queryTokens);
|
|
977
1004
|
finalResults = deduplicateResults(boosted);
|
|
978
1005
|
} else {
|
|
@@ -988,7 +1015,7 @@ function searchMemories(db, query, queryVec, useVecTable = true) {
|
|
|
988
1015
|
matchType: "hybrid"
|
|
989
1016
|
});
|
|
990
1017
|
}
|
|
991
|
-
const weighted =
|
|
1018
|
+
const weighted = applyThreeFactorReranking(ftsItems);
|
|
992
1019
|
const boosted = applyKeywordBoost(weighted, queryTokens);
|
|
993
1020
|
finalResults = deduplicateResults(boosted);
|
|
994
1021
|
}
|
|
@@ -1024,6 +1051,16 @@ function computeActivation(accessTimestamps, salience, now) {
|
|
|
1024
1051
|
const activation = sum > 0 ? Math.log(sum) + beta : beta * 0.1;
|
|
1025
1052
|
return sigmoid(activation);
|
|
1026
1053
|
}
|
|
1054
|
+
function computeEbbinghaus(accessCount, lastAccessedAt, now) {
|
|
1055
|
+
const currentTime = now ?? /* @__PURE__ */ new Date();
|
|
1056
|
+
if (!lastAccessedAt) {
|
|
1057
|
+
return 0.1;
|
|
1058
|
+
}
|
|
1059
|
+
const S = Math.max(1, accessCount);
|
|
1060
|
+
const t = (currentTime.getTime() - new Date(lastAccessedAt).getTime()) / MS_PER_DAY;
|
|
1061
|
+
if (t <= 0) return 1;
|
|
1062
|
+
return Math.exp(-t / S);
|
|
1063
|
+
}
|
|
1027
1064
|
function sigmoid(x) {
|
|
1028
1065
|
return 1 / (1 + Math.exp(-x));
|
|
1029
1066
|
}
|
|
@@ -1038,9 +1075,12 @@ var DEFAULT_TIER_CONFIG = {
|
|
|
1038
1075
|
function determineTier(memory, config, now) {
|
|
1039
1076
|
const cfg = config ?? DEFAULT_TIER_CONFIG;
|
|
1040
1077
|
if (memory.tier === "hot") return "hot";
|
|
1078
|
+
const accessCount = memory.accessCount ?? 0;
|
|
1079
|
+
if (accessCount >= 10) return "hot";
|
|
1041
1080
|
const currentTime = now ?? /* @__PURE__ */ new Date();
|
|
1042
1081
|
const age = (currentTime.getTime() - new Date(memory.learnedAt).getTime()) / MS_PER_DAY2;
|
|
1043
1082
|
if (age <= cfg.warmDays) return "warm";
|
|
1083
|
+
if (memory.activationScore >= cfg.coldActivationThreshold) return "warm";
|
|
1044
1084
|
return "cold";
|
|
1045
1085
|
}
|
|
1046
1086
|
function estimateTokens(content) {
|
|
@@ -1048,6 +1088,19 @@ function estimateTokens(content) {
|
|
|
1048
1088
|
}
|
|
1049
1089
|
|
|
1050
1090
|
// src/engine.ts
|
|
1091
|
+
function cosineSim(a, b) {
|
|
1092
|
+
let dot = 0;
|
|
1093
|
+
let normA = 0;
|
|
1094
|
+
let normB = 0;
|
|
1095
|
+
const len = Math.min(a.length, b.length);
|
|
1096
|
+
for (let i = 0; i < len; i++) {
|
|
1097
|
+
dot += a[i] * b[i];
|
|
1098
|
+
normA += a[i] * a[i];
|
|
1099
|
+
normB += b[i] * b[i];
|
|
1100
|
+
}
|
|
1101
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
1102
|
+
return denom === 0 ? 0 : dot / denom;
|
|
1103
|
+
}
|
|
1051
1104
|
var SqliteMemoryEngine = class {
|
|
1052
1105
|
db = null;
|
|
1053
1106
|
stmts = null;
|
|
@@ -1102,14 +1155,26 @@ var SqliteMemoryEngine = class {
|
|
|
1102
1155
|
if (this.options.contradictionDetector && this.options.embedder) {
|
|
1103
1156
|
await this.checkContradictions(memory);
|
|
1104
1157
|
}
|
|
1158
|
+
await this.checkDuplicates(memory);
|
|
1105
1159
|
return memory;
|
|
1106
1160
|
}
|
|
1107
1161
|
async storeBatch(inputs) {
|
|
1108
1162
|
this.ensureInitialized();
|
|
1163
|
+
let memories;
|
|
1109
1164
|
if (this.options.embedder) {
|
|
1110
|
-
|
|
1165
|
+
memories = await storeBatchAsync(this.db, this.stmts, inputs, this.options.embedder);
|
|
1166
|
+
} else {
|
|
1167
|
+
memories = storeBatchInTransaction(this.db, this.stmts, inputs);
|
|
1168
|
+
}
|
|
1169
|
+
if (this.options.contradictionDetector && this.options.embedder && memories.length <= 50) {
|
|
1170
|
+
for (const memory of memories) {
|
|
1171
|
+
try {
|
|
1172
|
+
await this.checkContradictions(memory);
|
|
1173
|
+
} catch {
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1111
1176
|
}
|
|
1112
|
-
return
|
|
1177
|
+
return memories;
|
|
1113
1178
|
}
|
|
1114
1179
|
async getById(memoryId) {
|
|
1115
1180
|
this.ensureInitialized();
|
|
@@ -1315,10 +1380,11 @@ var SqliteMemoryEngine = class {
|
|
|
1315
1380
|
);
|
|
1316
1381
|
const activationTransaction = db.transaction(() => {
|
|
1317
1382
|
for (const mem of memories) {
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1383
|
+
const ebbinghaus = computeEbbinghaus(
|
|
1384
|
+
mem.access_count ?? 0,
|
|
1385
|
+
mem.last_accessed_at ?? null
|
|
1320
1386
|
);
|
|
1321
|
-
const score =
|
|
1387
|
+
const score = ebbinghaus * 0.8 + (mem.salience ?? 1) * 0.2;
|
|
1322
1388
|
updateActivation.run(score, mem.memory_id);
|
|
1323
1389
|
activationUpdated++;
|
|
1324
1390
|
}
|
|
@@ -1336,7 +1402,8 @@ var SqliteMemoryEngine = class {
|
|
|
1336
1402
|
const newTier = determineTier({
|
|
1337
1403
|
learnedAt: mem.learned_at,
|
|
1338
1404
|
activationScore: mem.activation_score ?? 1,
|
|
1339
|
-
tier: mem.tier ?? "warm"
|
|
1405
|
+
tier: mem.tier ?? "warm",
|
|
1406
|
+
accessCount: mem.access_count ?? 0
|
|
1340
1407
|
});
|
|
1341
1408
|
if (newTier !== (mem.tier ?? "warm")) {
|
|
1342
1409
|
updateTier.run(newTier, mem.memory_id);
|
|
@@ -1405,6 +1472,48 @@ var SqliteMemoryEngine = class {
|
|
|
1405
1472
|
} catch {
|
|
1406
1473
|
}
|
|
1407
1474
|
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Check for near-duplicate memories after storing.
|
|
1477
|
+
* Uses direct cosine similarity (not the combined search score) to avoid
|
|
1478
|
+
* false positives from the multi-factor reranking.
|
|
1479
|
+
* If cosine similarity > 0.95, auto-create an 'updates' relation (new supersedes old).
|
|
1480
|
+
* Only runs for single store() calls, not storeBatch() (too slow for bulk).
|
|
1481
|
+
*/
|
|
1482
|
+
async checkDuplicates(newMemory) {
|
|
1483
|
+
if (!this.options.embedder) return;
|
|
1484
|
+
try {
|
|
1485
|
+
const queryVec = await this.options.embedder.embed(newMemory.content);
|
|
1486
|
+
const similar = searchMemories(
|
|
1487
|
+
this.db,
|
|
1488
|
+
{
|
|
1489
|
+
userId: newMemory.userId,
|
|
1490
|
+
query: newMemory.content,
|
|
1491
|
+
limit: 5,
|
|
1492
|
+
filters: { onlyLatest: true }
|
|
1493
|
+
},
|
|
1494
|
+
queryVec,
|
|
1495
|
+
this.vectorEnabled
|
|
1496
|
+
);
|
|
1497
|
+
for (const result of similar.results) {
|
|
1498
|
+
if (result.memory.memoryId === newMemory.memoryId) continue;
|
|
1499
|
+
if (!result.memory.embedding || result.memory.embedding.length === 0) continue;
|
|
1500
|
+
const embB = new Float32Array(result.memory.embedding);
|
|
1501
|
+
const similarity = cosineSim(queryVec, embB);
|
|
1502
|
+
if (similarity > 0.95) {
|
|
1503
|
+
try {
|
|
1504
|
+
await this.relate(
|
|
1505
|
+
newMemory.memoryId,
|
|
1506
|
+
result.memory.memoryId,
|
|
1507
|
+
"updates",
|
|
1508
|
+
`Auto-detected duplicate: cosine similarity ${similarity.toFixed(3)}`
|
|
1509
|
+
);
|
|
1510
|
+
} catch {
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
} catch {
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1408
1517
|
ensureInitialized() {
|
|
1409
1518
|
if (!this.db || !this.stmts || !this.relStmts) {
|
|
1410
1519
|
throw new Error("Engine not initialized. Call initialize() first.");
|
|
@@ -1418,10 +1527,12 @@ export {
|
|
|
1418
1527
|
DEFAULT_TIER_CONFIG,
|
|
1419
1528
|
SqliteMemoryEngine,
|
|
1420
1529
|
applyKeywordBoost,
|
|
1530
|
+
applyThreeFactorReranking,
|
|
1421
1531
|
bruteForceVectorSearch,
|
|
1422
1532
|
buildFtsQuery,
|
|
1423
1533
|
buildSearchSql,
|
|
1424
1534
|
computeActivation,
|
|
1535
|
+
computeEbbinghaus,
|
|
1425
1536
|
createEngine,
|
|
1426
1537
|
createPreparedStatements,
|
|
1427
1538
|
createRelation,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memrosetta/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"better-sqlite3": "^11.0.0",
|
|
21
21
|
"nanoid": "^5.0.0",
|
|
22
22
|
"sqlite-vec": "^0.1.7",
|
|
23
|
-
"@memrosetta/types": "0.2.
|
|
23
|
+
"@memrosetta/types": "0.2.19"
|
|
24
24
|
},
|
|
25
25
|
"optionalDependencies": {
|
|
26
|
-
"@memrosetta/embeddings": "0.2.
|
|
26
|
+
"@memrosetta/embeddings": "0.2.19"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/better-sqlite3": "^7.6.0",
|