Compare commits

...

1 Commits

Author SHA1 Message Date
1e7bb2a453 feat: add localStorage persistence to Spatial Memory Schema
Some checks failed
CI / test (pull_request) Failing after 13s
CI / validate (pull_request) Failing after 13s
Review Approval Gate / verify-review (pull_request) Failing after 2s
Auto-saves on placeMemory/removeMemory. Auto-restores on init.
Prunes archive memories on quota exceeded.

Closes #1158
2026-04-10 12:43:00 +00:00

View File

@@ -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
};
})();