@memrosetta/core 0.2.19 → 0.2.20

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
@@ -1,5 +1,5 @@
1
1
  import Database from 'better-sqlite3';
2
- import { Memory, MemoryInput, RelationType, MemoryRelation, SearchResult, SearchFilters, SearchQuery, SearchResponse, IMemoryEngine, CompressResult, MaintenanceResult, MemoryTier, TierConfig } from '@memrosetta/types';
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
5
  interface SchemaOptions {
@@ -12,6 +12,13 @@ declare function generateMemoryId(): string;
12
12
  declare function nowIso(): string;
13
13
  declare function keywordsToString(keywords: readonly string[] | undefined): string | null;
14
14
  declare function stringToKeywords(str: string | null): readonly string[];
15
+ /**
16
+ * Derive the logical state of a memory from its existing fields.
17
+ * - invalidated: invalidatedAt is set (takes precedence)
18
+ * - superseded: isLatest is false
19
+ * - current: isLatest is true and not invalidated
20
+ */
21
+ declare function deriveMemoryState(memory: Memory): MemoryState;
15
22
 
16
23
  interface MemoryRow {
17
24
  readonly id: number;
@@ -204,7 +211,7 @@ declare function updateAccessTracking(db: Database.Database, memoryIds: readonly
204
211
  *
205
212
  * Results are weighted by activation score and access tracking is updated.
206
213
  */
207
- declare function searchMemories(db: Database.Database, query: SearchQuery, queryVec?: Float32Array, useVecTable?: boolean): SearchResponse;
214
+ declare function searchMemories(db: Database.Database, query: SearchQuery, queryVec?: Float32Array, useVecTable?: boolean, skipAccessTracking?: boolean): SearchResponse;
208
215
 
209
216
  interface SqliteEngineOptions {
210
217
  readonly dbPath: string;
@@ -235,6 +242,7 @@ declare class SqliteMemoryEngine implements IMemoryEngine {
235
242
  compress(userId: string): Promise<CompressResult>;
236
243
  maintain(userId: string): Promise<MaintenanceResult>;
237
244
  setTier(memoryId: string, tier: MemoryTier): Promise<void>;
245
+ quality(userId: string): Promise<MemoryQuality>;
238
246
  /**
239
247
  * Check the newly stored memory against existing similar memories
240
248
  * for contradictions using the NLI model.
@@ -254,6 +262,15 @@ declare class SqliteMemoryEngine implements IMemoryEngine {
254
262
  * Only runs for single store() calls, not storeBatch() (too slow for bulk).
255
263
  */
256
264
  private checkDuplicates;
265
+ /**
266
+ * Auto-create 'extends' relations when a new memory shares keywords
267
+ * with existing memories. Builds graph density for future graph-based retrieval.
268
+ *
269
+ * Only runs for individual store() calls, not storeBatch().
270
+ * Requires at least 2 overlapping keywords to create a relation.
271
+ * Graceful: errors are silently swallowed.
272
+ */
273
+ private autoRelate;
257
274
  private ensureInitialized;
258
275
  }
259
276
  declare function createEngine(options: SqliteEngineOptions): SqliteMemoryEngine;
@@ -311,4 +328,4 @@ declare function determineTier(memory: {
311
328
  */
312
329
  declare function estimateTokens(content: string): number;
313
330
 
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 };
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 };
package/dist/index.js CHANGED
@@ -183,6 +183,11 @@ function stringToKeywords(str) {
183
183
  if (!str || str.trim() === "") return [];
184
184
  return str.split(" ").filter((s) => s.length > 0);
185
185
  }
186
+ function deriveMemoryState(memory) {
187
+ if (memory.invalidatedAt) return "invalidated";
188
+ if (!memory.isLatest) return "superseded";
189
+ return "current";
190
+ }
186
191
 
187
192
  // src/mapper.ts
188
193
  function rowToMemory(row) {
@@ -608,9 +613,29 @@ function buildSearchSql(query) {
608
613
  whereClauses.push("m.confidence >= ?");
609
614
  params.push(filters.minConfidence);
610
615
  }
611
- const onlyLatest = filters?.onlyLatest ?? true;
612
- if (onlyLatest) {
613
- whereClauses.push("m.is_latest = 1");
616
+ if (filters?.states && filters.states.length > 0) {
617
+ const stateConditions = [];
618
+ if (filters.states.includes("current")) {
619
+ stateConditions.push("(m.is_latest = 1 AND m.invalidated_at IS NULL)");
620
+ }
621
+ if (filters.states.includes("superseded")) {
622
+ stateConditions.push("(m.is_latest = 0)");
623
+ }
624
+ if (filters.states.includes("invalidated")) {
625
+ stateConditions.push("(m.invalidated_at IS NOT NULL)");
626
+ }
627
+ if (stateConditions.length > 0) {
628
+ whereClauses.push(`(${stateConditions.join(" OR ")})`);
629
+ }
630
+ } else {
631
+ const onlyLatest = filters?.onlyLatest ?? true;
632
+ if (onlyLatest) {
633
+ whereClauses.push("m.is_latest = 1");
634
+ }
635
+ const excludeInvalidated = filters?.excludeInvalidated ?? true;
636
+ if (excludeInvalidated) {
637
+ whereClauses.push("m.invalidated_at IS NULL");
638
+ }
614
639
  }
615
640
  if (filters?.eventDateRange?.start) {
616
641
  whereClauses.push("m.event_date_start >= ?");
@@ -620,10 +645,6 @@ function buildSearchSql(query) {
620
645
  whereClauses.push("m.event_date_end <= ?");
621
646
  params.push(filters.eventDateRange.end);
622
647
  }
623
- const excludeInvalidated = filters?.excludeInvalidated ?? true;
624
- if (excludeInvalidated) {
625
- whereClauses.push("m.invalidated_at IS NULL");
626
- }
627
648
  const limit = query.limit ?? DEFAULT_LIMIT;
628
649
  params.push(limit);
629
650
  const sql = [
@@ -676,9 +697,29 @@ function ftsSearch(db, query) {
676
697
  function bruteForceVectorSearch(db, queryVec, userId, limit, filters) {
677
698
  const whereClauses = ["user_id = ?", "embedding IS NOT NULL"];
678
699
  const params = [userId];
679
- const onlyLatest = filters?.onlyLatest ?? true;
680
- if (onlyLatest) {
681
- whereClauses.push("is_latest = 1");
700
+ if (filters?.states && filters.states.length > 0) {
701
+ const stateConditions = [];
702
+ if (filters.states.includes("current")) {
703
+ stateConditions.push("(is_latest = 1 AND invalidated_at IS NULL)");
704
+ }
705
+ if (filters.states.includes("superseded")) {
706
+ stateConditions.push("(is_latest = 0)");
707
+ }
708
+ if (filters.states.includes("invalidated")) {
709
+ stateConditions.push("(invalidated_at IS NOT NULL)");
710
+ }
711
+ if (stateConditions.length > 0) {
712
+ whereClauses.push(`(${stateConditions.join(" OR ")})`);
713
+ }
714
+ } else {
715
+ const onlyLatest = filters?.onlyLatest ?? true;
716
+ if (onlyLatest) {
717
+ whereClauses.push("is_latest = 1");
718
+ }
719
+ const excludeInvalidated = filters?.excludeInvalidated ?? true;
720
+ if (excludeInvalidated) {
721
+ whereClauses.push("invalidated_at IS NULL");
722
+ }
682
723
  }
683
724
  if (filters?.memoryTypes && filters.memoryTypes.length > 0) {
684
725
  const mtPlaceholders = filters.memoryTypes.map(() => "?").join(",");
@@ -707,10 +748,6 @@ function bruteForceVectorSearch(db, queryVec, userId, limit, filters) {
707
748
  whereClauses.push("event_date_end <= ?");
708
749
  params.push(filters.eventDateRange.end);
709
750
  }
710
- const excludeInvalidated = filters?.excludeInvalidated ?? true;
711
- if (excludeInvalidated) {
712
- whereClauses.push("invalidated_at IS NULL");
713
- }
714
751
  const sql = `SELECT * FROM memories WHERE ${whereClauses.join(" AND ")}`;
715
752
  const rows = db.prepare(sql).all(...params);
716
753
  if (rows.length === 0) {
@@ -748,9 +785,29 @@ function vectorSearch(db, queryVec, userId, limit, filters, useVecTable = true)
748
785
  const placeholders = rowids.map(() => "?").join(",");
749
786
  let sql = `SELECT * FROM memories WHERE id IN (${placeholders}) AND user_id = ?`;
750
787
  const params = [...rowids, userId];
751
- const onlyLatest = filters?.onlyLatest ?? true;
752
- if (onlyLatest) {
753
- sql += " AND is_latest = 1";
788
+ if (filters?.states && filters.states.length > 0) {
789
+ const stateConditions = [];
790
+ if (filters.states.includes("current")) {
791
+ stateConditions.push("(is_latest = 1 AND invalidated_at IS NULL)");
792
+ }
793
+ if (filters.states.includes("superseded")) {
794
+ stateConditions.push("(is_latest = 0)");
795
+ }
796
+ if (filters.states.includes("invalidated")) {
797
+ stateConditions.push("(invalidated_at IS NOT NULL)");
798
+ }
799
+ if (stateConditions.length > 0) {
800
+ sql += ` AND (${stateConditions.join(" OR ")})`;
801
+ }
802
+ } else {
803
+ const onlyLatest = filters?.onlyLatest ?? true;
804
+ if (onlyLatest) {
805
+ sql += " AND is_latest = 1";
806
+ }
807
+ const excludeInvalidated = filters?.excludeInvalidated ?? true;
808
+ if (excludeInvalidated) {
809
+ sql += " AND invalidated_at IS NULL";
810
+ }
754
811
  }
755
812
  if (filters?.memoryTypes && filters.memoryTypes.length > 0) {
756
813
  const mtPlaceholders = filters.memoryTypes.map(() => "?").join(",");
@@ -779,10 +836,6 @@ function vectorSearch(db, queryVec, userId, limit, filters, useVecTable = true)
779
836
  sql += " AND event_date_end <= ?";
780
837
  params.push(filters.eventDateRange.end);
781
838
  }
782
- const excludeInvalidated = filters?.excludeInvalidated ?? true;
783
- if (excludeInvalidated) {
784
- sql += " AND invalidated_at IS NULL";
785
- }
786
839
  const rows = db.prepare(sql).all(...params);
787
840
  return rows.map((row) => ({
788
841
  memory: rowToMemory(row),
@@ -950,7 +1003,7 @@ function updateAccessTracking(db, memoryIds) {
950
1003
  });
951
1004
  updateAll(memoryIds);
952
1005
  }
953
- function searchMemories(db, query, queryVec, useVecTable = true) {
1006
+ function searchMemories(db, query, queryVec, useVecTable = true, skipAccessTracking = false) {
954
1007
  const startTime = performance.now();
955
1008
  const queryTokens = extractQueryTokens(query.query);
956
1009
  const ftsResults = ftsSearch(db, query);
@@ -959,7 +1012,9 @@ function searchMemories(db, query, queryVec, useVecTable = true) {
959
1012
  const weighted = applyThreeFactorReranking(ftsResults);
960
1013
  const boosted = applyKeywordBoost(weighted, queryTokens);
961
1014
  finalResults = deduplicateResults(boosted);
962
- updateAccessTracking(db, finalResults.map((r) => r.memory.memoryId));
1015
+ if (!skipAccessTracking) {
1016
+ updateAccessTracking(db, finalResults.map((r) => r.memory.memoryId));
1017
+ }
963
1018
  return {
964
1019
  results: finalResults,
965
1020
  totalCount: finalResults.length,
@@ -1088,19 +1143,6 @@ function estimateTokens(content) {
1088
1143
  }
1089
1144
 
1090
1145
  // 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
- }
1104
1146
  var SqliteMemoryEngine = class {
1105
1147
  db = null;
1106
1148
  stmts = null;
@@ -1156,6 +1198,7 @@ var SqliteMemoryEngine = class {
1156
1198
  await this.checkContradictions(memory);
1157
1199
  }
1158
1200
  await this.checkDuplicates(memory);
1201
+ await this.autoRelate(memory);
1159
1202
  return memory;
1160
1203
  }
1161
1204
  async storeBatch(inputs) {
@@ -1424,6 +1467,41 @@ var SqliteMemoryEngine = class {
1424
1467
  this.ensureInitialized();
1425
1468
  this.db.prepare("UPDATE memories SET tier = ? WHERE memory_id = ?").run(tier, memoryId);
1426
1469
  }
1470
+ async quality(userId) {
1471
+ this.ensureInitialized();
1472
+ const db = this.db;
1473
+ const total = db.prepare("SELECT COUNT(*) as c FROM memories WHERE user_id = ?").get(userId).c;
1474
+ const fresh = db.prepare(
1475
+ "SELECT COUNT(*) as c FROM memories WHERE user_id = ? AND is_latest = 1 AND invalidated_at IS NULL"
1476
+ ).get(userId).c;
1477
+ const invalidated = db.prepare(
1478
+ "SELECT COUNT(*) as c FROM memories WHERE user_id = ? AND invalidated_at IS NOT NULL"
1479
+ ).get(userId).c;
1480
+ const superseded = db.prepare(
1481
+ "SELECT COUNT(*) as c FROM memories WHERE user_id = ? AND is_latest = 0"
1482
+ ).get(userId).c;
1483
+ const withRelations = db.prepare(`
1484
+ SELECT COUNT(DISTINCT mid) as c FROM (
1485
+ SELECT src_memory_id as mid FROM memory_relations
1486
+ WHERE src_memory_id IN (SELECT memory_id FROM memories WHERE user_id = ?)
1487
+ UNION
1488
+ SELECT dst_memory_id as mid FROM memory_relations
1489
+ WHERE dst_memory_id IN (SELECT memory_id FROM memories WHERE user_id = ?)
1490
+ )
1491
+ `).get(userId, userId).c;
1492
+ const avgRow = db.prepare(
1493
+ "SELECT AVG(activation_score) as avg FROM memories WHERE user_id = ? AND is_latest = 1"
1494
+ ).get(userId);
1495
+ const avgActivation = avgRow.avg ?? 0;
1496
+ return {
1497
+ total,
1498
+ fresh,
1499
+ invalidated,
1500
+ superseded,
1501
+ withRelations,
1502
+ avgActivation
1503
+ };
1504
+ }
1427
1505
  /**
1428
1506
  * Check the newly stored memory against existing similar memories
1429
1507
  * for contradictions using the NLI model.
@@ -1449,7 +1527,9 @@ var SqliteMemoryEngine = class {
1449
1527
  filters: { onlyLatest: true }
1450
1528
  },
1451
1529
  queryVec,
1452
- this.vectorEnabled
1530
+ this.vectorEnabled,
1531
+ true
1532
+ // skipAccessTracking
1453
1533
  );
1454
1534
  for (const result of similar.results) {
1455
1535
  if (result.memory.memoryId === newMemory.memoryId) continue;
@@ -1483,27 +1563,21 @@ var SqliteMemoryEngine = class {
1483
1563
  if (!this.options.embedder) return;
1484
1564
  try {
1485
1565
  const queryVec = await this.options.embedder.embed(newMemory.content);
1486
- const similar = searchMemories(
1566
+ const candidates = bruteForceVectorSearch(
1487
1567
  this.db,
1488
- {
1489
- userId: newMemory.userId,
1490
- query: newMemory.content,
1491
- limit: 5,
1492
- filters: { onlyLatest: true }
1493
- },
1494
1568
  queryVec,
1495
- this.vectorEnabled
1569
+ newMemory.userId,
1570
+ 10,
1571
+ { onlyLatest: true }
1496
1572
  );
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);
1573
+ for (const candidate of candidates) {
1574
+ if (candidate.memory.memoryId === newMemory.memoryId) continue;
1575
+ const similarity = 1 - candidate.distance;
1502
1576
  if (similarity > 0.95) {
1503
1577
  try {
1504
1578
  await this.relate(
1505
1579
  newMemory.memoryId,
1506
- result.memory.memoryId,
1580
+ candidate.memory.memoryId,
1507
1581
  "updates",
1508
1582
  `Auto-detected duplicate: cosine similarity ${similarity.toFixed(3)}`
1509
1583
  );
@@ -1514,6 +1588,55 @@ var SqliteMemoryEngine = class {
1514
1588
  } catch {
1515
1589
  }
1516
1590
  }
1591
+ /**
1592
+ * Auto-create 'extends' relations when a new memory shares keywords
1593
+ * with existing memories. Builds graph density for future graph-based retrieval.
1594
+ *
1595
+ * Only runs for individual store() calls, not storeBatch().
1596
+ * Requires at least 2 overlapping keywords to create a relation.
1597
+ * Graceful: errors are silently swallowed.
1598
+ */
1599
+ async autoRelate(newMemory) {
1600
+ if (!newMemory.keywords || newMemory.keywords.length === 0) return;
1601
+ try {
1602
+ const keywordList = newMemory.keywords;
1603
+ const existing = this.db.prepare(`
1604
+ SELECT memory_id, keywords FROM memories
1605
+ WHERE user_id = ? AND is_latest = 1 AND memory_id != ?
1606
+ AND invalidated_at IS NULL
1607
+ ORDER BY learned_at DESC LIMIT 10
1608
+ `).all(newMemory.userId, newMemory.memoryId);
1609
+ for (const row of existing) {
1610
+ if (!row.keywords) continue;
1611
+ const existingKeywords = row.keywords.split(" ").map((k) => k.trim().toLowerCase());
1612
+ const overlap = keywordList.filter((k) => existingKeywords.includes(k.toLowerCase()));
1613
+ if (overlap.length >= 3) {
1614
+ const existingRelation = this.db.prepare(
1615
+ `SELECT 1 FROM memory_relations
1616
+ WHERE (src_memory_id = ? AND dst_memory_id = ?)
1617
+ OR (src_memory_id = ? AND dst_memory_id = ?)
1618
+ LIMIT 1`
1619
+ ).get(
1620
+ newMemory.memoryId,
1621
+ row.memory_id,
1622
+ row.memory_id,
1623
+ newMemory.memoryId
1624
+ );
1625
+ if (existingRelation) continue;
1626
+ try {
1627
+ await this.relate(
1628
+ newMemory.memoryId,
1629
+ row.memory_id,
1630
+ "extends",
1631
+ `Auto: ${overlap.length} shared keywords (${overlap.join(", ")})`
1632
+ );
1633
+ } catch {
1634
+ }
1635
+ }
1636
+ }
1637
+ } catch {
1638
+ }
1639
+ }
1517
1640
  ensureInitialized() {
1518
1641
  if (!this.db || !this.stmts || !this.relStmts) {
1519
1642
  throw new Error("Engine not initialized. Call initialize() first.");
@@ -1538,6 +1661,7 @@ export {
1538
1661
  createRelation,
1539
1662
  createRelationStatements,
1540
1663
  deduplicateResults,
1664
+ deriveMemoryState,
1541
1665
  determineTier,
1542
1666
  ensureSchema,
1543
1667
  estimateTokens,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memrosetta/core",
3
- "version": "0.2.19",
3
+ "version": "0.2.20",
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.19"
23
+ "@memrosetta/types": "0.2.20"
24
24
  },
25
25
  "optionalDependencies": {
26
- "@memrosetta/embeddings": "0.2.19"
26
+ "@memrosetta/embeddings": "0.2.20"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/better-sqlite3": "^7.6.0",