memory-improvements / src /memoryDecay.ts
loudiman's picture
Add memoryDecay module
0460020 verified
// ============================================================
// IMPROVEMENT 2: Memory Decay / Time-Weighted Retrieval
// Based on MemoryOS (arxiv 2506.06326) heat-based replacement
// ============================================================
import { MemoryRecord, ScoredMemory, MemoryConfig, DEFAULT_MEMORY_CONFIG } from './types';
export function computeDecayFactor(memory: MemoryRecord, now: number = Date.now(), config: MemoryConfig = DEFAULT_MEMORY_CONFIG): number {
const ageHours = (now - memory.lastAccessedAt) / (1000 * 60 * 60);
const rawDecay = Math.pow(2, -ageHours / config.decayHalfLifeHours);
return Math.max(rawDecay, config.minDecayFactor);
}
export function computeHeatScore(memory: MemoryRecord, now: number = Date.now(), config: MemoryConfig = DEFAULT_MEMORY_CONFIG): number {
const decayFactor = computeDecayFactor(memory, now, config);
const frequencyBoost = 1 + Math.log2(1 + memory.accessCount);
return memory.importance * frequencyBoost * decayFactor;
}
export function applyDecayWeighting(memories: Array<{ record: MemoryRecord; cosineSimilarity: number }>, config: MemoryConfig = DEFAULT_MEMORY_CONFIG): ScoredMemory[] {
const now = Date.now();
const RECENCY_WEIGHT = 0.6;
return memories.map(({ record, cosineSimilarity }) => {
const decayFactor = computeDecayFactor(record, now, config);
const decayWeight = RECENCY_WEIGHT * decayFactor + (1 - RECENCY_WEIGHT) * record.importance;
const finalScore = cosineSimilarity * decayWeight;
return { ...record, cosineSimilarity, decayedScore: decayFactor, finalScore };
}).sort((a, b) => b.finalScore - a.finalScore);
}
export async function evictLowHeatMemories(db: any, config: MemoryConfig = DEFAULT_MEMORY_CONFIG): Promise<number> {
const countResult = await db.get('SELECT COUNT(*) as count FROM memories');
if (countResult.count <= config.maxMemories) return 0;
const allMemories = await db.getAll(`SELECT id, importance, access_count, last_accessed_at, created_at FROM memories`);
const now = Date.now();
const scored = allMemories.map((row: any) => {
const record: MemoryRecord = { id: row.id, text: '', embedding: [], type: 'semantic', source: 'user', createdAt: row.created_at, lastAccessedAt: row.last_accessed_at, accessCount: row.access_count, importance: row.importance };
return { id: row.id, heat: computeHeatScore(record, now, config), importance: row.importance };
});
const evictionCandidates = scored.filter((m: any) => m.importance < 0.9).sort((a: any, b: any) => a.heat - b.heat).slice(0, config.evictionBatchSize);
if (evictionCandidates.length === 0) return 0;
const ids = evictionCandidates.map((m: any) => m.id);
await db.run(`DELETE FROM memories WHERE id IN (${ids.map(() => '?').join(',')})`, ids);
return evictionCandidates.length;
}
export function buildAccessUpdateQuery(memoryIds: string[]) {
const placeholders = memoryIds.map(() => '?').join(',');
return { sql: `UPDATE memories SET last_accessed_at = ?, access_count = access_count + 1 WHERE id IN (${placeholders})`, params: [Date.now(), ...memoryIds] };
}