diff options
Diffstat (limited to 'modules/core')
| -rw-r--r-- | modules/core/core-bundle.js | 47 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/core/module-predictor.js | 15 | ||||
| -rw-r--r-- | modules/core/virtual-fs.js | 32 |
4 files changed, 81 insertions, 15 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index cd510b7..84d194d 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -942,6 +942,17 @@ class VirtualFileSystem { } checkpoint(name) { if (!name) name = new Date().toISOString(); + name = String(name).trim(); + if (!name || name.indexOf('/') !== -1) { + throw new Error('checkpoint name must be non-empty and must not contain "/"'); + } + // Two unnamed calls can land in the same millisecond; without this the second silently + // overwrites the first (measured: 6 calls -> 3 distinct names). + if (this.listCheckpoints().indexOf(name) !== -1) { + let n = 2; + while (this.listCheckpoints().indexOf(name + '-' + n) !== -1) n++; + name = name + '-' + n; + } const createdAt = new Date().toISOString(); // Strip system dirs to prevent fractal bloat const rootClone = JSON.parse(JSON.stringify(this.root)); @@ -961,12 +972,15 @@ class VirtualFileSystem { } this.fsWriteJSON('/_snapshots/checkpoint-' + name + '.json', data); this.saveToStorage(); - // Auto-manage: keep max 3 checkpoints, delete oldest + // Auto-manage: keep the 3 NEWEST checkpoints — by createdAt, not by name, so a custom + // name ("aaa-oldest") cannot pin itself forever while newer snapshots are deleted. const all = this.listCheckpoints(); if (all.length > 3) { - const sorted = all.slice().sort(); - const toDelete = sorted.slice(0, all.length - 3); - for (const old of toDelete) this.deleteCheckpoint(old); + const aged = all.map(n => { + const snap = this.fsReadJSON('/_snapshots/checkpoint-' + n + '.json'); + return { name: n, at: (snap && snap.createdAt) || '' }; + }).sort((a, b) => (a.at < b.at ? -1 : (a.at > b.at ? 1 : 0))); + for (const old of aged.slice(0, aged.length - 3)) this.deleteCheckpoint(old.name); } return name; } @@ -976,8 +990,18 @@ class VirtualFileSystem { console.error('Rollback failed: checkpoint not found or invalid'); return false; } + // The snapshot deliberately omits scaffolding (bloat guard at :944-950). Restoring the + // clone verbatim deletes every checkpoint, every agent session and the trash, so keep the + // live copies and re-attach them after the root swap. + const keep = {}; + for (const dir of ['_snapshots', '_sessions', '.trash']) { + const node = this.root && this.root.content ? this.root.content[dir] : null; + if (node) keep[dir] = node; + } this.root = raw.root; this.currentPath = raw.currentPath || []; + if (!this.root.content) this.root.content = {}; + for (const dir of Object.keys(keep)) this.root.content[dir] = keep[dir]; this.saveToStorage(); return true; } @@ -18261,6 +18285,7 @@ class PredictorModule { return this._dataPoints.filter(dp => { if (!dp.observedDuration) return false; // outcomes only, never predictions if (dp.usable === false) return false; // aborted runs are not evidence + if (!(dp.observedDuration >= 1.5)) return false; // belt and braces: never evidence if (dp.features) return this._similarity(features, dp.features) > 0.3; return false; }); @@ -18412,7 +18437,7 @@ class PredictorModule { // ─── Commands ─── showStatus() { - const actualExecutions = this._dataPoints.filter(dp => dp.observedDuration).length; + const actualExecutions = this._dataPoints.filter(dp => dp.observedDuration && dp.usable !== false).length; const paired = this._accuracy.total; const unpaired = this._dataPoints.filter(dp => dp.paired === false).length; const accPct = paired > 0 ? ((this._accuracy.correct / paired) * 100).toFixed(0) + '%' : 'n/a'; @@ -18517,8 +18542,16 @@ class PredictorModule { const data = fs.fsReadJSON('/system/metrics/predictor.json'); if (!data) return; - // Prediction-only points are noise for similarity; keep outcomes. - this._dataPoints = (data.dataPoints || []).filter(dp => dp && dp.observedDuration); + // Legacy (pre-v2) points carry no `usable`. Backfill it from the measurement — an + // aborted run must not enter similarity as evidence (measured: a 0.079 s point + // dragged avgDuration to ~0 and floored every prediction at 1 s). + this._dataPoints = (data.dataPoints || []) + .filter(dp => dp && dp.observedDuration) + .map(dp => { + if (typeof dp.usable !== 'boolean') dp.usable = dp.observedDuration >= 1.5; + return dp; + }) + .filter(dp => dp.usable !== false); this._predictions = data.predictions || 0; if (data.v === this._SCHEMA) { diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index b278f51..18137ec 100644 --- a/modules/core/module-loader.js +++ b/modules/core/module-loader.js @@ -4,7 +4,7 @@ // Core module bundle loaded synchronously in main.html. // This loader only handles version cache-busting, splash, terminal init, and optional ext modules. - const LATEST_VER = 'v4.6.102'; + const LATEST_VER = 'v4.6.103'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { diff --git a/modules/core/module-predictor.js b/modules/core/module-predictor.js index aebda87..b534d34 100644 --- a/modules/core/module-predictor.js +++ b/modules/core/module-predictor.js @@ -118,6 +118,7 @@ class PredictorModule { return this._dataPoints.filter(dp => { if (!dp.observedDuration) return false; // outcomes only, never predictions if (dp.usable === false) return false; // aborted runs are not evidence + if (!(dp.observedDuration >= 1.5)) return false; // belt and braces: never evidence if (dp.features) return this._similarity(features, dp.features) > 0.3; return false; }); @@ -269,7 +270,7 @@ class PredictorModule { // ─── Commands ─── showStatus() { - const actualExecutions = this._dataPoints.filter(dp => dp.observedDuration).length; + const actualExecutions = this._dataPoints.filter(dp => dp.observedDuration && dp.usable !== false).length; const paired = this._accuracy.total; const unpaired = this._dataPoints.filter(dp => dp.paired === false).length; const accPct = paired > 0 ? ((this._accuracy.correct / paired) * 100).toFixed(0) + '%' : 'n/a'; @@ -374,8 +375,16 @@ class PredictorModule { const data = fs.fsReadJSON('/system/metrics/predictor.json'); if (!data) return; - // Prediction-only points are noise for similarity; keep outcomes. - this._dataPoints = (data.dataPoints || []).filter(dp => dp && dp.observedDuration); + // Legacy (pre-v2) points carry no `usable`. Backfill it from the measurement — an + // aborted run must not enter similarity as evidence (measured: a 0.079 s point + // dragged avgDuration to ~0 and floored every prediction at 1 s). + this._dataPoints = (data.dataPoints || []) + .filter(dp => dp && dp.observedDuration) + .map(dp => { + if (typeof dp.usable !== 'boolean') dp.usable = dp.observedDuration >= 1.5; + return dp; + }) + .filter(dp => dp.usable !== false); this._predictions = data.predictions || 0; if (data.v === this._SCHEMA) { diff --git a/modules/core/virtual-fs.js b/modules/core/virtual-fs.js index 09b3548..3ed1b40 100644 --- a/modules/core/virtual-fs.js +++ b/modules/core/virtual-fs.js @@ -940,6 +940,17 @@ class VirtualFileSystem { } checkpoint(name) { if (!name) name = new Date().toISOString(); + name = String(name).trim(); + if (!name || name.indexOf('/') !== -1) { + throw new Error('checkpoint name must be non-empty and must not contain "/"'); + } + // Two unnamed calls can land in the same millisecond; without this the second silently + // overwrites the first (measured: 6 calls -> 3 distinct names). + if (this.listCheckpoints().indexOf(name) !== -1) { + let n = 2; + while (this.listCheckpoints().indexOf(name + '-' + n) !== -1) n++; + name = name + '-' + n; + } const createdAt = new Date().toISOString(); // Strip system dirs to prevent fractal bloat const rootClone = JSON.parse(JSON.stringify(this.root)); @@ -959,12 +970,15 @@ class VirtualFileSystem { } this.fsWriteJSON('/_snapshots/checkpoint-' + name + '.json', data); this.saveToStorage(); - // Auto-manage: keep max 3 checkpoints, delete oldest + // Auto-manage: keep the 3 NEWEST checkpoints — by createdAt, not by name, so a custom + // name ("aaa-oldest") cannot pin itself forever while newer snapshots are deleted. const all = this.listCheckpoints(); if (all.length > 3) { - const sorted = all.slice().sort(); - const toDelete = sorted.slice(0, all.length - 3); - for (const old of toDelete) this.deleteCheckpoint(old); + const aged = all.map(n => { + const snap = this.fsReadJSON('/_snapshots/checkpoint-' + n + '.json'); + return { name: n, at: (snap && snap.createdAt) || '' }; + }).sort((a, b) => (a.at < b.at ? -1 : (a.at > b.at ? 1 : 0))); + for (const old of aged.slice(0, aged.length - 3)) this.deleteCheckpoint(old.name); } return name; } @@ -974,8 +988,18 @@ class VirtualFileSystem { console.error('Rollback failed: checkpoint not found or invalid'); return false; } + // The snapshot deliberately omits scaffolding (bloat guard at :944-950). Restoring the + // clone verbatim deletes every checkpoint, every agent session and the trash, so keep the + // live copies and re-attach them after the root swap. + const keep = {}; + for (const dir of ['_snapshots', '_sessions', '.trash']) { + const node = this.root && this.root.content ? this.root.content[dir] : null; + if (node) keep[dir] = node; + } this.root = raw.root; this.currentPath = raw.currentPath || []; + if (!this.root.content) this.root.content = {}; + for (const dir of Object.keys(keep)) this.root.content[dir] = keep[dir]; this.saveToStorage(); return true; } |