@memrosetta/core 0.2.20 → 0.2.22

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
@@ -44,6 +44,8 @@ interface MemoryRow {
44
44
  readonly access_count: number | null;
45
45
  readonly last_accessed_at: string | null;
46
46
  readonly compressed_from: string | null;
47
+ readonly use_count: number | null;
48
+ readonly success_count: number | null;
47
49
  }
48
50
  declare function rowToMemory(row: MemoryRow): Memory;
49
51
  declare function serializeEmbedding(embedding: readonly number[] | Float32Array): Buffer;
@@ -167,6 +169,17 @@ declare function rrfMergeWeighted(ftsResults: readonly {
167
169
  readonly memory: Memory;
168
170
  readonly rank: number;
169
171
  }[], k?: number, limit?: number, ftsWeight?: number, vecWeight?: number): readonly SearchResult[];
172
+ /**
173
+ * Score-level fusion: alpha * normalizedVecSim + (1 - alpha) * normalizedFtsSim.
174
+ *
175
+ * Unlike RRF (which discards score magnitude), this preserves score information.
176
+ * Both FTS and vector scores are min-max normalized within the result set so
177
+ * neither modality dominates due to scale differences.
178
+ *
179
+ * Items found by both sources get the combined score (strongest signal).
180
+ * Items found by only one source get at most alpha or (1-alpha) of max.
181
+ */
182
+ declare function convexCombinationMerge(ftsResults: readonly SearchResult[], vecResults: readonly VectorSearchResult[], alpha?: number, limit?: number): readonly SearchResult[];
170
183
  /**
171
184
  * Generative Agents-inspired 3-factor reranking.
172
185
  * final_score = w_recency * recency + w_importance * importance + w_relevance * relevance
@@ -207,7 +220,7 @@ declare function updateAccessTracking(db: Database.Database, memoryIds: readonly
207
220
  *
208
221
  * When queryVec is not provided, performs FTS-only search (backward compatible).
209
222
  * When queryVec is provided, performs hybrid search combining FTS + vector
210
- * results via Reciprocal Rank Fusion.
223
+ * results via convex combination score fusion.
211
224
  *
212
225
  * Results are weighted by activation score and access tracking is updated.
213
226
  */
@@ -242,6 +255,7 @@ declare class SqliteMemoryEngine implements IMemoryEngine {
242
255
  compress(userId: string): Promise<CompressResult>;
243
256
  maintain(userId: string): Promise<MaintenanceResult>;
244
257
  setTier(memoryId: string, tier: MemoryTier): Promise<void>;
258
+ feedback(memoryId: string, helpful: boolean): Promise<void>;
245
259
  quality(userId: string): Promise<MemoryQuality>;
246
260
  /**
247
261
  * Check the newly stored memory against existing similar memories
@@ -328,4 +342,4 @@ declare function determineTier(memory: {
328
342
  */
329
343
  declare function estimateTokens(content: string): number;
330
344
 
331
- 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, deriveMemoryState, determineTier, ensureSchema, estimateTokens, extractQueryTokens, ftsSearch, generateMemoryId, getRelationsByMemory, keywordsToString, normalizeScores, nowIso, rowToMemory, rrfMerge, rrfMergeWeighted, searchMemories, serializeEmbedding, storeBatchAsync, storeBatchInTransaction, storeMemory, storeMemoryAsync, stringToKeywords, updateAccessTracking, vectorSearch };
345
+ 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, convexCombinationMerge, createEngine, createPreparedStatements, createRelation, createRelationStatements, deduplicateResults, deriveMemoryState, 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
@@ -24,7 +24,9 @@ CREATE TABLE memories (
24
24
  activation_score REAL DEFAULT 1.0,
25
25
  access_count INTEGER DEFAULT 0,
26
26
  last_accessed_at TEXT,
27
- compressed_from TEXT
27
+ compressed_from TEXT,
28
+ use_count INTEGER DEFAULT 0,
29
+ success_count INTEGER DEFAULT 0
28
30
  );
29
31
 
30
32
  CREATE INDEX idx_memories_user_id ON memories(user_id);
@@ -72,6 +74,10 @@ CREATE TRIGGER memories_au AFTER UPDATE ON memories BEGIN
72
74
  INSERT INTO memories_fts(rowid, content, keywords) VALUES (new.id, new.content, new.keywords);
73
75
  END;
74
76
  `;
77
+ var SCHEMA_V5 = `
78
+ ALTER TABLE memories ADD COLUMN use_count INTEGER DEFAULT 0;
79
+ ALTER TABLE memories ADD COLUMN success_count INTEGER DEFAULT 0;
80
+ `;
75
81
  function schemaV2(dim) {
76
82
  return `CREATE VIRTUAL TABLE IF NOT EXISTS vec_memories USING vec0(embedding float[${dim}]);`;
77
83
  }
@@ -106,7 +112,7 @@ function ensureSchema(db, options) {
106
112
  db.exec(schemaV2(dim));
107
113
  version = 2;
108
114
  }
109
- version = 4;
115
+ version = 5;
110
116
  db.prepare("INSERT INTO schema_version (version, embedding_dimension) VALUES (?, ?)").run(version, dim);
111
117
  return;
112
118
  }
@@ -132,6 +138,12 @@ function ensureSchema(db, options) {
132
138
  }
133
139
  db.prepare("UPDATE schema_version SET version = ?").run(4);
134
140
  }
141
+ if (currentVersion < 5) {
142
+ if (currentVersion >= 1) {
143
+ db.exec(SCHEMA_V5);
144
+ }
145
+ db.prepare("UPDATE schema_version SET version = ?").run(5);
146
+ }
135
147
  if (options?.vectorEnabled) {
136
148
  const hasVecTable = db.prepare(
137
149
  "SELECT name FROM sqlite_master WHERE type='table' AND name='vec_memories'"
@@ -213,7 +225,9 @@ function rowToMemory(row) {
213
225
  activationScore: row.activation_score ?? 1,
214
226
  accessCount: row.access_count ?? 0,
215
227
  ...row.last_accessed_at != null ? { lastAccessedAt: row.last_accessed_at } : {},
216
- ...row.compressed_from != null ? { compressedFrom: row.compressed_from } : {}
228
+ ...row.compressed_from != null ? { compressedFrom: row.compressed_from } : {},
229
+ useCount: row.use_count ?? 0,
230
+ successCount: row.success_count ?? 0
217
231
  };
218
232
  }
219
233
  function deserializeEmbedding(buf) {
@@ -229,8 +243,8 @@ function serializeEmbedding(embedding) {
229
243
  function createPreparedStatements(db) {
230
244
  return {
231
245
  insertMemory: db.prepare(`
232
- INSERT INTO memories (memory_id, user_id, namespace, memory_type, content, raw_text, document_date, learned_at, source_id, confidence, salience, is_latest, embedding, keywords, event_date_start, event_date_end, invalidated_at, tier, activation_score, access_count, last_accessed_at, compressed_from)
233
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
246
+ INSERT INTO memories (memory_id, user_id, namespace, memory_type, content, raw_text, document_date, learned_at, source_id, confidence, salience, is_latest, embedding, keywords, event_date_start, event_date_end, invalidated_at, tier, activation_score, access_count, last_accessed_at, compressed_from, use_count, success_count)
247
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
234
248
  `),
235
249
  getById: db.prepare("SELECT * FROM memories WHERE id = ?"),
236
250
  getByMemoryId: db.prepare("SELECT * FROM memories WHERE memory_id = ?"),
@@ -269,8 +283,12 @@ function storeMemory(db, stmts, input) {
269
283
  // access_count
270
284
  null,
271
285
  // last_accessed_at
272
- null
286
+ null,
273
287
  // compressed_from
288
+ 0,
289
+ // use_count
290
+ 0
291
+ // success_count
274
292
  );
275
293
  const row = stmts.getByMemoryId.get(memoryId);
276
294
  return rowToMemory(row);
@@ -312,8 +330,12 @@ async function storeMemoryAsync(db, stmts, input, embedder) {
312
330
  // access_count
313
331
  null,
314
332
  // last_accessed_at
315
- null
333
+ null,
316
334
  // compressed_from
335
+ 0,
336
+ // use_count
337
+ 0
338
+ // success_count
317
339
  );
318
340
  if (embeddingVec && info.lastInsertRowid) {
319
341
  const rowid = Number(info.lastInsertRowid);
@@ -379,8 +401,12 @@ async function storeBatchAsync(db, stmts, inputs, embedder) {
379
401
  // access_count
380
402
  null,
381
403
  // last_accessed_at
382
- null
404
+ null,
383
405
  // compressed_from
406
+ 0,
407
+ // use_count
408
+ 0
409
+ // success_count
384
410
  );
385
411
  if (info.lastInsertRowid) {
386
412
  db.prepare("INSERT INTO vec_memories(rowid, embedding) VALUES (CAST(? AS INTEGER), ?)").run(
@@ -619,7 +645,7 @@ function buildSearchSql(query) {
619
645
  stateConditions.push("(m.is_latest = 1 AND m.invalidated_at IS NULL)");
620
646
  }
621
647
  if (filters.states.includes("superseded")) {
622
- stateConditions.push("(m.is_latest = 0)");
648
+ stateConditions.push("(m.is_latest = 0 AND m.invalidated_at IS NULL)");
623
649
  }
624
650
  if (filters.states.includes("invalidated")) {
625
651
  stateConditions.push("(m.invalidated_at IS NOT NULL)");
@@ -703,7 +729,7 @@ function bruteForceVectorSearch(db, queryVec, userId, limit, filters) {
703
729
  stateConditions.push("(is_latest = 1 AND invalidated_at IS NULL)");
704
730
  }
705
731
  if (filters.states.includes("superseded")) {
706
- stateConditions.push("(is_latest = 0)");
732
+ stateConditions.push("(is_latest = 0 AND invalidated_at IS NULL)");
707
733
  }
708
734
  if (filters.states.includes("invalidated")) {
709
735
  stateConditions.push("(invalidated_at IS NOT NULL)");
@@ -791,7 +817,7 @@ function vectorSearch(db, queryVec, userId, limit, filters, useVecTable = true)
791
817
  stateConditions.push("(is_latest = 1 AND invalidated_at IS NULL)");
792
818
  }
793
819
  if (filters.states.includes("superseded")) {
794
- stateConditions.push("(is_latest = 0)");
820
+ stateConditions.push("(is_latest = 0 AND invalidated_at IS NULL)");
795
821
  }
796
822
  if (filters.states.includes("invalidated")) {
797
823
  stateConditions.push("(invalidated_at IS NOT NULL)");
@@ -908,6 +934,42 @@ function rrfMergeWeighted(ftsResults, vecResults, k = 20, limit = 10, ftsWeight
908
934
  matchType: "hybrid"
909
935
  }));
910
936
  }
937
+ function convexCombinationMerge(ftsResults, vecResults, alpha = 0.5, limit = 10) {
938
+ const vecSims = /* @__PURE__ */ new Map();
939
+ for (const vr of vecResults) {
940
+ vecSims.set(vr.memory.memoryId, {
941
+ sim: 1 - vr.distance,
942
+ memory: vr.memory
943
+ });
944
+ }
945
+ const vecValues = [...vecSims.values()].map((v) => v.sim);
946
+ const vecMin = vecValues.length > 0 ? Math.min(...vecValues) : 0;
947
+ const vecMax = vecValues.length > 0 ? Math.max(...vecValues) : 1;
948
+ const vecRange = vecMax - vecMin || 1;
949
+ const ftsValues = ftsResults.map((r) => r.score);
950
+ const ftsMin = ftsValues.length > 0 ? Math.min(...ftsValues) : 0;
951
+ const ftsMax = ftsValues.length > 0 ? Math.max(...ftsValues) : 1;
952
+ const ftsRange = ftsMax - ftsMin || 1;
953
+ const merged = /* @__PURE__ */ new Map();
954
+ for (const fr of ftsResults) {
955
+ const normFts = (fr.score - ftsMin) / ftsRange;
956
+ const vecEntry = vecSims.get(fr.memory.memoryId);
957
+ const normVec = vecEntry ? (vecEntry.sim - vecMin) / vecRange : 0;
958
+ const score = alpha * normVec + (1 - alpha) * normFts;
959
+ merged.set(fr.memory.memoryId, { score, memory: fr.memory });
960
+ }
961
+ for (const [id, entry] of vecSims) {
962
+ if (merged.has(id)) continue;
963
+ const normVec = (entry.sim - vecMin) / vecRange;
964
+ const score = alpha * normVec;
965
+ merged.set(id, { score, memory: entry.memory });
966
+ }
967
+ return [...merged.values()].sort((a, b) => b.score - a.score).slice(0, limit).map(({ score, memory }) => ({
968
+ memory,
969
+ score,
970
+ matchType: "hybrid"
971
+ }));
972
+ }
911
973
  function cosineSimilarity(a, b) {
912
974
  let dot = 0;
913
975
  let normA = 0;
@@ -1046,34 +1108,10 @@ function searchMemories(db, query, queryVec, useVecTable = true, skipAccessTrack
1046
1108
  finalResults = deduplicateResults(boosted);
1047
1109
  } else {
1048
1110
  const limit = query.limit ?? DEFAULT_LIMIT;
1049
- const ftsHasEnough = ftsResults.length >= limit;
1050
- if (ftsHasEnough) {
1051
- const vecIds = new Set(vecResults.map((r) => r.memory.memoryId));
1052
- const reranked = ftsResults.map((r) => ({
1053
- ...r,
1054
- score: r.score * (vecIds.has(r.memory.memoryId) ? 1.3 : 1)
1055
- }));
1056
- reranked.sort((a, b) => b.score - a.score);
1057
- const weighted = applyThreeFactorReranking(reranked);
1058
- const boosted = applyKeywordBoost(weighted, queryTokens);
1059
- finalResults = deduplicateResults(boosted);
1060
- } else {
1061
- const ftsIds = new Set(ftsResults.map((r) => r.memory.memoryId));
1062
- const ftsItems = [...ftsResults];
1063
- for (const vr of vecResults) {
1064
- if (ftsItems.length >= limit) break;
1065
- if (ftsIds.has(vr.memory.memoryId)) continue;
1066
- ftsItems.push({
1067
- memory: vr.memory,
1068
- score: (1 - vr.distance) * 0.5,
1069
- // Lower score than FTS items
1070
- matchType: "hybrid"
1071
- });
1072
- }
1073
- const weighted = applyThreeFactorReranking(ftsItems);
1074
- const boosted = applyKeywordBoost(weighted, queryTokens);
1075
- finalResults = deduplicateResults(boosted);
1076
- }
1111
+ const merged = convexCombinationMerge(ftsResults, vecResults, 0.3, limit);
1112
+ const weighted = applyThreeFactorReranking(merged);
1113
+ const boosted = applyKeywordBoost(weighted, queryTokens);
1114
+ finalResults = deduplicateResults(boosted);
1077
1115
  }
1078
1116
  updateAccessTracking(db, finalResults.map((r) => r.memory.memoryId));
1079
1117
  return {
@@ -1209,10 +1247,22 @@ var SqliteMemoryEngine = class {
1209
1247
  } else {
1210
1248
  memories = storeBatchInTransaction(this.db, this.stmts, inputs);
1211
1249
  }
1212
- if (this.options.contradictionDetector && this.options.embedder && memories.length <= 50) {
1250
+ if (memories.length <= 50) {
1213
1251
  for (const memory of memories) {
1252
+ if (this.options.embedder && this.options.contradictionDetector) {
1253
+ try {
1254
+ await this.checkContradictions(memory);
1255
+ } catch {
1256
+ }
1257
+ }
1258
+ if (this.options.embedder) {
1259
+ try {
1260
+ await this.checkDuplicates(memory);
1261
+ } catch {
1262
+ }
1263
+ }
1214
1264
  try {
1215
- await this.checkContradictions(memory);
1265
+ await this.autoRelate(memory);
1216
1266
  } catch {
1217
1267
  }
1218
1268
  }
@@ -1338,6 +1388,7 @@ var SqliteMemoryEngine = class {
1338
1388
  const coldMemories = db.prepare(`
1339
1389
  SELECT * FROM memories
1340
1390
  WHERE user_id = ? AND tier = 'cold' AND activation_score < 0.1 AND is_latest = 1
1391
+ AND invalidated_at IS NULL
1341
1392
  ORDER BY namespace, learned_at
1342
1393
  `).all(userId);
1343
1394
  if (coldMemories.length === 0) return { compressed: 0, removed: 0 };
@@ -1398,8 +1449,12 @@ var SqliteMemoryEngine = class {
1398
1449
  // access_count
1399
1450
  null,
1400
1451
  // last_accessed_at
1401
- rows[0].memory_id
1452
+ rows[0].memory_id,
1402
1453
  // compressed_from
1454
+ 0,
1455
+ // use_count
1456
+ 0
1457
+ // success_count
1403
1458
  );
1404
1459
  compressed++;
1405
1460
  for (const row of rows) {
@@ -1467,6 +1522,29 @@ var SqliteMemoryEngine = class {
1467
1522
  this.ensureInitialized();
1468
1523
  this.db.prepare("UPDATE memories SET tier = ? WHERE memory_id = ?").run(tier, memoryId);
1469
1524
  }
1525
+ async feedback(memoryId, helpful) {
1526
+ this.ensureInitialized();
1527
+ const db = this.db;
1528
+ db.transaction(() => {
1529
+ if (helpful) {
1530
+ db.prepare(
1531
+ "UPDATE memories SET use_count = use_count + 1, success_count = success_count + 1 WHERE memory_id = ?"
1532
+ ).run(memoryId);
1533
+ } else {
1534
+ db.prepare(
1535
+ "UPDATE memories SET use_count = use_count + 1 WHERE memory_id = ?"
1536
+ ).run(memoryId);
1537
+ }
1538
+ const row = db.prepare(
1539
+ "SELECT salience, use_count, success_count FROM memories WHERE memory_id = ?"
1540
+ ).get(memoryId);
1541
+ if (row && row.use_count > 0) {
1542
+ const successRate = row.success_count / row.use_count;
1543
+ const newSalience = Math.min(1, Math.max(0.1, 0.5 + 0.5 * successRate));
1544
+ db.prepare("UPDATE memories SET salience = ? WHERE memory_id = ?").run(newSalience, memoryId);
1545
+ }
1546
+ })();
1547
+ }
1470
1548
  async quality(userId) {
1471
1549
  this.ensureInitialized();
1472
1550
  const db = this.db;
@@ -1656,6 +1734,7 @@ export {
1656
1734
  buildSearchSql,
1657
1735
  computeActivation,
1658
1736
  computeEbbinghaus,
1737
+ convexCombinationMerge,
1659
1738
  createEngine,
1660
1739
  createPreparedStatements,
1661
1740
  createRelation,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memrosetta/core",
3
- "version": "0.2.20",
3
+ "version": "0.2.22",
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.20"
23
+ "@memrosetta/types": "0.2.21"
24
24
  },
25
25
  "optionalDependencies": {
26
- "@memrosetta/embeddings": "0.2.20"
26
+ "@memrosetta/embeddings": "0.2.21"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/better-sqlite3": "^7.6.0",