diff --git a/nexus/components/memory-optimizer.js b/nexus/components/memory-optimizer.js new file mode 100644 index 00000000..d06cd493 --- /dev/null +++ b/nexus/components/memory-optimizer.js @@ -0,0 +1,99 @@ +// ═══════════════════════════════════════════ +// PROJECT MNEMOSYNE — MEMORY OPTIMIZER (GOFAI) +// ═══════════════════════════════════════════ +// +// Heuristic-based memory pruning and organization. +// Operates without LLMs to maintain a lean, high-signal spatial index. +// +// Heuristics: +// 1. Strength Decay: Memories lose strength over time if not accessed. +// 2. Redundancy: Simple string similarity to identify duplicates. +// 3. Isolation: Memories with no connections are lower priority. +// 4. Aging: Old memories in 'working' are moved to 'archive'. +// ═══════════════════════════════════════════ + +const MemoryOptimizer = (() => { + const DECAY_RATE = 0.01; // Strength lost per optimization cycle + const PRUNE_THRESHOLD = 0.1; // Remove if strength < this + const SIMILARITY_THRESHOLD = 0.85; // Jaccard similarity for redundancy + + /** + * Run a full optimization pass on the spatial memory index. + * @param {object} spatialMemory - The SpatialMemory component instance. + * @returns {object} Summary of actions taken. + */ + function optimize(spatialMemory) { + const memories = spatialMemory.getAllMemories(); + const results = { pruned: 0, moved: 0, updated: 0 }; + + // 1. Strength Decay & Aging + memories.forEach(mem => { + let strength = mem.strength || 0.7; + strength -= DECAY_RATE; + + if (strength < PRUNE_THRESHOLD) { + spatialMemory.removeMemory(mem.id); + results.pruned++; + return; + } + + // Move old working memories to archive + if (mem.category === 'working') { + const timestamp = mem.timestamp || new Date().toISOString(); + const age = Date.now() - new Date(timestamp).getTime(); + if (age > 1000 * 60 * 60 * 24) { // 24 hours + spatialMemory.removeMemory(mem.id); + spatialMemory.placeMemory({ ...mem, category: 'archive', strength }); + results.moved++; + return; + } + } + + spatialMemory.updateMemory(mem.id, { strength }); + results.updated++; + }); + + // 2. Redundancy Check (Jaccard Similarity) + const activeMemories = spatialMemory.getAllMemories(); + for (let i = 0; i < activeMemories.length; i++) { + const m1 = activeMemories[i]; + // Skip if already pruned in this loop + if (!spatialMemory.getAllMemories().find(m => m.id === m1.id)) continue; + + for (let j = i + 1; j < activeMemories.length; j++) { + const m2 = activeMemories[j]; + if (m1.category !== m2.category) continue; + + const sim = _calculateSimilarity(m1.content, m2.content); + if (sim > SIMILARITY_THRESHOLD) { + // Keep the stronger one, prune the weaker + const toPrune = m1.strength >= m2.strength ? m2.id : m1.id; + spatialMemory.removeMemory(toPrune); + results.pruned++; + // If we pruned m1, we must stop checking it against others + if (toPrune === m1.id) break; + } + } + } + + console.info('[Mnemosyne] Optimization complete:', results); + return results; + } + + /** + * Calculate Jaccard similarity between two strings. + * @private + */ + function _calculateSimilarity(s1, s2) { + if (!s1 || !s2) return 0; + const set1 = new Set(s1.toLowerCase().split(/\s+/)); + const set2 = new Set(s2.toLowerCase().split(/\s+/)); + const intersection = new Set([...set1].filter(x => set2.has(x))); + const union = new Set([...set1, ...set2]); + return intersection.size / union.size; + } + + return { optimize }; +})(); + +export { MemoryOptimizer };