From e452ce7785e5e47ec01a409d6da1f59564f8f365 Mon Sep 17 00:00:00 2001 From: Hermes SysOp Date: Thu, 12 Nov 2026 17:02:47 +0100 Subject: fix(newos4): stop reporting network and VFS size as facts we do not have (v4.6.161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//_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. --- modules/core/core-bundle.js | 152 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 144 insertions(+), 8 deletions(-) (limited to 'modules/core/core-bundle.js') diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index ddcac45..fe86c8f 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -14297,9 +14297,45 @@ maxFetchChars: this.maxFetchChars, } }; walk('/'); - data.vfs = { files: fileCount, dirs: dirCount, size: (JSON.stringify(fs.root || {}).length / 1024).toFixed(1) + 'KB' }; + // Stored bytes: the UTF-8 length of each FILE's content, summed. The + // old JSON.stringify(fs.root||{}).length counted UTF-16 code units + // plus the serialisation structure (braces, quotes, keys, commas) — + // ~2x for plain ASCII and wrong the other way for non-ASCII. This is + // the unit `vfs-size` and `doctor` report. + var u8 = function(s) { + if (typeof s !== 'string' || s.length === 0) return 0; + if (typeof Blob !== 'undefined') { try { return new Blob([s]).size; } catch (e) {} } + if (typeof TextEncoder !== 'undefined') { try { return new TextEncoder().encode(s).length; } catch (e) {} } + var n = 0; + for (var j = 0; j < s.length; j++) { + var cp = s.codePointAt(j); + if (cp > 0xffff) j++; // surrogate pair consumed + n += cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4; + } + return n; + }; + var measure = function(node, depth) { + var out = { bytes: 0, files: 0 }; + if (!node || !node.content || depth > 64) return out; + for (var k in node.content) { + if (!Object.prototype.hasOwnProperty.call(node.content, k)) continue; + var c = node.content[k]; + if (!c || typeof c !== 'object') continue; + if (c.type === 'file') { out.bytes += u8(c.content); out.files++; } + else { var sub = measure(c, depth + 1); out.bytes += sub.bytes; out.files += sub.files; } + } + return out; + }; + var vfsB = measure(fs.root, 0); + data.vfs = { + files: fileCount, dirs: dirCount, + sizeBytes: vfsB.bytes, + size: vfsB.bytes < 1024 ? vfsB.bytes + ' B' + : vfsB.bytes < 1048576 ? (vfsB.bytes / 1024).toFixed(1) + ' KB' + : (vfsB.bytes / 1048576).toFixed(2) + ' MB' + }; } - } catch (e) { data.vfs = { files: 0, dirs: 0, size: '0KB' }; } + } catch (e) { data.vfs = { files: 0, dirs: 0, size: '0 B', sizeBytes: 0 }; } // Ollama data.ollama = { @@ -14359,7 +14395,15 @@ maxFetchChars: this.maxFetchChars, data.health = { modules: data.modules.loaded + '/' + data.modules.total, vfsSize: data.vfs ? data.vfs.size : '?', - online: typeof navigator !== 'undefined' ? navigator.onLine : true, + vfsSizeBytes: data.vfs && typeof data.vfs.sizeBytes === 'number' ? data.vfs.sizeBytes : null, + // NO `online` boolean. `navigator.onLine` reports false on a fully + // connected Android WebView, which is how the debug panel and the + // dashboard told working users they were offline. `doctor` owns the only + // reachability verdict; SystemModule exposes no last verdict, so until one + // is probed the honest value is 'unknown'. + network: 'unknown', + networkKnown: false, + networkHint: 'run doctor for a live probe', errors: totalErrors, errorBreakdown: errCounts, predictorAccuracy: predictorAcc @@ -19954,6 +19998,10 @@ class PredictorModule { async _pushToServer() { const cfg = this._wdConfig(); if (!cfg) { this._storeMode = 'cache-only (no WebDAV config)'; return false; } + // The store is a /spaces//_data path, i.e. auth-gated. A config without credentials + // produces a guaranteed 401 on every boot (measured: 107 × 401 on predictor.json in one day) + // and that 401 is what the console reports as an error. No credentials, no request. + if (!cfg.user || !cfg.pass) { this._storeMode = 'cache-only (no credentials)'; return false; } try { const body = this.terminal.fs.readFile('/system/metrics/predictor.json') || '{}'; const put = () => fetch(this._storeUrl(cfg), { method: 'PUT', headers: this._storeHeaders(cfg), body: body }); @@ -19976,6 +20024,9 @@ class PredictorModule { async _pullFromServer(force) { const cfg = this._wdConfig(); if (!cfg) { this._storeMode = 'cache-only (no WebDAV config)'; return false; } + // Same rule as the push: an auth-gated path read without credentials is a 401, not a + // "server (empty)" answer, and it costs a console error on every page load. + if (!cfg.user || !cfg.pass) { this._storeMode = 'cache-only (no credentials)'; return false; } if (!force && this._cacheStamp && (Date.now() - this._cacheStamp) < this._STORE_TTL_MS) return false; try { const resp = await fetch(this._storeUrl(cfg), { headers: this._storeHeaders(cfg) }); @@ -20173,6 +20224,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']; @@ -20180,11 +20290,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', @@ -20217,7 +20334,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); @@ -20233,10 +20350,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 }; } @@ -20372,7 +20493,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'); @@ -20616,6 +20744,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; } -- cgit v1.2.3