aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/core/module-memory-graph.js12
-rw-r--r--modules/core/module-world-model.js28
-rw-r--r--modules/ext/skill-pack-core.js5
-rw-r--r--modules/test/test-world-model-save.js24
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);