diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/core/module-memory-graph.js | 12 | ||||
| -rw-r--r-- | modules/core/module-world-model.js | 28 | ||||
| -rw-r--r-- | modules/ext/skill-pack-core.js | 5 | ||||
| -rw-r--r-- | modules/test/test-world-model-save.js | 24 |
4 files changed, 62 insertions, 7 deletions
diff --git a/modules/core/module-memory-graph.js b/modules/core/module-memory-graph.js index ffc79e0..093d2e4 100644 --- a/modules/core/module-memory-graph.js +++ b/modules/core/module-memory-graph.js @@ -469,9 +469,16 @@ class MemoryGraphModule { // v4.5.90b fix: Use configurable _decayDays (P1 from review) const staleThreshold = this._decayDays * 24 * 60 * 60 * 1000; var stale = []; + var untracked = []; for (var name of Object.keys(index)) { var stats = index[name] || {}; if (stats.immune) continue; + // A skill with no usage telemetry has never been measured. Calling it stale is how + // the audit came to recommend archiving the system's entire procedural memory. + if (stats.lastUsed === undefined && stats.useCount === undefined) { + untracked.push(name); + continue; + } var useCount = stats.useCount || 0; var lastUsed = stats.lastUsed ? new Date(stats.lastUsed).getTime() : 0; if (useCount === 0 && (now - lastUsed > staleThreshold || lastUsed === 0)) { @@ -479,12 +486,17 @@ class MemoryGraphModule { stale.push({ name: name, daysUnused: daysUnused }); } } + this._untrackedSkills = untracked; return stale; } catch (e) { return []; } } + getUntrackedSkills() { + return Array.isArray(this._untrackedSkills) ? this._untrackedSkills : []; + } + // v4.5.89: Configure decay policy decayConfig(args) { if (args.length < 2) { diff --git a/modules/core/module-world-model.js b/modules/core/module-world-model.js index cbfd1fb..4e88f59 100644 --- a/modules/core/module-world-model.js +++ b/modules/core/module-world-model.js @@ -580,14 +580,28 @@ class WorldModelModule { if (prevModel) { try { fs.fsWriteJSON('/system/world-model.prev.json', prevModel); } catch (_) {} } // v4.5.89b fix: Clean stale temp from previous crash (P1 from review) try { fs.deleteFile('/system/world-model.tmp.json'); } catch (_) {} - fs.fsWriteJSON('/system/world-model.tmp.json', this._model); - const tmp = fs.fsReadJSON('/system/world-model.tmp.json'); - if (tmp) { - fs.fsWriteJSON('/system/world-model.json', tmp); - fs.deleteFile('/system/world-model.tmp.json'); - } else { - console.error('[WorldModel] Save failed: temp file corrupt after write'); + // Atomic + verified: never promote a write we did not read back byte-for-byte. + // Measured 2026-10-23: the old block promoted /system/world-model.json even when every + // write returned false, and logged nothing — that is how a graph got replaced by a + // partial model. fs.fsWriteJSON() returns a boolean; ignoring it hides quota failures. + const payload = JSON.stringify(this._model); + const wrote = fs.writeFile('/system/world-model.tmp.json', payload); + if (!wrote) { + console.error('[WorldModel] Save failed: write returned false (storage quota?)'); + return; } + const back = fs.readFile('/system/world-model.tmp.json'); + if (back !== payload) { + console.error('[WorldModel] Save failed: read-back mismatch (' + + (back ? back.length : 0) + '/' + payload.length + ' bytes) — keeping previous model'); + return; + } + const promoted = fs.writeFile('/system/world-model.json', back); + if (!promoted) { + console.error('[WorldModel] Save failed: promote returned false'); + return; + } + fs.deleteFile('/system/world-model.tmp.json'); } catch (e) { console.error('[WorldModel] Save error:', e.message); } diff --git a/modules/ext/skill-pack-core.js b/modules/ext/skill-pack-core.js index f204e70..5c317a4 100644 --- a/modules/ext/skill-pack-core.js +++ b/modules/ext/skill-pack-core.js @@ -1054,8 +1054,10 @@ class SkillPackCoreModule { // 1. Stale check const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule']; var staleSkills = []; + var untracked = []; if (mg && typeof mg.getStaleSkills === 'function') { staleSkills = mg.getStaleSkills(); + untracked = (typeof mg.getUntrackedSkills === 'function') ? mg.getUntrackedSkills() : []; } for (var s of staleSkills) { if (!index[s.name] || index[s.name].immune) continue; @@ -1093,6 +1095,9 @@ class SkillPackCoreModule { this.terminal.print('Total skills: ' + allNames.length, 'info'); this.terminal.print('Healthy: ' + healthy, 'success'); this.terminal.print('Stale: ' + stale.length + (stale.length > 0 ? ' (' + stale.join(', ') + ')' : ''), stale.length > 0 ? 'warning' : 'info'); + if (untracked.length) { + this.terminal.print('Untracked: ' + untracked.length + ' (no usage telemetry — NOT stale)', 'info'); + } this.terminal.print('Duplicates: ' + duplicates.length + (duplicates.length > 0 ? ' (' + duplicates.join(', ') + ')' : ''), duplicates.length > 0 ? 'warning' : 'info'); this.terminal.print('Broken refs: ' + brokenRefs, brokenRefs > 0 ? 'error' : 'info'); this.terminal.print('', ''); diff --git a/modules/test/test-world-model-save.js b/modules/test/test-world-model-save.js new file mode 100644 index 0000000..eabba84 --- /dev/null +++ b/modules/test/test-world-model-save.js @@ -0,0 +1,24 @@ +// modules/test/test-world-model-save.js — run: node modules/test/test-world-model-save.js +const fs = require('fs'), path = require('path'); +const src = fs.readFileSync(path.join(__dirname, '../core/module-world-model.js'), 'utf8'); +const WorldModelModule = new Function(src + '\nreturn WorldModelModule;')(); +const wm = new WorldModelModule(); +wm._model = { version: 'test', nodes: 1 }; + +const calls = []; +const fake = { + readDir: () => [], mkdir: () => {}, + readFile: () => '', + writeFile: (p) => { calls.push(p); return false; }, // every write fails + fsReadJSON: () => ({ version: 'test', nodes: 1 }), // truthy: old code would promote + fsWriteJSON: (p) => { calls.push(p); return false; }, + deleteFile: () => true, +}; +const errs = []; const orig = console.error; +console.error = (m) => errs.push(String(m)); +wm._saveToVFS(fake); +console.error = orig; + +const promoted = calls.some(p => p === '/system/world-model.json'); +console.log(JSON.stringify({ calls, errs, promoted })); +process.exit(promoted || errs.length === 0 ? 1 : 0); |