aboutsummaryrefslogtreecommitdiff
path: root/modules/core
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-06-10 06:00:30 +0200
committerAgando <roduccomm@gmail.com>2026-06-10 06:00:30 +0200
commit39b2df36ee5ccea52558d0537bfee87058558e94 (patch)
tree7cac66cf09b128c07fa0300da0a46d016d4459e5 /modules/core
parente8d4985bbb60797936d31b65604a7f94c76bf623 (diff)
auto: 2026-06-10_06:00 changes
Diffstat (limited to 'modules/core')
-rw-r--r--modules/core/module-aiassist.js119
-rw-r--r--modules/core/module-loader.js4
-rw-r--r--modules/core/module-neural-vfs.js44
-rw-r--r--modules/core/ollama-manager.js4
-rw-r--r--modules/core/terminal.js6
-rw-r--r--modules/core/virtual-fs.js354
6 files changed, 469 insertions, 62 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index ecd4637..3af7f9b 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -44,53 +44,31 @@
}
} catch (e) {}
}
- buildSystemPrompt() {
+buildSystemPrompt() {
+ // Check for user-overridden prompt in localStorage
+ try {
+ const saved = localStorage.getItem('webx11_ai_system_prompt');
+ if (saved) {
+ const now = new Date();
+ const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
+ const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
+ return `Current date: ${dateStr}\nCurrent time: ${timeStr}\n\n${saved}`;
+ }
+ } catch (e) {}
const now = new Date();
- const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
- const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
- const optimizationActive = this.terminal?.ollamaManager?.optimizationEnabled !== false;
- // Neural VFS staleness check
+ const dateStr = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0');
+ const nvfs = this.terminal?.moduleManager?.modules?.NeuralVFSModule;
let nvfsNote = '';
try {
- const nvfs = this.terminal?.moduleManager?.modules?.NeuralVFSModule;
if (nvfs && nvfs.lastScan && nvfs.lastScan !== '(loaded from disk)') {
const ageMs = Date.now() - new Date(nvfs.lastScan).getTime();
- if (ageMs > 14400000) { // > 4 hours
- nvfsNote = `\nNote: Neural VFS index is ${Math.floor(ageMs/3600000)}h old. nvfs-resolve results may be stale. Use list_files/read_file directly for live data.`;
- }
+ if (ageMs > 14400000) nvfsNote = ' |nvfs ' + Math.floor(ageMs/3600000) + 'h stale';
}
} catch (e) {}
- return `Current date: ${dateStr}
-Current time: ${timeStr}${nvfsNote}
-
-You are WebX11AI — embedded in the WebX11 newOS4 terminal. 3-layer system: VFS → Modules → AI. You can edit your own modules via edit_module/test_module/reload_module.
-
-=== WORKFLOW ===
-1. Tool → Observe → Answer. Don't re-verify what a tool just told you.
-2. find_files("about X") → nvfs_resolve(X) FIRST. NVFS is built-in core, never WPM.
-3. load_skills("anti-loop-protocol") BEFORE tool #4 to prevent loops.
-4. If editing a module: load_skills("module-editing-workflow") FIRST.
-5. run_command may be blocked by security-level/guardrails — read the output.
-6. Concise. Bullets. If a tool fails, try another.
-
-=== CAPABILITY MAP ===
-[FS] list_files, read_file, write_file, make_directory, delete_file, get_current_dir
-[NVFS] nvfs_resolve(concept), run_command: nvfs-status, nvfs-index, nvfs-tree. Skills: vfs-content-search, neural-vfs-management.
-[MODULES] view_module, edit_module, test_module, reload_module, list_modified_modules, get_module_status. CORE modules (never WPM): NVFS, Floating, Config, Vault, Sandbox, Orchestrator, Memory-Graph, World-Model, Predictor, Neural-VFS.
-[WPM] wpm_install, wpm_search, list_available_modules
-[SEARCH] web_search, web_fetch
-[AGENTS] delegate_task, list_agents, spawn_agent. A2A: a2a-send, a2a-list. Leases: lease-create, lease-list, lease-revoke.
-[SANDBOX] run_command: sandbox-status, sandbox-run, sandbox-config
-[CONFIG] run_command: config, config-set, ai-config
-[SKILLS] load_skills, create_skill, patch_skill, list_skills
-[PROFILE/SESSION] user_get/set/list/forget, session_save/load/recent/stats/terminal
-[SMART-CTX] smart_context_list, smart_context_set
-[MINDS] create_mindmap
-[SAFETY] run_command: guardrail, guardrail-add/remove, shadow, shadow-commit, shadow-rollback
-[VAULT] run_command: secret-set/get/list/del, secret-setpin, secret-status
-[CHECKPOINTS] run_command: checkpoint, checkpoint-list, checkpoint-del, rollback
-[WEBDAV] run_command: webdav-config/ls/push/pull/backup/restore
-[ALL OTHERS] cron, theme, voice, edit, diff, alias, mindmap, ll, cls, trash, gui, server, modules, ollama, context, world-model, memgraph, model, webxai, predict, predictor-config, memgraph-export, tar, untar, wget`;
+ return 'Current date: ' + dateStr + nvfsNote + '\n'
+ + 'VFS\u2192Modules\u2192AI. nvfs-index for commands. Tool\u2192Observe\u2192Answer. nvfs-resolve. Destructive cmds need confirm.\n'
+ + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch | Mod: view/edit/test/reload | Config | Skills\n'
+ + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, guardrail, wget, tar, secret-* \u2014 discoverable via help';
}
getToolDefinitions() {
if (!this._toolDefs) this._toolDefs = this._buildToolDefs();
@@ -449,7 +427,9 @@ You are WebX11AI — embedded in the WebX11 newOS4 terminal. 3-layer system: VFS
'ai-ignore': 'ai-ignore — Suppress validator warnings for this session',
'ai-stop': 'ai-stop — Cancel current AI operation',
'ai-model': 'ai-model [model] — Show or set AI model',
- 'ai-config': 'ai-config [key] [value] — View or set AI loop config (max-turns, max-context, confirm-timeout(0=disabled))'
+ 'ai-config': 'ai-config [key] [value] — View or set AI loop config (max-turns, max-context, confirm-timeout(0=disabled))',
+ 'mem-debug': 'mem-debug — Show memory breakdown by component',
+ 'reset-mem': 'reset-mem — Reset AI memory (logs, patterns) without losing VFS or config'
};
}
registerCommands() {
@@ -472,6 +452,8 @@ You are WebX11AI — embedded in the WebX11 newOS4 terminal. 3-layer system: VFS
this.commands['shadow'] = (args) => this.shadowExec(args);
this.commands['shadow-commit'] = () => this.shadowCommit();
this.commands['shadow-rollback'] = () => this.shadowRollback();
+ this.commands['mem-debug'] = () => this.memDebug();
+ this.commands['reset-mem'] = () => this.resetMem();
}
async chat(args) {
const userMessage = args.join(' ');
@@ -735,6 +717,10 @@ You are WebX11AI — embedded in the WebX11 newOS4 terminal. 3-layer system: VFS
duration: t1,
error: this._isToolError(toolResult) ? true : false
});
+ // Auto-trim to 50 — prevents unbounded memory growth
+ if (this._toolCallLog.length > 50) {
+ this._toolCallLog = this._toolCallLog.slice(-50);
+ }
// Telemetry
const sm = this.terminal.moduleManager.modules['ServerMonitorModule'];
if (sm && sm.logToolCall) sm.logToolCall(fn.name, (t1 * 1000) || 1, this._isToolError(toolResult));
@@ -801,6 +787,10 @@ You are WebX11AI — embedded in the WebX11 newOS4 terminal. 3-layer system: VFS
duration: t1,
error: this._isToolError(toolResult) ? true : false
});
+ // Auto-trim to 50 — prevents unbounded memory growth
+ if (this._toolCallLog.length > 50) {
+ this._toolCallLog = this._toolCallLog.slice(-50);
+ }
// Telemetry
const sm2 = this.terminal.moduleManager.modules['ServerMonitorModule'];
if (sm2 && sm2.logToolCall) sm2.logToolCall(fn.name, (t1 * 1000) || 1, this._isToolError(toolResult));
@@ -1350,7 +1340,10 @@ You are WebX11AI — embedded in the WebX11 newOS4 terminal. 3-layer system: VFS
'cal','tree','find','grep','head','tail','sort','wc','du','diff','basename','dirname',
'yes','seq','cut','uniq','expand','printf','base64','help','clear','set','export',
'unalias','history','ll','h','mem','status','df','free','top','ps','health-check',
- 'system-health','modules','listmods','modules-list','get-help-info','alias']);
+ 'system-health','modules','listmods','modules-list','get-help-info','alias',
+ 'nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync',
+ 'floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle',
+ 'mem-debug','reset-mem']);
return !safeRead.has(cmd);
}
return ['write_file','delete_file','make_directory','edit_module'].includes(name);
@@ -1843,6 +1836,7 @@ You are WebX11AI — embedded in the WebX11 newOS4 terminal. 3-layer system: VFS
}
} catch (e) {
this._errorLog.push({ tool: name, args: a, error: e.message, timestamp: Date.now() });
+ if (this._errorLog.length > 20) this._errorLog.shift();
return JSON.stringify({ error: e.message, stack: e.stack?.substring(0, 200) });
}
}
@@ -2007,6 +2001,7 @@ You are WebX11AI — embedded in the WebX11 newOS4 terminal. 3-layer system: VFS
}
} catch (e) {
this._errorLog.push({ tool: name, args: a, error: e.message, timestamp: Date.now() });
+ if (this._errorLog.length > 20) this._errorLog.shift();
return JSON.stringify({ error: e.message, stack: e.stack?.substring(0, 200) });
}
}
@@ -2049,10 +2044,46 @@ You are WebX11AI — embedded in the WebX11 newOS4 terminal. 3-layer system: VFS
}
setSystemPrompt(args) {
if (!args[0]) {
- this.terminal.print(`Current system prompt: ${this.systemPrompt.substring(0, 50)}...`, 'info'); return;
+ this.terminal.print('=== System Prompt ===', 'info');
+ this.systemPrompt.split('\n').forEach(line => this.terminal.print(line, 'text-muted'));
+ this.terminal.print(`=== End (${this.systemPrompt.length} chars) ===`, 'info');
+ return;
}
this.systemPrompt = args.join(' ');
- this.terminal.print('System prompt updated', 'success');
+ // Persist to localStorage — next page load will use this instead of built-in
+ try { localStorage.setItem('webx11_ai_system_prompt', this.systemPrompt); } catch (e) {}
+ this.terminal.print('System prompt updated (persisted)', 'success');
+ }
+ memDebug() {
+ const toolLogSize = this._toolCallLog.length;
+ const errLogSize = this._errorLog.length;
+ const freqKeys = Object.keys(this._commandFreq).length;
+ const contextSize = this.context.length;
+ const vfsSize = (localStorage.getItem('webx11_filesystem') || '').length +
+ (localStorage.getItem('webx11_filesystem_v2') || '').length;
+ const aiCtxSize = (localStorage.getItem('webx11_ai_context') || '').length;
+ this.terminal.print('=== Memory Debug ===', 'info');
+ this.terminal.print(` _toolCallLog: ${toolLogSize} entries (cap: 50)`, toolLogSize > 30 ? 'warning' : 'text-muted');
+ this.terminal.print(` _errorLog: ${errLogSize} entries (cap: 20)`, errLogSize > 10 ? 'warning' : 'text-muted');
+ this.terminal.print(` _commandFreq: ${freqKeys} tracked patterns`, freqKeys > 15 ? 'warning' : 'text-muted');
+ this.terminal.print(` AI context: ${contextSize} messages (max: ${this.maxContext * 2})`, 'text-muted');
+ this.terminal.print(` VFS localStorage: ${(vfsSize / 1024).toFixed(1)} KB`, 'text-muted');
+ this.terminal.print(` AI context localStorage: ${(aiCtxSize / 1024).toFixed(1)} KB`, 'text-muted');
+ const heap = performance.memory ? performance.memory.usedJSHeapSize : null;
+ if (heap) {
+ this.terminal.print(` JS heap: ${(heap / 1048576).toFixed(1)} MB / ${(performance.memory.jsHeapSizeLimit / 1048576).toFixed(1)} MB`, 'text-muted');
+ }
+ this.terminal.print('Use reset-mem to clear AI internal logs without touching VFS/config', 'info');
+ }
+ resetMem() {
+ this._toolCallLog = [];
+ this._errorLog = [];
+ this._commandFreq = {};
+ this.context = [];
+ localStorage.removeItem('webx11_ai_context');
+ localStorage.removeItem('webx11_ai_patterns');
+ this.terminal.print('AI memory reset: tool logs, error logs, command patterns, and context cleared', 'success');
+ this.terminal.print('VFS, config, skills, system prompt, and floating elements preserved', 'text-muted');
}
showModel(args) {
const om = this.terminal.ollamaManager;
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index f0d3f4c..ab73202 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -105,7 +105,7 @@
window.__moduleLoaderReload = reloadWithSelection;
- const LATEST_VER = '2026-06-09-v4.5.27';
+ const LATEST_VER = '2026-06-10-v4.5.34';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
@@ -164,7 +164,7 @@
// Phase 4: Ext modules (filtered)
if (typeof webx11._loadExtModules === 'function') {
- const extKeys = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'tutorial', 'shell', 'busybox', 'floating'];
+ const extKeys = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'tutorial', 'shell', 'busybox', 'floating', 'console'];
const extSelected = selected
? extKeys.filter(k => isSelected(k + '.js', selected))
: extKeys;
diff --git a/modules/core/module-neural-vfs.js b/modules/core/module-neural-vfs.js
index bdf1ffb..613d53a 100644
--- a/modules/core/module-neural-vfs.js
+++ b/modules/core/module-neural-vfs.js
@@ -7,6 +7,7 @@ class NeuralVFSModule {
'nvfs-tree': { argType: 'file' }, 'nvfs-config': { argType: 'file' },
'nvfs-alias': { argType: 'file' },
'nvfs-sync': { argType: 'file' },
+ 'nvfs-save': { argType: 'none' },
};
this.terminal = null;
this.index = {};
@@ -55,7 +56,8 @@ class NeuralVFSModule {
'nvfs-tree': 'nvfs-tree [concept] — Show concept-to-file mapping as a tree',
'nvfs-config': 'nvfs-config [key] [value] — View or set Neural VFS config (fuzzy-limit, tree-files, tree-concepts, resolve-files, deep-index, deep-model)',
'nvfs-alias': 'nvfs-alias list|add|remove|clear — Manage concept aliases',
- 'nvfs-sync': 'nvfs-sync [--deep] — Incremental background sync (auto-sync dirty files)'
+ 'nvfs-sync': 'nvfs-sync [--deep] — Incremental background sync (auto-sync dirty files)',
+ 'nvfs-save': 'nvfs-save — Force-save current in-memory index to VFS (survives refresh)'
};
}
@@ -67,6 +69,7 @@ class NeuralVFSModule {
this.commands['nvfs-config'] = (args) => this.nvfsConfig(args);
this.commands['nvfs-alias'] = (args) => this.nvfsAlias(args);
this.commands['nvfs-sync'] = async (args) => this.nvfsSync(args);
+ this.commands['nvfs-save'] = (args) => this.nvfsSave();
}
// ── Core Index Builder (async, chunked) ──
@@ -76,6 +79,19 @@ class NeuralVFSModule {
const fs = this.terminal.fs;
if (!fs) return this.terminal.print('VFS not available', 'error');
+ // Safety guard: warn if deep mode is off but the saved index has LLM concepts
+ if (!useDeep) {
+ const prevIndex = fs.fsReadJSON(this.indexPath) || {};
+ const hasLLM = Object.values(prevIndex).some(entries =>
+ Array.isArray(entries) && entries.some(e => e.source === 'llm')
+ );
+ if (hasLLM) {
+ this.terminal.print('⚠️ Deep indexing is OFF but the saved index contains LLM-generated concepts.', 'warning');
+ this.terminal.print(' Running nvfs-index without --deep will STRIP all LLM concepts from re-indexed files.', 'warning');
+ this.terminal.print(' To keep them: nvfs-config deep-index on or nvfs-index --deep', 'warning');
+ }
+ }
+
this.terminal.print(`🔍 Scanning VFS for semantic concepts${useDeep ? ' (deep mode)' : ''}...`, 'info');
const start = Date.now();
@@ -634,7 +650,7 @@ class NeuralVFSModule {
const hash = this._contentHash(content);
const cached = this._getCached(path, hash);
if (!cached) {
- this._reindexFile(path, content, this.index);
+ this._reindexFile(path, content, this.index, this._deepIndexEnabled);
this._hashCache[path] = { hash };
reindexed++;
}
@@ -768,6 +784,30 @@ Concepts:`;
const llmConcepts = results.reduce((s, r) => s + r.concepts.length, 0);
this.terminal?.print(`🧠 Deep pass complete: ${llmConcepts} LLM-generated concepts across ${results.length} files`, 'success');
}
+ // Persist LLM-generated concepts to disk so they survive page refresh
+ // Must force-save because _saveIndex() calls saveToStorage() which has a
+ // debounce guard (_storageSavePending) that may still be set from the
+ // initial buildIndex() save — without clearing it, the LLM concepts are
+ // never written to localStorage and are lost on page refresh.
+ const vfs = this.terminal?.fs;
+ if (vfs) {
+ vfs._storageSavePending = false;
+ this._saveIndex(vfs);
+ }
+ }
+
+ // ── Nvfs Save (force-persist in-memory index) ──
+
+ nvfsSave() {
+ const fs = this.terminal?.fs;
+ if (!fs) return this.terminal?.print('VFS not available', 'error');
+ const count = Object.keys(this.index).length;
+ if (count === 0) return this.terminal?.print('No index in memory to save', 'warning');
+ // Bypass debounce guard to ensure write
+ fs._storageSavePending = false;
+ this._saveIndex(fs);
+ this.terminal?.print(`✅ Saved ${count} concepts to ${this.indexPath}`, 'success');
+ this.terminal?.print(' Index will survive page refresh', 'info');
}
// ── Nvfs Sync (incremental background) ──
diff --git a/modules/core/ollama-manager.js b/modules/core/ollama-manager.js
index 9999184..253e319 100644
--- a/modules/core/ollama-manager.js
+++ b/modules/core/ollama-manager.js
@@ -14,9 +14,9 @@ class OllamaManager {
// --- AI Agent Loop Optimization Toggle ---
this.optimizationEnabled = localStorage.getItem('webx11_optimization_enabled') !== 'false';
// --- Streaming Toggle ---
- this.streamEnabled = localStorage.getItem('webx11_stream_enabled') !== 'false';
+ this.streamEnabled = localStorage.getItem('webx11_stream_enabled') === 'true';
// --- Validator Agent Config ---
- this.validatorEnabled = localStorage.getItem('webx11_validator_enabled') !== 'false';
+ this.validatorEnabled = localStorage.getItem('webx11_validator_enabled') === 'true';
this.validatorModel = localStorage.getItem('webx11_validator_model') || 'ministral-3:3b-cloud';
this.validatorEndpoint = localStorage.getItem('webx11_validator_endpoint') || '';
// Build fallback endpoints
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index 87636d6..fb9f332 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -10,7 +10,7 @@
this.destructiveCmds = new Set(['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']);
this.systemCmds = new Set(['reboot','shutdown','format','kill','halt','poweroff']);
this.suspiciousFlags = new Set(['--dump','--execute','--eval','--exec','--shell','--script']);
- this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle']);
+ this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem']);
// Load persisted overrides from localStorage
try { const d = localStorage.getItem('webx11_security_destructive'); if(d){JSON.parse(d).forEach(c=>this.destructiveCmds.add(c));} }catch(e){}
try { const s = localStorage.getItem('webx11_security_system'); if(s){JSON.parse(s).forEach(c=>this.systemCmds.add(c));} }catch(e){}
@@ -1183,7 +1183,7 @@
} catch (e) {}
}
async _loadExtModules(filterKeys) {
- const autoLoadKeys = filterKeys || ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating'];
+ const autoLoadKeys = filterKeys || ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating', 'console'];
const cacheVer = localStorage.getItem('webx11_cache_ver') || '2026-05-27-14';
try {
const res = await fetch('packages.json?v=' + cacheVer);
@@ -1202,7 +1202,7 @@
}
async reloadPersistedModules() {
const registry = this.moduleManager.moduleRegistry;
- const skipList = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating'];
+ const skipList = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating', 'console'];
for (const mod of registry) {
if (mod.enabled && mod.url) {
// Skip ext modules — they load via _loadExtModules
diff --git a/modules/core/virtual-fs.js b/modules/core/virtual-fs.js
index f6e2826..9d3ea79 100644
--- a/modules/core/virtual-fs.js
+++ b/modules/core/virtual-fs.js
@@ -3,6 +3,308 @@ class VirtualFileSystem {
this.root = { type: 'dir', content: {} };
this.currentPath = [];
this._ready = this.loadFromStorage();
+ this._virtualMounts = null; // lazy-init on first /sys/ access
+ // Register /sys/ in emit-change exclusion (also added below)
+ }
+ // ── Virtual /sys/ filesystem (live runtime state, never persisted) ──
+ _initVirtualMounts() {
+ if (this._virtualMounts) return;
+ const vm = (name, factory) => ({ _factory: factory, _name: name });
+ const dir = () => ({ _dir: true, content: {} });
+ const vmCache = {}; // factories can cache costly results within same tick
+ const mm = () => typeof window !== 'undefined' ? window.webx11?.moduleManager?.modules : null;
+ const ai = () => mm()?.WebX11AIAssistantModule || null;
+ const heap = () => typeof performance !== 'undefined' && performance.memory ? performance.memory : null;
+ const fmt = (bytes) => {
+ if (!bytes && bytes !== 0) return 'N/A';
+ const kb = bytes / 1024;
+ if (kb < 1024) return kb.toFixed(1) + ' KB';
+ return (kb / 1024).toFixed(1) + ' MB';
+ };
+ const fmtBytes = (bytes) => {
+ if (!bytes && bytes !== 0) return 'N/A';
+ if (bytes < 1024) return bytes + ' B';
+ const kb = bytes / 1024;
+ if (kb < 1024) return kb.toFixed(1) + ' KB';
+ return (kb / 1024).toFixed(1) + ' MB';
+ };
+ // Walk VFS root to count files/dirs and find largest
+ const vfsWalk = (dir, prefix) => {
+ let files = 0, dirs = 0, total = 0, largest = [];
+ const walk = (node, p) => {
+ if (!node) return;
+ if (node._factory || node._dir) return; // skip virtual
+ if (node.type === 'file') {
+ files++;
+ const size = node.content ? node.content.length : 0;
+ total += size;
+ largest.push({ path: p, size });
+ } else if (node.type === 'dir') {
+ dirs++;
+ for (const [name, child] of Object.entries(node.content || {})) {
+ walk(child, p + '/' + name);
+ }
+ }
+ };
+ walk(dir, prefix);
+ largest.sort((a, b) => b.size - a.size);
+ return { files, dirs, total, largest: largest.slice(0, 20) };
+ };
+ this._virtualMounts = {
+ // /sys/info
+ info: vm('info', () => [
+ '=== newOS4 Runtime Debug (/sys/) ===',
+ `Version: ${window.NEWOS4_VERSION || '?'}`,
+ `JS heap: ${fmt(heap()?.usedJSHeapSize)} / ${fmt(heap()?.jsHeapSizeLimit)}`,
+ `DOM nodes: ${typeof document !== 'undefined' ? document.querySelectorAll('*').length : 'N/A'}`,
+ `Loaded modules: ${mm() ? Object.keys(mm()).length : 'N/A'}`,
+ '',
+ ' cat /sys/heap/stats — Memory overview',
+ ' cat /sys/heap/breakdown — What consumes what',
+ ' cat /sys/logs/tool-calls — AI tool call log',
+ ' cat /sys/logs/errors — AI error log',
+ ' cat /sys/vfs/stats — VFS file/dir counts',
+ ' cat /sys/vfs/largest — Top 20 largest files',
+ ' ls /sys/modules/ — Loaded modules',
+ ' cat /sys/config/ai — AI config parameters',
+ ' cat /sys/dom/node-count — DOM element breakdown',
+ ].join('\n')),
+ // /sys/heap/
+ heap: { _dir: true, content: {
+ stats: vm('stats', () => {
+ const h = heap();
+ const lsSize = (k) => { try { return (localStorage.getItem(k) || '').length / 1024; } catch(e) { return 0; } };
+ return [
+ '=== Heap Stats ===',
+ ` JS heap used: ${fmt(h?.usedJSHeapSize)}`,
+ ` JS heap limit: ${fmt(h?.jsHeapSizeLimit)}`,
+ ` JS heap % used: ${h ? ((h.usedJSHeapSize / h.jsHeapSizeLimit) * 100).toFixed(1) + '%' : 'N/A'}`,
+ '',
+ ` VFS localStorage: ${lsSize('webx11_filesystem_v2').toFixed(1)} KB`,
+ ` AI localStorage: ${lsSize('webx11_ai_context').toFixed(1)} KB`,
+ ` Config localStorage: ${lsSize('webx11_ai_config').toFixed(1)} KB`,
+ ].join('\n');
+ }),
+ breakdown: vm('breakdown', () => {
+ const a = ai();
+ const cCount = a?.context?.length || 0;
+ const lCount = a?._toolCallLog?.length || 0;
+ const eCount = a?._errorLog?.length || 0;
+ const modules = mm() ? Object.keys(mm()).length : 0;
+ const domNodes = typeof document !== 'undefined' ? document.querySelectorAll('*').length : 0;
+ return [
+ '=== Heap Breakdown (estimates) ===',
+ ` Module code + state: ~${(modules * 1.2).toFixed(1)} MB (${modules} modules × ~1.2MB)`,
+ ` VFS tree (in-memory): varies with file count`,
+ ` AI context messages: ${cCount} (max: ${a?.maxContext || '?'})`,
+ ` AI tool call log: ${lCount} (cap: 50)`,
+ ` AI error log: ${eCount} (cap: 20)`,
+ ` DOM nodes: ${domNodes}`,
+ ].join('\n');
+ })
+ }},
+ // /sys/modules/
+ modules: { _dir: true, content: {
+ list: vm('list', () => {
+ const m = mm();
+ if (!m) return '(moduleManager not available)';
+ return Object.keys(m).sort().join('\n');
+ })
+ // module subdirectories are dynamic — handled in _virtualLs
+ }},
+ // /sys/vfs/
+ vfs: { _dir: true, content: {
+ stats: vm('stats', () => {
+ const w = vfsWalk(this.root, '');
+ return [
+ '=== VFS Stats ===',
+ ` Files: ${w.files}`,
+ ` Directories: ${w.dirs}`,
+ ` Total content: ${fmtBytes(w.total)}`,
+ ` localStorage: ${((localStorage.getItem('webx11_filesystem_v2') || '').length / 1024).toFixed(1)} KB (compressed)`,
+ ].join('\n');
+ }),
+ largest: vm('largest', () => {
+ const w = vfsWalk(this.root, '');
+ if (w.largest.length === 0) return '(empty VFS)';
+ const maxW = Math.max(...w.largest.map(f => f.path.length), 8);
+ let out = '=== Top 20 Largest Files ===\n';
+ w.largest.forEach((f, i) => {
+ out += ` ${(i + 1).toString().padStart(2)}. ${f.path.padEnd(maxW)} ${fmtBytes(f.size)}\n`;
+ });
+ return out;
+ })
+ }},
+ // /sys/logs/
+ logs: { _dir: true, content: {
+ 'tool-calls': vm('tool-calls', () => {
+ const a = ai();
+ if (!a || !a._toolCallLog || a._toolCallLog.length === 0) return '(empty)';
+ return a._toolCallLog.map((e, i) => `[${i}] ${JSON.stringify(e)}`).join('\n');
+ }),
+ 'errors': vm('errors', () => {
+ const a = ai();
+ if (!a || !a._errorLog || a._errorLog.length === 0) return '(empty)';
+ return a._errorLog.map((e, i) => `[${i}] ${JSON.stringify(e)}`).join('\n');
+ }),
+ 'commands': vm('commands', () => {
+ const a = ai();
+ if (!a || !a._commandFreq || Object.keys(a._commandFreq).length === 0) return '(empty)';
+ const sorted = Object.entries(a._commandFreq).sort((a,b) => b[1] - a[1]);
+ return sorted.map(([cmd, n]) => `${cmd.padEnd(20)} ${n}x`).join('\n');
+ })
+ }},
+ // /sys/dom/
+ dom: { _dir: true, content: {
+ 'node-count': vm('node-count', () => {
+ if (typeof document === 'undefined') return 'N/A';
+ const all = document.querySelectorAll('*');
+ const byTag = {};
+ all.forEach(el => { const t = el.tagName.toLowerCase(); byTag[t] = (byTag[t] || 0) + 1; });
+ const sorted = Object.entries(byTag).sort((a,b) => b[1] - a[1]);
+ let out = `Total DOM nodes: ${all.length}\n\n=== By Tag ===\n`;
+ sorted.forEach(([tag, count]) => {
+ out += ` ${tag.padEnd(15)} ${count}\n`;
+ });
+ return out;
+ })
+ }},
+ // /sys/config/
+ config: { _dir: true, content: {
+ version: vm('version', () => {
+ return `NEWOS4_VERSION: ${window.NEWOS4_VERSION || '?'}\n`;
+ }),
+ ai: vm('ai', () => {
+ const a = ai();
+ if (!a) return '(AI module not loaded)';
+ return [
+ '=== AI Config ===',
+ ` maxContext: ${a.maxContext}`,
+ ` MAX_TOOL_TURNS: ${a.MAX_TOOL_TURNS}`,
+ ` maxContextTokens: ${a.maxContextTokens}`,
+ ` confirmTimeoutMs: ${a._confirmTimeoutMs}`,
+ ` skillDebounceMs: ${a._skillDebounceMs}`,
+ ` context messages: ${a.context?.length || 0}`,
+ ` tool call log cap: 50 (auto-capped)`,
+ ` error log cap: 20 (auto-capped)`,
+ ].join('\n');
+ })
+ }}
+ };
+ }
+ // Check if a path is under /sys/
+ _isVirtualPath(path) {
+ if (!path) return false;
+ const normalized = path.startsWith('/') ? path : '/' + path;
+ return normalized === '/sys' || normalized.startsWith('/sys/');
+ }
+ // Resolve a virtual path: returns {dir: virtualNode, name: lastSegment} or null
+ _resolveVirtualPath(path) {
+ this._initVirtualMounts();
+ const normalized = path.startsWith('/') ? path : '/' + path;
+ let segments = normalized.split('/').filter(s => s);
+ // Remove leading 'sys'
+ if (segments[0] === 'sys') segments = segments.slice(1);
+ if (segments.length === 0) {
+ // /sys itself — return the root virtual mounts node as a dir
+ const allContent = {};
+ for (const [k, v] of Object.entries(this._virtualMounts)) {
+ if (v._dir) allContent[k] = { type: 'dir', content: v.content, _virtual: true };
+ else allContent[k] = { type: 'file', content: null, _virtual: true, _factory: v._factory };
+ }
+ // Add dynamic module subdirs to modules listing
+ if (this._virtualMounts.modules?._dir) {
+ const mm = typeof window !== 'undefined' ? window.webx11?.moduleManager?.modules : null;
+ if (mm) {
+ for (const modName of Object.keys(mm)) {
+ if (!allContent[modName]) {
+ allContent[modName] = { type: 'dir', content: {}, _virtual: true };
+ }
+ }
+ }
+ }
+ return { dir: { content: allContent }, name: null };
+ }
+ // Walk the tree
+ let node = this._virtualMounts;
+ for (let i = 0; i < segments.length - 1; i++) {
+ const seg = segments[i];
+ if (!node[seg]) {
+ // Check dynamic module subdir under /sys/modules/
+ const mods = typeof window !== 'undefined' ? window.webx11?.moduleManager?.modules : null;
+ if (node === this._virtualMounts?.modules?.content && mods && mods[seg]) {
+ // Dynamic module subdir — create virtual node
+ node = {
+ _dir: true,
+ content: {
+ info: { type: 'file', content: null, _virtual: true, _factory: () => {
+ const mod = mods[seg];
+ const cmdKeys = mod.commands ? Object.keys(mod.commands) : [];
+ return [
+ `Module: ${seg}`,
+ ` Type: ${typeof mod}`,
+ ` Commands: ${cmdKeys.join(', ') || '(none)'}`,
+ ` Has help: ${mod.getHelpInfo ? 'yes' : 'no'}`,
+ ` Constructor: ${mod.constructor?.name || 'anonymous'}`,
+ ].join('\n');
+ }}
+ }
+ };
+ continue;
+ }
+ return null;
+ }
+ node = node[seg];
+ if (node && node._dir) node = node.content || {};
+ }
+ const lastName = segments[segments.length - 1];
+ if (node && node[lastName]) {
+ const entry = node[lastName];
+ if (entry._dir) {
+ // Static subdir with known content
+ const allContent = {};
+ for (const [k, v] of Object.entries(entry.content || {})) {
+ if (v._dir) allContent[k] = { type: 'dir', content: v.content, _virtual: true };
+ else allContent[k] = { type: 'file', content: null, _virtual: true, _factory: v._factory };
+ }
+ // Add dynamic module subdirs
+ if (segments[0] === 'modules' || (segments.length >= 2 && segments[segments.length - 2] === 'modules')) {
+ const mm = typeof window !== 'undefined' ? window.webx11?.moduleManager?.modules : null;
+ if (mm) {
+ for (const modName of Object.keys(mm)) {
+ if (!allContent[modName]) {
+ allContent[modName] = { type: 'dir', content: {}, _virtual: true };
+ }
+ }
+ }
+ }
+ return { dir: { content: allContent }, name: null };
+ } else {
+ return { dir: { content: node }, name: lastName };
+ }
+ }
+ // Check if it's a module subdirectory itself (e.g. /sys/modules/calc)
+ const mods = typeof window !== 'undefined' ? window.webx11?.moduleManager?.modules : null;
+ if (node && mods && mods[lastName] && segments.length >= 1) {
+ // It's a module name — return its info dir
+ return {
+ dir: { content: {
+ info: { type: 'file', content: null, _virtual: true, _factory: () => {
+ const mod = mods[lastName];
+ const cmdKeys = mod.commands ? Object.keys(mod.commands) : [];
+ return [
+ `Module: ${lastName}`,
+ ` Type: ${typeof mod}`,
+ ` Commands: ${cmdKeys.join(', ') || '(none)'}`,
+ ` Has help: ${mod.getHelpInfo ? 'yes' : 'no'}`,
+ ` Constructor: ${mod.constructor?.name || 'anonymous'}`,
+ ].join('\n');
+ }}
+ }},
+ name: null
+ };
+ }
+ return null;
}
_u8ToBase64(u8) {
let s = '';
@@ -70,7 +372,7 @@ class VirtualFileSystem {
// Skip internal index/system files to prevent re-trigger loops
if (!path) return;
const normalized = path.startsWith('/') ? path : '/' + path;
- const sysPaths = ['/.semantic_index', '/.memory_graph', '/.trash', '/_snapshots', '/_sessions', '/logs', '/memory', '/system', '/_mods', '/backups'];
+ const sysPaths = ['/.semantic_index', '/.memory_graph', '/.trash', '/_snapshots', '/_sessions', '/logs', '/memory', '/system', '/_mods', '/backups', '/sys'];
if (sysPaths.some(p => normalized.startsWith(p))) return;
if (typeof window !== 'undefined' && window.__nvfsMarkDirty) {
window.__nvfsMarkDirty(path);
@@ -119,6 +421,7 @@ class VirtualFileSystem {
return result;
}
_resolvePath(path) {
+ if (this._isVirtualPath(path)) return this._resolveVirtualPath(path);
const segs = this._toAbsoluteSegments(path);
if (segs === null) return null;
if (segs.length === 0) return { dir: this.root, name: null };
@@ -130,6 +433,10 @@ class VirtualFileSystem {
return { dir, name: segs[segs.length - 1] };
}
_resolveDir(path) {
+ if (this._isVirtualPath(path)) {
+ const r = this._resolveVirtualPath(path);
+ return r ? r.dir : null;
+ }
const segs = this._toAbsoluteSegments(path);
if (segs === null) return null;
if (segs.length === 0) return this.root;
@@ -156,12 +463,19 @@ class VirtualFileSystem {
dir = this.getCurrentDir();
}
if (!dir) return [];
- return Object.keys(dir.content).map(name => ({
+ const entries = Object.keys(dir.content).map(name => ({
name, type: dir.content[name].type,
- size: dir.content[name].content ? dir.content[name].content.length : 0
+ size: dir.content[name]._factory ? 0 : (dir.content[name].content ? dir.content[name].content.length : 0),
+ _virtual: !!dir.content[name]._virtual
}));
+ // Inject virtual /sys/ at root level
+ if ((!path || path === '/' || path === '') && !entries.some(e => e.name === 'sys' && e._virtual)) {
+ entries.push({ name: 'sys', type: 'dir', size: 0, _virtual: true });
+ }
+ return entries;
}
writeFile(path, content) {
+ if (this._isVirtualPath(path)) return false;
const resolved = this._resolvePath(path);
if (!resolved || !resolved.name) return false;
const name = resolved.name;
@@ -176,10 +490,14 @@ class VirtualFileSystem {
const resolved = this._resolvePath(path);
if (!resolved || !resolved.name) return null;
const node = resolved.dir.content[resolved.name];
+ if (node && node._factory) {
+ return node._factory();
+ }
if (node && node.type === 'file') return node.content;
return null;
}
deleteFile(path) {
+ if (this._isVirtualPath(path)) return false;
const resolved = this._resolvePath(path);
if (!resolved || !resolved.name) return false;
const node = resolved.dir.content[resolved.name];
@@ -243,6 +561,7 @@ class VirtualFileSystem {
return this.trashDir(path);
}
mkdirRecursive(path) {
+ if (this._isVirtualPath(path)) return false;
const segs = this._toAbsoluteSegments(path);
if (!segs) return false;
let dir = this.root;
@@ -259,6 +578,7 @@ class VirtualFileSystem {
return true;
}
rename(oldPath, newPath) {
+ if (this._isVirtualPath(oldPath) || this._isVirtualPath(newPath)) return false;
const src = this._resolvePath(oldPath);
if (!src || !src.name) return false;
const dst = this._resolvePath(newPath);
@@ -273,6 +593,7 @@ class VirtualFileSystem {
return true;
}
copy(srcPath, dstPath) {
+ if (this._isVirtualPath(srcPath) || this._isVirtualPath(dstPath)) return false;
const src = this._resolvePath(srcPath);
if (!src || !src.name) return false;
const dst = this._resolvePath(dstPath);
@@ -294,6 +615,7 @@ class VirtualFileSystem {
return this.rename(srcPath, dstPath);
}
rm(path) {
+ if (this._isVirtualPath(path)) return false;
const resolved = this._resolvePath(path);
if (!resolved || !resolved.name) return false;
const node = resolved.dir.content[resolved.name];
@@ -309,6 +631,7 @@ class VirtualFileSystem {
return false;
}
rmdir(path) {
+ if (this._isVirtualPath(path)) return false;
const resolved = this._resolvePath(path);
if (!resolved || !resolved.name) return false;
const node = resolved.dir.content[resolved.name];
@@ -322,6 +645,7 @@ class VirtualFileSystem {
return false;
}
touch(path) {
+ if (this._isVirtualPath(path)) return false;
return this.writeFile(path, '');
}
readDir(path) {
@@ -330,16 +654,22 @@ class VirtualFileSystem {
let dir;
if (resolved.name) {
const node = resolved.dir.content[resolved.name];
- if (node && node.type === 'dir') dir = node;
+ if (node && (node.type === 'dir' || node._virtual)) dir = node;
else return null;
} else {
dir = resolved.dir;
}
- return Object.keys(dir.content).map(name => ({
+ const entries = Object.keys(dir.content).map(name => ({
name,
- type: dir.content[name].type,
- size: dir.content[name].content ? dir.content[name].content.length : 0
+ type: dir.content[name].type || (dir.content[name]._virtual ? 'file' : '?'),
+ size: dir.content[name]._factory ? 0 : (dir.content[name].content ? dir.content[name].content.length : 0),
+ _virtual: !!dir.content[name]._virtual
}));
+ // Inject virtual /sys/ at root level
+ if ((!path || path === '/' || path === '') && !entries.some(e => e.name === 'sys' && e._virtual)) {
+ entries.push({ name: 'sys', type: 'dir', size: 0, _virtual: true });
+ }
+ return entries;
}
fsWriteJSON(path, obj) {
return this.writeFile(path, JSON.stringify(obj));
@@ -354,7 +684,7 @@ class VirtualFileSystem {
}
}
tree(path = '', prefix = '') {
- const resolved = this._resolvePath(path);
+ const resolved = this._resolvePath(path || '/');
if (!resolved) return '';
let dir;
if (resolved.name) {
@@ -366,12 +696,15 @@ class VirtualFileSystem {
}
let result = '';
const entries = Object.keys(dir.content).sort();
+ // Inject /sys/ at root level in tree view
+ const atRoot = !path || path === '' || path === '/';
+ if (atRoot && !entries.includes('sys')) entries.push('sys');
for (let i = 0; i < entries.length; i++) {
const name = entries[i];
const isLast = i === entries.length - 1;
const connector = isLast ? '└── ' : '├── ';
result += prefix + connector + name + '\n';
- if (dir.content[name].type === 'dir') {
+ if (dir.content[name].type === 'dir' || dir.content[name]._virtual) {
const ext = isLast ? ' ' : '│ ';
result += this.tree(path + '/' + name, prefix + ext);
}
@@ -379,6 +712,7 @@ class VirtualFileSystem {
return result;
}
find(pattern, path = '') {
+ if (this._isVirtualPath(path || '/')) return [];
const resolved = this._resolvePath(path);
if (!resolved) return [];
let dir;
@@ -415,6 +749,7 @@ class VirtualFileSystem {
return results;
}
chmod(path, mode) {
+ if (this._isVirtualPath(path)) return false;
const resolved = this._resolvePath(path);
if (!resolved || !resolved.name) return false;
const node = resolved.dir.content[resolved.name];
@@ -424,6 +759,7 @@ class VirtualFileSystem {
return true;
}
du(path = '') {
+ if (this._isVirtualPath(path || '/')) return 0;
const resolved = this._resolvePath(path);
if (!resolved) return 0;
let dir;