diff --git a/nexus/components/spatial-memory.js b/nexus/components/spatial-memory.js index f963de6f..2fd20d00 100644 --- a/nexus/components/spatial-memory.js +++ b/nexus/components/spatial-memory.js @@ -76,6 +76,12 @@ const SpatialMemory = (() => { } }; + // ─── PERSISTENCE CONFIG ────────────────────────────── + const STORAGE_KEY = 'mnemosyne_spatial_memory'; + const STORAGE_VERSION = 1; + let _dirty = false; + let _lastSavedHash = ''; + // ─── STATE ──────────────────────────────────────────── let _scene = null; let _regionMarkers = {}; @@ -183,6 +189,8 @@ const SpatialMemory = (() => { _drawConnections(mem.id, mem.connections); } + _dirty = true; + saveToStorage(); console.info('[Mnemosyne] Spatial memory placed:', mem.id, 'in', region.label); return crystal; } @@ -247,6 +255,8 @@ const SpatialMemory = (() => { } delete _memoryObjects[memId]; + _dirty = true; + saveToStorage(); } // ─── ANIMATE ───────────────────────────────────────── @@ -286,7 +296,9 @@ const SpatialMemory = (() => { _regionMarkers[key] = createRegionMarker(key, region); }); - console.info('[Mnemosyne] Spatial Memory Schema initialized —', Object.keys(REGIONS).length, 'regions'); + // Restore persisted memories + const restored = loadFromStorage(); + console.info('[Mnemosyne] Spatial Memory Schema initialized —', Object.keys(REGIONS).length, 'regions,', restored, 'memories restored'); return REGIONS; } @@ -320,6 +332,99 @@ const SpatialMemory = (() => { return Object.values(_memoryObjects).map(o => o.data); } + // ─── LOCALSTORAGE PERSISTENCE ──────────────────────── + function _indexHash(index) { + // Simple hash of memory IDs + count to detect changes + const ids = (index.memories || []).map(m => m.id).sort().join(','); + return index.memories.length + ':' + ids; + } + + function saveToStorage() { + if (typeof localStorage === 'undefined') { + console.warn('[Mnemosyne] localStorage unavailable — skipping save'); + return false; + } + try { + const index = exportIndex(); + const hash = _indexHash(index); + if (hash === _lastSavedHash) return false; // no change + + const payload = JSON.stringify(index); + localStorage.setItem(STORAGE_KEY, payload); + _lastSavedHash = hash; + _dirty = false; + console.info('[Mnemosyne] Saved', index.memories.length, 'memories to localStorage'); + return true; + } catch (e) { + if (e.name === 'QuotaExceededError' || e.code === 22) { + console.warn('[Mnemosyne] localStorage quota exceeded — pruning archive memories'); + _pruneArchiveMemories(); + try { + const index = exportIndex(); + localStorage.setItem(STORAGE_KEY, JSON.stringify(index)); + _lastSavedHash = _indexHash(index); + console.info('[Mnemosyne] Saved after prune:', index.memories.length, 'memories'); + return true; + } catch (e2) { + console.error('[Mnemosyne] Save failed even after prune:', e2); + return false; + } + } + console.error('[Mnemosyne] Save failed:', e); + return false; + } + } + + function loadFromStorage() { + if (typeof localStorage === 'undefined') { + console.warn('[Mnemosyne] localStorage unavailable — starting empty'); + return 0; + } + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) { + console.info('[Mnemosyne] No saved state found — starting fresh'); + return 0; + } + const index = JSON.parse(raw); + if (index.version !== STORAGE_VERSION) { + console.warn('[Mnemosyne] Saved version mismatch (got', index.version, 'expected', + STORAGE_VERSION + ') — starting fresh'); + return 0; + } + const count = importIndex(index); + _lastSavedHash = _indexHash(index); + return count; + } catch (e) { + console.error('[Mnemosyne] Load failed:', e); + return 0; + } + } + + function _pruneArchiveMemories() { + // Remove oldest archive-region memories first + const archive = getMemoriesInRegion('archive'); + const working = Object.values(_memoryObjects).filter(o => o.region !== 'archive'); + // Sort archive by timestamp ascending (oldest first) + archive.sort((a, b) => { + const ta = a.data.timestamp || a.mesh.userData.createdAt || ''; + const tb = b.data.timestamp || b.mesh.userData.createdAt || ''; + return ta.localeCompare(tb); + }); + const toRemove = Math.max(1, Math.ceil(archive.length * 0.25)); + for (let i = 0; i < toRemove && i < archive.length; i++) { + removeMemory(archive[i].data.id); + } + console.info('[Mnemosyne] Pruned', toRemove, 'archive memories'); + } + + function clearStorage() { + if (typeof localStorage !== 'undefined') { + localStorage.removeItem(STORAGE_KEY); + _lastSavedHash = ''; + console.info('[Mnemosyne] Cleared localStorage'); + } + } + // ─── PERSISTENCE ───────────────────────────────────── function exportIndex() { return { @@ -369,7 +474,8 @@ const SpatialMemory = (() => { return { init, placeMemory, removeMemory, update, getMemoryAtPosition, getRegionAtPosition, getMemoriesInRegion, getAllMemories, - exportIndex, importIndex, searchNearby, REGIONS + exportIndex, importIndex, searchNearby, REGIONS, + saveToStorage, loadFromStorage, clearStorage }; })();