@memrosetta/core 0.2.17 → 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 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 applyActivationWeighting(results) {
875
- return results.map((r) => {
876
- const activationWeight = 0.5 + 0.5 * r.memory.activationScore;
877
- return {
878
- ...r,
879
- score: r.score * activationWeight
880
- };
881
- }).sort((a, b) => b.score - a.score);
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 = applyActivationWeighting(ftsResults);
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 = applyActivationWeighting(vecOnly);
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 = applyActivationWeighting(ftsResults);
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 = applyActivationWeighting(reranked);
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 = applyActivationWeighting(ftsItems);
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
- return storeBatchAsync(this.db, this.stmts, inputs, this.options.embedder);
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 storeBatchInTransaction(this.db, this.stmts, inputs);
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 timestamps = [mem.learned_at, mem.last_accessed_at].filter(
1319
- (t) => t != null
1383
+ const ebbinghaus = computeEbbinghaus(
1384
+ mem.access_count ?? 0,
1385
+ mem.last_accessed_at ?? null
1320
1386
  );
1321
- const score = computeActivation(timestamps, mem.salience);
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.17",
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.17"
23
+ "@memrosetta/types": "0.2.19"
24
24
  },
25
25
  "optionalDependencies": {
26
- "@memrosetta/embeddings": "0.2.17"
26
+ "@memrosetta/embeddings": "0.2.19"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/better-sqlite3": "^7.6.0",