// ============================================================ // 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 { 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] }; }