@memrosetta/core 0.2.21 → 0.3.0

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
@@ -2,6 +2,14 @@ import Database from 'better-sqlite3';
2
2
  import { Memory, MemoryState, MemoryInput, RelationType, MemoryRelation, SearchResult, SearchFilters, SearchQuery, SearchResponse, IMemoryEngine, CompressResult, MaintenanceResult, MemoryTier, MemoryQuality, TierConfig } from '@memrosetta/types';
3
3
  import { Embedder, ContradictionDetector } from '@memrosetta/embeddings';
4
4
 
5
+ /**
6
+ * Thrown when a memory ID referenced in an operation does not exist.
7
+ */
8
+ declare class MemoryNotFoundError extends Error {
9
+ readonly memoryId: string;
10
+ constructor(memoryId: string);
11
+ }
12
+
5
13
  interface SchemaOptions {
6
14
  readonly vectorEnabled?: boolean;
7
15
  readonly embeddingDimension?: number;
@@ -169,6 +177,17 @@ declare function rrfMergeWeighted(ftsResults: readonly {
169
177
  readonly memory: Memory;
170
178
  readonly rank: number;
171
179
  }[], k?: number, limit?: number, ftsWeight?: number, vecWeight?: number): readonly SearchResult[];
180
+ /**
181
+ * Score-level fusion: alpha * normalizedVecSim + (1 - alpha) * normalizedFtsSim.
182
+ *
183
+ * Unlike RRF (which discards score magnitude), this preserves score information.
184
+ * Both FTS and vector scores are min-max normalized within the result set so
185
+ * neither modality dominates due to scale differences.
186
+ *
187
+ * Items found by both sources get the combined score (strongest signal).
188
+ * Items found by only one source get at most alpha or (1-alpha) of max.
189
+ */
190
+ declare function convexCombinationMerge(ftsResults: readonly SearchResult[], vecResults: readonly VectorSearchResult[], alpha?: number, limit?: number): readonly SearchResult[];
172
191
  /**
173
192
  * Generative Agents-inspired 3-factor reranking.
174
193
  * final_score = w_recency * recency + w_importance * importance + w_relevance * relevance
@@ -209,7 +228,7 @@ declare function updateAccessTracking(db: Database.Database, memoryIds: readonly
209
228
  *
210
229
  * When queryVec is not provided, performs FTS-only search (backward compatible).
211
230
  * When queryVec is provided, performs hybrid search combining FTS + vector
212
- * results via Reciprocal Rank Fusion.
231
+ * results via convex combination score fusion.
213
232
  *
214
233
  * Results are weighted by activation score and access tracking is updated.
215
234
  */
@@ -331,4 +350,4 @@ declare function determineTier(memory: {
331
350
  */
332
351
  declare function estimateTokens(content: string): number;
333
352
 
334
- 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 };
353
+ export { DEFAULT_TIER_CONFIG, MemoryNotFoundError, 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
@@ -1,3 +1,13 @@
1
+ // src/errors.ts
2
+ var MemoryNotFoundError = class extends Error {
3
+ memoryId;
4
+ constructor(memoryId) {
5
+ super(`Memory not found: ${memoryId}`);
6
+ this.name = "MemoryNotFoundError";
7
+ this.memoryId = memoryId;
8
+ }
9
+ };
10
+
1
11
  // src/schema.ts
2
12
  var SCHEMA_V1 = `
3
13
  -- memories table
@@ -443,11 +453,11 @@ function createRelationStatements(db) {
443
453
  function createRelation(db, stmts, srcMemoryId, dstMemoryId, relationType, reason) {
444
454
  const srcExists = stmts.checkMemoryExists.get(srcMemoryId);
445
455
  if (!srcExists) {
446
- throw new Error(`Source memory not found: ${srcMemoryId}`);
456
+ throw new MemoryNotFoundError(srcMemoryId);
447
457
  }
448
458
  const dstExists = stmts.checkMemoryExists.get(dstMemoryId);
449
459
  if (!dstExists) {
450
- throw new Error(`Destination memory not found: ${dstMemoryId}`);
460
+ throw new MemoryNotFoundError(dstMemoryId);
451
461
  }
452
462
  const createdAt = nowIso();
453
463
  stmts.insertRelation.run(
@@ -792,7 +802,7 @@ function vectorSearch(db, queryVec, userId, limit, filters, useVecTable = true)
792
802
  if (!useVecTable) {
793
803
  return bruteForceVectorSearch(db, queryVec, userId, limit, filters);
794
804
  }
795
- const candidateLimit = Math.min(limit * 5, 200);
805
+ const candidateLimit = Math.min(limit * 10, 500);
796
806
  const vecBuf = Buffer.from(queryVec.buffer, queryVec.byteOffset, queryVec.byteLength);
797
807
  let candidates;
798
808
  try {
@@ -802,7 +812,10 @@ function vectorSearch(db, queryVec, userId, limit, filters, useVecTable = true)
802
812
  WHERE embedding MATCH ?
803
813
  AND k = ?
804
814
  `).all(vecBuf, candidateLimit);
805
- } catch {
815
+ } catch (err) {
816
+ const msg = err instanceof Error ? err.message : String(err);
817
+ process.stderr.write(`[memrosetta] vec_memories KNN unavailable, using brute-force fallback: ${msg}
818
+ `);
806
819
  return bruteForceVectorSearch(db, queryVec, userId, limit, filters);
807
820
  }
808
821
  if (candidates.length === 0) return [];
@@ -863,6 +876,9 @@ function vectorSearch(db, queryVec, userId, limit, filters, useVecTable = true)
863
876
  params.push(filters.eventDateRange.end);
864
877
  }
865
878
  const rows = db.prepare(sql).all(...params);
879
+ if (rows.length < limit) {
880
+ return bruteForceVectorSearch(db, queryVec, userId, limit, filters);
881
+ }
866
882
  return rows.map((row) => ({
867
883
  memory: rowToMemory(row),
868
884
  distance: distanceMap.get(row.id) ?? Infinity
@@ -934,6 +950,42 @@ function rrfMergeWeighted(ftsResults, vecResults, k = 20, limit = 10, ftsWeight
934
950
  matchType: "hybrid"
935
951
  }));
936
952
  }
953
+ function convexCombinationMerge(ftsResults, vecResults, alpha = 0.5, limit = 10) {
954
+ const vecSims = /* @__PURE__ */ new Map();
955
+ for (const vr of vecResults) {
956
+ vecSims.set(vr.memory.memoryId, {
957
+ sim: 1 - vr.distance,
958
+ memory: vr.memory
959
+ });
960
+ }
961
+ const vecValues = [...vecSims.values()].map((v) => v.sim);
962
+ const vecMin = vecValues.length > 0 ? Math.min(...vecValues) : 0;
963
+ const vecMax = vecValues.length > 0 ? Math.max(...vecValues) : 1;
964
+ const vecRange = vecMax - vecMin || 1;
965
+ const ftsValues = ftsResults.map((r) => r.score);
966
+ const ftsMin = ftsValues.length > 0 ? Math.min(...ftsValues) : 0;
967
+ const ftsMax = ftsValues.length > 0 ? Math.max(...ftsValues) : 1;
968
+ const ftsRange = ftsMax - ftsMin || 1;
969
+ const merged = /* @__PURE__ */ new Map();
970
+ for (const fr of ftsResults) {
971
+ const normFts = (fr.score - ftsMin) / ftsRange;
972
+ const vecEntry = vecSims.get(fr.memory.memoryId);
973
+ const normVec = vecEntry ? (vecEntry.sim - vecMin) / vecRange : 0;
974
+ const score = alpha * normVec + (1 - alpha) * normFts;
975
+ merged.set(fr.memory.memoryId, { score, memory: fr.memory });
976
+ }
977
+ for (const [id, entry] of vecSims) {
978
+ if (merged.has(id)) continue;
979
+ const normVec = (entry.sim - vecMin) / vecRange;
980
+ const score = alpha * normVec;
981
+ merged.set(id, { score, memory: entry.memory });
982
+ }
983
+ return [...merged.values()].sort((a, b) => b.score - a.score).slice(0, limit).map(({ score, memory }) => ({
984
+ memory,
985
+ score,
986
+ matchType: "hybrid"
987
+ }));
988
+ }
937
989
  function cosineSimilarity(a, b) {
938
990
  let dot = 0;
939
991
  let normA = 0;
@@ -1072,34 +1124,10 @@ function searchMemories(db, query, queryVec, useVecTable = true, skipAccessTrack
1072
1124
  finalResults = deduplicateResults(boosted);
1073
1125
  } else {
1074
1126
  const limit = query.limit ?? DEFAULT_LIMIT;
1075
- const ftsHasEnough = ftsResults.length >= limit;
1076
- if (ftsHasEnough) {
1077
- const vecIds = new Set(vecResults.map((r) => r.memory.memoryId));
1078
- const reranked = ftsResults.map((r) => ({
1079
- ...r,
1080
- score: r.score * (vecIds.has(r.memory.memoryId) ? 1.3 : 1)
1081
- }));
1082
- reranked.sort((a, b) => b.score - a.score);
1083
- const weighted = applyThreeFactorReranking(reranked);
1084
- const boosted = applyKeywordBoost(weighted, queryTokens);
1085
- finalResults = deduplicateResults(boosted);
1086
- } else {
1087
- const ftsIds = new Set(ftsResults.map((r) => r.memory.memoryId));
1088
- const ftsItems = [...ftsResults];
1089
- for (const vr of vecResults) {
1090
- if (ftsItems.length >= limit) break;
1091
- if (ftsIds.has(vr.memory.memoryId)) continue;
1092
- ftsItems.push({
1093
- memory: vr.memory,
1094
- score: (1 - vr.distance) * 0.5,
1095
- // Lower score than FTS items
1096
- matchType: "hybrid"
1097
- });
1098
- }
1099
- const weighted = applyThreeFactorReranking(ftsItems);
1100
- const boosted = applyKeywordBoost(weighted, queryTokens);
1101
- finalResults = deduplicateResults(boosted);
1102
- }
1127
+ const merged = convexCombinationMerge(ftsResults, vecResults, 0.3, limit);
1128
+ const weighted = applyThreeFactorReranking(merged);
1129
+ const boosted = applyKeywordBoost(weighted, queryTokens);
1130
+ finalResults = deduplicateResults(boosted);
1103
1131
  }
1104
1132
  updateAccessTracking(db, finalResults.map((r) => r.memory.memoryId));
1105
1133
  return {
@@ -1714,6 +1742,7 @@ function createEngine(options) {
1714
1742
  }
1715
1743
  export {
1716
1744
  DEFAULT_TIER_CONFIG,
1745
+ MemoryNotFoundError,
1717
1746
  SqliteMemoryEngine,
1718
1747
  applyKeywordBoost,
1719
1748
  applyThreeFactorReranking,
@@ -1722,6 +1751,7 @@ export {
1722
1751
  buildSearchSql,
1723
1752
  computeActivation,
1724
1753
  computeEbbinghaus,
1754
+ convexCombinationMerge,
1725
1755
  createEngine,
1726
1756
  createPreparedStatements,
1727
1757
  createRelation,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memrosetta/core",
3
- "version": "0.2.21",
3
+ "version": "0.3.0",
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.21"
23
+ "@memrosetta/types": "0.3.0"
24
24
  },
25
25
  "optionalDependencies": {
26
- "@memrosetta/embeddings": "0.2.21"
26
+ "@memrosetta/embeddings": "0.3.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/better-sqlite3": "^7.6.0",