aboutsummaryrefslogtreecommitdiff
path: root/modules/core
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-23 17:46:41 +0200
committerAgando <roduccomm@gmail.com>2026-10-23 17:46:41 +0200
commit71d78fa055d00ed51a9067c441d562807ea95404 (patch)
tree2a845a877a0fd2a1ee424ffa800a7ed205955d49 /modules/core
parent7f4732e5089b1430e736bc6a8b8578a674f31f36 (diff)
fix(newos4): predictor arithmetic, deployed-source viewer, ls text output (v4.6.57)
Acting on the "Request to the newOS4 team" packet (spaces/agando/guides/, re-verified by its author on 4.6.56). Three verified defects fixed: Packet 5 (predictor arithmetic) - module-predictor.js - the persisted field named mape held a SUM while predictor-status divided by the paired count: renamed to errorSum, older payloads read through - errorPct is stored raw; the 500% clamp is now render-only, so a 475x miss stays visible instead of being flattened to the ceiling - predictions are floored at 1 s and marked floored; a floored prediction is consumed but never scored as 100% error (a 0 s prediction is not a prediction) - observations shorter than 1.5 s (aborted/instant runs) are marked unusable and no longer train _findSimilar - one 0.079 s run was rounding the next prediction to 0 s - status prints an unpaired and an unscored count alongside the mean Packet 10 (verification cannot see its own fixes) - module-aiassist.js - view_module now returns the DEPLOYED source, prefixed with file and version, and only falls back to the _mods copy with a "may be stale" banner. The unversioned _mods/<Class>.js edit cache used to shadow the deployed file forever, so the viewer showed pre-fix code while the bundle was new. Proven by seeding a stale _mods/PredictorModule.js and calling the tool. Packet 1b (ls exits clean with no output) - module-fs.js - ls rendered only via printClickable(), which writes straight to the DOM, so any capture (AI run_command, diagnostics) saw an empty result and reported "empty directory". It now always prints a text line and keeps the clickable chips when nothing is capturing. Test: modules/test/test-predictor-accuracy.js 34/34 (new cases Q, R, S).
Diffstat (limited to 'modules/core')
-rw-r--r--modules/core/core-bundle.js77
-rw-r--r--modules/core/module-aiassist.js14
-rw-r--r--modules/core/module-fs.js8
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/module-predictor.js55
5 files changed, 105 insertions, 51 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index 9ac94ed..c654021 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -6391,7 +6391,13 @@ class FileSystemModule {
const total = filtered.reduce((s, f) => s + (f.size || 0), 0);
this.terminal.print('total ' + filtered.length + ' (' + total + ' bytes)', 'text-muted');
} else {
- this.terminal.printClickable(filtered);
+ // v4.6.57: printClickable() writes straight to the DOM, so anything that
+ // captures output (the AI run_command tool, the diagnostics sweep) saw nothing
+ // and reported "directory empty" (packet §1b). Always emit a text line.
+ const names = filtered.map(f => f.name + (f.type === 'dir' ? '/' : '')).join(' ');
+ this.terminal.print(names);
+ const capturing = !!(this.terminal._printCaptureStack && this.terminal._printCaptureStack.length);
+ if (!capturing && typeof this.terminal.printClickable === 'function') this.terminal.printClickable(filtered);
}
};
this.commands['cat'] = (args) => {
@@ -11315,9 +11321,11 @@ buildSystemPrompt() {
case 'view_module': {
const modName = a.name || '';
const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName);
- const code = this.terminal.fs.readFile(`_mods/${modName}.js`);
- if (code) return code;
const url = entry ? entry.url : null;
+ // v4.6.57: deployed source FIRST. The unversioned `_mods/<Name>.js` edit cache
+ // used to shadow it forever, so view_module returned pre-fix code while the
+ // running bundle was new (packet §10). The cache is only a labelled fallback.
+ const deployedVer = (typeof localStorage !== 'undefined' && localStorage.getItem('webx11_cache_ver')) || 'dev';
// Try registry URL first
if (url) {
try {
@@ -11325,7 +11333,7 @@ buildSystemPrompt() {
if (resp.ok) {
const src = await resp.text();
this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, src);
- return src;
+ return '// deployed: ' + url + ' @ ' + deployedVer + '\n' + src;
}
} catch (e) { /* fall through */ }
}
@@ -11347,11 +11355,15 @@ buildSystemPrompt() {
if (resp.ok) {
const src = await resp.text();
this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, src);
- return src;
+ return '// deployed: ' + fetchUrl + ' @ ' + deployedVer + '\n' + src;
}
}
}
} catch (e) { /* fall through */ }
+ const cachedSrc = this.terminal.fs.readFile(`_mods/${modName}.js`);
+ if (cachedSrc) {
+ return '// VFS COPY of ' + modName + '.js — may be stale (not the deployed file)\n' + cachedSrc;
+ }
return JSON.stringify({ error: `Cannot find source for ${modName} — not in registry, not in modules-index.json` });
}
case 'edit_module': {
@@ -17771,8 +17783,8 @@ class PredictorModule {
this._pending = []; // predictions waiting for an outcome (bounded, TTL'd)
this._predictions = 0;
this._accuracy = { correct: 0, total: 0 }; // total = paired comparisons only
- this._mape = 0; // sum of capped |predicted-observed|/observed
- this._turnsErr = 0; // sum of |predicted turns - observed turns|
+ this._errorSum = 0; // SUM of raw |predicted-observed|/observed (mean = /paired)
+ this._turnsErr = 0; // sum of |predicted turns - observed turns|
this._CONFIDENCE_THRESHOLD = 0.5;
this._MIN_DATA_POINTS = 5;
this._PAIR_TTL_MS = 15 * 60 * 1000;
@@ -17874,8 +17886,9 @@ class PredictorModule {
_findSimilar(features) {
return this._dataPoints.filter(dp => {
if (!dp.observedDuration) return false; // outcomes only, never predictions
- if (!dp.features) return false;
- return this._similarity(features, dp.features) > 0.3;
+ if (dp.usable === false) return false; // aborted runs are not evidence
+ if (dp.features) return this._similarity(features, dp.features) > 0.3;
+ return false;
});
}
@@ -17892,7 +17905,8 @@ class PredictorModule {
duration: baseDuration,
toolTurns: baseTurns,
confidence: 0.1,
- similarCount: 0
+ similarCount: 0,
+ floored: false
};
}
@@ -17903,13 +17917,16 @@ class PredictorModule {
const confidence = Math.min(similar.length / minPts, 1) * 0.8 + 0.1;
const complexityMultiplier = 1 + (features.complexity - 3) * 0.1;
- const adjustedDuration = Math.round(avgDuration * Math.max(complexityMultiplier, 0.5));
+ const adjustedDuration = avgDuration * Math.max(complexityMultiplier, 0.5);
+ const floored = adjustedDuration < 1; // no prediction is "0 seconds"
+ const duration = Math.max(1, Math.round(adjustedDuration));
return {
- duration: adjustedDuration,
- toolTurns: Math.round(avgTurns),
+ duration,
+ toolTurns: Math.max(1, Math.round(avgTurns)),
confidence: Math.min(confidence, 0.95),
- similarCount: similar.length
+ similarCount: similar.length,
+ floored
};
}
@@ -17937,6 +17954,7 @@ class PredictorModule {
predictedDuration: p.duration,
predictedTurns: p.toolTurns,
confidence: p.confidence,
+ floored: p.floored === true,
source: source || 'loop',
ts: Date.now()
};
@@ -17992,18 +18010,23 @@ class PredictorModule {
timestamp: new Date().toISOString()
};
+ rec.usable = observed >= 1.5; // aborted/instant runs say nothing about duration
+
if (pred) {
const absErr = Math.abs(pred.predictedDuration - observed);
rec.predictedDuration = pred.predictedDuration;
rec.predictedTurns = pred.predictedTurns;
- // capped: one 15s-vs-1s miss must not poison the mean-error readout forever
- rec.errorPct = Math.min(observed > 0 ? absErr / observed : 5, 5);
+ // RAW error — the renderer clamps, the store does not (a 475x miss must stay 475x)
+ rec.errorPct = observed > 0 ? absErr / observed : null;
rec.turnsError = Math.abs((pred.predictedTurns || 0) - (toolTurns || 0));
- this._accuracy.total++;
- this._mape += rec.errorPct;
- this._turnsErr += rec.turnsError;
- // 30% relative, with a 3 s floor so rounding on short tasks does not flip the verdict
- if (absErr <= Math.max(0.30 * observed, 3)) this._accuracy.correct++;
+ rec.scored = rec.usable && rec.errorPct !== null && pred.floored !== true;
+ if (rec.scored) {
+ this._accuracy.total++;
+ this._errorSum += rec.errorPct;
+ this._turnsErr += rec.turnsError;
+ // 30% relative, with a 3 s floor so rounding on short tasks does not flip the verdict
+ if (absErr <= Math.max(0.30 * observed, 3)) this._accuracy.correct++;
+ }
}
this._dataPoints.push(rec);
@@ -18019,9 +18042,12 @@ class PredictorModule {
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';
+ const meanErr = paired > 0 ? this._errorSum / paired : 0;
const mape = paired > 0
- ? ` · mean error ${((this._mape / paired) * 100).toFixed(0)}% · turns ±${(this._turnsErr / paired).toFixed(1)}`
+ ? ` · mean error ${(Math.min(meanErr, 5) * 100).toFixed(0)}%${meanErr > 5 ? '+' : ''}` +
+ ` · turns ±${(this._turnsErr / paired).toFixed(1)}`
: '';
+ const unscored = this._dataPoints.filter(dp => dp.scored === false).length;
this.terminal.print('📊 Predictor Status', 'header');
this.terminal.print(` Total data points: ${this._dataPoints.length}`, 'info');
@@ -18030,6 +18056,7 @@ class PredictorModule {
this.terminal.print(` Accuracy: ${accPct} (${this._accuracy.correct}/${paired} paired)${mape}`, 'info');
this.terminal.print(` Awaiting outcome: ${this._pending.length}`, 'info');
this.terminal.print(` Unpaired runs: ${unpaired}`, 'info');
+ this.terminal.print(` Unscored pairs: ${unscored} (aborted run or floored prediction)`, 'info');
this.terminal.print(` Confidence threshold: ${this._CONFIDENCE_THRESHOLD}`, 'info');
this.terminal.print(` Min data points: ${this._MIN_DATA_POINTS}`, 'info');
this.terminal.print(` Pair TTL: ${Math.round(this._PAIR_TTL_MS / 60000)}m`, 'info');
@@ -18082,7 +18109,7 @@ class PredictorModule {
if (data.v === this._SCHEMA) {
const acc = data.accuracy || {};
this._accuracy = { correct: acc.correct || 0, total: acc.total || 0 };
- this._mape = data.mape || 0;
+ this._errorSum = (typeof data.errorSum === 'number') ? data.errorSum : (data.mape || 0);
this._turnsErr = data.turnsErr || 0;
const cfg = data.config || {};
if (typeof cfg.confidenceThreshold === 'number') this._CONFIDENCE_THRESHOLD = cfg.confidenceThreshold;
@@ -18093,7 +18120,7 @@ class PredictorModule {
} else {
// v1 counted every observation in `total` but only some in `correct` → 0%.
this._accuracy = { correct: 0, total: 0 };
- this._mape = 0;
+ this._errorSum = 0;
this._pending = [];
}
} catch (e) {
@@ -18111,7 +18138,7 @@ class PredictorModule {
dataPoints: this._dataPoints.slice(-500),
predictions: this._predictions,
accuracy: this._accuracy,
- mape: this._mape,
+ errorSum: this._errorSum,
turnsErr: this._turnsErr,
config: {
confidenceThreshold: this._CONFIDENCE_THRESHOLD,
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index 83d073a..628e68a 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -3094,9 +3094,11 @@ buildSystemPrompt() {
case 'view_module': {
const modName = a.name || '';
const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName);
- const code = this.terminal.fs.readFile(`_mods/${modName}.js`);
- if (code) return code;
const url = entry ? entry.url : null;
+ // v4.6.57: deployed source FIRST. The unversioned `_mods/<Name>.js` edit cache
+ // used to shadow it forever, so view_module returned pre-fix code while the
+ // running bundle was new (packet §10). The cache is only a labelled fallback.
+ const deployedVer = (typeof localStorage !== 'undefined' && localStorage.getItem('webx11_cache_ver')) || 'dev';
// Try registry URL first
if (url) {
try {
@@ -3104,7 +3106,7 @@ buildSystemPrompt() {
if (resp.ok) {
const src = await resp.text();
this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, src);
- return src;
+ return '// deployed: ' + url + ' @ ' + deployedVer + '\n' + src;
}
} catch (e) { /* fall through */ }
}
@@ -3126,11 +3128,15 @@ buildSystemPrompt() {
if (resp.ok) {
const src = await resp.text();
this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, src);
- return src;
+ return '// deployed: ' + fetchUrl + ' @ ' + deployedVer + '\n' + src;
}
}
}
} catch (e) { /* fall through */ }
+ const cachedSrc = this.terminal.fs.readFile(`_mods/${modName}.js`);
+ if (cachedSrc) {
+ return '// VFS COPY of ' + modName + '.js — may be stale (not the deployed file)\n' + cachedSrc;
+ }
return JSON.stringify({ error: `Cannot find source for ${modName} — not in registry, not in modules-index.json` });
}
case 'edit_module': {
diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js
index 686a1a6..03c3733 100644
--- a/modules/core/module-fs.js
+++ b/modules/core/module-fs.js
@@ -63,7 +63,13 @@ class FileSystemModule {
const total = filtered.reduce((s, f) => s + (f.size || 0), 0);
this.terminal.print('total ' + filtered.length + ' (' + total + ' bytes)', 'text-muted');
} else {
- this.terminal.printClickable(filtered);
+ // v4.6.57: printClickable() writes straight to the DOM, so anything that
+ // captures output (the AI run_command tool, the diagnostics sweep) saw nothing
+ // and reported "directory empty" (packet §1b). Always emit a text line.
+ const names = filtered.map(f => f.name + (f.type === 'dir' ? '/' : '')).join(' ');
+ this.terminal.print(names);
+ const capturing = !!(this.terminal._printCaptureStack && this.terminal._printCaptureStack.length);
+ if (!capturing && typeof this.terminal.printClickable === 'function') this.terminal.printClickable(filtered);
}
};
this.commands['cat'] = (args) => {
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index 8b4f606..b31fd83 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.56';
+ const LATEST_VER = 'v4.6.57';
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 cb1209e..c8d526d 100644
--- a/modules/core/module-predictor.js
+++ b/modules/core/module-predictor.js
@@ -11,8 +11,8 @@ class PredictorModule {
this._pending = []; // predictions waiting for an outcome (bounded, TTL'd)
this._predictions = 0;
this._accuracy = { correct: 0, total: 0 }; // total = paired comparisons only
- this._mape = 0; // sum of capped |predicted-observed|/observed
- this._turnsErr = 0; // sum of |predicted turns - observed turns|
+ this._errorSum = 0; // SUM of raw |predicted-observed|/observed (mean = /paired)
+ this._turnsErr = 0; // sum of |predicted turns - observed turns|
this._CONFIDENCE_THRESHOLD = 0.5;
this._MIN_DATA_POINTS = 5;
this._PAIR_TTL_MS = 15 * 60 * 1000;
@@ -114,8 +114,9 @@ class PredictorModule {
_findSimilar(features) {
return this._dataPoints.filter(dp => {
if (!dp.observedDuration) return false; // outcomes only, never predictions
- if (!dp.features) return false;
- return this._similarity(features, dp.features) > 0.3;
+ if (dp.usable === false) return false; // aborted runs are not evidence
+ if (dp.features) return this._similarity(features, dp.features) > 0.3;
+ return false;
});
}
@@ -132,7 +133,8 @@ class PredictorModule {
duration: baseDuration,
toolTurns: baseTurns,
confidence: 0.1,
- similarCount: 0
+ similarCount: 0,
+ floored: false
};
}
@@ -143,13 +145,16 @@ class PredictorModule {
const confidence = Math.min(similar.length / minPts, 1) * 0.8 + 0.1;
const complexityMultiplier = 1 + (features.complexity - 3) * 0.1;
- const adjustedDuration = Math.round(avgDuration * Math.max(complexityMultiplier, 0.5));
+ const adjustedDuration = avgDuration * Math.max(complexityMultiplier, 0.5);
+ const floored = adjustedDuration < 1; // no prediction is "0 seconds"
+ const duration = Math.max(1, Math.round(adjustedDuration));
return {
- duration: adjustedDuration,
- toolTurns: Math.round(avgTurns),
+ duration,
+ toolTurns: Math.max(1, Math.round(avgTurns)),
confidence: Math.min(confidence, 0.95),
- similarCount: similar.length
+ similarCount: similar.length,
+ floored
};
}
@@ -177,6 +182,7 @@ class PredictorModule {
predictedDuration: p.duration,
predictedTurns: p.toolTurns,
confidence: p.confidence,
+ floored: p.floored === true,
source: source || 'loop',
ts: Date.now()
};
@@ -232,18 +238,23 @@ class PredictorModule {
timestamp: new Date().toISOString()
};
+ rec.usable = observed >= 1.5; // aborted/instant runs say nothing about duration
+
if (pred) {
const absErr = Math.abs(pred.predictedDuration - observed);
rec.predictedDuration = pred.predictedDuration;
rec.predictedTurns = pred.predictedTurns;
- // capped: one 15s-vs-1s miss must not poison the mean-error readout forever
- rec.errorPct = Math.min(observed > 0 ? absErr / observed : 5, 5);
+ // RAW error — the renderer clamps, the store does not (a 475x miss must stay 475x)
+ rec.errorPct = observed > 0 ? absErr / observed : null;
rec.turnsError = Math.abs((pred.predictedTurns || 0) - (toolTurns || 0));
- this._accuracy.total++;
- this._mape += rec.errorPct;
- this._turnsErr += rec.turnsError;
- // 30% relative, with a 3 s floor so rounding on short tasks does not flip the verdict
- if (absErr <= Math.max(0.30 * observed, 3)) this._accuracy.correct++;
+ rec.scored = rec.usable && rec.errorPct !== null && pred.floored !== true;
+ if (rec.scored) {
+ this._accuracy.total++;
+ this._errorSum += rec.errorPct;
+ this._turnsErr += rec.turnsError;
+ // 30% relative, with a 3 s floor so rounding on short tasks does not flip the verdict
+ if (absErr <= Math.max(0.30 * observed, 3)) this._accuracy.correct++;
+ }
}
this._dataPoints.push(rec);
@@ -259,9 +270,12 @@ class PredictorModule {
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';
+ const meanErr = paired > 0 ? this._errorSum / paired : 0;
const mape = paired > 0
- ? ` · mean error ${((this._mape / paired) * 100).toFixed(0)}% · turns ±${(this._turnsErr / paired).toFixed(1)}`
+ ? ` · mean error ${(Math.min(meanErr, 5) * 100).toFixed(0)}%${meanErr > 5 ? '+' : ''}` +
+ ` · turns ±${(this._turnsErr / paired).toFixed(1)}`
: '';
+ const unscored = this._dataPoints.filter(dp => dp.scored === false).length;
this.terminal.print('📊 Predictor Status', 'header');
this.terminal.print(` Total data points: ${this._dataPoints.length}`, 'info');
@@ -270,6 +284,7 @@ class PredictorModule {
this.terminal.print(` Accuracy: ${accPct} (${this._accuracy.correct}/${paired} paired)${mape}`, 'info');
this.terminal.print(` Awaiting outcome: ${this._pending.length}`, 'info');
this.terminal.print(` Unpaired runs: ${unpaired}`, 'info');
+ this.terminal.print(` Unscored pairs: ${unscored} (aborted run or floored prediction)`, 'info');
this.terminal.print(` Confidence threshold: ${this._CONFIDENCE_THRESHOLD}`, 'info');
this.terminal.print(` Min data points: ${this._MIN_DATA_POINTS}`, 'info');
this.terminal.print(` Pair TTL: ${Math.round(this._PAIR_TTL_MS / 60000)}m`, 'info');
@@ -322,7 +337,7 @@ class PredictorModule {
if (data.v === this._SCHEMA) {
const acc = data.accuracy || {};
this._accuracy = { correct: acc.correct || 0, total: acc.total || 0 };
- this._mape = data.mape || 0;
+ this._errorSum = (typeof data.errorSum === 'number') ? data.errorSum : (data.mape || 0);
this._turnsErr = data.turnsErr || 0;
const cfg = data.config || {};
if (typeof cfg.confidenceThreshold === 'number') this._CONFIDENCE_THRESHOLD = cfg.confidenceThreshold;
@@ -333,7 +348,7 @@ class PredictorModule {
} else {
// v1 counted every observation in `total` but only some in `correct` → 0%.
this._accuracy = { correct: 0, total: 0 };
- this._mape = 0;
+ this._errorSum = 0;
this._pending = [];
}
} catch (e) {
@@ -351,7 +366,7 @@ class PredictorModule {
dataPoints: this._dataPoints.slice(-500),
predictions: this._predictions,
accuracy: this._accuracy,
- mape: this._mape,
+ errorSum: this._errorSum,
turnsErr: this._turnsErr,
config: {
confidenceThreshold: this._CONFIDENCE_THRESHOLD,