summaryrefslogtreecommitdiff
path: root/modules/core/module-world-model.js
diff options
context:
space:
mode:
authorHermes SysOp <sysop@webx11.duckdns.org>2026-11-12 17:02:47 +0100
committerHermes SysOp <sysop@webx11.duckdns.org>2026-11-12 17:02:47 +0100
commite452ce7785e5e47ec01a409d6da1f59564f8f365 (patch)
treee2dc45ecf479e55df571045390bba3d927d8a4e2 /modules/core/module-world-model.js
parentf140aa7f920c687d0c576763da956dd7017c1a27 (diff)
fix(newos4): stop reporting network and VFS size as facts we do not have (v4.6.161)
1. The last two `navigator.onLine`-as-fact payloads - module-aiassist.js: the debug/health payload carried `online: navigator.onLine`, consumed by downloads/dashboard.html, which rendered "🔴 Offline" — on an Android WebView (reports false while fully connected) that told working users they were offline. The key is gone; the payload carries `network:'unknown'`, `networkKnown:false` and `networkHint`. - module-world-model.js: the snapshot had the same field, and it is also handed to the LLM through `_modelDigest()`/`ask()`, so the model was told the user was offline. Now `network:'unknown'` + `networkKnown:false`. - `_loadFromVFS()` scrubs `system.online` from a model persisted by an older build — otherwise the stale boolean would survive inside the digest for the life of the VFS. - downloads/dashboard.html: `networkText()` renders reachable/unreachable/http-* only when doctor produced them ("(doctor)"), `unprobed` as "— not probed", and everything else as "— unknown (run doctor for a live probe)". A legacy payload's `online:false` lands in "unknown", never "Offline". The row label is "Network", not "Online". - Neither payload invents a probe: SystemModule stores no last-doctor verdict, so 'unknown' is the honest value until one exists. 2. `vfsSize` in both payloads was a different unit again - aiassist: `JSON.stringify(fs.root).length / 1024`; world-model: `JSON.stringify(fs.root).length`. Same lie v4.6.160 fixed in `vfs-size` — UTF-16 code units plus serialisation structure. Both now sum UTF-8 bytes of file content (`_utf8Len`/`_measureVfs`, Blob → TextEncoder → code-point fallback, depth/node capped) and carry `sizeBytes` beside the human string so the rows reconcile. - world-model's `_gatherFileInfo().size` used a THIRD unit on an adjacent line (`JSON.stringify(fs.root).length/1024`); now the same measurement, plus `sizeBytes`. - `showModel()` renders exact bytes and tolerates a pre-fix persisted string. 3. Predictor made a guaranteed-401 request on every boot (UI-plan finding F6) - `_wdConfig()` returns `user:''`/`pass:''` when the saved WebDAV config has a URL but no credentials, and `_pullFromServer`/`_pushToServer` fetched the auth-gated `/spaces/<user>/_data/predictor.json` anyway — 107 × 401 in one day, logged by the browser as console errors. Both now refuse without credentials (`cache-only (no credentials)`). Verified: 42/42 node tests, 2 new files (38 payload/honesty assertions, 8 predictor-store assertions); the predictor suite is 4/8 against the pre-fix source, and the payload suite fails 14 assertions against HEAD (`online:false` present, vfsSize === JSON.stringify length). Live Chromium on v4.6.161, on the served dashboard: a legacy payload with `online:false` renders "Network — unknown (run doctor for a live probe)", the current payload renders "85.8 KB (87880 B)" and the same unknown, and a doctor-produced `unreachable` renders "⚠️ unreachable (doctor)". Served bytes identical for core-bundle.js, main.html and downloads/dashboard.html. `maintain.py --fix` all PASS.
Diffstat (limited to 'modules/core/module-world-model.js')
-rw-r--r--modules/core/module-world-model.js95
1 files changed, 90 insertions, 5 deletions
diff --git a/modules/core/module-world-model.js b/modules/core/module-world-model.js
index 6e8f3c3..f7e2bbb 100644
--- a/modules/core/module-world-model.js
+++ b/modules/core/module-world-model.js
@@ -153,6 +153,65 @@ class WorldModelModule {
}
}
+ // ─── Measurement helpers ───
+
+ // UTF-8 byte length of a string. `JSON.stringify(root).length` — the unit vfsSize used to
+ // report — is UTF-16 string units PLUS every structural character of the serialisation
+ // (braces, quotes, keys, commas): it measured 1961 for a 1046-byte tree, and counts 'é' as 1.
+ // Blob is exact in the browser and in Node 22; TextEncoder is the fallback, then a manual
+ // code-point count (never `s.length`, which would reintroduce the UTF-16 lie).
+ _utf8Len(s) {
+ if (typeof s !== 'string' || s.length === 0) return 0;
+ if (typeof Blob !== 'undefined') {
+ try { return new Blob([s]).size; } catch (e) { /* fall through */ }
+ }
+ if (typeof TextEncoder !== 'undefined') {
+ try { return new TextEncoder().encode(s).length; } catch (e) { /* fall through */ }
+ }
+ let n = 0;
+ for (let i = 0; i < s.length; i++) {
+ const cp = s.codePointAt(i);
+ if (cp > 0xffff) i++; // surrogate pair consumed
+ n += cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4;
+ }
+ return n;
+ }
+
+ // Stored bytes of a VFS subtree: the UTF-8 length of each FILE's content, summed. A directory
+ // node contributes nothing of its own, so a nested tree is never double counted. This is the
+ // same unit `vfs-size` and `doctor` report. Depth- and node-capped so a malformed or cyclic
+ // tree cannot hang synthesis.
+ _measureVfs(root) {
+ const out = { bytes: 0, files: 0 };
+ let nodes = 0;
+ const walk = (node, depth) => {
+ if (!node || typeof node !== 'object' || depth > 64) return;
+ const kids = node.content;
+ if (!kids || typeof kids !== 'object') return;
+ for (const name of Object.keys(kids)) {
+ if (++nodes > 20000) return;
+ const child = kids[name];
+ if (!child || typeof child !== 'object') continue;
+ if (child.type === 'file') {
+ out.bytes += this._utf8Len(child.content);
+ out.files += 1;
+ } else {
+ walk(child, depth + 1);
+ }
+ }
+ };
+ walk(root, 0);
+ return out;
+ }
+
+ // Human form for display; the exact byte count is printed beside it so the rows reconcile.
+ _fmtBytes(bytes) {
+ if (typeof bytes !== 'number' || !isFinite(bytes)) return '?';
+ if (bytes < 1024) return bytes + ' B';
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
+ return (bytes / 1024 / 1024).toFixed(2) + ' MB';
+ }
+
_gatherSystemInfo(fs, moduleCount, version) {
// Get real health data from ServerMonitorModule's _latencyLog and DebugSupervisorModule
const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule'];
@@ -160,11 +219,18 @@ class WorldModelModule {
const errorCount = ds ? Object.keys(ds._errorDB?.errors || {}).length : 0;
const latencyErrors = sm?._latencyLog ? Object.values(sm._latencyLog).filter(e => e.status === 'error').length : 0;
const totalErrors = errorCount + latencyErrors;
+ const vfs = this._measureVfs(fs?.root || null);
return {
version: `newOS4 ${version}`,
modules: moduleCount || Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0,
- vfsSize: JSON.stringify(fs?.root || {}).length,
- online: typeof navigator !== 'undefined' ? navigator.onLine : true,
+ // Stored bytes of file content — the one unit `vfs-size` and `doctor` report.
+ vfsSize: vfs.bytes,
+ vfsFiles: vfs.files,
+ // No reachability verdict is manufactured here. `navigator.onLine` reports false on a
+ // fully connected Android WebView, and `doctor` is the only place that probes. Until
+ // SystemModule exposes a last-doctor verdict, 'unknown' is the honest value.
+ network: 'unknown',
+ networkKnown: false,
memory: typeof performance !== 'undefined' && performance.memory
? `${(performance.memory.usedJSHeapSize / 1048576).toFixed(1)}MB`
: 'N/A',
@@ -197,7 +263,7 @@ class WorldModelModule {
}
_gatherFileInfo(fs) {
- if (!fs) return { total: 0, size: '0KB' };
+ if (!fs) return { total: 0, size: '0 B', sizeBytes: 0 };
let fileCount = 0, dirCount = 0;
const walk = (path) => {
const entries = fs.ls(path);
@@ -213,10 +279,14 @@ class WorldModelModule {
}
};
walk('/');
+ // Same unit as system.vfsSize — the two are printed on adjacent lines, so they must
+ // reconcile. Stored UTF-8 bytes of file content, not JSON.stringify(fs.root).length.
+ const vfs = this._measureVfs(fs.root);
return {
total: fileCount,
directories: dirCount,
- size: `${(JSON.stringify(fs.root).length / 1024).toFixed(1)}KB`
+ size: this._fmtBytes(vfs.bytes),
+ sizeBytes: vfs.bytes
};
}
@@ -352,7 +422,14 @@ class WorldModelModule {
this.terminal.print('🌐 World Model', 'header');
this.terminal.print(` Version: ${m.version} (last synthsized: ${new Date(m.synthesized).toLocaleTimeString()})`, 'info');
- this.terminal.print(` ⚙️ System — ${m.system.modules} modules loaded, ${m.system.vfsSize} VFS, ${m.system.memory}`, 'info');
+ // Tolerate a model loaded from VFS that predates the byte fix (vfsSize was a "1.9KB"
+ // string then): render whatever shape is there instead of "? B".
+ const vfsLine = typeof m.system.vfsSize === 'number'
+ ? (m.system.vfsSize < 1024
+ ? `${m.system.vfsSize} bytes`
+ : `${this._fmtBytes(m.system.vfsSize)} (${m.system.vfsSize} bytes)`)
+ : String(m.system.vfsSize);
+ this.terminal.print(` ⚙️ System — ${m.system.modules} modules loaded, ${vfsLine} VFS, ${m.system.memory}`, 'info');
this.terminal.print(` 🧩 Modules — ${m.modules.loaded} loaded (${m.modules.modifiedCount} modified)`, 'info');
this.terminal.print(` 📁 Files — ${m.files.total} files, ${m.files.directories} dirs, ${m.files.size}`, 'info');
this.terminal.print(` 📚 Skills — ${m.skills.count} installed`, 'info');
@@ -596,6 +673,14 @@ class WorldModelModule {
if (!fs) return;
const data = fs.fsReadJSON('/system/world-model.json');
if (data) {
+ // A model persisted by a pre-v4.6.161 build carries `system.online` — a
+ // navigator.onLine boolean the digest would hand to the LLM as fact. Scrub it on
+ // load: no probe has run in this session, so 'unknown' is the honest value.
+ if (data.system && typeof data.system === 'object' && 'online' in data.system) {
+ delete data.system.online;
+ if (typeof data.system.network === 'undefined') data.system.network = 'unknown';
+ data.system.networkKnown = false;
+ }
this._model = data;
this._lastRefresh = data.synthesized;
}