@memrosetta/core 0.2.18 → 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 +57 -6
- package/dist/index.js +278 -43
- package/package.json +3 -3
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;
|
|
@@ -160,6 +167,21 @@ declare function rrfMergeWeighted(ftsResults: readonly {
|
|
|
160
167
|
readonly memory: Memory;
|
|
161
168
|
readonly rank: number;
|
|
162
169
|
}[], k?: number, limit?: number, ftsWeight?: number, vecWeight?: number): readonly SearchResult[];
|
|
170
|
+
/**
|
|
171
|
+
* Generative Agents-inspired 3-factor reranking.
|
|
172
|
+
* final_score = w_recency * recency + w_importance * importance + w_relevance * relevance
|
|
173
|
+
*
|
|
174
|
+
* - recency: exponential decay from learnedAt (0.995^hours)
|
|
175
|
+
* - importance: memory.salience (0-1)
|
|
176
|
+
* - relevance: original search score (from FTS/vector/RRF)
|
|
177
|
+
*
|
|
178
|
+
* All three are min-max normalized before combining.
|
|
179
|
+
*/
|
|
180
|
+
declare function applyThreeFactorReranking(results: readonly SearchResult[], weights?: {
|
|
181
|
+
recency?: number;
|
|
182
|
+
importance?: number;
|
|
183
|
+
relevance?: number;
|
|
184
|
+
}): readonly SearchResult[];
|
|
163
185
|
/**
|
|
164
186
|
* Remove duplicate search results based on content identity.
|
|
165
187
|
* Keeps the first (highest-scored) occurrence when duplicates exist.
|
|
@@ -189,7 +211,7 @@ declare function updateAccessTracking(db: Database.Database, memoryIds: readonly
|
|
|
189
211
|
*
|
|
190
212
|
* Results are weighted by activation score and access tracking is updated.
|
|
191
213
|
*/
|
|
192
|
-
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;
|
|
193
215
|
|
|
194
216
|
interface SqliteEngineOptions {
|
|
195
217
|
readonly dbPath: string;
|
|
@@ -220,6 +242,7 @@ declare class SqliteMemoryEngine implements IMemoryEngine {
|
|
|
220
242
|
compress(userId: string): Promise<CompressResult>;
|
|
221
243
|
maintain(userId: string): Promise<MaintenanceResult>;
|
|
222
244
|
setTier(memoryId: string, tier: MemoryTier): Promise<void>;
|
|
245
|
+
quality(userId: string): Promise<MemoryQuality>;
|
|
223
246
|
/**
|
|
224
247
|
* Check the newly stored memory against existing similar memories
|
|
225
248
|
* for contradictions using the NLI model.
|
|
@@ -231,6 +254,23 @@ declare class SqliteMemoryEngine implements IMemoryEngine {
|
|
|
231
254
|
* swallowed so that memory storage is never blocked.
|
|
232
255
|
*/
|
|
233
256
|
private checkContradictions;
|
|
257
|
+
/**
|
|
258
|
+
* Check for near-duplicate memories after storing.
|
|
259
|
+
* Uses direct cosine similarity (not the combined search score) to avoid
|
|
260
|
+
* false positives from the multi-factor reranking.
|
|
261
|
+
* If cosine similarity > 0.95, auto-create an 'updates' relation (new supersedes old).
|
|
262
|
+
* Only runs for single store() calls, not storeBatch() (too slow for bulk).
|
|
263
|
+
*/
|
|
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;
|
|
234
274
|
private ensureInitialized;
|
|
235
275
|
}
|
|
236
276
|
declare function createEngine(options: SqliteEngineOptions): SqliteMemoryEngine;
|
|
@@ -257,19 +297,30 @@ declare function createEngine(options: SqliteEngineOptions): SqliteMemoryEngine;
|
|
|
257
297
|
* @returns activation score in [0, 1]
|
|
258
298
|
*/
|
|
259
299
|
declare function computeActivation(accessTimestamps: readonly string[], salience: number, now?: Date): number;
|
|
300
|
+
/**
|
|
301
|
+
* Ebbinghaus forgetting curve: R = e^(-t/S)
|
|
302
|
+
* R = retention (0-1)
|
|
303
|
+
* t = days since last access
|
|
304
|
+
* S = strength (access_count, minimum 1)
|
|
305
|
+
*
|
|
306
|
+
* Works with existing fields (access_count, last_accessed_at) without
|
|
307
|
+
* needing an access history table like ACT-R does.
|
|
308
|
+
*/
|
|
309
|
+
declare function computeEbbinghaus(accessCount: number, lastAccessedAt: string | null, now?: Date): number;
|
|
260
310
|
|
|
261
311
|
declare const DEFAULT_TIER_CONFIG: TierConfig;
|
|
262
312
|
/**
|
|
263
313
|
* Determine the appropriate tier for a memory based on its properties.
|
|
264
314
|
*
|
|
265
|
-
* - hot: manually promoted memories stay hot
|
|
266
|
-
* - warm: memories within warmDays of creation
|
|
267
|
-
* - cold: older than warmDays
|
|
315
|
+
* - hot: manually promoted memories stay hot, OR auto-promoted via high access count (>= 10)
|
|
316
|
+
* - warm: memories within warmDays of creation, OR old but with high activation
|
|
317
|
+
* - cold: older than warmDays AND low activation
|
|
268
318
|
*/
|
|
269
319
|
declare function determineTier(memory: {
|
|
270
320
|
readonly learnedAt: string;
|
|
271
321
|
readonly activationScore: number;
|
|
272
322
|
readonly tier: string;
|
|
323
|
+
readonly accessCount?: number;
|
|
273
324
|
}, config?: TierConfig, now?: Date): MemoryTier;
|
|
274
325
|
/**
|
|
275
326
|
* Estimate token count from content length.
|
|
@@ -277,4 +328,4 @@ declare function determineTier(memory: {
|
|
|
277
328
|
*/
|
|
278
329
|
declare function estimateTokens(content: string): number;
|
|
279
330
|
|
|
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 };
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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),
|
|
@@ -871,14 +924,41 @@ function cosineSimilarity(a, b) {
|
|
|
871
924
|
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
872
925
|
return denom === 0 ? 0 : dot / denom;
|
|
873
926
|
}
|
|
874
|
-
function
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
927
|
+
function applyThreeFactorReranking(results, weights) {
|
|
928
|
+
if (results.length === 0) return results;
|
|
929
|
+
const w = {
|
|
930
|
+
recency: weights?.recency ?? 1,
|
|
931
|
+
importance: weights?.importance ?? 1,
|
|
932
|
+
relevance: weights?.relevance ?? 1
|
|
933
|
+
};
|
|
934
|
+
const now = Date.now();
|
|
935
|
+
const scored = results.map((r) => {
|
|
936
|
+
const hoursSince = (now - new Date(r.memory.learnedAt).getTime()) / (1e3 * 60 * 60);
|
|
937
|
+
const recency = Math.pow(0.995, Math.max(0, hoursSince));
|
|
938
|
+
const importance = r.memory.salience ?? 1;
|
|
939
|
+
const relevance = r.score;
|
|
940
|
+
return { ...r, recency, importance, relevance };
|
|
941
|
+
});
|
|
942
|
+
const NORM_EPSILON = 0.01;
|
|
943
|
+
const safeNormalize = (values) => {
|
|
944
|
+
let min = Infinity;
|
|
945
|
+
let max = -Infinity;
|
|
946
|
+
for (const v of values) {
|
|
947
|
+
if (v < min) min = v;
|
|
948
|
+
if (v > max) max = v;
|
|
949
|
+
}
|
|
950
|
+
const range = max - min;
|
|
951
|
+
if (range < NORM_EPSILON) return values.map(() => 1);
|
|
952
|
+
return values.map((v) => (v - min) / range);
|
|
953
|
+
};
|
|
954
|
+
const recencies = safeNormalize(scored.map((s) => s.recency));
|
|
955
|
+
const importances = safeNormalize(scored.map((s) => s.importance));
|
|
956
|
+
const relevances = safeNormalize(scored.map((s) => s.relevance));
|
|
957
|
+
return scored.map((s, i) => ({
|
|
958
|
+
memory: s.memory,
|
|
959
|
+
score: w.recency * recencies[i] + w.importance * importances[i] + w.relevance * relevances[i],
|
|
960
|
+
matchType: s.matchType
|
|
961
|
+
})).sort((a, b) => b.score - a.score);
|
|
882
962
|
}
|
|
883
963
|
function deduplicateResults(results) {
|
|
884
964
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -923,16 +1003,18 @@ function updateAccessTracking(db, memoryIds) {
|
|
|
923
1003
|
});
|
|
924
1004
|
updateAll(memoryIds);
|
|
925
1005
|
}
|
|
926
|
-
function searchMemories(db, query, queryVec, useVecTable = true) {
|
|
1006
|
+
function searchMemories(db, query, queryVec, useVecTable = true, skipAccessTracking = false) {
|
|
927
1007
|
const startTime = performance.now();
|
|
928
1008
|
const queryTokens = extractQueryTokens(query.query);
|
|
929
1009
|
const ftsResults = ftsSearch(db, query);
|
|
930
1010
|
let finalResults;
|
|
931
1011
|
if (!queryVec) {
|
|
932
|
-
const weighted =
|
|
1012
|
+
const weighted = applyThreeFactorReranking(ftsResults);
|
|
933
1013
|
const boosted = applyKeywordBoost(weighted, queryTokens);
|
|
934
1014
|
finalResults = deduplicateResults(boosted);
|
|
935
|
-
|
|
1015
|
+
if (!skipAccessTracking) {
|
|
1016
|
+
updateAccessTracking(db, finalResults.map((r) => r.memory.memoryId));
|
|
1017
|
+
}
|
|
936
1018
|
return {
|
|
937
1019
|
results: finalResults,
|
|
938
1020
|
totalCount: finalResults.length,
|
|
@@ -955,11 +1037,11 @@ function searchMemories(db, query, queryVec, useVecTable = true) {
|
|
|
955
1037
|
// Convert distance back to similarity
|
|
956
1038
|
matchType: "vector"
|
|
957
1039
|
}));
|
|
958
|
-
const weighted =
|
|
1040
|
+
const weighted = applyThreeFactorReranking(vecOnly);
|
|
959
1041
|
const boosted = applyKeywordBoost(weighted, queryTokens);
|
|
960
1042
|
finalResults = deduplicateResults(boosted);
|
|
961
1043
|
} else if (vecResults.length === 0) {
|
|
962
|
-
const weighted =
|
|
1044
|
+
const weighted = applyThreeFactorReranking(ftsResults);
|
|
963
1045
|
const boosted = applyKeywordBoost(weighted, queryTokens);
|
|
964
1046
|
finalResults = deduplicateResults(boosted);
|
|
965
1047
|
} else {
|
|
@@ -972,7 +1054,7 @@ function searchMemories(db, query, queryVec, useVecTable = true) {
|
|
|
972
1054
|
score: r.score * (vecIds.has(r.memory.memoryId) ? 1.3 : 1)
|
|
973
1055
|
}));
|
|
974
1056
|
reranked.sort((a, b) => b.score - a.score);
|
|
975
|
-
const weighted =
|
|
1057
|
+
const weighted = applyThreeFactorReranking(reranked);
|
|
976
1058
|
const boosted = applyKeywordBoost(weighted, queryTokens);
|
|
977
1059
|
finalResults = deduplicateResults(boosted);
|
|
978
1060
|
} else {
|
|
@@ -988,7 +1070,7 @@ function searchMemories(db, query, queryVec, useVecTable = true) {
|
|
|
988
1070
|
matchType: "hybrid"
|
|
989
1071
|
});
|
|
990
1072
|
}
|
|
991
|
-
const weighted =
|
|
1073
|
+
const weighted = applyThreeFactorReranking(ftsItems);
|
|
992
1074
|
const boosted = applyKeywordBoost(weighted, queryTokens);
|
|
993
1075
|
finalResults = deduplicateResults(boosted);
|
|
994
1076
|
}
|
|
@@ -1024,6 +1106,16 @@ function computeActivation(accessTimestamps, salience, now) {
|
|
|
1024
1106
|
const activation = sum > 0 ? Math.log(sum) + beta : beta * 0.1;
|
|
1025
1107
|
return sigmoid(activation);
|
|
1026
1108
|
}
|
|
1109
|
+
function computeEbbinghaus(accessCount, lastAccessedAt, now) {
|
|
1110
|
+
const currentTime = now ?? /* @__PURE__ */ new Date();
|
|
1111
|
+
if (!lastAccessedAt) {
|
|
1112
|
+
return 0.1;
|
|
1113
|
+
}
|
|
1114
|
+
const S = Math.max(1, accessCount);
|
|
1115
|
+
const t = (currentTime.getTime() - new Date(lastAccessedAt).getTime()) / MS_PER_DAY;
|
|
1116
|
+
if (t <= 0) return 1;
|
|
1117
|
+
return Math.exp(-t / S);
|
|
1118
|
+
}
|
|
1027
1119
|
function sigmoid(x) {
|
|
1028
1120
|
return 1 / (1 + Math.exp(-x));
|
|
1029
1121
|
}
|
|
@@ -1038,9 +1130,12 @@ var DEFAULT_TIER_CONFIG = {
|
|
|
1038
1130
|
function determineTier(memory, config, now) {
|
|
1039
1131
|
const cfg = config ?? DEFAULT_TIER_CONFIG;
|
|
1040
1132
|
if (memory.tier === "hot") return "hot";
|
|
1133
|
+
const accessCount = memory.accessCount ?? 0;
|
|
1134
|
+
if (accessCount >= 10) return "hot";
|
|
1041
1135
|
const currentTime = now ?? /* @__PURE__ */ new Date();
|
|
1042
1136
|
const age = (currentTime.getTime() - new Date(memory.learnedAt).getTime()) / MS_PER_DAY2;
|
|
1043
1137
|
if (age <= cfg.warmDays) return "warm";
|
|
1138
|
+
if (memory.activationScore >= cfg.coldActivationThreshold) return "warm";
|
|
1044
1139
|
return "cold";
|
|
1045
1140
|
}
|
|
1046
1141
|
function estimateTokens(content) {
|
|
@@ -1102,14 +1197,27 @@ var SqliteMemoryEngine = class {
|
|
|
1102
1197
|
if (this.options.contradictionDetector && this.options.embedder) {
|
|
1103
1198
|
await this.checkContradictions(memory);
|
|
1104
1199
|
}
|
|
1200
|
+
await this.checkDuplicates(memory);
|
|
1201
|
+
await this.autoRelate(memory);
|
|
1105
1202
|
return memory;
|
|
1106
1203
|
}
|
|
1107
1204
|
async storeBatch(inputs) {
|
|
1108
1205
|
this.ensureInitialized();
|
|
1206
|
+
let memories;
|
|
1109
1207
|
if (this.options.embedder) {
|
|
1110
|
-
|
|
1208
|
+
memories = await storeBatchAsync(this.db, this.stmts, inputs, this.options.embedder);
|
|
1209
|
+
} else {
|
|
1210
|
+
memories = storeBatchInTransaction(this.db, this.stmts, inputs);
|
|
1211
|
+
}
|
|
1212
|
+
if (this.options.contradictionDetector && this.options.embedder && memories.length <= 50) {
|
|
1213
|
+
for (const memory of memories) {
|
|
1214
|
+
try {
|
|
1215
|
+
await this.checkContradictions(memory);
|
|
1216
|
+
} catch {
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1111
1219
|
}
|
|
1112
|
-
return
|
|
1220
|
+
return memories;
|
|
1113
1221
|
}
|
|
1114
1222
|
async getById(memoryId) {
|
|
1115
1223
|
this.ensureInitialized();
|
|
@@ -1315,10 +1423,11 @@ var SqliteMemoryEngine = class {
|
|
|
1315
1423
|
);
|
|
1316
1424
|
const activationTransaction = db.transaction(() => {
|
|
1317
1425
|
for (const mem of memories) {
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1426
|
+
const ebbinghaus = computeEbbinghaus(
|
|
1427
|
+
mem.access_count ?? 0,
|
|
1428
|
+
mem.last_accessed_at ?? null
|
|
1320
1429
|
);
|
|
1321
|
-
const score =
|
|
1430
|
+
const score = ebbinghaus * 0.8 + (mem.salience ?? 1) * 0.2;
|
|
1322
1431
|
updateActivation.run(score, mem.memory_id);
|
|
1323
1432
|
activationUpdated++;
|
|
1324
1433
|
}
|
|
@@ -1336,7 +1445,8 @@ var SqliteMemoryEngine = class {
|
|
|
1336
1445
|
const newTier = determineTier({
|
|
1337
1446
|
learnedAt: mem.learned_at,
|
|
1338
1447
|
activationScore: mem.activation_score ?? 1,
|
|
1339
|
-
tier: mem.tier ?? "warm"
|
|
1448
|
+
tier: mem.tier ?? "warm",
|
|
1449
|
+
accessCount: mem.access_count ?? 0
|
|
1340
1450
|
});
|
|
1341
1451
|
if (newTier !== (mem.tier ?? "warm")) {
|
|
1342
1452
|
updateTier.run(newTier, mem.memory_id);
|
|
@@ -1357,6 +1467,41 @@ var SqliteMemoryEngine = class {
|
|
|
1357
1467
|
this.ensureInitialized();
|
|
1358
1468
|
this.db.prepare("UPDATE memories SET tier = ? WHERE memory_id = ?").run(tier, memoryId);
|
|
1359
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
|
+
}
|
|
1360
1505
|
/**
|
|
1361
1506
|
* Check the newly stored memory against existing similar memories
|
|
1362
1507
|
* for contradictions using the NLI model.
|
|
@@ -1382,7 +1527,9 @@ var SqliteMemoryEngine = class {
|
|
|
1382
1527
|
filters: { onlyLatest: true }
|
|
1383
1528
|
},
|
|
1384
1529
|
queryVec,
|
|
1385
|
-
this.vectorEnabled
|
|
1530
|
+
this.vectorEnabled,
|
|
1531
|
+
true
|
|
1532
|
+
// skipAccessTracking
|
|
1386
1533
|
);
|
|
1387
1534
|
for (const result of similar.results) {
|
|
1388
1535
|
if (result.memory.memoryId === newMemory.memoryId) continue;
|
|
@@ -1405,6 +1552,91 @@ var SqliteMemoryEngine = class {
|
|
|
1405
1552
|
} catch {
|
|
1406
1553
|
}
|
|
1407
1554
|
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Check for near-duplicate memories after storing.
|
|
1557
|
+
* Uses direct cosine similarity (not the combined search score) to avoid
|
|
1558
|
+
* false positives from the multi-factor reranking.
|
|
1559
|
+
* If cosine similarity > 0.95, auto-create an 'updates' relation (new supersedes old).
|
|
1560
|
+
* Only runs for single store() calls, not storeBatch() (too slow for bulk).
|
|
1561
|
+
*/
|
|
1562
|
+
async checkDuplicates(newMemory) {
|
|
1563
|
+
if (!this.options.embedder) return;
|
|
1564
|
+
try {
|
|
1565
|
+
const queryVec = await this.options.embedder.embed(newMemory.content);
|
|
1566
|
+
const candidates = bruteForceVectorSearch(
|
|
1567
|
+
this.db,
|
|
1568
|
+
queryVec,
|
|
1569
|
+
newMemory.userId,
|
|
1570
|
+
10,
|
|
1571
|
+
{ onlyLatest: true }
|
|
1572
|
+
);
|
|
1573
|
+
for (const candidate of candidates) {
|
|
1574
|
+
if (candidate.memory.memoryId === newMemory.memoryId) continue;
|
|
1575
|
+
const similarity = 1 - candidate.distance;
|
|
1576
|
+
if (similarity > 0.95) {
|
|
1577
|
+
try {
|
|
1578
|
+
await this.relate(
|
|
1579
|
+
newMemory.memoryId,
|
|
1580
|
+
candidate.memory.memoryId,
|
|
1581
|
+
"updates",
|
|
1582
|
+
`Auto-detected duplicate: cosine similarity ${similarity.toFixed(3)}`
|
|
1583
|
+
);
|
|
1584
|
+
} catch {
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
} catch {
|
|
1589
|
+
}
|
|
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
|
+
}
|
|
1408
1640
|
ensureInitialized() {
|
|
1409
1641
|
if (!this.db || !this.stmts || !this.relStmts) {
|
|
1410
1642
|
throw new Error("Engine not initialized. Call initialize() first.");
|
|
@@ -1418,15 +1650,18 @@ export {
|
|
|
1418
1650
|
DEFAULT_TIER_CONFIG,
|
|
1419
1651
|
SqliteMemoryEngine,
|
|
1420
1652
|
applyKeywordBoost,
|
|
1653
|
+
applyThreeFactorReranking,
|
|
1421
1654
|
bruteForceVectorSearch,
|
|
1422
1655
|
buildFtsQuery,
|
|
1423
1656
|
buildSearchSql,
|
|
1424
1657
|
computeActivation,
|
|
1658
|
+
computeEbbinghaus,
|
|
1425
1659
|
createEngine,
|
|
1426
1660
|
createPreparedStatements,
|
|
1427
1661
|
createRelation,
|
|
1428
1662
|
createRelationStatements,
|
|
1429
1663
|
deduplicateResults,
|
|
1664
|
+
deriveMemoryState,
|
|
1430
1665
|
determineTier,
|
|
1431
1666
|
ensureSchema,
|
|
1432
1667
|
estimateTokens,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memrosetta/core",
|
|
3
|
-
"version": "0.2.
|
|
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.
|
|
23
|
+
"@memrosetta/types": "0.2.20"
|
|
24
24
|
},
|
|
25
25
|
"optionalDependencies": {
|
|
26
|
-
"@memrosetta/embeddings": "0.2.
|
|
26
|
+
"@memrosetta/embeddings": "0.2.20"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/better-sqlite3": "^7.6.0",
|