File size: 3,096 Bytes
0460020
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// ============================================================
// 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] };
}