aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-world-model.js
diff options
context:
space:
mode:
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;
}