aboutsummaryrefslogtreecommitdiff
path: root/modules/core
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-07-09 06:00:32 +0200
committerAgando <roduccomm@gmail.com>2026-07-09 06:00:32 +0200
commitcf7461ffeb4f53f3f50c9c692a2e4fdcecf17e82 (patch)
treee7780cd41271381cf108c9c81d71a3cb65cfd555 /modules/core
parentea4d09fb5ccba12ed774fcf7c913edfa1e6a851a (diff)
auto: 2026-07-09_06:00 changes
Diffstat (limited to 'modules/core')
-rw-r--r--modules/core/core-bundle.js3424
-rw-r--r--modules/core/module-aiassist.js934
-rw-r--r--modules/core/module-chatstudio.js3
-rw-r--r--modules/core/module-debug-supervisor.js701
-rw-r--r--modules/core/module-duckduckgo.js38
-rw-r--r--modules/core/module-dynamicprompts.js102
-rw-r--r--modules/core/module-io.js80
-rw-r--r--modules/core/module-leases.js17
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/module-memory-graph.js303
-rw-r--r--modules/core/module-my.js82
-rw-r--r--modules/core/module-neural-vfs.js58
-rw-r--r--modules/core/module-orchestrator.js140
-rw-r--r--modules/core/module-smartctx.js112
-rw-r--r--modules/core/module-world-model.js96
-rw-r--r--modules/core/ollama-manager.js2
-rw-r--r--modules/core/terminal.js291
-rw-r--r--modules/core/virtual-fs.js12
18 files changed, 5497 insertions, 900 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index d576f14..aab0206 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -39,6 +39,7 @@ class VirtualFileSystem {
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 ds = () => mm()?.DebugSupervisorModule || null;
const heap = () => typeof performance !== 'undefined' && performance.memory ? performance.memory : null;
const fmt = (bytes) => {
if (!bytes && bytes !== 0) return 'N/A';
@@ -114,7 +115,7 @@ class VirtualFileSystem {
const a = ai();
const cCount = a?.context?.length || 0;
const lCount = a?._toolCallLog?.length || 0;
- const eCount = a?._errorLog?.length || 0;
+ const eCount = ds() ? Object.keys(ds()._errorDB?.errors || {}).length : 0;
const modules = mm() ? Object.keys(mm()).length : 0;
const domNodes = typeof document !== 'undefined' ? document.querySelectorAll('*').length : 0;
return [
@@ -168,9 +169,12 @@ class VirtualFileSystem {
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');
+ const d = ds();
+ if (!d || !d._errorDB || Object.keys(d._errorDB.errors).length === 0) return '(empty)';
+ return Object.entries(d._errorDB.errors)
+ .sort((a, b) => b[1].lastSeen - a[1].lastSeen)
+ .map(([hash, e]) => `[${hash}] ${e.type}/${e.severity} ×${e.count}: ${e.message.substring(0, 100)}`)
+ .join('\n');
}),
'commands': vm('commands', () => {
const a = ai();
@@ -1188,7 +1192,7 @@ class OllamaManager {
this.streamEnabled = localStorage.getItem('webx11_stream_enabled') === 'true';
// --- Validator Agent Config ---
this.validatorEnabled = localStorage.getItem('webx11_validator_enabled') === 'true';
- this.validatorModel = localStorage.getItem('webx11_validator_model') || 'ministral-3:3b-cloud';
+ this.validatorModel = localStorage.getItem('webx11_validator_model') || 'gemma4:31b-cloud';
this.validatorEndpoint = localStorage.getItem('webx11_validator_endpoint') || '';
// --- Model Parameters (temperature, top_p, max_tokens, thinking) ---
this.modelParams = this._loadModelParams();
@@ -2005,15 +2009,7 @@ class OllamaManager {
// DeepSeek: empty response with no tools — retry once with stream forcing
if (isDeepSeek && content === '' && (!toolCalls || toolCalls.length === 0)) {
- this.terminal.log('WARN', 'Ollama', 'DeepSeek empty response — model=' + useModel + ', messages=' + messages.length + ', tools=' + (tools ? tools.length : 0));
this.terminal.print('⚠️ DeepSeek returned empty — retrying with stream...', 'warning');
- // Auto-truncate context: keep system prompt + last 5 messages
- if (messages.length > 6) {
- var sysMsg = messages[0];
- var recentMsgs = messages.slice(-5);
- body.messages = [sysMsg].concat(recentMsgs);
- this.terminal.print('📏 Context truncated from ' + messages.length + ' to ' + body.messages.length + ' messages for retry', 'info');
- }
body.stream = true;
delete body.stop;
this._abortController = new AbortController();
@@ -2048,16 +2044,12 @@ class OllamaManager {
}
}
if (fullText) {
- this.terminal.log('INFO', 'Ollama', 'DeepSeek stream retry succeeded: ' + fullText.length + ' chars');
return {
message: { role: 'assistant', content: fullText },
tool_calls: null,
error: null
};
}
- this.terminal.log('WARN', 'Ollama', 'DeepSeek stream retry also returned empty');
- } else {
- this.terminal.log('WARN', 'Ollama', 'DeepSeek stream retry HTTP ' + retryResponse.status);
}
return {
message: { role: 'assistant', content: '' },
@@ -2441,19 +2433,71 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
this.lastRouteTime = 0;
this.lastRouteCmd = '';
this.blockedCount = 0;
- 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','mem-debug','reset-mem','reset','factory-reset','reset-vfs','js','js-run','node','debug','debug-ai','session-new','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze']);
- // 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){}
- try { const w = localStorage.getItem('webx11_security_whitelist'); if(w){JSON.parse(w).forEach(c=>this.whitelist.add(c));} }catch(e){}
- try { const f = localStorage.getItem('webx11_security_flags'); if(f){JSON.parse(f).forEach(c=>this.suspiciousFlags.add(c));} }catch(e){}
+ this._baseDestructive = ['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot'];
+ this._baseSystem = ['reboot','shutdown','format','kill','halt','poweroff'];
+ this._baseSuspiciousFlags = ['--dump','--execute','--eval','--exec','--shell','--script'];
+ this._baseWhitelist = ['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','reset','factory-reset','reset-vfs','js','js-run','node','debug','debug-ai','debug-stats','debug-history','debug-analyze','debug-trends','debug-fix','debug-health','session-new','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','dashboard','context-auto'];
+ this.destructiveCmds = new Set(this._baseDestructive);
+ this.systemCmds = new Set(this._baseSystem);
+ this.suspiciousFlags = new Set(this._baseSuspiciousFlags);
+ this.whitelist = new Set(this._baseWhitelist);
+ // Load persisted user deltas (add/remove) so hardcoded defaults can be demoted.
+ this._loadSecurityDeltas();
this._registerBuiltIn();
}
+ _loadSecurityDeltas() {
+ try {
+ const schema = this.terminal._getDataSchemaVersion ? this.terminal._getDataSchemaVersion() : 1;
+ localStorage.setItem('webx11_data_schema_version', String(schema));
+ const raw = localStorage.getItem('webx11_security_deltas');
+ if (!raw) {
+ // One-time migration from legacy flat keys to delta format.
+ const legacy = {};
+ try { legacy.destructive = JSON.parse(localStorage.getItem('webx11_security_destructive') || '[]'); } catch (e) {}
+ try { legacy.system = JSON.parse(localStorage.getItem('webx11_security_system') || '[]'); } catch (e) {}
+ try { legacy.whitelist = JSON.parse(localStorage.getItem('webx11_security_whitelist') || '[]'); } catch (e) {}
+ try { legacy.flags = JSON.parse(localStorage.getItem('webx11_security_flags') || '[]'); } catch (e) {}
+ const delta = { add: {}, remove: {} };
+ for (const c of (legacy.destructive || [])) if (!this._baseDestructive.includes(c)) { if (!delta.add.destructive) delta.add.destructive = []; delta.add.destructive.push(c); }
+ for (const c of (legacy.system || [])) if (!this._baseSystem.includes(c)) { if (!delta.add.system) delta.add.system = []; delta.add.system.push(c); }
+ for (const c of (legacy.whitelist || [])) if (!this._baseWhitelist.includes(c)) { if (!delta.add.whitelist) delta.add.whitelist = []; delta.add.whitelist.push(c); }
+ for (const f of (legacy.flags || [])) if (!this._baseSuspiciousFlags.includes(f)) { if (!delta.add.flags) delta.add.flags = []; delta.add.flags.push(f); }
+ localStorage.setItem('webx11_security_deltas', JSON.stringify(delta));
+ localStorage.removeItem('webx11_security_destructive');
+ localStorage.removeItem('webx11_security_system');
+ localStorage.removeItem('webx11_security_whitelist');
+ localStorage.removeItem('webx11_security_flags');
+ return;
+ }
+ const d = JSON.parse(raw);
+ if (d.remove) {
+ for (const c of (d.remove.destructive || [])) this.destructiveCmds.delete(c);
+ for (const c of (d.remove.system || [])) this.systemCmds.delete(c);
+ for (const c of (d.remove.whitelist || [])) this.whitelist.delete(c);
+ for (const f of (d.remove.flags || [])) this.suspiciousFlags.delete(f);
+ }
+ if (d.add) {
+ for (const c of (d.add.destructive || [])) this.destructiveCmds.add(c);
+ for (const c of (d.add.system || [])) this.systemCmds.add(c);
+ for (const c of (d.add.whitelist || [])) this.whitelist.add(c);
+ for (const f of (d.add.flags || [])) this.suspiciousFlags.add(f);
+ }
+ } catch (e) {}
+ }
+ _saveSecurityDeltas() {
+ const delta = { add: {}, remove: {} };
+ for (const c of this.destructiveCmds) if (!this._baseDestructive.includes(c)) { if (!delta.add.destructive) delta.add.destructive = []; delta.add.destructive.push(c); }
+ for (const c of this._baseDestructive) if (!this.destructiveCmds.has(c)) { if (!delta.remove.destructive) delta.remove.destructive = []; delta.remove.destructive.push(c); }
+ for (const c of this.systemCmds) if (!this._baseSystem.includes(c)) { if (!delta.add.system) delta.add.system = []; delta.add.system.push(c); }
+ for (const c of this._baseSystem) if (!this.systemCmds.has(c)) { if (!delta.remove.system) delta.remove.system = []; delta.remove.system.push(c); }
+ for (const c of this.whitelist) if (!this._baseWhitelist.includes(c)) { if (!delta.add.whitelist) delta.add.whitelist = []; delta.add.whitelist.push(c); }
+ for (const c of this._baseWhitelist) if (!this.whitelist.has(c)) { if (!delta.remove.whitelist) delta.remove.whitelist = []; delta.remove.whitelist.push(c); }
+ for (const f of this.suspiciousFlags) if (!this._baseSuspiciousFlags.includes(f)) { if (!delta.add.flags) delta.add.flags = []; delta.add.flags.push(f); }
+ for (const f of this._baseSuspiciousFlags) if (!this.suspiciousFlags.has(f)) { if (!delta.remove.flags) delta.remove.flags = []; delta.remove.flags.push(f); }
+ localStorage.setItem('webx11_security_deltas', JSON.stringify(delta));
+ }
_registerBuiltIn() {
- const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','agent-loops','bench','bench-compare'];
+ const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','dashboard'];
internals.forEach(c => this.registerCommand(c, { category: 'builtin' }));
}
setLevel(n) {
@@ -2472,8 +2516,13 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
}
registerCommand(name, meta = {}) {
this.commandMeta.set(name, { destructive: false, category: 'user', ...meta });
+ this.invalidateCommandCache();
+ }
+ invalidateCommandCache() {
+ this._commandCacheDirty = true;
}
_collectAllCommands() {
+ if (!this._commandCacheDirty && this._commandCache) return this._commandCache;
const cmds = new Set([...this.commandMeta.keys()]);
for (const modName in this.terminal.moduleManager.modules) {
const mod = this.terminal.moduleManager.modules[modName];
@@ -2483,7 +2532,9 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
}
const builtins = ['clear','help','ls','df','security','security-level'];
builtins.forEach(c => cmds.add(c));
- return [...cmds];
+ this._commandCache = [...cmds];
+ this._commandCacheDirty = false;
+ return this._commandCache;
}
_levenshtein(a, b) {
a = a.toLowerCase(); b = b.toLowerCase();
@@ -2712,6 +2763,19 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
// Apply config overrides after everything is initialized
this._applyTerminalConfig();
}
+ /* ---------- helpers ---------- */
+ static _escapeHtml(str) {
+ if (str == null) return '';
+ return String(str).replace(/[&<>'"]/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[m]));
+ }
+ _escapeHtml(str) { return WebX11Terminal._escapeHtml(str); }
+ _getDataSchemaVersion() { return 1; }
+ _escapeYamlString(str) {
+ return '"' + String(str == null ? '' : str).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
+ }
+ _invalidateCommandCache() {
+ if (this.security) this.security.invalidateCommandCache();
+ }
initEventListeners() {
this.inputField.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
@@ -3629,6 +3693,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
this._safeRegister('ServerMonitorModule', 'server monitor');
this._safeRegister('SmartContextModule', 'smart context');
this._safeRegister('SimpleGUIModule', 'simple GUI builder');
+ this._safeRegister('DebugSupervisorModule', 'debug supervisor');
// v4.5.47: Critical ext modules promoted to boot path
this._safeRegister('EditorModule', 'editor');
this._safeRegister('ShellModule', 'shell');
@@ -3676,6 +3741,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
DiffModule: typeof DiffModule !== 'undefined' ? DiffModule : undefined,
BusyBoxModule: typeof BusyBoxModule !== 'undefined' ? BusyBoxModule : undefined,
SmartContextModule: typeof SmartContextModule !== 'undefined' ? SmartContextModule : undefined,
+ DebugSupervisorModule: typeof DebugSupervisorModule !== 'undefined' ? DebugSupervisorModule : undefined,
};
const cls = registry[className];
if (typeof cls === 'undefined') {
@@ -3737,7 +3803,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
} catch (e) {}
}
async _loadExtModules(filterKeys) {
- const autoLoadKeys = filterKeys || ['debug-panel'];
+ const autoLoadKeys = filterKeys || ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'cryptogzip', 'tutorial', 'floating', 'console', 'media'];
const cacheVer = localStorage.getItem('webx11_cache_ver') || '2026-05-27-14';
let index = null;
let lastErr = null;
@@ -3760,9 +3826,6 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
return;
}
- // Cache packages index for lazy-load dispatch
- try { localStorage.setItem('webx11_packages_index', JSON.stringify(index)); } catch (e) {}
-
for (const key of autoLoadKeys) {
const pkg = index.packages[key];
if (pkg) {
@@ -3773,7 +3836,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
}
async reloadPersistedModules() {
const registry = this.moduleManager.moduleRegistry;
- const skipList = ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating', 'console', 'media', 'three'];
+ const skipList = ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating', 'console', 'media'];
for (const mod of registry) {
if (mod.enabled && mod.url) {
// Skip ext modules — they load via _loadExtModules
@@ -3808,10 +3871,12 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
splitPipe(input) {
const parts = [];
let current = '';
- let inQuote = false;
+ let inSingle = false;
+ let inDouble = false;
for (const ch of input) {
- if (ch === '"' || ch === "'") { inQuote = !inQuote; current += ch; }
- else if (ch === '|' && !inQuote) { parts.push(current.trim()); current = ''; }
+ if (ch === '"' && !inSingle) { inDouble = !inDouble; current += ch; }
+ else if (ch === "'" && !inDouble) { inSingle = !inSingle; current += ch; }
+ else if (ch === '|' && !inSingle && !inDouble) { parts.push(current.trim()); current = ''; }
else { current += ch; }
}
parts.push(current.trim());
@@ -3851,8 +3916,11 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
const originalPrint = this.print.bind(this);
let output = '';
this.print = (text, type) => { output += text + '\n'; };
- await this.dispatchCommand(cmd, args);
- this.print = originalPrint;
+ try {
+ await this.dispatchCommand(cmd, args);
+ } finally {
+ this.print = originalPrint;
+ }
const trimmed = output.trim();
if (trimmed.length > this._pipeBufferSize) {
return trimmed.slice(0, this._pipeBufferSize);
@@ -3868,6 +3936,21 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
if (cmd === 'load-local' || cmd === 'loadlocal') { this.loadLocalModule(args[0]); return; }
if (cmd === 'smart-prompt') { const val = args[0] && args[0] === 'off' ? 'off' : 'on'; localStorage.setItem('webx11_smart_prompt', val); this.print('Smart prompt ' + val, 'success'); this.updatePrompt(); return; }
if (cmd === 'mem') { this.printMemoryStats(); return; }
+ if (cmd === 'dashboard') {
+ // Collect dashboard data and open the dashboard page
+ const aiMod = this.moduleManager?.modules?.WebX11AIAssistantModule;
+ if (aiMod && typeof aiMod._collectDashboardData === 'function') {
+ aiMod._collectDashboardData().then(function() {
+ this.print('📊 Dashboard data collected — opening dashboard...', 'success');
+ window.open('/spaces/newos4/downloads/dashboard.html', 'newOS4 Dashboard', 'width=1200,height=800');
+ }.bind(this));
+ } else {
+ // Fallback: just open the page (it will try localStorage)
+ this.print('📊 Opening dashboard...', 'info');
+ window.open('/spaces/newos4/downloads/dashboard.html', 'newOS4 Dashboard', 'width=1200,height=800');
+ }
+ return;
+ }
if (cmd === 'config') {
if (args.length === 0) {
this.print('=== Terminal Config ===', 'info');
@@ -3930,7 +4013,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
if (action === 'add') { this.security.destructiveCmds.add(args[1]); }
else if (action === 'remove') { this.security.destructiveCmds.delete(args[1]); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_destructive', JSON.stringify([...this.security.destructiveCmds]));
+ this.security._saveSecurityDeltas();
this.print(`Updated destructive set (${this.security.destructiveCmds.size} commands)`, 'success');
return;
}
@@ -3946,7 +4029,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
if (action === 'add') { this.security.systemCmds.add(args[1]); }
else if (action === 'remove') { this.security.systemCmds.delete(args[1]); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_system', JSON.stringify([...this.security.systemCmds]));
+ this.security._saveSecurityDeltas();
this.print(`Updated system set (${this.security.systemCmds.size} commands)`, 'success');
return;
}
@@ -3962,7 +4045,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
if (action === 'add') { this.security.whitelist.add(args[1]); }
else if (action === 'remove') { this.security.whitelist.delete(args[1]); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_whitelist', JSON.stringify([...this.security.whitelist]));
+ this.security._saveSecurityDeltas();
this.print(`Updated whitelist (${this.security.whitelist.size} commands)`, 'success');
return;
}
@@ -3979,15 +4062,15 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
if (action === 'add') { this.security.suspiciousFlags.add(flag); }
else if (action === 'remove') { this.security.suspiciousFlags.delete(flag); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_flags', JSON.stringify([...this.security.suspiciousFlags]));
+ this.security._saveSecurityDeltas();
this.print(`Updated suspicious flags (${this.security.suspiciousFlags.size} flags)`, 'success');
return;
}
if (cmd === 'cmd-allow') { this._resolveCmdConfirm(true); return; }
if (cmd === 'cmd-deny') { this._resolveCmdConfirm(false); return; }
if (cmd === 'vfs' && args[0] === 'reset') {
- if (args[1] !== '--force') {
- this.print('⚠ This will permanently delete all files! Use vfs reset --force to confirm.', 'warning');
+ if (args[1] !== '--confirm' && args[1] !== '--force') {
+ this.print('⚠ This will permanently delete all files! Use vfs reset --confirm to confirm.', 'warning');
return;
}
this.fs.reset();
@@ -4029,8 +4112,8 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
return;
}
if (cmd === 'empty-trash') {
- if (args[0] !== '--force') {
- this.print('⚠ This will permanently delete all trashed files! Use empty-trash --force to confirm.', 'warning');
+ if (args[0] !== '--confirm' && args[0] !== '--force') {
+ this.print('⚠ This will permanently delete all trashed files! Use empty-trash --confirm to confirm.', 'warning');
return;
}
if (this.fs.emptyTrash()) {
@@ -4147,44 +4230,14 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
} catch (_e) {}
}
return; }
- catch (err) { this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error'); return; }
- }
- }
- // Enhanced: check fuzzy suggestions before showing "not found"
- // Lazy-load: check if command belongs to an unloaded ext module
- if (!this._extModuleCommandMap) {
- this._extModuleCommandMap = {};
- this._extModuleUrlMap = {};
- try {
- const idx = JSON.parse(localStorage.getItem('webx11_packages_index') || '{}');
- if (idx.packages) {
- for (const [key, pkg] of Object.entries(idx.packages)) {
- if (pkg.commands && Array.isArray(pkg.commands)) {
- for (const c of pkg.commands) {
- this._extModuleCommandMap[c] = key;
- this._extModuleUrlMap[key] = pkg.url;
- }
- }
- }
- }
- } catch (e) {}
- }
- if (this._extModuleCommandMap[cmd] && !this.moduleManager.modules[this._extModuleCommandMap[cmd]]) {
- const key = this._extModuleCommandMap[cmd];
- const url = this._extModuleUrlMap[key];
- if (url) {
- this.print('⏳ Loading module for "' + cmd + '"...', 'info');
- const loaded = await this.moduleManager.loadModuleFromURL(url);
- if (loaded) {
- // Retry dispatch
- const mod = this.moduleManager.modules[key];
- if (mod && mod.commands && mod.commands[cmd]) {
- try { await mod.commands[cmd](args); return; }
- catch (err) { this.print('Error: ' + err.message, 'error'); return; }
- }
+ catch (err) {
+ this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error');
+ console.error(`[Terminal] Error in ${modName}.${cmd}:`, err);
+ return;
}
}
}
+ // Enhanced: check fuzzy suggestions before showing "not found"
const suggestion = this.security.suggest(cmd);
if (suggestion && suggestion.dist <= 2 && suggestion.dist > 0) {
this.print(`Command not found: ${cmd}`, 'error');
@@ -4218,7 +4271,17 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
files.forEach(f => {
const div = document.createElement('div');
div.className = `file-item ${f.type === 'dir' ? 'dir' : ''}`;
- div.innerHTML = `<span>${f.type === 'dir' ? '📁' : '📄'} ${f.name}</span> <span style="font-size:0.8em">${f.size}b</span>`;
+ const icon = document.createElement('span');
+ icon.textContent = f.type === 'dir' ? '📁' : '📄';
+ const name = document.createElement('span');
+ name.textContent = ' ' + f.name;
+ const size = document.createElement('span');
+ size.style.fontSize = '0.8em';
+ size.textContent = ' ' + f.size + 'b';
+ div.appendChild(icon);
+ div.appendChild(name);
+ div.appendChild(document.createTextNode(' '));
+ div.appendChild(size);
div.onclick = () => {
if (f.type === 'file') {
const content = this.fs.readFile(f.name);
@@ -4257,14 +4320,29 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
modules.forEach(mod => {
const div = document.createElement('div');
div.className = `module-item ${mod.loaded ? '' : 'disabled'}`;
- const badge = mod.url ? '' : '<span class="module-badge builtin">Built-in</span>';
- div.innerHTML = `
- <div>
- <strong>${mod.name}</strong>${badge}
- <div style="font-size:0.8em; color:#666">${mod.url || 'Built-in'}</div>
- </div>
- ${mod.url ? `<button class="btn btn-danger" onclick="webx11.removeModule('${mod.name}')" style="padding:4px 8px;">✕</button>` : ''}
- `;
+ const info = document.createElement('div');
+ const strong = document.createElement('strong');
+ strong.textContent = mod.name;
+ info.appendChild(strong);
+ if (!mod.url) {
+ const badge = document.createElement('span');
+ badge.className = 'module-badge builtin';
+ badge.textContent = 'Built-in';
+ info.appendChild(badge);
+ }
+ const sub = document.createElement('div');
+ sub.style.cssText = 'font-size:0.8em; color:#666';
+ sub.textContent = mod.url || 'Built-in';
+ info.appendChild(sub);
+ div.appendChild(info);
+ if (mod.url) {
+ const btn = document.createElement('button');
+ btn.className = 'btn btn-danger';
+ btn.style.padding = '4px 8px';
+ btn.textContent = '✕';
+ btn.onclick = () => this.removeModule(mod.name);
+ div.appendChild(btn);
+ }
list.appendChild(div);
});
}
@@ -4376,6 +4454,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
renderConfigList() {
const list = document.getElementById('config-list');
if (!list) return;
+ list.innerHTML = '';
const prefix = 'webx11_config_';
const entries = [];
for (let i = 0; i < localStorage.length; i++) {
@@ -4388,14 +4467,28 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
list.innerHTML = '<div style="padding:20px; text-align:center; color:#666">No configuration saved.</div>';
return;
}
- list.innerHTML = entries.map(e => {
- const displayVal = e.key.toLowerCase().includes('password') || e.key.toLowerCase().includes('token') || e.key.toLowerCase().includes('secret')
- ? '****' : e.value;
- return `<div class="module-item">
- <div><strong>${e.key}</strong> <span style="color:var(--text-muted)">${displayVal}</span></div>
- <button class="btn btn-danger" onclick="webx11.deleteConfigFromUI('${e.key.replace(/'/g, "\\'")}')" style="padding:4px 8px; font-size:0.8em">✕</button>
- </div>`;
- }).join('');
+ entries.forEach(e => {
+ const item = document.createElement('div');
+ item.className = 'module-item';
+ const info = document.createElement('div');
+ const strong = document.createElement('strong');
+ strong.textContent = e.key;
+ info.appendChild(strong);
+ const val = document.createElement('span');
+ val.style.color = 'var(--text-muted)';
+ const sensitive = /password|token|secret|key|private|credential|auth/i.test(e.key);
+ val.textContent = sensitive ? '****' : e.value;
+ info.appendChild(document.createTextNode(' '));
+ info.appendChild(val);
+ const btn = document.createElement('button');
+ btn.className = 'btn btn-danger';
+ btn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ btn.textContent = '✕';
+ btn.onclick = () => this.deleteConfigFromUI(e.key);
+ item.appendChild(info);
+ item.appendChild(btn);
+ list.appendChild(item);
+ });
}
addConfigFromUI() {
const key = document.getElementById('config-key-input').value.trim();
@@ -4448,10 +4541,24 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
// Stats
if (statsEl) {
const cats = new Set(Object.values(index).map(s => s.category || 'general'));
- statsEl.innerHTML = `<span>📚 ${names.length} skills</span><span>📂 ${cats.size} categories</span>${query ? `<span>🔍 ${filtered.length} match '${query}'</span>` : ''}`;
+ statsEl.innerHTML = '';
+ const totalSpan = document.createElement('span');
+ totalSpan.textContent = '📚 ' + names.length + ' skills';
+ statsEl.appendChild(totalSpan);
+ const catSpan = document.createElement('span');
+ catSpan.textContent = '📂 ' + cats.size + ' categories';
+ statsEl.appendChild(catSpan);
+ if (query) {
+ const matchSpan = document.createElement('span');
+ matchSpan.textContent = '🔍 ' + filtered.length + ' match';
+ statsEl.appendChild(matchSpan);
+ }
}
if (filtered.length === 0) {
- list.innerHTML = '<div style="padding:20px; text-align:center; color:#666">' + (query ? 'No skills match your search.' : 'No skills yet. Create one below!') + '</div>';
+ const empty = document.createElement('div');
+ empty.style.cssText = 'padding:20px; text-align:center; color:#666';
+ empty.textContent = query ? 'No skills match your search.' : 'No skills yet. Create one below!';
+ list.appendChild(empty);
return;
}
// Group by category
@@ -4477,18 +4584,41 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
const tags = (s.tags || []).join(', ');
const div = document.createElement('div');
div.className = 'module-item';
- div.innerHTML = `
- <div>
- <strong style="color:var(--success-color)">${name}</strong>
- <div style="font-size:0.85em; color:#aaa">${s.description || ''}</div>
- <div style="font-size:0.75em; color:#666">${tags ? '🏷 ' + tags : ''}</div>
- </div>
- <div style="display:flex;gap:4px">
- <button class="btn" onclick="webx11.viewSkillUI('${name}')" style="padding:4px 8px; font-size:0.8em">👁</button>
- <button class="btn" onclick="webx11.editSkillUI('${name}')" style="padding:4px 8px; font-size:0.8em">✏️</button>
- <button class="btn btn-danger" onclick="webx11.deleteSkillUI('${name}')" style="padding:4px 8px; font-size:0.8em">✕</button>
- </div>
- `;
+ const info = document.createElement('div');
+ const strong = document.createElement('strong');
+ strong.style.color = 'var(--success-color)';
+ strong.textContent = name;
+ info.appendChild(strong);
+ const desc = document.createElement('div');
+ desc.style.cssText = 'font-size:0.85em; color:#aaa';
+ desc.textContent = s.description || '';
+ info.appendChild(desc);
+ const tagsDiv = document.createElement('div');
+ tagsDiv.style.cssText = 'font-size:0.75em; color:#666';
+ tagsDiv.textContent = tags ? '🏷 ' + tags : '';
+ info.appendChild(tagsDiv);
+ const actions = document.createElement('div');
+ actions.style.cssText = 'display:flex;gap:4px';
+ const viewBtn = document.createElement('button');
+ viewBtn.className = 'btn';
+ viewBtn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ viewBtn.textContent = '👁';
+ viewBtn.onclick = () => this.viewSkillUI(name);
+ const editBtn = document.createElement('button');
+ editBtn.className = 'btn';
+ editBtn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ editBtn.textContent = '✏️';
+ editBtn.onclick = () => this.editSkillUI(name);
+ const delBtn = document.createElement('button');
+ delBtn.className = 'btn btn-danger';
+ delBtn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ delBtn.textContent = '✕';
+ delBtn.onclick = () => this.deleteSkillUI(name);
+ actions.appendChild(viewBtn);
+ actions.appendChild(editBtn);
+ actions.appendChild(delBtn);
+ div.appendChild(info);
+ div.appendChild(actions);
body.appendChild(div);
}
section.appendChild(body);
@@ -4552,9 +4682,9 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
alert(`Skill '${name}' already exists. Delete it first or use skill-patch command.`);
return;
}
- let content = contentInput || `---\nname: ${name}\ndescription: ${description}\ntags: [${tags.join(', ')}]\ncategory: ${category}\n---\n## Steps\n1. (to be filled)\n`;
+ let content = contentInput || `---\nname: ${this._escapeYamlString(name)}\ndescription: ${this._escapeYamlString(description)}\ntags: [${tags.map(t => this._escapeYamlString(t)).join(', ')}]\ncategory: ${this._escapeYamlString(category)}\n---\n## Steps\n1. (to be filled)\n`;
if (!content.startsWith('---')) {
- content = `---\nname: ${name}\ndescription: ${description}\ntags: [${tags.join(', ')}]\ncategory: ${category}\n---\n${content}`;
+ content = `---\nname: ${this._escapeYamlString(name)}\ndescription: ${this._escapeYamlString(description)}\ntags: [${tags.map(t => this._escapeYamlString(t)).join(', ')}]\ncategory: ${this._escapeYamlString(category)}\n---\n${content}`;
}
fs.writeFile(`/skills/${name}.md`, content);
const index = fs.fsReadJSON('/skills/index.json') || {};
@@ -7093,11 +7223,14 @@ class OllamaModule {
const mod = this._getMod(name);
if (!mod) return this.terminal.print(`Module not found: ${name}`, 'error');
const mm = this.terminal.moduleManager;
+ // Find the module's URL from registry
const regEntry = mm.moduleRegistry.find(m => m.name === name);
const url = regEntry ? regEntry.url : null;
if (!url) return this.terminal.print(`Module '${name}' has no URL — cannot reload (built-in)`, 'warning');
this.terminal.print(`🔄 Reloading '${name}' from ${url}...`, 'info');
+ // Unregister old instance
mm.unregisterModule(name);
+ // Re-fetch and re-register
const success = await mm.loadModuleFromURL(url);
if (success) {
this.terminal.print(`✅ Module '${name}' reloaded successfully`, 'success');
@@ -7768,13 +7901,31 @@ buildSystemPrompt() {
} catch (e) {}
let base = `Current date: ${dateStr} (${dayStr}) ${timeStr}${nvfsNote}\n`
+ 'VFS→Modules→AI. nvfs-index for commands. Tool→Observe→Answer. nvfs-resolve. Destructive cmds need confirm.\n'
- + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind: vaultmind_connect(network) — connect wallet, vaultmind_status — check session, vaultmind_query(ep,msg,agent) — Hyperliquid prices/portfolio/chat | wallet_lookup(address) — read-only portfolio for any wallet | get_usage_stats(model,provider,since_hours) — LLM token/latency stats\n'
- + 'web_fetch tool: GET for normal pages (returns title+content+word_count), POST for JSON APIs like Hyperliquid /info (pass method="POST" and body as JSON string).\n'
- + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, guardrail, wget, tar, secret-* — discoverable via help. '
- + 'JS: node <file.js> executes JS from VFS (alias for js-run), node -e "expr" for inline eval. No real Node.js runtime — browser JS only.';
+ + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind | wallet-lookup | get_usage_stats\\n'
+ + 'web_fetch: GET for pages, POST with method+body for JSON APIs. JS: node <file.js> or node -e "expr" (browser JS only).\n'
+ + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, world-model, predict, guardrail, tar, secret-* — via help.\n'
+ + 'predict_task: estimate duration before complex tasks. world_model_query: ask about system state, changes, health.';
if (profilePrompt) {
base = profilePrompt + '\n\n' + base;
}
+ // Apply agent loop profile prepend/append directives
+ var lp = this._activeLoopProfile;
+ if (lp && lp.systemPrompt) {
+ if (lp.systemPrompt.prepend) base = lp.systemPrompt.prepend + '\n\n' + base;
+ if (lp.systemPrompt.append) base = base + '\n\n' + lp.systemPrompt.append;
+ }
+ // Inject auto-fix suggestions from DebugSupervisorModule
+ try {
+ var ds = this.terminal?.moduleManager?.modules?.DebugSupervisorModule;
+ if (ds && this._autoFixSuggestions && this._autoFixSuggestions.length > 0) {
+ base += '\n\n[Auto-Fix Suggestions]\n';
+ for (var fi = 0; fi < this._autoFixSuggestions.length; fi++) {
+ var s = this._autoFixSuggestions[fi];
+ base += '- ' + s.module + ': ' + s.suggestion + ' (' + s.reason + ')\n';
+ }
+ base += '[/Auto-Fix Suggestions]';
+ }
+ } catch (e) {}
return base;
}
getToolDefinitions() {
@@ -7784,13 +7935,13 @@ buildSystemPrompt() {
_filterToolsByProfile(allTools) {
var p = this._activeLoopProfile;
if (!p || !p.tools) return allTools;
- if (p.tools.mode === "all") {
+ if (p.tools.mode === 'all') {
if (p.tools.disabled && p.tools.disabled.length > 0) {
return allTools.filter(function(t) { return p.tools.disabled.indexOf(t.function.name) === -1; });
}
return allTools;
}
- if (p.tools.mode === "custom" && p.tools.enabled && p.tools.enabled.length > 0) {
+ if (p.tools.mode === 'custom' && p.tools.enabled && p.tools.enabled.length > 0) {
return allTools.filter(function(t) { return p.tools.enabled.indexOf(t.function.name) !== -1; });
}
return allTools;
@@ -7841,6 +7992,13 @@ buildSystemPrompt() {
}, {
type: 'function',
function: {
+ name: 'get_dashboard_data',
+ description: 'Full dashboard telemetry: modules, agents, memory, VFS, Ollama, token usage, tool latency, system health',
+ parameters: { type: 'object', properties: {}, required: [] }
+ }
+ }, {
+ type: 'function',
+ function: {
name: 'get_help',
description: 'Command reference',
parameters: { type: 'object', properties: {}, required: [] }
@@ -7907,15 +8065,18 @@ buildSystemPrompt() {
type: 'function',
function: {
name: 'edit_module',
- description: 'Edit module source (DESTRUCTIVE)',
- parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] }
+ description: 'Edit module source (DESTRUCTIVE). If code is provided, writes directly to VFS and auto-tests. If no code, opens the visual editor (requires manual Save).',
+ parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' }, code: { type: 'string', description: 'Full new source code. When provided, writes directly and auto-tests without opening the editor.' } }, required: ['name'] }
}
}, {
type: 'function',
function: {
name: 'test_module',
- description: 'Test edited module from /_mods/',
- parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] }
+ description: 'Test an edited module: loads it, checks JS errors, verifies commands registered, runs optional test assertions. Auto-rolls-back on failure.',
+ parameters: { type: 'object', properties: {
+ name: { type: 'string', description: 'Module class name' },
+ tests: { type: 'array', description: 'Optional test assertions: [{command, expect}]', items: { type: 'object', properties: { command: { type: 'string' }, expect: { type: 'string' } } } }
+ }, required: ['name'] }
}
}, {
type: 'function',
@@ -8116,7 +8277,7 @@ buildSystemPrompt() {
required: []
}
}
-}, {
+ }, {
type: 'function',
function: {
name: 'smart_context_set',
@@ -8146,32 +8307,8 @@ buildSystemPrompt() {
}, {
type: 'function',
function: {
- name: 'create_3d_scene',
- description: 'Create a 3D scene with Three.js objects. Supports: boxes, spheres, torus, text, particles, lines. Also supports presets: "solar" (solar system), "grid" (color grid). Describe what you want in natural language and the module will parse it.',
- parameters: {
- type: 'object',
- properties: {
- description: { type: 'string', description: 'Natural language description of the 3D scene (e.g. "a red box and two blue spheres with particles")' }
- },
- required: ['description']
- }
- }
- }, {
- type: 'function',
- function: {
- name: 'visualize_mindmap_3d',
- description: 'Render the current mindmap as an interactive 3D force-directed graph with colored spheres and curved connecting lines. Requires MindmapModule to have an active mindmap.',
- parameters: {
- type: 'object',
- properties: {},
- required: []
- }
- }
- }, {
- type: 'function',
- function: {
name: 'vaultmind_query',
- description: "Query VaultMind AI for Hyperliquid market data (prices, portfolio, transactions, copy-trade) or ask an AI agent. Uses same-origin /vmai/ API.",
+ description: "Query VaultMind AI for Hyperliquid market data (prices, portfolio, transactions, copy-trade) or ask an AI agent. Uses same-origin /vmai-api/ API.",
parameters: {
type: 'object',
properties: {
@@ -8234,8 +8371,132 @@ buildSystemPrompt() {
required: []
}
}
- }];
- }
+ }, {
+ type: 'function',
+ function: {
+ name: 'agent_loop_list',
+ description: 'List all available agent loop profiles with their settings and benchmark results.',
+ parameters: { type: 'object', properties: {}, required: [] }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'agent_loop_switch',
+ description: 'Switch to a named agent loop profile. Changes tools, max turns, system prompt directives, and loop behavior.',
+ parameters: {
+ type: 'object',
+ properties: { name: { type: 'string', description: 'Profile name to switch to' } },
+ required: ['name']
+ }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'agent_loop_create',
+ description: 'Create a new agent loop profile with custom settings (tools, max turns, temperature, system prompt directives).',
+ parameters: {
+ type: 'object',
+ properties: {
+ name: { type: 'string', description: 'Profile name (lowercase, hyphens)' },
+ description: { type: 'string', description: 'Profile description' },
+ maxTurns: { type: 'number', description: 'Max tool turns (default: 8)' },
+ toolsMode: { type: 'string', description: 'all or custom', enum: ['all', 'custom'] },
+ toolsEnabled: { type: 'array', items: { type: 'string' }, description: 'Tool names to enable (if mode=custom)' },
+ toolsDisabled: { type: 'array', items: { type: 'string' }, description: 'Tool names to disable (if mode=all)' },
+ prependPrompt: { type: 'string', description: 'Directive to prepend to system prompt' },
+ appendPrompt: { type: 'string', description: 'Directive to append to system prompt' }
+ },
+ required: ['name']
+ }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'agent_loop_benchmark',
+ description: 'Run the benchmark suite on a specified agent loop profile. Returns task results with latency, tokens, and pass/fail.',
+ parameters: {
+ type: 'object',
+ properties: {
+ profile: { type: 'string', description: 'Profile name to benchmark (default: active)' },
+ tasks: { type: 'array', items: { type: 'string' }, description: 'Specific task IDs to run (default: all)' }
+ },
+ required: []
+ }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'predict_task',
+ description: 'Predict task duration, tool turns, and confidence based on historical execution data. Use before starting complex tasks to estimate resource needs.',
+ parameters: {
+ type: 'object',
+ properties: {
+ task: { type: 'string', description: 'Task description to predict' }
+ },
+ required: ['task']
+ }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'world_model_query',
+ description: 'Query the system world model for self-awareness: modules, skills, files, agents, health, memory graph, recent changes, active projects, telemetry. Ask questions like "what changed" or "how many modules".',
+ parameters: {
+ type: 'object',
+ properties: {
+ question: { type: 'string', description: 'What to ask: modules, skills, files, agents, health, memory, changes, projects, telemetry, summary' }
+ },
+ required: ['question']
+ }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'health_check',
+ description: 'Run a system health check after a module edit. Verifies: no JS errors, all core commands work, world model is valid, agents are alive.',
+ parameters: { type: 'object', properties: {} }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'diff_module',
+ description: 'Show a diff between the original module source and the edited version in _mods/. Returns line-by-line changes.',
+ parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'ship_module',
+ description: 'Full pipeline: edit → test → health_check → regression suite. Auto-rolls-back on any failure. Set commit:true to also persist to server (requires human approval). Without commit, runs as dry-run.',
+ parameters: { type: 'object', properties: {
+ name: { type: 'string', description: 'Module class name' },
+ code: { type: 'string', description: 'Full new source code' },
+ tests: { type: 'array', description: 'Test assertions [{command, expect}]', items: { type: 'object', properties: { command: { type: 'string' }, expect: { type: 'string' } } } },
+ commit: { type: 'boolean', description: 'If true, request server persistence (needs human approval). Default: false.' }
+ }, required: ['name', 'code'] }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'commit_module',
+ description: 'Request to persist an edited module from VFS _mods/ to the server file system via WebDAV. Creates a pending commit — user must type commit-approve in the terminal to execute. Includes bundle sync and version bump.',
+ parameters: { type: 'object', properties: {
+ name: { type: 'string', description: 'Module class name' },
+ file: { type: 'string', description: 'Target server path (e.g. modules/ext/shell.js). Auto-detect from module registry if omitted.' }
+ }, required: ['name'] }
+ }
+ }
+ ];
+ }
getHelpInfo() {
return {
'ai': 'ai <question> — Ask the AI assistant anything',
@@ -8258,6 +8519,8 @@ buildSystemPrompt() {
'reset-mem': 'reset-mem — Reset AI memory (logs, context, profile, guardrails) without losing VFS or config',
'vaultmind-connect': 'vaultmind-connect [testnet|mainnet] — Connect VaultMind wallet using hyperliquid_key from SecretVault',
'vaultmind-status': 'vaultmind-status — Check VaultMind session status',
+ 'vaultmind-query': 'vaultmind-query <prices|portfolio|transactions|copy-trade|chat> [message] — Query VaultMind API',
+ 'wallet-lookup': 'wallet-lookup <address> — Look up Hyperliquid wallet summary',
};
}
registerCommands() {
@@ -8273,6 +8536,21 @@ buildSystemPrompt() {
this.commands['ai-model'] = (args) => this.showModel(args);
this.commands['ai-compress'] = () => this._manualCompress();
this.commands['ai-config'] = (args) => this.aiConfig(args);
+ this.commands['commit-approve'] = (args) => this._executeCommit(args.join(' '));
+ this.commands['commit-deny'] = () => {
+ if (!this._pendingCommit) return this.terminal.print('No pending commit.', 'warning');
+ const name = this._pendingCommit.name;
+ this._pendingCommit = null;
+ this.terminal.print('❌ Commit denied for ' + name, 'warning');
+ };
+ this.commands['commit-status'] = () => {
+ if (!this._pendingCommit) return this.terminal.print('No pending commit.', 'info');
+ const pc = this._pendingCommit;
+ this.terminal.print('📋 Pending commit: ' + pc.name, 'info');
+ this.terminal.print(' Target: ' + pc.serverPath, 'info');
+ if (pc.bundlePath) this.terminal.print(' Bundle: ' + pc.bundlePath, 'info');
+ this.terminal.print(' Changes: ' + pc.changes + ' line(s)', 'info');
+ };
this.commands['agent-loops'] = (args) => this.agentLoopsCmd(args);
this.commands['bench'] = (args) => this.benchCmd(args);
this.commands['bench-compare'] = (args) => this.benchCompareCmd(args);
@@ -8283,8 +8561,6 @@ buildSystemPrompt() {
this.commands['debug-ai-log'] = () => this._showToolCallLog();
this.commands['archive-list'] = () => this._archiveList();
this.commands['archive-read'] = (args) => this._archiveRead(args);
- this.commands['workflow-list'] = () => this._workflowList();
- this.commands['workflow-run'] = (args) => this._workflowRun(args);
this.commands['guardrail'] = (args) => this.guardrailCmd(args);
this.commands['guardrail-add'] = (args) => this.guardrailCmd(['add', ...args]);
this.commands['guardrail-remove'] = (args) => this.guardrailCmd(['remove', ...args]);
@@ -8295,6 +8571,8 @@ buildSystemPrompt() {
this.commands['reset-mem'] = () => this.resetMem();
this.commands['vaultmind-connect'] = (args) => this._vaultmindConnectCmd(args);
this.commands['vaultmind-status'] = () => this._vaultmindStatusCmd();
+ this.commands['vaultmind-query'] = (args) => this._vaultmindQueryCmd(args);
+ this.commands['wallet-lookup'] = (args) => this._walletLookupCmd(args);
}
async chat(args) {
const userMessage = args.join(' ');
@@ -8314,6 +8592,18 @@ buildSystemPrompt() {
}
this._chatLock = true;
this.terminal.markDirty();
+ // Auto-route context based on intent
+ try {
+ const scm = this.terminal?.moduleManager?.modules?.SmartContextModule;
+ if (scm && typeof scm.autoRouteContext === 'function') {
+ const routeResult = scm.autoRouteContext(userMessage);
+ if (routeResult.switched) {
+ this.terminal.print('🔄 Context auto-switched: ' + routeResult.from + ' → ' + routeResult.to + ' (' + routeResult.confidence + '% confidence)', 'info');
+ // Rebuild system prompt after context switch
+ this.systemPrompt = this.buildSystemPrompt();
+ }
+ }
+ } catch (e) { console.warn('[AI] Auto-routing error:', e.message); }
// Re-read context from localStorage (auto-restore may have written it after init)
this._loadContext();
// Compress context if it exceeds the token threshold
@@ -8397,12 +8687,31 @@ buildSystemPrompt() {
if (loopResult.toolTurns >= 2 && !this._cancelled) {
await this.afterActionReflection(userMessage, loopResult.toolTurns);
}
+ // Feed result to predictor for learning (cold module activation)
+ if (loopResult && !this._cancelled && loopResult.toolTurns >= 1) {
+ try {
+ const predictor = this.terminal.moduleManager.modules['PredictorModule'];
+ if (predictor && typeof predictor.recordResult === 'function') {
+ const features = predictor._extractFeatures(userMessage);
+ const duration = (Date.now() - lockStart) / 1000;
+ predictor.recordResult(features, duration, loopResult.toolTurns, !this._cancelled);
+ }
+ } catch (e) { /* predictor learning is best-effort */ }
+ // Record task in memory graph for direct AI conversations (not just orchestrator)
+ try {
+ const mg = this.terminal.moduleManager.modules['MemoryGraphModule'];
+ if (mg && typeof mg.recordTask === 'function') {
+ mg.recordTask(userMessage, loopResult.finalAnswer || '', 'ai-assistant');
+ }
+ } catch (e) { /* memory graph is best-effort */ }
+ }
+ // Self-improvement: post-run anomaly detection
if (loopResult && !this._cancelled) {
try {
var om = this.terminal ? this.terminal.ollamaManager : null;
var usageData = om && om._usageLog && om._usageLog.length > 0 ? om._usageLog[om._usageLog.length - 1] : null;
this._postRunReview(loopResult, this._toolCallLog.slice(-(loopResult.toolTurns || 0)), usageData);
- } catch (e) {}
+ } catch (e) { /* self-improvement is best-effort */ }
}
// Auto-create skills from repeated errors
if (this._errorLog.length >= 3) {
@@ -8425,7 +8734,7 @@ buildSystemPrompt() {
// Log the AI interaction
this.terminal.log('INFO', 'AI', 'Query: ' + userMessage.substring(0, 100));
if (loopResult && loopResult.toolTurns) {
- var toolNames = this._toolCallLog.slice(-(loopResult.toolTurns || 0)).map(function(e) { return e.name; }).join(', ');
+ const toolNames = this._toolCallLog.slice(-(loopResult.toolTurns || 0)).map(function(e) { return e.name; }).join(', ');
this.terminal.log('INFO', 'AI', 'Used ' + loopResult.toolTurns + ' tool(s): ' + toolNames);
}
}
@@ -8449,6 +8758,280 @@ buildSystemPrompt() {
});
}
}
+ // ─── Self-Improvement Helpers ───
+ async _captureCommandOutput(cmd) {
+ const originalPrint = this.terminal.print.bind(this.terminal);
+ let output = '';
+ this.terminal.print = (text, type) => { output += text + '\n'; };
+ try { await this.terminal.security.checkAndRun(cmd); }
+ finally { this.terminal.print = originalPrint; }
+ return output.trim();
+ }
+ async _captureShellCommand(cmd) {
+ // Routes through executeCommand (shell parser) for >, |, &&, export, etc.
+ const originalPrint = this.terminal.print.bind(this.terminal);
+ let output = '';
+ this.terminal.print = (text, type) => { output += text + '\n'; };
+ try { await this.terminal.executeCommand(cmd); }
+ finally { this.terminal.print = originalPrint; }
+ return output.trim();
+ }
+ async _captureConsoleErrors(durationMs) {
+ durationMs = durationMs || 500;
+ var errors = [];
+ var origError = console.error;
+ var origOnError = window.onerror;
+ console.error = function() {
+ var args = Array.prototype.slice.call(arguments);
+ errors.push(args.join(' '));
+ };
+ window.onerror = function(msg, src, line, col, err) {
+ errors.push(msg + (err ? ' — ' + err.message : ''));
+ };
+ await new Promise(function(r) { setTimeout(r, durationMs); });
+ console.error = origError;
+ window.onerror = origOnError;
+ return errors;
+ }
+ _regressionTasks() {
+ return [
+ { id: 'shell-export', module: 'ShellModule', command: 'export REGTEST=ok', expect: 'REGTEST', useShell: true },
+ { id: 'shell-redirect', module: 'ShellModule', command: 'echo hi > _regtest.txt', expect: 'Redirected', useShell: true },
+ { id: 'shell-chain', module: 'ShellModule', command: 'echo a && echo b', expect: 'b', useShell: true },
+ { id: 'shell-pipe', module: 'ShellModule', command: 'echo hello | grep hello', expect: 'hello', useShell: true },
+ { id: 'fs-ls', module: 'SystemModule', command: 'ls', expect: '' },
+ { id: 'fs-pwd', module: 'SystemModule', command: 'pwd', expect: '/' },
+ { id: 'orch-agents', module: 'OrchestratorModule', command: 'list-agents', expect: 'agent' },
+ { id: 'ai-tools', module: 'WebX11AIAssistantModule', command: 'list_modules', expect: 'Module' },
+ { id: 'calc-basic', module: 'CalcModule', command: 'calc 1+1', expect: '2' },
+ { id: 'calc-advanced', module: 'CalcModule', command: 'calc 2*3', expect: '6' },
+ { id: 'webdav-ls', module: 'WebDAVModule', command: 'webdav-ls', expect: '' },
+ { id: 'cron-list', module: 'CronModule', command: 'cron-list', expect: '' },
+ { id: 'theme-list', module: 'ThemeModule', command: 'theme-list', expect: '' },
+ { id: 'mindmap-list', module: 'MindmapModule', command: 'mindmap-list', expect: '' },
+ { id: 'media-status', module: 'MediaModule', command: 'media-status', expect: '' },
+ { id: 'skill-list', module: 'SkillPackCoreModule', command: 'skill-list', expect: '' },
+ { id: 'crypto-encode', module: 'CryptoGzipModule', command: 'crypto-encode test', expect: '' },
+ { id: 'debug-toggle', module: 'DebugPanelModule', command: 'debug', expect: '' },
+ { id: 'voice-status', module: 'VoiceModule', command: 'voice-status', expect: '' },
+ { id: 'wpm-status', module: 'WPMModule', command: 'wpm-status', expect: '' },
+ { id: 'console-help', module: 'ConsoleModule', command: 'console-help', expect: '' },
+ { id: 'tutorial-status', module: 'TutorialModule', command: 'tutorial-status', expect: '' },
+ ];
+ }
+ async _runRegressionSuite(moduleName) {
+ const tests = this._regressionTasks().filter(t => t.module === moduleName);
+ if (tests.length === 0) return { module: moduleName, total: 0, passed: 0, failed: 0, results: [], allPassed: true, message: 'No regression tests for ' + moduleName };
+ const results = [];
+ for (const t of tests) {
+ try {
+ const out = t.useShell ? await this._captureShellCommand(t.command) : await this._captureCommandOutput(t.command);
+ const pass = t.expect ? out.includes(t.expect) : out.length > 0;
+ results.push({ id: t.id, command: t.command, pass, output: out.substring(0, 100) });
+ } catch (e) {
+ results.push({ id: t.id, command: t.command, pass: false, output: 'Error: ' + e.message });
+ }
+ }
+ const passed = results.filter(r => r.pass).length;
+ return { module: moduleName, total: results.length, passed, failed: results.length - passed, results, allPassed: passed === results.length };
+ }
+ // ─── Phase D: Server Persistence ───
+ async _webdavPush(remotePath, content) {
+ const webdav = this.terminal.moduleManager.modules['WebDAVModule'];
+ if (!webdav) throw new Error('WebDAV module not loaded');
+ // Use the webdav module's internal request method
+ const resp = await webdav._webdavRequest('PUT', remotePath, {
+ body: content,
+ headers: { 'Content-Type': 'text/plain' }
+ });
+ if (!resp.ok && resp.status !== 201 && resp.status !== 204) {
+ throw new Error('WebDAV PUT failed: HTTP ' + resp.status);
+ }
+ return true;
+ }
+ async _webdavGet(remotePath) {
+ const webdav = this.terminal.moduleManager.modules['WebDAVModule'];
+ if (!webdav) throw new Error('WebDAV module not loaded');
+ const resp = await webdav._webdavRequest('GET', remotePath);
+ if (!resp.ok) throw new Error('WebDAV GET failed: HTTP ' + resp.status);
+ return await resp.text();
+ }
+ _extractClassName(code) {
+ const m = code.match(/^class\s+(\w+)/m);
+ return m ? m[1] : null;
+ }
+ _extractClassBlock(code, className) {
+ // Extract the class definition from standalone code (without self-registration block)
+ const classStart = code.indexOf('class ' + className);
+ if (classStart === -1) return null;
+ // Find matching closing brace by counting
+ let depth = 0;
+ let end = classStart;
+ for (let i = classStart; i < code.length; i++) {
+ if (code[i] === '{') depth++;
+ else if (code[i] === '}') {
+ depth--;
+ if (depth === 0) { end = i + 1; break; }
+ }
+ }
+ return code.substring(classStart, end);
+ }
+ async _syncBundle(bundlePath, modName, newCode) {
+ try {
+ // Download current bundle
+ const bundle = await this._webdavGet(bundlePath);
+ // Find the class block in the bundle
+ const className = modName;
+ const classStart = bundle.indexOf('class ' + className);
+ if (classStart === -1) return { synced: false, reason: 'Class not found in bundle' };
+ // Find matching closing brace
+ let depth = 0;
+ let classEnd = classStart;
+ for (let i = classStart; i < bundle.length; i++) {
+ if (bundle[i] === '{') depth++;
+ else if (bundle[i] === '}') {
+ depth--;
+ if (depth === 0) { classEnd = i + 1; break; }
+ }
+ }
+ // Extract new class block from the edited code
+ const newClassBlock = this._extractClassBlock(newCode, className);
+ if (!newClassBlock) return { synced: false, reason: 'Could not extract class from new code' };
+ // Replace in bundle
+ const newBundle = bundle.substring(0, classStart) + newClassBlock + bundle.substring(classEnd);
+ // Validate with vm.Script equivalent (Function constructor)
+ try { new Function(newBundle); } catch (e) {
+ return { synced: false, reason: 'Bundle syntax error after replace: ' + e.message };
+ }
+ // Upload
+ await this._webdavPush(bundlePath, newBundle);
+ return { synced: true, bytes: newBundle.length };
+ } catch (e) {
+ return { synced: false, reason: e.message };
+ }
+ }
+ _parseVersion(ver) {
+ // Handle both old format (2026-07-06-v4.6.01) and new format (v4.6.01)
+ const m = ver.match(/v(\d+)\.(\d+)\.(\d+)/);
+ if (!m) return { major: 4, minor: 6, patch: 0 };
+ return { major: parseInt(m[1]), minor: parseInt(m[2]), patch: parseInt(m[3]) };
+ }
+ _bumpVersionString(ver) {
+ const v = this._parseVersion(ver);
+ return 'v' + v.major + '.' + v.minor + '.' + (v.patch + 1);
+ }
+ async _bumpVersion() {
+ // Read current version from the page
+ const currentVer = window.NEWOS4_VERSION || 'v4.6.01';
+ const newVer = this._bumpVersionString(currentVer);
+ // 1. Update main.html — replace all ?v= occurrences
+ const mainHtml = await this._webdavGet('main.html');
+ const updatedHtml = mainHtml.replace(/\?v=[^"']+/g, '?v=' + newVer);
+ await this._webdavPush('main.html', updatedHtml);
+ // 2. Update module-loader.js — replace LATEST_VER
+ const loader = await this._webdavGet('modules/core/module-loader.js');
+ const updatedLoader = loader.replace(/const LATEST_VER = '[^']+'/, "const LATEST_VER = '" + newVer + "'");
+ await this._webdavPush('modules/core/module-loader.js', updatedLoader);
+ return { from: currentVer, to: newVer };
+ }
+ _calculateConfidence(modName, testResult, healthResult, regResult, changeCount) {
+ var score = 100;
+ if (changeCount > 50) score -= 20;
+ if (changeCount > 100) score -= 20;
+ if (regResult.total === 0) score -= 30;
+ var critical = ['ShellModule', 'SystemModule', 'OrchestratorModule', 'WebX11AIAssistantModule'];
+ if (critical.indexOf(modName) !== -1) score -= 15;
+ if (testResult.tests && testResult.tests.length >= 3) score += 10;
+ if (healthResult.jsErrors && healthResult.jsErrors.length > 0) score -= 50;
+ return Math.max(0, Math.min(100, score));
+ }
+
+ async _executeCommit(comment) {
+ const pc = this._pendingCommit;
+ if (!pc) return this.terminal.print('No pending commit. Use commit_module first.', 'warning');
+ // Rate limit check
+ const today = new Date().toISOString().split('T')[0];
+ const commitLog = this.terminal.fs.fsReadJSON('/_mods/commit-log.json') || {};
+ if (commitLog[today] && commitLog[today] >= 5) {
+ return this.terminal.print('⚠️ Rate limit: 5 commits today. Try tomorrow.', 'warning');
+ }
+ // Cooldown check (10 min)
+ if (commitLog._lastCommitTime && Date.now() - commitLog._lastCommitTime < 600000) {
+ const wait = Math.ceil((600000 - (Date.now() - commitLog._lastCommitTime)) / 60000);
+ return this.terminal.print('⏱ Cooldown: ' + wait + ' min until next commit.', 'warning');
+ }
+ this.terminal.print('🚀 Executing commit for ' + pc.name + '...', 'info');
+ const results = { module: pc.name, steps: [] };
+ // Step 1: Push standalone file
+ try {
+ await this._webdavPush(pc.serverPath, pc.code);
+ results.steps.push({ step: 'standalone', status: 'ok', path: pc.serverPath });
+ this.terminal.print(' ✅ Standalone: ' + pc.serverPath, 'success');
+ } catch (e) {
+ results.steps.push({ step: 'standalone', status: 'fail', error: e.message });
+ this.terminal.print(' ❌ Standalone failed: ' + e.message, 'error');
+ return results;
+ }
+ // Step 1b: Post-commit syntax verification
+ try {
+ const verifyResp = await fetch(pc.serverPath + '?v=' + Date.now());
+ const committedCode = await verifyResp.text();
+ new Function(committedCode);
+ results.steps.push({ step: 'verify', status: 'ok' });
+ this.terminal.print(' ✅ Post-commit syntax check passed', 'success');
+ } catch (e) {
+ results.steps.push({ step: 'verify', status: 'fail', error: e.message });
+ this.terminal.print(' ❌ Post-commit syntax check failed: ' + e.message, 'error');
+ // Auto-revert
+ try {
+ await this._webdavPush(pc.serverPath, pc.origCode || '');
+ this.terminal.print(' ⏪ Auto-reverted to original code', 'warning');
+ results.reverted = true;
+ results.syntaxError = e.message;
+ } catch (revertErr) {
+ this.terminal.print(' ❌ Auto-revert FAILED: ' + revertErr.message, 'error');
+ results.revertFailed = true;
+ }
+ this._pendingCommit = null;
+ return results;
+ }
+ // Step 2: Sync bundle
+ if (pc.bundlePath) {
+ try {
+ const syncResult = await this._syncBundle(pc.bundlePath, pc.name, pc.code);
+ results.steps.push({ step: 'bundle', status: syncResult.synced ? 'ok' : 'skip', detail: syncResult });
+ if (syncResult.synced) this.terminal.print(' ✅ Bundle: ' + pc.bundlePath, 'success');
+ else this.terminal.print(' ⚠️ Bundle sync skipped: ' + syncResult.reason, 'warning');
+ } catch (e) {
+ results.steps.push({ step: 'bundle', status: 'fail', error: e.message });
+ this.terminal.print(' ❌ Bundle sync failed: ' + e.message, 'error');
+ }
+ }
+ // Step 3: Bump version
+ try {
+ const verResult = await this._bumpVersion();
+ results.steps.push({ step: 'version', status: 'ok', from: verResult.from, to: verResult.to });
+ this.terminal.print(' ✅ Version: ' + verResult.from + ' → ' + verResult.to, 'success');
+ results.newVersion = verResult.to;
+ } catch (e) {
+ results.steps.push({ step: 'version', status: 'fail', error: e.message });
+ this.terminal.print(' ⚠️ Version bump failed: ' + e.message, 'warning');
+ }
+ // Step 4: Cleanup _mods/
+ this.terminal.fs.deleteFile('_mods/' + pc.name + '.js');
+ this.terminal.fs.deleteFile('_mods/' + pc.name + '.orig.js');
+ this.terminal.fs.deleteFile('_mods/' + pc.name + '.meta.json');
+ // Step 5: Rate limit update
+ commitLog[today] = (commitLog[today] || 0) + 1;
+ commitLog._lastCommitTime = Date.now();
+ this.terminal.fs.fsWriteJSON('/_mods/commit-log.json', commitLog);
+ // Clear pending
+ this._pendingCommit = null;
+ this.terminal.print('🎉 Commit complete! Refresh to load ' + (results.newVersion || 'new version'), 'success');
+ if (comment) this.terminal.print(' Comment: ' + comment, 'info');
+ results.success = true;
+ return results;
+ }
_summarizeObservation(result, maxLen) {
if (!result || typeof result !== 'string' || result.length <= maxLen) return result || '';
const lines = result.split('\n');
@@ -8560,7 +9143,7 @@ buildSystemPrompt() {
var profile = this._activeLoopProfile || {};
var loopCfg = profile.loop || {};
var maxTurns = loopCfg.maxTurns || this.MAX_TOOL_TURNS;
- var loopDetect = loopCfg.loopDetection || { enabled: false, threshold: 6, windowMs: 30000 };
+ var loopDetect = loopCfg.loopDetection || { enabled: true, threshold: 6, windowMs: 30000 };
var confEsc = loopCfg.confidenceEscalation || { enabled: true, threshold: 90 };
var finalPrompt = loopCfg.finalTurnPrompt || 'This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know.';
// Memory-augmented planning: inject prior loop lessons as negative constraints
@@ -8592,9 +9175,9 @@ buildSystemPrompt() {
if (this._cancelled) { this.terminal.print('AI loop cancelled', 'warning'); return { finalAnswer: null, toolTurns }; }
toolTurns++;
// On last turn, inject wrap-up prompt
- if (toolTurns === this.MAX_TOOL_TURNS) {
- this.terminal.print(`⏱ Final turn (${toolTurns}/${this.MAX_TOOL_TURNS}) — AI must provide final answer`, 'warning');
- messages.push({ role: 'system', content: 'This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know.' });
+ if (toolTurns === maxTurns) {
+ this.terminal.print(`⏱ Final turn (${toolTurns}/${maxTurns}) — AI must provide final answer`, 'warning');
+ messages.push({ role: 'system', content: finalPrompt });
}
const thinkStart = Date.now();
const res = await this.terminal.ollamaManager.chatWithTools(messages, this.getToolDefinitions());
@@ -8613,7 +9196,7 @@ buildSystemPrompt() {
const vm = this.terminal.moduleManager && this.terminal.moduleManager.modules['VoiceModule'];
if (vm && vm.speakText) vm.speakText(text);
}
- this.terminal.print(`💬 AI answered (turn ${toolTurns}/${this.MAX_TOOL_TURNS}, ${thinkTime}s)`, 'info');
+ this.terminal.print(`💬 AI answered (turn ${toolTurns}/${maxTurns}, ${thinkTime}s)`, 'info');
return { finalAnswer: text, toolTurns };
}
if (res.tool_calls && res.tool_calls.length > 0) {
@@ -8631,7 +9214,7 @@ buildSystemPrompt() {
entry.count++;
entry.ts = now;
_seenCalls.set(sig, entry);
- if (loopDetect.enabled !== false && entry.count >= (loopDetect.threshold || 6)) {
+ if (entry.count >= (loopDetect.threshold || 6)) {
this.terminal.print(`🔄 Loop detected: ${tc.function.name} called ${entry.count}x with same args in 30s — breaking loop`, 'warning');
looping = true;
break;
@@ -8642,7 +9225,7 @@ buildSystemPrompt() {
messages.push({ role: 'system', content: 'You are repeating the same tool call with identical arguments. Stop calling tools and provide your final answer based on what you already know.' });
continue;
}
- this.terminal.print(`🔧 AI wants ${res.tool_calls.length} tool(s) — turn ${toolTurns}/${this.MAX_TOOL_TURNS} (${thinkTime}s)`, 'info');
+ this.terminal.print(`🔧 AI wants ${res.tool_calls.length} tool(s) — turn ${toolTurns}/${maxTurns} (${thinkTime}s)`, 'info');
messages.push(res.message);
let pendingText = res.message?.content || null;
const destructiveTools = res.tool_calls.filter(tc => this.isDestructive(tc.function.name, tc.function.arguments));
@@ -8702,15 +9285,15 @@ buildSystemPrompt() {
}
// Only block on destructive tools
if (destructiveTools.length > 0) {
- // 🎯 CONFIDENCE ESCALATION — auto-approve only if confidence >= 90%
+ // 🎯 CONFIDENCE ESCALATION — auto-approve only if confidence >= threshold (from profile)
let autoApprove = false;
- if (pendingText) {
+ if (confEsc.enabled !== false && pendingText) {
const confidence = this._parseConfidence(pendingText);
- if (confidence !== null && confidence >= 90) {
+ if (confidence !== null && confidence >= (confEsc.threshold || 90)) {
autoApprove = true;
- this.terminal.print(`🎯 AI confidence: ${confidence}% — auto-approved`, 'success');
+ this.terminal.print(`🎯 AI confidence: ${confidence}% — auto-approved (threshold: ${confEsc.threshold || 90}%)`, 'info');
} else if (confidence !== null) {
- this.terminal.print(`🎯 AI confidence: ${confidence}% — below 90% threshold, requiring manual approval`, 'warning');
+ this.terminal.print(`🎯 AI confidence: ${confidence}% — below ${confEsc.threshold || 90}% threshold, requiring manual approval`, 'warning');
} else {
this.terminal.print('🎯 No confidence declared for destructive action — requiring manual approval', 'warning');
}
@@ -8795,7 +9378,7 @@ buildSystemPrompt() {
}
// Loop exited without final answer — auto-summary
const toolNames = Object.keys(this._commandFreq).slice(-3).join(', ');
- this.terminal.print(`🤖 AI reached the ${toolTurns}/${this.MAX_TOOL_TURNS} tool limit.`, 'warning');
+ this.terminal.print(`🤖 AI reached the ${toolTurns}/${maxTurns} tool limit.`, 'warning');
this.terminal.print('Tools were executed but no final answer was produced — try a more specific query.', 'info');
return { finalAnswer: null, toolTurns };
}
@@ -9275,7 +9858,7 @@ buildSystemPrompt() {
}
try {
this.terminal.fs.root = JSON.parse(JSON.stringify(this._shadowSnapshot));
- this.terminal.fs.saveToLocalStorage();
+ this.terminal.fs.saveToStorage();
this._shadowSnapshot = null;
this.terminal.print('↩️ VFS rolled back to pre-shadow state', 'success');
} catch (e) {
@@ -9296,8 +9879,6 @@ buildSystemPrompt() {
'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',
'debug','debug-ai','session-new','mem-debug','reset-mem',
- // Node bridge — alias for js-run
- 'node',
// Synced from terminal whitelist — read-only orchestration/A2A/agent commands
'orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result',
'orchestrator-status','orchestrator-list','orchestrator-config','orchestrator-restart-policy',
@@ -9406,35 +9987,17 @@ buildSystemPrompt() {
const fs = this.terminal.fs;
const mm = this.terminal.moduleManager;
const om = this.terminal.ollamaManager;
- var agentInfo = {};
- try {
- var orch = mm.modules['OrchestratorModule'];
- var agentReg = mm.modules['AgentRegistryModule'];
- if (orch) {
- var agents = orch._agents || {};
- var busy = 0, failed = 0;
- for (var ak in agents) {
- var s = agents[ak].status;
- if (s === 'busy') busy++;
- if (s === 'failed' || s === 'restarting') failed++;
- }
- agentInfo.activeAgents = Object.keys(agents).length;
- agentInfo.busyAgents = busy;
- agentInfo.failedAgents = failed;
- }
- if (agentReg) {
- agentInfo.registeredAgents = Object.keys(agentReg._agents || {}).length;
- }
- } catch (e) {}
return JSON.stringify({
files: fs.ls().length,
dirs: fs.currentPath.length ? '/' + fs.currentPath.join('/') : '/',
modules: Object.keys(mm.modules).length,
ollama: om.connected ? `Connected (${om.currentModel})` : 'Disconnected',
- memory: performance.memory ? (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB' : 'N/A',
- agents: agentInfo
+ memory: performance.memory ? (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB' : 'N/A'
});
}
+ case 'get_dashboard_data': {
+ return JSON.stringify(await this._collectDashboardData());
+ }
case 'get_help': {
const cmds = this.terminal.moduleManager.getAllCommands();
return JSON.stringify(Object.keys(cmds).sort());
@@ -9648,8 +10211,12 @@ buildSystemPrompt() {
if (!task) return JSON.stringify({ error: 'No task provided' });
const orch = this.terminal.moduleManager.modules['OrchestratorModule'];
if (!orch) return JSON.stringify({ error: 'Orchestrator not available' });
- orch.delegateTask(task);
- return JSON.stringify({ message: `Task delegated: ${task.substring(0, 80)}` });
+ try {
+ const result = await orch.delegateTask(task);
+ return JSON.stringify({ success: true, task: task.substring(0, 80), result: typeof result === 'string' ? result.substring(0, 500) : 'completed' });
+ } catch (e) {
+ return JSON.stringify({ error: 'Delegation failed: ' + e.message });
+ }
}
case 'spawn_agent': {
const role = a.role || 'worker';
@@ -9841,26 +10408,6 @@ buildSystemPrompt() {
mindmap._svg([]);
return JSON.stringify({ success: true, topic, children: added.length ? added : 'none', nodes: mindmap._countNodes(cur.root), rendered: 'inline SVG in terminal' });
}
- case 'create_3d_scene': {
- const three = this.terminal.moduleManager.modules['ThreeModule'];
- if (!three) return JSON.stringify({ error: 'ThreeModule not loaded' });
- try {
- const result = await three.createSceneFromDescription(a.description || '');
- return JSON.stringify({ success: true, result });
- } catch (e) {
- return JSON.stringify({ error: '3D scene creation failed: ' + e.message });
- }
- }
- case 'visualize_mindmap_3d': {
- const three = this.terminal.moduleManager.modules['ThreeModule'];
- if (!three) return JSON.stringify({ error: 'ThreeModule not loaded' });
- try {
- const result = await three.visualizeMindmap3D(a.name || '');
- return JSON.stringify({ success: true, result });
- } catch (e) {
- return JSON.stringify({ error: '3D mindmap visualization failed: ' + e.message });
- }
- }
case 'view_module':
case 'edit_module':
case 'test_module':
@@ -9922,7 +10469,7 @@ buildSystemPrompt() {
return JSON.stringify({ error: 'Invalid endpoint. Valid: ' + validEps.join(', ') });
}
try {
- const epMap = { prices: '/vmai/market/prices', portfolio: '/vmai/portfolio', transactions: '/vmai/transactions/summary', 'copy-trade': '/vmai/copy-trading/traders', chat: '/vmai/chat' };
+ const epMap = { prices: '/vmai-api/market/prices', portfolio: '/vmai-api/portfolio', transactions: '/vmai-api/transactions/summary', 'copy-trade': '/vmai-api/copy-trading/traders', chat: '/vmai-api/chat' };
let url = epMap[ep];
let opts = { method: 'GET', headers: { 'Accept': 'application/json' } };
// Portfolio, transactions, copy-trade need auth token
@@ -9969,12 +10516,87 @@ buildSystemPrompt() {
const stats = om.getUsageStats(filters);
return JSON.stringify({ success: true, stats });
}
+ case 'agent_loop_list': {
+ var profiles = this._loopProfiles;
+ var list = Object.keys(profiles).map(function(k) {
+ var p = profiles[k];
+ return { name: k, displayName: p.displayName, description: p.description, active: this._activeLoopProfile === p, maxTurns: p.loop ? p.loop.maxTurns : this.MAX_TOOL_TURNS, toolsMode: p.tools ? p.tools.mode : 'all', benchmark: p.benchmark || null };
+ }.bind(this));
+ return JSON.stringify({ profiles: list, count: list.length, active: this._activeLoopProfile ? this._activeLoopProfile.name : 'none' });
+ }
+ case 'agent_loop_switch': {
+ var name = (a.name || '').trim();
+ if (!name) return JSON.stringify({ error: 'Profile name required' });
+ if (!this._loopProfiles[name]) return JSON.stringify({ error: "Profile '" + name + "' not found", available: Object.keys(this._loopProfiles) });
+ this._switchLoopProfile(name);
+ return JSON.stringify({ success: true, name: name, message: "Switched to '" + name + "'" });
+ }
+ case 'agent_loop_create': {
+ var name = (a.name || '').trim().replace(/\s+/g, '-').toLowerCase();
+ if (!name) return JSON.stringify({ error: 'Name required' });
+ var base = this._defaultLoopProfile(name, a.description || 'Custom profile');
+ if (a.maxTurns) base.loop.maxTurns = a.maxTurns;
+ if (a.toolsMode) base.tools.mode = a.toolsMode;
+ if (a.toolsEnabled) base.tools.enabled = a.toolsEnabled;
+ if (a.toolsDisabled) base.tools.disabled = a.toolsDisabled;
+ if (a.prependPrompt) base.systemPrompt.prepend = a.prependPrompt;
+ if (a.appendPrompt) base.systemPrompt.append = a.appendPrompt;
+ this._loopProfiles[name] = base;
+ this._saveLoopProfiles();
+ return JSON.stringify({ success: true, name: name, message: "Profile '" + name + "' created" });
+ }
+ case 'agent_loop_benchmark': {
+ var profileName = a.profile || (this._activeLoopProfile ? this._activeLoopProfile.name : 'default');
+ var taskIds = a.tasks;
+ this._runBenchmark(profileName, taskIds).then(function(results) {
+ this.terminal.print("📊 Benchmark complete for '" + profileName + "'", 'success');
+ }.bind(this)).catch(function(e) {
+ this.terminal.print('Benchmark error: ' + e.message, 'error');
+ }.bind(this));
+ return JSON.stringify({ success: true, message: "Benchmark started on '" + profileName + "'", tasks: taskIds || 'all' });
+ }
+ case 'predict_task': {
+ var predictor = this.terminal.moduleManager.modules['PredictorModule'];
+ if (!predictor) return JSON.stringify({ error: 'Predictor module not loaded' });
+ var taskDesc = a.task || '';
+ if (!taskDesc) return JSON.stringify({ error: 'task parameter required' });
+ var pFeatures = predictor._extractFeatures(taskDesc);
+ var pSimilar = predictor._findSimilar(pFeatures);
+ var pResult = predictor._computePrediction(pSimilar, pFeatures);
+ return JSON.stringify({
+ task: taskDesc.substring(0, 80),
+ estimatedDuration: pResult.duration + 's',
+ estimatedToolTurns: pResult.toolTurns,
+ confidence: Math.round(pResult.confidence * 100) + '%',
+ similarTasks: pResult.similarCount,
+ recommendation: pResult.confidence >= predictor._CONFIDENCE_THRESHOLD && pResult.duration > 30
+ ? 'Set TTL to ' + Math.ceil(pResult.duration * 1.5) + 's'
+ : 'No special action needed'
+ });
+ }
+ case 'world_model_query': {
+ var wm = this.terminal.moduleManager.modules['WorldModelModule'];
+ if (!wm) return JSON.stringify({ error: 'WorldModelModule not loaded' });
+ if (!wm._model) return JSON.stringify({ error: 'World model not synthesized yet — run world-model-refresh' });
+ var wmAnswer = wm._answerQuery(a.question || '');
+ return JSON.stringify({ question: a.question, answer: wmAnswer, version: wm._model.version, synthesized: wm._model.synthesized });
+ }
+ case 'health_check':
+ case 'diff_module':
+ case 'ship_module':
+ case 'commit_module':
+ return await this.runModuleTool(name, argsStr);
default:
return JSON.stringify({ error: `Unknown tool: ${name}` });
}
} catch (e) {
this._errorLog.push({ tool: name, args: a, error: e.message, timestamp: Date.now() });
if (this._errorLog.length > 20) this._errorLog.shift();
+ // Also feed into DebugSupervisorModule
+ try {
+ var ds = this.terminal?.moduleManager?.modules?.DebugSupervisorModule;
+ if (ds) ds.recordToolError(name, a, e.message);
+ } catch (dsErr) {}
return JSON.stringify({ error: e.message, stack: e.stack?.substring(0, 200) });
}
}
@@ -10068,10 +10690,61 @@ buildSystemPrompt() {
}
case 'test_module': {
const modName = a.name || '';
+ const tests = a.tests || null;
const code = this.terminal.fs.readFile(`_mods/${modName}.js`);
if (!code) return JSON.stringify({ error: `No local edit for ${modName}` });
- this.terminal.moduleManager.loadModuleFromCode(code, `blob://_mods/${modName}.js`);
- return `Module "${modName}" loaded from local edit for testing`;
+ // Save current module state for rollback
+ const oldInstance = this.terminal.moduleManager.modules[modName];
+ const oldCommands = oldInstance ? Object.keys(oldInstance.commands) : [];
+ // Load new version
+ const ok = await this.terminal.moduleManager.loadModuleFromCode(code, `blob://_mods/${modName}.js`);
+ if (!ok) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: 'Module failed to load', rolledBack: true });
+ }
+ // Check for JS errors after load
+ const jsErrors = await this._captureConsoleErrors(300);
+ if (jsErrors.length > 0) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: 'JS errors after load', errors: jsErrors.slice(0, 5), rolledBack: true });
+ }
+ // Verify commands registered
+ const newInstance = this.terminal.moduleManager.modules[modName];
+ const newCommands = newInstance ? Object.keys(newInstance.commands) : [];
+ const lostCommands = oldCommands.filter(c => !newCommands.includes(c));
+ if (lostCommands.length > 0) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: 'Commands lost after edit', lost: lostCommands, rolledBack: true });
+ }
+ // Run test assertions if provided
+ if (tests && Array.isArray(tests) && tests.length > 0) {
+ const results = [];
+ for (const t of tests) {
+ const cmd = t.command || '';
+ // Use shell parser for commands with >, |, &&, ||, ;, export, set
+ const useShell = /[>|&;]/.test(cmd) || cmd.startsWith('export ') || cmd.startsWith('set ');
+ const output = useShell ? await this._captureShellCommand(cmd) : await this._captureCommandOutput(cmd);
+ const passed = t.expect ? output.includes(t.expect) : output.length > 0;
+ results.push({ command: cmd, expected: t.expect || '(any output)', actual: output.substring(0, 200), pass: passed });
+ }
+ const failed = results.filter(r => !r.pass).length;
+ if (failed > 0) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: failed + '/' + results.length + ' tests failed', results, rolledBack: true });
+ }
+ return JSON.stringify({ success: true, module: modName, commands: newCommands.length, tests: results });
+ }
+ // Smoke test: run first command if available
+ if (newCommands.length > 0) {
+ try {
+ const smokeOut = await this._captureCommandOutput(newCommands[0]);
+ return JSON.stringify({ success: true, module: modName, commands: newCommands.length, smokeTest: { command: newCommands[0], output: smokeOut.substring(0, 200) } });
+ } catch (e) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: 'Smoke test failed: ' + e.message, rolledBack: true });
+ }
+ }
+ return JSON.stringify({ success: true, module: modName, commands: newCommands.length });
}
case 'reload_module': {
const modName = a.name || '';
@@ -10135,11 +10808,181 @@ buildSystemPrompt() {
}
return JSON.stringify({ modules, total: modules.length, hasLocalEdits: hasMods });
}
+ case 'health_check': {
+ const results = {};
+ const coreCmds = ['ls', 'pwd', 'echo test', 'help', 'grep', 'find', 'tree', 'cat'];
+ results.coreCommands = {};
+ for (const cmd of coreCmds) {
+ try {
+ const out = await this._captureCommandOutput(cmd);
+ results.coreCommands[cmd] = (out.length > 0 || cmd === 'ls') ? 'ok' : 'empty output';
+ } catch (e) {
+ results.coreCommands[cmd] = 'FAIL: ' + e.message;
+ }
+ }
+ const mm = this.terminal.moduleManager;
+ results.moduleCount = Object.keys(mm.modules).length;
+ const wm = this.terminal.fs.fsReadJSON('system/world-model.json');
+ results.worldModelVersion = wm ? wm.version : 'not found';
+ results.versionMatch = wm && wm.version === (typeof window !== 'undefined' ? window.NEWOS4_VERSION : '');
+ const orch = mm.modules['OrchestratorModule'];
+ if (orch) {
+ const agents = Object.values(orch._agents);
+ results.agents = { total: agents.length, healthy: agents.filter(a => a.status !== 'failed').length, failed: agents.filter(a => a.status === 'failed').length };
+ }
+ results.jsErrors = await this._captureConsoleErrors(500);
+ const allCoreOk = Object.values(results.coreCommands).every(v => v === 'ok');
+ results.healthy = allCoreOk && results.versionMatch && results.jsErrors.length === 0;
+ return JSON.stringify(results);
+ }
+ case 'diff_module': {
+ const modName = a.name || '';
+ const newCode = this.terminal.fs.readFile(`_mods/${modName}.js`);
+ if (!newCode) return JSON.stringify({ error: `No edit for ${modName}` });
+ let origCode = this.terminal.fs.readFile(`_mods/${modName}.orig.js`);
+ if (!origCode || origCode === 'placeholder') {
+ const instance = this.terminal.moduleManager.modules[modName];
+ if (instance) origCode = instance.constructor.toString();
+ }
+ if (!origCode) return JSON.stringify({ error: 'No original source found' });
+ const origLines = origCode.split('\n');
+ const newLines = newCode.split('\n');
+ const changes = [];
+ const maxLines = Math.max(origLines.length, newLines.length);
+ for (let i = 0; i < maxLines; i++) {
+ if (origLines[i] !== newLines[i]) {
+ if (origLines[i] && !newLines[i]) changes.push({ line: i+1, type: 'removed', content: origLines[i].substring(0, 120) });
+ else if (!origLines[i] && newLines[i]) changes.push({ line: i+1, type: 'added', content: newLines[i].substring(0, 120) });
+ else changes.push({ line: i+1, type: 'changed', old: (origLines[i]||'').substring(0, 80), new: (newLines[i]||'').substring(0, 80) });
+ }
+ }
+ return JSON.stringify({ module: modName, originalLines: origLines.length, newLines: newLines.length, changes: changes.length, diff: changes.slice(0, 50) });
+ }
+ case 'ship_module': {
+ const modName = a.name || '';
+ const newCode = a.code || '';
+ const tests = a.tests || [];
+ const shouldCommit = a.commit === true;
+ if (!modName || !newCode) return JSON.stringify({ error: 'name and code required' });
+ // Step 1: Write to _mods/
+ this.terminal.fs.writeFile(`_mods/${modName}.js`, newCode);
+ this.terminal.fs.writeFile(`_mods/${modName}.meta.json`, JSON.stringify({ editedAt: new Date().toISOString(), editedBy: 'ship_module' }));
+ // Step 2: Test
+ const testResult = await this.runModuleTool('test_module', JSON.stringify({ name: modName, tests }));
+ const testParsed = JSON.parse(testResult);
+ if (testParsed.error) {
+ return JSON.stringify({ step: 'test', error: testParsed.error, rolledBack: testParsed.rolledBack, pipeline: 'aborted at test' });
+ }
+ // Step 3: Health check
+ const healthResult = await this.runModuleTool('health_check', '{}');
+ const healthParsed = JSON.parse(healthResult);
+ if (!healthParsed.healthy) {
+ await this.runModuleTool('reload_module', JSON.stringify({ name: modName }));
+ return JSON.stringify({ step: 'health_check', error: 'System unhealthy after edit', health: healthParsed, pipeline: 'aborted at health_check, rolled back' });
+ }
+ // Step 4: Regression suite
+ const regResult = await this._runRegressionSuite(modName);
+ if (!regResult.allPassed) {
+ await this.runModuleTool('reload_module', JSON.stringify({ name: modName }));
+ return JSON.stringify({ step: 'regression', error: 'Regression tests failed', regression: regResult, pipeline: 'aborted at regression, rolled back' });
+ }
+ // Step 5: Calculate confidence score
+ const changeCount = newCode.split('\n').length;
+ const confidence = this._calculateConfidence(modName, testParsed, healthParsed, regResult, changeCount);
+ // Step 6: Commit (if requested)
+ if (shouldCommit) {
+ if (confidence >= 70) {
+ // Auto-commit: execute directly
+ const commitResult = await this.runModuleTool('commit_module', JSON.stringify({ name: modName }));
+ const commitParsed = JSON.parse(commitResult);
+ if (commitParsed.pendingCommit) {
+ // Execute the pending commit immediately
+ const execResult = await this._executeCommit('auto: confidence=' + confidence);
+ return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression → AUTO-COMMIT (confidence=' + confidence + ')', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, autoCommitted: true, commitResult: execResult });
+ }
+ return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression → auto-commit', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, commit: commitParsed });
+ } else {
+ // Confidence too low — human approval required
+ const commitResult = await this.runModuleTool('commit_module', JSON.stringify({ name: modName }));
+ return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression → HUMAN APPROVAL REQUIRED (confidence=' + confidence + ' < 70)', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, commit: JSON.parse(commitResult), humanApprovalRequired: true });
+ }
+ }
+ return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression (dry-run)', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, next: 'Call commit_module or ship_module with commit:true to persist' });
+ }
+ case 'commit_module': {
+ const modName = a.name || '';
+ const targetFile = a.file || '';
+ const newCode = this.terminal.fs.readFile(`_mods/${modName}.js`);
+ if (!newCode) return JSON.stringify({ error: `No local edit for ${modName}. Run edit_module or ship_module first.` });
+
+ // Auto-detect target file from module registry
+ let serverPath = targetFile;
+ if (!serverPath) {
+ const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName);
+ if (entry && entry.url) {
+ serverPath = entry.url.replace(/^https?:\/\/[^/]+/, '').replace(/\?v=.*/, '');
+ }
+ }
+ if (!serverPath) return JSON.stringify({ error: 'Cannot determine server path. Provide file= parameter (e.g. modules/ext/shell.js).' });
+
+ // Get diff
+ let origCode = this.terminal.fs.readFile(`_mods/${modName}.orig.js`);
+ if (!origCode || origCode === 'placeholder') {
+ const instance = this.terminal.moduleManager.modules[modName];
+ if (instance) origCode = instance.constructor.toString();
+ }
+ const origLines = origCode ? origCode.split('\n') : [];
+ const newLines = newCode.split('\n');
+ let changeCount = 0;
+ for (let i = 0; i < Math.max(origLines.length, newLines.length); i++) {
+ if (origLines[i] !== newLines[i]) changeCount++;
+ }
+
+ // Detect bundle path
+ let bundlePath = null;
+ if (serverPath.includes('/modules/core/') || serverPath.includes('module-aiassist')) {
+ bundlePath = 'modules/core/core-bundle.js';
+ } else if (serverPath.includes('/modules/ext/')) {
+ bundlePath = 'modules/ext/critical-ext-bundle.js';
+ }
+
+ // Store pending commit
+ this._pendingCommit = {
+ name: modName,
+ code: newCode,
+ origCode: origCode || '',
+ serverPath: serverPath,
+ bundlePath: bundlePath,
+ changes: changeCount,
+ createdAt: Date.now()
+ };
+
+ this.terminal.print('📋 Commit request created for ' + modName, 'info');
+ this.terminal.print(' Target: ' + serverPath, 'info');
+ if (bundlePath) this.terminal.print(' Bundle: ' + bundlePath, 'info');
+ this.terminal.print(' Changes: ' + changeCount + ' line(s) different', 'info');
+ this.terminal.print(' Type \'commit-approve\' to persist or \'commit-deny\' to cancel', 'warning');
+
+ return JSON.stringify({
+ pendingCommit: true,
+ module: modName,
+ serverPath: serverPath,
+ bundlePath: bundlePath,
+ changes: changeCount,
+ newLines: newLines.length,
+ message: 'Commit request created. User must type commit-approve to execute.'
+ });
+ }
default: return JSON.stringify({ error: `Unknown module tool: ${name}` });
}
} catch (e) {
this._errorLog.push({ tool: name, args: a, error: e.message, timestamp: Date.now() });
if (this._errorLog.length > 20) this._errorLog.shift();
+ // Also feed into DebugSupervisorModule
+ try {
+ var ds = this.terminal?.moduleManager?.modules?.DebugSupervisorModule;
+ if (ds) ds.recordToolError(name, a, e.message);
+ } catch (dsErr) {}
return JSON.stringify({ error: e.message, stack: e.stack?.substring(0, 200) });
}
}
@@ -10183,8 +11026,8 @@ buildSystemPrompt() {
setSystemPrompt(args) {
if (!args[0]) {
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');
+ this.systemPrompt.split('\n').forEach(function(line) { this.terminal.print(line, 'text-muted'); }.bind(this));
+ this.terminal.print('=== End (' + this.systemPrompt.length + ' chars) ===', 'info');
return;
}
var arg = args.join(' ');
@@ -10337,8 +11180,6 @@ buildSystemPrompt() {
this.terminal.print(`confirm-timeout: ${this._confirmTimeoutMs > 0 ? this._confirmTimeoutMs / 1000 + 's' : 'disabled (no auto-deny)'} (default: 120s)`, '');
this.terminal.print(`chat-lock-timeout: ${this._chatLockTimeoutMs / 1000}s (default: 10s)`, '');
this.terminal.print(`skill-debounce: ${this._skillDebounceMs / 60000}min (default: 30min)`, '');
- var lp = this._activeLoopProfile;
- this.terminal.print('loop-detection: ' + (lp && lp.loop && lp.loop.loopDetection ? (lp.loop.loopDetection.enabled ? 'on' : 'off') : 'off (default)') + ' (profile: ' + (lp ? lp.name : 'none') + ')', '');
const om = this.terminal?.ollamaManager;
if (om && om.modelParams) {
this.terminal.print(`temperature: ${om.modelParams.temperature} (default: 0.7)`, '');
@@ -10347,7 +11188,7 @@ buildSystemPrompt() {
this.terminal.print(`thinking: ${om.modelParams.thinking} (default: off)`, '');
}
this.terminal.print('Usage: ai-config <key> <value>', 'info');
- this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout(0=disabled), chat-lock-timeout, skill-debounce, trivial-optimization, temperature, top-p, max-tokens, thinking, loop-detection', 'info');
+ this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout(0=disabled), chat-lock-timeout, skill-debounce, trivial-optimization, temperature, top-p, max-tokens, thinking', 'info');
return;
}
const key = args[0];
@@ -10400,17 +11241,8 @@ buildSystemPrompt() {
if (!validModes.includes(val)) return this.terminal.print(`thinking must be one of: ${validModes.join(', ')}`, 'warning');
const om = this.terminal?.ollamaManager;
if (om && om.modelParams) { om.modelParams.thinking = val; om._saveModelParams(); }
- } else if (key === 'loop-detection') {
- var lp = this._activeLoopProfile;
- if (!lp) { this.terminal.print('No active profile', 'error'); return; }
- if (!lp.loop) lp.loop = {};
- if (!lp.loop.loopDetection) lp.loop.loopDetection = { enabled: false, threshold: 6, windowMs: 30000 };
- lp.loop.loopDetection.enabled = val !== 'off';
- this._saveLoopProfile(lp);
- this.terminal.print('Loop detection ' + (lp.loop.loopDetection.enabled ? 'enabled' : 'disabled') + ' (profile: ' + lp.name + ')', 'success');
- return;
} else {
- return this.terminal.print(`Unknown key: ${key}. Valid: max-turns, max-context, max-context-tokens, confirm-timeout, chat-lock-timeout, skill-debounce, trivial-optimization, temperature, top-p, max-tokens, thinking, loop-detection`, 'warning');
+ return this.terminal.print(`Unknown key: ${key}. Valid: max-turns, max-context, max-context-tokens, confirm-timeout, chat-lock-timeout, skill-debounce, trivial-optimization, temperature, top-p, max-tokens, thinking`, 'warning');
}
// Persist
localStorage.setItem('webx11_ai_config', JSON.stringify({
@@ -10485,6 +11317,19 @@ buildSystemPrompt() {
}
try { localStorage.setItem('webx11_ai_patterns', JSON.stringify(this._commandFreq)); } catch (e) {}
}
+ _buildToolToModuleMap() {
+ this._toolToModuleMap = {};
+ const mm = this.terminal.moduleManager;
+ if (!mm || !mm.modules) return;
+ for (const [modName, mod] of Object.entries(mm.modules)) {
+ if (mod.commands) {
+ for (const cmdName of Object.keys(mod.commands)) {
+ this._toolToModuleMap[cmdName] = modName;
+ }
+ }
+ }
+ }
+
async _autoCreateErrorSkill() {
const grouped = {};
for (const e of this._errorLog) {
@@ -10597,76 +11442,6 @@ buildSystemPrompt() {
this.terminal.print('Error reading archive: ' + e.message, 'error');
}
}
- _workflowList() {
- var workflows = [
- { name: 'daily-health-report', desc: 'Schedule a daily system health report using Cron + WorldModel + ServerMonitor' },
- { name: 'predict-and-monitor', desc: 'Predict task duration with Predictor, then schedule monitoring via Cron' },
- { name: 'compress-archives', desc: 'Compress and archive old session files using CryptoGzip + Cron' },
- { name: 'media-summarize', desc: 'Analyze a media file and summarize it with AI' },
- { name: 'sandbox-test', desc: 'Run a command in the sandbox and report results' }
- ];
- var out = '── Workflows (' + workflows.length + ') ──\n';
- for (var i = 0; i < workflows.length; i++) {
- out += ' ' + workflows[i].name + ' — ' + workflows[i].desc + '\n';
- }
- out += '── End ──\nUse: workflow-run <name>';
- this.terminal.print(out, 'info');
- }
- _workflowRun(args) {
- var name = (args && args[0]) || '';
- if (!name) {
- this.terminal.print('Usage: workflow-run <name> (e.g. daily-health-report)', 'warning');
- return;
- }
- var workflows = {
- 'daily-health-report': {
- steps: [
- 'Run: status — check current system state',
- 'Run: cron-every 1h "status" — schedule hourly health check',
- 'The WorldModel will auto-synthesize system awareness each cycle'
- ]
- },
- 'predict-and-monitor': {
- steps: [
- 'Run: predict "run system health check" — estimate duration',
- 'Run: cron-every 30m "status" — monitor progress',
- 'Check cron-list to see scheduled tasks'
- ]
- },
- 'compress-archives': {
- steps: [
- 'Run: archive-list — see current archives',
- 'Run: gzip /memory/archive/2026-06-25-*.json — compress old archives',
- 'Run: cron-every 1d "gzip /memory/archive/*.json" — auto-compress daily'
- ]
- },
- 'media-summarize': {
- steps: [
- 'Run: media list — see available media files',
- 'Run: media analyze <file> — analyze with AI',
- 'The AI will summarize the media content'
- ]
- },
- 'sandbox-test': {
- steps: [
- 'Run: sandbox-status — check sandbox pool',
- 'Run: sandbox-run "ls /" — test command in sandbox',
- 'Check output for isolation verification'
- ]
- }
- };
- var wf = workflows[name];
- if (!wf) {
- this.terminal.print('Workflow not found: ' + name + '. Use workflow-list to see available workflows.', 'error');
- return;
- }
- var out = '── Workflow: ' + name + ' ──\n';
- for (var i = 0; i < wf.steps.length; i++) {
- out += ' ' + (i + 1) + '. ' + wf.steps[i] + '\n';
- }
- out += '── End ──';
- this.terminal.print(out, 'info');
- }
_showLastRawResponse() {
if (!this._lastRawResponse) {
this.terminal.print('No AI response recorded yet. Send a message first.', 'warning');
@@ -10826,105 +11601,576 @@ buildSystemPrompt() {
this.terminal.print('Error: ' + e.message, 'error');
}
}
+ _vaultmindStatusCmd() {
+ const token = localStorage.getItem('vaultmind_token');
+ if (token) {
+ this.terminal.print('✅ VaultMind session active (token: ' + token.substring(0, 12) + '...)', 'success');
+ } else {
+ this.terminal.print('❌ No VaultMind session. Use: vaultmind-connect [testnet|mainnet]', 'warning');
+ }
+ }
+ async _vaultmindQueryCmd(args) {
+ const ep = args[0];
+ const validEps = ['prices', 'portfolio', 'transactions', 'copy-trade', 'chat'];
+ if (!validEps.includes(ep)) {
+ this.terminal.print('Usage: vaultmind-query <prices|portfolio|transactions|copy-trade|chat> [message|agent]', 'warning'); return;
+ }
+ try {
+ const epMap = { prices: '/vmai-api/market/prices', portfolio: '/vmai-api/portfolio', transactions: '/vmai-api/transactions/summary', 'copy-trade': '/vmai-api/copy-trading/traders', chat: '/vmai-api/chat' };
+ let url = epMap[ep];
+ let opts = { method: 'GET', headers: { 'Accept': 'application/json' } };
+ const authEps = ['portfolio', 'transactions', 'copy-trade'];
+ if (authEps.includes(ep)) {
+ const token = localStorage.getItem('vaultmind_token');
+ if (token) opts.headers['Authorization'] = 'Bearer ' + token;
+ }
+ if (ep === 'chat') {
+ opts.method = 'POST';
+ opts.headers['Content-Type'] = 'application/json';
+ const token = localStorage.getItem('vaultmind_token');
+ if (token) opts.headers['Authorization'] = 'Bearer ' + token;
+ opts.body = JSON.stringify({
+ message: args.slice(1).join(' ') || 'What is the current market status?',
+ agent: 'market'
+ });
+ }
+ const resp = await fetch(url, opts);
+ const data = await resp.json();
+ this.terminal.print(JSON.stringify(data, null, 2), 'success');
+ } catch (e) {
+ this.terminal.print('VaultMind query error: ' + e.message, 'error');
+ }
+ }
+ async _walletLookupCmd(args) {
+ const addr = args[0];
+ if (!addr) { this.terminal.print('Usage: wallet-lookup <address>', 'warning'); return; }
+ try {
+ const resp = await fetch('/wallet/' + encodeURIComponent(addr));
+ const data = await resp.json();
+ this.terminal.print(JSON.stringify(data, null, 2), 'success');
+ } catch (e) {
+ this.terminal.print('Wallet lookup error: ' + e.message, 'error');
+ }
+ }
// ── Agent Loop Profile System ──
_defaultLoopProfile(name, desc) {
- return { name: name, displayName: desc, description: desc, version: 1, created: new Date().toISOString(), updated: new Date().toISOString(), systemPrompt: { base: null, prepend: "", append: "", useDefault: true }, loop: { strategy: "standard", maxTurns: this.MAX_TOOL_TURNS, loopDetection: { enabled: false, threshold: 6, windowMs: 30000 }, confidenceEscalation: { enabled: true, threshold: 90 }, finalTurnPrompt: "This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know." }, tools: { mode: "all", enabled: [], disabled: [] }, model: { temperature: null, topP: null, maxTokens: null, thinking: null }, provider: { name: null, model: null }, benchmark: { lastRun: null, avgLatency: null, avgTokens: null, taskCompletionRate: null, tasks: [] } };
+ return {
+ name: name, displayName: desc, description: desc,
+ version: 1, created: new Date().toISOString(), updated: new Date().toISOString(),
+ systemPrompt: { base: null, prepend: '', append: '', useDefault: true },
+ loop: {
+ strategy: 'standard', maxTurns: this.MAX_TOOL_TURNS,
+ loopDetection: { enabled: true, threshold: 6, windowMs: 30000 },
+ confidenceEscalation: { enabled: true, threshold: 90 },
+ finalTurnPrompt: 'This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know.'
+ },
+ tools: { mode: 'all', enabled: [], disabled: [] },
+ model: { temperature: null, topP: null, maxTokens: null, thinking: null },
+ provider: { name: null, model: null },
+ benchmark: { lastRun: null, avgLatency: null, avgTokens: null, taskCompletionRate: null, tasks: [] }
+ };
}
_initLoopProfiles() {
- try { var saved = localStorage.getItem("webx11_agent_loops"); if (saved) { this._loopProfiles = JSON.parse(saved); } } catch (e) {}
+ try {
+ var saved = localStorage.getItem('webx11_agent_loops');
+ if (saved) { this._loopProfiles = JSON.parse(saved); }
+ } catch (e) {}
if (!this._loopProfiles || Object.keys(this._loopProfiles).length === 0) {
this._loopProfiles = {};
- this._loopProfiles["default"] = this._defaultLoopProfile("default", "Balanced loop for general tasks");
- var p = this._defaultLoopProfile("minimal", "Fast, low-token for simple tasks"); p.loop.maxTurns = 4; p.tools = { mode: "custom", enabled: ["list_files","read_file","write_file","nvfs_resolve"], disabled: [] }; p.model.temperature = 0.3; this._loopProfiles["minimal"] = p;
- p = this._defaultLoopProfile("research", "Read-only exploration, web + files"); p.loop.maxTurns = 6; p.tools = { mode: "custom", enabled: ["web_search","web_fetch","read_file","list_files","nvfs_resolve","view_module","list_modules"], disabled: [] }; p.model.temperature = 0.5; this._loopProfiles["research"] = p;
- p = this._defaultLoopProfile("safe", "No destructive tools"); p.tools = { mode: "all", enabled: [], disabled: ["delete_file","edit_module","run_command","make_directory"] }; p.model.temperature = 0.5; this._loopProfiles["safe"] = p;
- p = this._defaultLoopProfile("creative", "More turns, higher temp"); p.loop.maxTurns = 12; p.model.temperature = 0.9; this._loopProfiles["creative"] = p;
- p = this._defaultLoopProfile("coder", "Code-focused, low temp"); p.loop.maxTurns = 10; p.model.temperature = 0.2; p.systemPrompt.append = "You are a code expert. Always show the exact code changes. Use edit_module for modifications."; this._loopProfiles["coder"] = p;
+ this._loopProfiles['default'] = this._defaultLoopProfile('default', 'Balanced loop for general tasks');
+ var p = this._defaultLoopProfile('minimal', 'Fast, low-token for simple tasks');
+ p.loop.maxTurns = 4; p.tools = { mode: 'custom', enabled: ['list_files','read_file','write_file','nvfs_resolve'], disabled: [] }; p.model.temperature = 0.3;
+ this._loopProfiles['minimal'] = p;
+ p = this._defaultLoopProfile('research', 'Read-only exploration, web + files');
+ p.loop.maxTurns = 6; p.tools = { mode: 'custom', enabled: ['web_search','web_fetch','read_file','list_files','nvfs_resolve','view_module','list_modules'], disabled: [] }; p.model.temperature = 0.5;
+ this._loopProfiles['research'] = p;
+ p = this._defaultLoopProfile('safe', 'No destructive tools');
+ p.tools = { mode: 'all', enabled: [], disabled: ['delete_file','edit_module','run_command','make_directory'] }; p.model.temperature = 0.5;
+ this._loopProfiles['safe'] = p;
+ p = this._defaultLoopProfile('creative', 'More turns, higher temp');
+ p.loop.maxTurns = 12; p.model.temperature = 0.9;
+ this._loopProfiles['creative'] = p;
+ p = this._defaultLoopProfile('coder', 'Code-focused, low temp');
+ p.loop.maxTurns = 10; p.model.temperature = 0.2; p.systemPrompt.append = 'You are a code expert. Always show the exact code changes. Use edit_module for modifications.';
+ this._loopProfiles['coder'] = p;
this._saveLoopProfiles();
}
- try { var active = localStorage.getItem("webx11_active_loop_profile"); if (active && this._loopProfiles[active]) { this._activeLoopProfile = this._loopProfiles[active]; } else { this._activeLoopProfile = this._loopProfiles["default"]; } } catch (e) { this._activeLoopProfile = this._loopProfiles["default"]; }
+ try {
+ var active = localStorage.getItem('webx11_active_loop_profile');
+ if (active && this._loopProfiles[active]) { this._activeLoopProfile = this._loopProfiles[active]; }
+ else { this._activeLoopProfile = this._loopProfiles['default']; }
+ } catch (e) { this._activeLoopProfile = this._loopProfiles['default']; }
+ }
+ _saveLoopProfiles() {
+ try { localStorage.setItem('webx11_agent_loops', JSON.stringify(this._loopProfiles)); } catch (e) {}
+ }
+ _saveLoopProfile(p) {
+ if (p && p.name) { p.updated = new Date().toISOString(); this._loopProfiles[p.name] = p; this._saveLoopProfiles(); }
}
- _saveLoopProfiles() { try { localStorage.setItem("webx11_agent_loops", JSON.stringify(this._loopProfiles)); } catch (e) {} }
- _saveLoopProfile(p) { if (p && p.name) { p.updated = new Date().toISOString(); this._loopProfiles[p.name] = p; this._saveLoopProfiles(); } }
_switchLoopProfile(name) {
if (!this._loopProfiles[name]) return false;
this._activeLoopProfile = this._loopProfiles[name];
- localStorage.setItem("webx11_active_loop_profile", name);
+ localStorage.setItem('webx11_active_loop_profile', name);
this._toolDefs = null;
this.systemPrompt = this.buildSystemPrompt();
var mp = this._activeLoopProfile.model;
var om = this.terminal ? this.terminal.ollamaManager : null;
- if (om && mp) { if (mp.temperature !== null) { om.modelParams.temperature = mp.temperature; om._saveModelParams(); } if (mp.topP !== null) { om.modelParams.top_p = mp.topP; om._saveModelParams(); } if (mp.maxTokens !== null) { om.modelParams.max_tokens = mp.maxTokens; om._saveModelParams(); } if (mp.thinking !== null) { om.modelParams.thinking = mp.thinking; om._saveModelParams(); } }
+ if (om && mp) {
+ if (mp.temperature !== null) { om.modelParams.temperature = mp.temperature; om._saveModelParams(); }
+ if (mp.topP !== null) { om.modelParams.top_p = mp.topP; om._saveModelParams(); }
+ if (mp.maxTokens !== null) { om.modelParams.max_tokens = mp.maxTokens; om._saveModelParams(); }
+ if (mp.thinking !== null) { om.modelParams.thinking = mp.thinking; om._saveModelParams(); }
+ }
return true;
}
agentLoopsCmd(args) {
if (!args[0]) {
- this.terminal.print("=== Agent Loop Profiles ===", "info");
+ this.terminal.print('=== Agent Loop Profiles ===', 'info');
var keys = Object.keys(this._loopProfiles);
- for (var ki = 0; ki < keys.length; ki++) { var k = keys[ki]; var p = this._loopProfiles[k]; var active = this._activeLoopProfile === p ? " ✅" : ""; var bench = p.benchmark && p.benchmark.taskCompletionRate != null ? " (bench: " + Math.round(p.benchmark.taskCompletionRate * 100) + "%)" : ""; var tools = p.tools && p.tools.mode === "custom" ? (p.tools.enabled ? p.tools.enabled.length : 0) + " tools" : "all tools"; this.terminal.print(" " + k + ": " + p.displayName + active + " — " + (p.loop ? p.loop.maxTurns : this.MAX_TOOL_TURNS) + " turns, " + tools + bench); }
- this.terminal.print("Usage: agent-loops <name> | new <name> <desc> | edit <name> <key> <val> | delete <name> | clone <src> <dst> | export <name> | reset <name>", "text-muted"); return;
+ for (var ki = 0; ki < keys.length; ki++) {
+ var k = keys[ki];
+ var p = this._loopProfiles[k];
+ var active = this._activeLoopProfile === p ? ' ✅' : '';
+ var bench = p.benchmark && p.benchmark.taskCompletionRate != null ? ' (bench: ' + Math.round(p.benchmark.taskCompletionRate * 100) + '%)' : '';
+ var tools = p.tools && p.tools.mode === 'custom' ? (p.tools.enabled ? p.tools.enabled.length : 0) + ' tools' : 'all tools';
+ this.terminal.print(' ' + k + ': ' + p.displayName + active + ' — ' + (p.loop ? p.loop.maxTurns : this.MAX_TOOL_TURNS) + ' turns, ' + tools + bench);
+ }
+ this.terminal.print('Usage: agent-loops <name> | new <name> <desc> | edit <name> <key> <val> | delete <name> | clone <src> <dst> | export <name> | reset <name>', 'text-muted');
+ return;
}
var sub = args[0];
- if (sub === "new") { var name = args[1] ? args[1].replace(/s+/g, "-").toLowerCase() : ""; if (!name) { this.terminal.print("Usage: agent-loops new <name> <description>", "warning"); return; } var desc = args.slice(2).join(" ") || "Custom profile"; this._loopProfiles[name] = this._defaultLoopProfile(name, desc); this._saveLoopProfiles(); this.terminal.print("Profile '" + name + "' created. Use: agent-loops edit " + name + " <key> <value>", "success"); }
- else if (sub === "edit") { var name = args[1]; if (!name || !this._loopProfiles[name]) { this.terminal.print("Profile '" + name + "' not found", "error"); return; } var key = args[2]; var val = args.slice(3).join(" "); if (!key || !val) { this.terminal.print("Usage: agent-loops edit <name> <key> <value>", "warning"); return; } this._setNestedKey(this._loopProfiles[name], key, val); this._saveLoopProfiles(); if (this._activeLoopProfile === this._loopProfiles[name]) { this._toolDefs = null; this.systemPrompt = this.buildSystemPrompt(); } this.terminal.print("Edited " + name + "." + key + " = " + val, "success"); }
- else if (sub === "delete") { var name = args[1]; if (!name) { this.terminal.print("Usage: agent-loops delete <name>", "warning"); return; } if (name === "default") { this.terminal.print("Cannot delete default profile", "error"); return; } if (this._activeLoopProfile === this._loopProfiles[name]) { this.terminal.print("Cannot delete active profile — switch first", "error"); return; } delete this._loopProfiles[name]; this._saveLoopProfiles(); this.terminal.print("Profile '" + name + "' deleted", "success"); }
- else if (sub === "clone") { var src = args[1], dst = args[2] ? args[2].replace(/s+/g, "-").toLowerCase() : ""; if (!src || !dst || !this._loopProfiles[src]) { this.terminal.print("Usage: agent-loops clone <src> <dst>", "warning"); return; } this._loopProfiles[dst] = JSON.parse(JSON.stringify(this._loopProfiles[src])); this._loopProfiles[dst].name = dst; this._loopProfiles[dst].created = new Date().toISOString(); this._saveLoopProfiles(); this.terminal.print("Cloned '" + src + "' -> '" + dst + "'", "success"); }
- else if (sub === "export") { var name = args[1]; if (!name || !this._loopProfiles[name]) { this.terminal.print("Profile '" + name + "' not found", "error"); return; } this.terminal.printJSON(JSON.stringify(this._loopProfiles[name], null, 2)); }
- else if (sub === "reset") { var name = args[1]; if (!name) { this.terminal.print("Usage: agent-loops reset <name>", "warning"); return; } var desc = this._loopProfiles[name] ? this._loopProfiles[name].displayName : "Reset profile"; this._loopProfiles[name] = this._defaultLoopProfile(name, desc); this._saveLoopProfiles(); if (this._activeLoopProfile && this._activeLoopProfile.name === name) { this._switchLoopProfile(name); } this.terminal.print("Profile '" + name + "' reset to defaults", "success"); }
- else { if (this._loopProfiles[sub]) { this._switchLoopProfile(sub); var p = this._activeLoopProfile; this.terminal.print("Switched to '" + sub + "': " + p.displayName, "success"); this.terminal.print(" Max turns: " + (p.loop ? p.loop.maxTurns : this.MAX_TOOL_TURNS) + " | Tools: " + (p.tools ? p.tools.mode : "all") + " | Temp: " + (p.model && p.model.temperature !== null ? p.model.temperature : "inherit"), "text-muted"); } else { this.terminal.print("Profile '" + sub + "' not found. Available: " + Object.keys(this._loopProfiles).join(", "), "error"); } }
- }
- _setNestedKey(obj, key, val) { var parts = key.split("."); var o = obj; for (var i = 0; i < parts.length - 1; i++) { if (!o[parts[i]]) o[parts[i]] = {}; o = o[parts[i]]; } var last = parts[parts.length - 1]; try { o[last] = JSON.parse(val); } catch (e) { o[last] = val; } }
+ if (sub === 'new') {
+ var name = args[1] ? args[1].replace(/\s+/g, '-').toLowerCase() : '';
+ if (!name) { this.terminal.print('Usage: agent-loops new <name> <description>', 'warning'); return; }
+ var desc = args.slice(2).join(' ') || 'Custom profile';
+ this._loopProfiles[name] = this._defaultLoopProfile(name, desc);
+ this._saveLoopProfiles();
+ this.terminal.print("Profile '" + name + "' created. Use: agent-loops edit " + name + " <key> <value>", 'success');
+ } else if (sub === 'edit') {
+ var name = args[1];
+ if (!name || !this._loopProfiles[name]) { this.terminal.print("Profile '" + name + "' not found", 'error'); return; }
+ var key = args[2];
+ var val = args.slice(3).join(' ');
+ if (!key || !val) { this.terminal.print('Usage: agent-loops edit <name> <key> <value>', 'warning'); return; }
+ this._setNestedKey(this._loopProfiles[name], key, val);
+ this._saveLoopProfiles();
+ if (this._activeLoopProfile === this._loopProfiles[name]) { this._toolDefs = null; this.systemPrompt = this.buildSystemPrompt(); }
+ this.terminal.print('Edited ' + name + '.' + key + ' = ' + val, 'success');
+ } else if (sub === 'delete') {
+ var name = args[1];
+ if (!name) { this.terminal.print('Usage: agent-loops delete <name>', 'warning'); return; }
+ if (name === 'default') { this.terminal.print('Cannot delete default profile', 'error'); return; }
+ if (this._activeLoopProfile === this._loopProfiles[name]) { this.terminal.print('Cannot delete active profile — switch first', 'error'); return; }
+ delete this._loopProfiles[name];
+ this._saveLoopProfiles();
+ this.terminal.print("Profile '" + name + "' deleted", 'success');
+ } else if (sub === 'clone') {
+ var src = args[1], dst = args[2] ? args[2].replace(/\s+/g, '-').toLowerCase() : '';
+ if (!src || !dst || !this._loopProfiles[src]) { this.terminal.print('Usage: agent-loops clone <src> <dst>', 'warning'); return; }
+ this._loopProfiles[dst] = JSON.parse(JSON.stringify(this._loopProfiles[src]));
+ this._loopProfiles[dst].name = dst;
+ this._loopProfiles[dst].created = new Date().toISOString();
+ this._saveLoopProfiles();
+ this.terminal.print("Cloned '" + src + "' -> '" + dst + "'", 'success');
+ } else if (sub === 'export') {
+ var name = args[1];
+ if (!name || !this._loopProfiles[name]) { this.terminal.print("Profile '" + name + "' not found", 'error'); return; }
+ this.terminal.printJSON(JSON.stringify(this._loopProfiles[name], null, 2));
+ } else if (sub === 'reset') {
+ var name = args[1];
+ if (!name) { this.terminal.print('Usage: agent-loops reset <name>', 'warning'); return; }
+ var desc = this._loopProfiles[name] ? this._loopProfiles[name].displayName : 'Reset profile';
+ this._loopProfiles[name] = this._defaultLoopProfile(name, desc);
+ this._saveLoopProfiles();
+ if (this._activeLoopProfile && this._activeLoopProfile.name === name) { this._switchLoopProfile(name); }
+ this.terminal.print("Profile '" + name + "' reset to defaults", 'success');
+ } else {
+ if (this._loopProfiles[sub]) {
+ this._switchLoopProfile(sub);
+ var p = this._activeLoopProfile;
+ this.terminal.print("Switched to '" + sub + "': " + p.displayName, 'success');
+ this.terminal.print(' Max turns: ' + (p.loop ? p.loop.maxTurns : this.MAX_TOOL_TURNS) + ' | Tools: ' + (p.tools ? p.tools.mode : 'all') + ' | Temp: ' + (p.model && p.model.temperature !== null ? p.model.temperature : 'inherit'), 'text-muted');
+ } else {
+ this.terminal.print("Profile '" + sub + "' not found. Available: " + Object.keys(this._loopProfiles).join(', '), 'error');
+ }
+ }
+ }
+ _setNestedKey(obj, key, val) {
+ var parts = key.split('.');
+ var o = obj;
+ for (var i = 0; i < parts.length - 1; i++) {
+ if (!o[parts[i]]) o[parts[i]] = {};
+ o = o[parts[i]];
+ }
+ var last = parts[parts.length - 1];
+ try { o[last] = JSON.parse(val); } catch (e) { o[last] = val; }
+ }
// ── BENCHMARK SUITE ──
- _benchmarkTasks() { return [ { id: "fs-navigate", prompt: "List all files in the root directory and tell me how many .js files exist", maxTurns: 3, successCheck: function(r, log) { return r.length > 10 && log.some(function(t) { return t.name === "list_files"; }); } }, { id: "fs-create-read", prompt: "Create a file called /tmp/bench-test.txt with 'hello bench', then read it back to verify", maxTurns: 4, successCheck: function(r) { return r.toLowerCase().indexOf("hello") !== -1; }, destructive: true }, { id: "nvfs-resolve", prompt: "What terminal command handles file operations? Use nvfs-resolve to find it", maxTurns: 3, successCheck: function(r, log) { return log.some(function(t) { return t.name === "nvfs_resolve"; }) && r.length > 20; } }, { id: "web-search", prompt: "Search the web for 'what is the capital of France' and tell me the answer", maxTurns: 3, successCheck: function(r) { return r.toLowerCase().indexOf("paris") !== -1; } }, { id: "multi-tool", prompt: "Read the file /package.json, then list all modules, then summarize both results", maxTurns: 6, successCheck: function(r, log) { return log.filter(function(t) { return ["read_file","list_files","list_modules"].indexOf(t.name) !== -1; }).length >= 2; } }, { id: "code-edit", prompt: "Create a file /tmp/bench-code.js with a function that adds two numbers, then read it back to verify", maxTurns: 5, successCheck: function(r, log) { return log.some(function(t) { return t.name === "write_file"; }); }, destructive: true } ]; }
+ _benchmarkTasks() {
+ return [
+ { id: 'fs-navigate', prompt: 'List all files in the root directory and tell me how many .js files exist', maxTurns: 3, successCheck: function(r, log) { return r.length > 10 && log.some(function(t) { return t.name === 'list_files'; }); } },
+ { id: 'fs-create-read', prompt: 'Create a file called /tmp/bench-test.txt with "hello bench", then read it back to verify', maxTurns: 4, successCheck: function(r) { return r.toLowerCase().indexOf('hello') !== -1; }, destructive: true },
+ { id: 'nvfs-resolve', prompt: 'What terminal command handles file operations? Use nvfs-resolve to find it', maxTurns: 3, successCheck: function(r, log) { return log.some(function(t) { return t.name === 'nvfs_resolve'; }) && r.length > 20; } },
+ { id: 'web-search', prompt: 'Search the web for "what is the capital of France" and tell me the answer', maxTurns: 3, successCheck: function(r) { return r.toLowerCase().indexOf('paris') !== -1; } },
+ { id: 'multi-tool', prompt: 'Read the file /package.json, then list all modules, then summarize both results', maxTurns: 6, successCheck: function(r, log) { return log.filter(function(t) { return ['read_file','list_files','list_modules'].indexOf(t.name) !== -1; }).length >= 2; } },
+ { id: 'code-edit', prompt: 'Create a file /tmp/bench-code.js with a function that adds two numbers, then read it back to verify', maxTurns: 5, successCheck: function(r, log) { return log.some(function(t) { return t.name === 'write_file'; }); }, destructive: true },
+ ];
+ }
async _runBenchmark(profileName, taskIds) {
- var tasks = this._benchmarkTasks(); if (taskIds) tasks = tasks.filter(function(t) { return taskIds.indexOf(t.id) !== -1; });
+ var tasks = this._benchmarkTasks();
+ if (taskIds) tasks = tasks.filter(function(t) { return taskIds.indexOf(t.id) !== -1; });
var prevProfile = this._activeLoopProfile ? this._activeLoopProfile.name : null;
if (profileName && this._loopProfiles[profileName]) this._switchLoopProfile(profileName);
- this.terminal.print("📊 Benchmark: " + profileName + " (" + tasks.length + " tasks)", "info");
+ this.terminal.print('📊 Benchmark: ' + profileName + ' (' + tasks.length + ' tasks)', 'info');
var results = [];
- for (var ti = 0; ti < tasks.length; ti++) { var task = tasks[ti]; this.terminal.print(" ▶ " + task.id + "...", "text-muted"); var t0 = Date.now(); var toolLogBefore = this._toolCallLog.length; var messages = [{ role: "user", content: task.prompt }]; messages.unshift({ role: "system", content: this.systemPrompt }); var savedMax = this._activeLoopProfile && this._activeLoopProfile.loop ? this._activeLoopProfile.loop.maxTurns : null; if (this._activeLoopProfile && this._activeLoopProfile.loop) { this._activeLoopProfile.loop.maxTurns = task.maxTurns; } try { var result = await this.agenticLoop(messages); var latency = Date.now() - t0; var newTools = this._toolCallLog.slice(toolLogBefore); var passed = task.successCheck(result.finalAnswer || "", newTools); var tokens = await this._estimateTokensForRun(newTools, result.finalAnswer); results.push({ id: task.id, turns: result.toolTurns, tokens: tokens, latency: latency, pass: passed }); this.terminal.print(" " + (passed ? "✅" : "❌") + " " + result.toolTurns + " turns, " + tokens + " tokens, " + (latency/1000).toFixed(1) + "s", passed ? "success" : "warning"); } catch (e) { results.push({ id: task.id, turns: 0, tokens: 0, latency: Date.now() - t0, pass: false, error: e.message }); this.terminal.print(" ❌ Error: " + e.message, "error"); } if (this._activeLoopProfile && this._activeLoopProfile.loop && savedMax != null) { this._activeLoopProfile.loop.maxTurns = savedMax; } this.context = []; }
+ for (var ti = 0; ti < tasks.length; ti++) {
+ var task = tasks[ti];
+ this.terminal.print(' ▶ ' + task.id + '...', 'text-muted');
+ var t0 = Date.now();
+ var toolLogBefore = this._toolCallLog.length;
+ var messages = [{ role: 'user', content: task.prompt }];
+ messages.unshift({ role: 'system', content: this.systemPrompt });
+ var savedMax = this._activeLoopProfile && this._activeLoopProfile.loop ? this._activeLoopProfile.loop.maxTurns : null;
+ if (this._activeLoopProfile && this._activeLoopProfile.loop) { this._activeLoopProfile.loop.maxTurns = task.maxTurns; }
+ try {
+ var result = await this.agenticLoop(messages);
+ var latency = Date.now() - t0;
+ var newTools = this._toolCallLog.slice(toolLogBefore);
+ var passed = task.successCheck(result.finalAnswer || '', newTools);
+ var tokens = await this._estimateTokensForRun(newTools, result.finalAnswer);
+ results.push({ id: task.id, turns: result.toolTurns, tokens: tokens, latency: latency, pass: passed });
+ this.terminal.print(' ' + (passed ? '✅' : '❌') + ' ' + result.toolTurns + ' turns, ' + tokens + ' tokens, ' + (latency/1000).toFixed(1) + 's', passed ? 'success' : 'warning');
+ } catch (e) {
+ results.push({ id: task.id, turns: 0, tokens: 0, latency: Date.now() - t0, pass: false, error: e.message });
+ this.terminal.print(' ❌ Error: ' + e.message, 'error');
+ }
+ if (this._activeLoopProfile && this._activeLoopProfile.loop && savedMax != null) { this._activeLoopProfile.loop.maxTurns = savedMax; }
+ this.context = [];
+ }
var profile = this._loopProfiles[profileName];
- if (profile) { var passed = results.filter(function(r) { return r.pass; }).length; profile.benchmark = { lastRun: new Date().toISOString(), tasks: results, avgLatency: Math.round(results.reduce(function(s, r) { return s + r.latency; }, 0) / results.length), avgTokens: Math.round(results.reduce(function(s, r) { return s + r.tokens; }, 0) / results.length), taskCompletionRate: passed / results.length }; this._saveLoopProfile(profile); }
- this.terminal.print("┌──────────────────────────────────────────────────────────┐", "info"); this.terminal.print("│ Benchmark: " + profileName, "info"); this.terminal.print("├──────────────────────────────────────────────────────────┤", "info");
- for (var ri = 0; ri < results.length; ri++) { var r = results[ri]; var status = r.pass ? "✅" : "❌"; this.terminal.print("│ " + r.id + " " + r.turns + "t " + r.tokens + "tok " + (r.latency/1000).toFixed(1) + "s " + status, "info"); }
- this.terminal.print("├──────────────────────────────────────────────────────────┤", "info");
- var passed = results.filter(function(r) { return r.pass; }).length; var avgLat = (results.reduce(function(s, r) { return s + r.latency; }, 0) / results.length / 1000).toFixed(1); var avgTok = Math.round(results.reduce(function(s, r) { return s + r.tokens; }, 0) / results.length);
- this.terminal.print("│ " + passed + "/" + results.length + " passed (" + Math.round(passed/results.length*100) + "%) | Avg: " + avgLat + "s, " + avgTok + "tok", "info"); this.terminal.print("└──────────────────────────────────────────────────────────┘", "info");
- if (prevProfile && prevProfile !== profileName) this._switchLoopProfile(prevProfile); return results;
- }
- async _estimateTokensForRun(toolLog, finalAnswer) { var om = this.terminal ? this.terminal.ollamaManager : null; if (om && om._usageLog && om._usageLog.length > 0) { var last = om._usageLog[om._usageLog.length - 1]; return last.total_tokens || 0; } var toolChars = toolLog.reduce(function(s, t) { return s + (t.result ? t.result.length : 0); }, 0); var answerChars = (finalAnswer || "").length; return Math.round((toolChars + answerChars) / 4); }
- benchCmd(args) { var profile = null; var taskIds = null; for (var ai = 0; ai < args.length; ai++) { if (args[ai].indexOf("--tasks=") === 0) taskIds = args[ai].split("=")[1].split(","); else if (args[ai].indexOf("--") !== 0) profile = args[ai]; } if (!profile) { this.terminal.print("Usage: bench [profile] [--tasks=id1,id2] [--repeat=N]", "warning"); this.terminal.print("Available tasks: " + this._benchmarkTasks().map(function(t) { return t.id; }).join(", "), "text-muted"); return; } this._runBenchmark(profile, taskIds); }
- benchCompareCmd(args) { var p1 = args[0], p2 = args[1]; if (!p1 || !p2) { this.terminal.print("Usage: bench-compare <profile1> <profile2>", "warning"); return; } var b1 = this._loopProfiles[p1] ? this._loopProfiles[p1].benchmark : null; var b2 = this._loopProfiles[p2] ? this._loopProfiles[p2].benchmark : null; if (!b1 || !b1.lastRun) { this.terminal.print("No benchmark for '" + p1 + "'. Run: bench " + p1, "warning"); return; } if (!b2 || !b2.lastRun) { this.terminal.print("No benchmark for '" + p2 + "'. Run: bench " + p2, "warning"); return; } this.terminal.print("┌──────────────────────────────────────────────────────────┐", "info"); this.terminal.print("│ " + p1 + " vs " + p2, "info"); this.terminal.print("├──────────────────────────────────────────────────────────┤", "info"); this.terminal.print("│ Pass rate: " + (b1.taskCompletionRate*100).toFixed(0) + "% vs " + (b2.taskCompletionRate*100).toFixed(0) + "%", "info"); this.terminal.print("│ Avg tokens: " + b1.avgTokens + " vs " + b2.avgTokens, "info"); this.terminal.print("│ Avg latency: " + (b1.avgLatency/1000).toFixed(1) + "s vs " + (b2.avgLatency/1000).toFixed(1) + "s", "info"); this.terminal.print("│ Last run: " + b1.lastRun.substring(0,10) + " vs " + b2.lastRun.substring(0,10), "info"); this.terminal.print("└──────────────────────────────────────────────────────────┘", "info"); }
+ if (profile) {
+ var passed = results.filter(function(r) { return r.pass; }).length;
+ profile.benchmark = {
+ lastRun: new Date().toISOString(),
+ tasks: results,
+ avgLatency: Math.round(results.reduce(function(s, r) { return s + r.latency; }, 0) / results.length),
+ avgTokens: Math.round(results.reduce(function(s, r) { return s + r.tokens; }, 0) / results.length),
+ taskCompletionRate: passed / results.length
+ };
+ this._saveLoopProfile(profile);
+ }
+ this.terminal.print('┌──────────────────────────────────────────────────────────┐', 'info');
+ this.terminal.print('│ Benchmark: ' + profileName, 'info');
+ this.terminal.print('├──────────────────────────────────────────────────────────┤', 'info');
+ for (var ri = 0; ri < results.length; ri++) {
+ var r = results[ri];
+ var status = r.pass ? '✅' : '❌';
+ this.terminal.print('│ ' + r.id + ' ' + r.turns + 't ' + r.tokens + 'tok ' + (r.latency/1000).toFixed(1) + 's ' + status, 'info');
+ }
+ this.terminal.print('├──────────────────────────────────────────────────────────┤', 'info');
+ var passed = results.filter(function(r) { return r.pass; }).length;
+ var avgLat = (results.reduce(function(s, r) { return s + r.latency; }, 0) / results.length / 1000).toFixed(1);
+ var avgTok = Math.round(results.reduce(function(s, r) { return s + r.tokens; }, 0) / results.length);
+ this.terminal.print('│ ' + passed + '/' + results.length + ' passed (' + Math.round(passed/results.length*100) + '%) | Avg: ' + avgLat + 's, ' + avgTok + 'tok', 'info');
+ this.terminal.print('└──────────────────────────────────────────────────────────┘', 'info');
+ if (prevProfile && prevProfile !== profileName) this._switchLoopProfile(prevProfile);
+ return results;
+ }
+ async _estimateTokensForRun(toolLog, finalAnswer) {
+ var om = this.terminal ? this.terminal.ollamaManager : null;
+ if (om && om._usageLog && om._usageLog.length > 0) {
+ var last = om._usageLog[om._usageLog.length - 1];
+ return last.total_tokens || 0;
+ }
+ var toolChars = toolLog.reduce(function(s, t) { return s + (t.result ? t.result.length : 0); }, 0);
+ var answerChars = (finalAnswer || '').length;
+ return Math.round((toolChars + answerChars) / 4);
+ }
+ benchCmd(args) {
+ var profile = null;
+ var taskIds = null;
+ for (var ai = 0; ai < args.length; ai++) {
+ if (args[ai].indexOf('--tasks=') === 0) taskIds = args[ai].split('=')[1].split(',');
+ else if (args[ai].indexOf('--') !== 0) profile = args[ai];
+ }
+ if (!profile) {
+ this.terminal.print('Usage: bench [profile] [--tasks=id1,id2] [--repeat=N]', 'warning');
+ this.terminal.print('Available tasks: ' + this._benchmarkTasks().map(function(t) { return t.id; }).join(', '), 'text-muted');
+ return;
+ }
+ this._runBenchmark(profile, taskIds);
+ }
+ benchCompareCmd(args) {
+ var p1 = args[0], p2 = args[1];
+ if (!p1 || !p2) { this.terminal.print('Usage: bench-compare <profile1> <profile2>', 'warning'); return; }
+ var b1 = this._loopProfiles[p1] ? this._loopProfiles[p1].benchmark : null;
+ var b2 = this._loopProfiles[p2] ? this._loopProfiles[p2].benchmark : null;
+ if (!b1 || !b1.lastRun) { this.terminal.print("No benchmark for '" + p1 + "'. Run: bench " + p1, 'warning'); return; }
+ if (!b2 || !b2.lastRun) { this.terminal.print("No benchmark for '" + p2 + "'. Run: bench " + p2, 'warning'); return; }
+ this.terminal.print('┌──────────────────────────────────────────────────────────┐', 'info');
+ this.terminal.print('│ ' + p1 + ' vs ' + p2, 'info');
+ this.terminal.print('├──────────────────────────────────────────────────────────┤', 'info');
+ this.terminal.print('│ Pass rate: ' + (b1.taskCompletionRate*100).toFixed(0) + '% vs ' + (b2.taskCompletionRate*100).toFixed(0) + '%', 'info');
+ this.terminal.print('│ Avg tokens: ' + b1.avgTokens + ' vs ' + b2.avgTokens, 'info');
+ this.terminal.print('│ Avg latency: ' + (b1.avgLatency/1000).toFixed(1) + 's vs ' + (b2.avgLatency/1000).toFixed(1) + 's', 'info');
+ this.terminal.print('│ Last run: ' + b1.lastRun.substring(0,10) + ' vs ' + b2.lastRun.substring(0,10), 'info');
+ this.terminal.print('└──────────────────────────────────────────────────────────┘', 'info');
+ }
// ── SELF-IMPROVEMENT LOOP ──
_postRunReview(result, toolLog, usageData) {
- var anomalies = []; var maxT = this._activeLoopProfile && this._activeLoopProfile.loop ? this._activeLoopProfile.loop.maxTurns : this.MAX_TOOL_TURNS;
- if (result.toolTurns >= maxT * 0.8) { anomalies.push({ type: "max_turns_near_limit", value: result.toolTurns, max: maxT }); }
- if (usageData && usageData.total_tokens > 50000) { anomalies.push({ type: "high_token_usage", value: usageData.total_tokens }); }
- var failedTools = toolLog.filter(function(t) { return t.error; }); if (failedTools.length > 2) { var uniqueTools = []; failedTools.forEach(function(t) { if (uniqueTools.indexOf(t.name) === -1) uniqueTools.push(t.name); }); anomalies.push({ type: "tool_failures", count: failedTools.length, tools: uniqueTools }); }
- var bench = this._activeLoopProfile ? this._activeLoopProfile.benchmark : null; if (bench && bench.avgTokens && usageData) { if (usageData.total_tokens > bench.avgTokens * 1.5) { anomalies.push({ type: "above_benchmark", value: usageData.total_tokens, benchmark: bench.avgTokens, ratio: (usageData.total_tokens / bench.avgTokens).toFixed(1) }); } }
+ var anomalies = [];
+ var maxT = this._activeLoopProfile && this._activeLoopProfile.loop ? this._activeLoopProfile.loop.maxTurns : this.MAX_TOOL_TURNS;
+ if (result.toolTurns >= maxT * 0.8) {
+ anomalies.push({ type: 'max_turns_near_limit', value: result.toolTurns, max: maxT });
+ }
+ if (usageData && usageData.total_tokens > 50000) {
+ anomalies.push({ type: 'high_token_usage', value: usageData.total_tokens });
+ }
+ var failedTools = toolLog.filter(function(t) { return t.error; });
+ if (failedTools.length > 2) {
+ var uniqueTools = [];
+ failedTools.forEach(function(t) { if (uniqueTools.indexOf(t.name) === -1) uniqueTools.push(t.name); });
+ anomalies.push({ type: 'tool_failures', count: failedTools.length, tools: uniqueTools });
+ }
+ var bench = this._activeLoopProfile ? this._activeLoopProfile.benchmark : null;
+ if (bench && bench.avgTokens && usageData) {
+ if (usageData.total_tokens > bench.avgTokens * 1.5) {
+ anomalies.push({ type: 'above_benchmark', value: usageData.total_tokens, benchmark: bench.avgTokens, ratio: (usageData.total_tokens / bench.avgTokens).toFixed(1) });
+ }
+ }
if (anomalies.length === 0) return;
- var suggestions = []; var profileAdjusted = false;
- for (var ai = 0; ai < anomalies.length; ai++) { var a = anomalies[ai];
- if (a.type === "max_turns_near_limit") {
- suggestions.push("This task used " + result.toolTurns + "/" + maxT + " turns — consider increasing max-turns or splitting the task");
- if (this._activeLoopProfile && this._activeLoopProfile.loop) { var newMax = (this._activeLoopProfile.loop.maxTurns || this.MAX_TOOL_TURNS) + 2; this._activeLoopProfile.loop.maxTurns = newMax; this._saveLoopProfile(this._activeLoopProfile); suggestions.push(" → Auto-adjusted maxTurns to " + newMax); profileAdjusted = true; }
- } else if (a.type === "high_token_usage") suggestions.push("High token usage (" + (a.value/1000).toFixed(0) + "K) — consider a minimal-tools profile for simpler tasks");
- else if (a.type === "tool_failures") {
- suggestions.push(a.count + " tool failures (" + a.tools.join(", ") + ") — check if modules are loaded or paths correct");
- if (this._activeLoopProfile && this._activeLoopProfile.systemPrompt) { var constraint = "[Constraint] Avoid tools that failed: " + a.tools.join(", ") + ". Verify module state before calling these tools."; if (this._activeLoopProfile.systemPrompt.append) { if (this._activeLoopProfile.systemPrompt.append.indexOf(constraint) === -1) { this._activeLoopProfile.systemPrompt.append += "\n" + constraint; } } else { this._activeLoopProfile.systemPrompt.append = constraint; } this._saveLoopProfile(this._activeLoopProfile); suggestions.push(" → Added constraint to profile: avoid " + a.tools.join(", ")); profileAdjusted = true; }
- } else if (a.type === "above_benchmark") suggestions.push("This run used " + a.ratio + "x more tokens than benchmark avg (" + a.benchmark + ") — consider profile optimization");
- }
- var suggestion = suggestions.join("\n");
- try { var fs = this.terminal.fs; var index = fs.fsReadJSON("/skills/index.json") || {}; var autoSkills = Object.entries(index).filter(function(e) { return (e[1].tags || []).indexOf("auto-improvement") !== -1; }); if (!this._loopLessonCleaned) { this._loopLessonCleaned = true; autoSkills.forEach(function(e) { try { fs.unlink("/skills/" + e[0] + ".md"); } catch (ex) {} delete index[e[0]]; }); autoSkills = []; } var now = Date.now(); autoSkills = autoSkills.filter(function(e) { var created = new Date(e[1].created).getTime(); if (now - created > 86400000) { try { fs.unlink("/skills/" + e[0] + ".md"); } catch (ex) {} delete index[e[0]]; return false; } return true; }); var anomalyTypes = anomalies.map(function(a) { return a.type; }).sort().join(","); var isDuplicate = autoSkills.length > 0 && autoSkills[autoSkills.length - 1][1].anomalyTypes === anomalyTypes; if (!isDuplicate && autoSkills.length >= 3) { var oldest = autoSkills[0]; try { fs.unlink("/skills/" + oldest[0] + ".md"); } catch (ex) {} delete index[oldest[0]]; autoSkills.shift(); } if (!isDuplicate) { var skillName = "loop-lesson-" + Date.now(); var content = "---\nname: " + skillName + "\ndescription: Auto-saved from agent loop anomaly detection\ntags: [auto-improvement, agent-loop, auto]\ncategory: system\n---\n## Anomalies Detected\n\n" + JSON.stringify(anomalies, null, 2) + "\n\n## Suggestions\n\n" + suggestion + "\n\n## Context\n- Profile: " + (this._activeLoopProfile ? this._activeLoopProfile.name : "none") + "\n- Turns: " + result.toolTurns + "\n- Tools used: " + toolLog.map(function(t) { return t.name; }).join(", "); fs.mkdirRecursive("/skills"); fs.writeFile("/skills/" + skillName + ".md", content); index[skillName] = { name: skillName, description: "Auto-saved loop lesson", tags: ["auto-improvement", "agent-loop", "auto"], category: "system", created: new Date().toISOString(), updated: new Date().toISOString(), anomalyTypes: anomalyTypes }; fs.fsWriteJSON("/skills/index.json", index); } } catch (e) {}
- var msg = "💡 Self-improvement: " + anomalies.length + " anomaly(ies)"; if (profileAdjusted) msg += " — profile auto-adjusted";
- this.terminal.print(msg, "info");
+ var suggestions = [];
+ var profileAdjusted = false;
+ for (var ai = 0; ai < anomalies.length; ai++) {
+ var a = anomalies[ai];
+ if (a.type === 'max_turns_near_limit') {
+ suggestions.push('This task used ' + result.toolTurns + '/' + maxT + ' turns — consider increasing max-turns or splitting the task');
+ // Auto-correct: bump maxTurns by +2
+ if (this._activeLoopProfile && this._activeLoopProfile.loop) {
+ var newMax = (this._activeLoopProfile.loop.maxTurns || this.MAX_TOOL_TURNS) + 2;
+ this._activeLoopProfile.loop.maxTurns = newMax;
+ this._saveLoopProfile(this._activeLoopProfile);
+ suggestions.push(' → Auto-adjusted maxTurns to ' + newMax);
+ profileAdjusted = true;
+ }
+ }
+ else if (a.type === 'high_token_usage') suggestions.push('High token usage (' + (a.value/1000).toFixed(0) + 'K) — consider a minimal-tools profile for simpler tasks');
+ else if (a.type === 'tool_failures') {
+ suggestions.push(a.count + ' tool failures (' + a.tools.join(', ') + ') — check if modules are loaded or paths correct');
+ // Auto-fix: map failed tools to modules and suggest fix
+ if (!this._autoFixSuggestions) this._autoFixSuggestions = [];
+ if (!this._toolToModuleMap) this._buildToolToModuleMap();
+ for (var fi = 0; fi < a.tools.length; fi++) {
+ var failedMod = this._toolToModuleMap[a.tools[fi]];
+ if (failedMod && this._autoFixSuggestions.length < 3) {
+ this._autoFixSuggestions.push({
+ module: failedMod,
+ tool: a.tools[fi],
+ reason: 'Repeated tool failure in agent loop',
+ suggestion: 'Consider running ship_module to fix ' + failedMod
+ });
+ }
+ }
+ // Auto-correct: inject negative constraint into profile append
+ if (this._activeLoopProfile && this._activeLoopProfile.systemPrompt) {
+ var constraint = '[Constraint] Avoid tools that failed: ' + a.tools.join(', ') + '. Verify module state before calling these tools.';
+ if (this._activeLoopProfile.systemPrompt.append) {
+ if (this._activeLoopProfile.systemPrompt.append.indexOf(constraint) === -1) {
+ this._activeLoopProfile.systemPrompt.append += '\n' + constraint;
+ }
+ } else {
+ this._activeLoopProfile.systemPrompt.append = constraint;
+ }
+ this._saveLoopProfile(this._activeLoopProfile);
+ suggestions.push(' → Added constraint to profile: avoid ' + a.tools.join(', '));
+ profileAdjusted = true;
+ }
+ }
+ else if (a.type === 'above_benchmark') suggestions.push('This run used ' + a.ratio + 'x more tokens than benchmark avg (' + a.benchmark + ') — consider profile optimization');
+ }
+ var suggestion = suggestions.join('\n');
+ try {
+ var fs = this.terminal.fs;
+ var index = fs.fsReadJSON('/skills/index.json') || {};
+ var autoSkills = Object.entries(index).filter(function(e) { return (e[1].tags || []).indexOf('auto-improvement') !== -1; });
+ // One-time cleanup: remove all existing loop-lesson files on first run
+ if (!this._loopLessonCleaned) {
+ this._loopLessonCleaned = true;
+ autoSkills.forEach(function(e) {
+ try { fs.unlink('/skills/' + e[0] + '.md'); } catch (ex) {}
+ delete index[e[0]];
+ });
+ autoSkills = [];
+ }
+ // Age limit: remove lessons older than 24h
+ var now = Date.now();
+ autoSkills = autoSkills.filter(function(e) {
+ var created = new Date(e[1].created).getTime();
+ if (now - created > 86400000) {
+ try { fs.unlink('/skills/' + e[0] + '.md'); } catch (ex) {}
+ delete index[e[0]];
+ return false;
+ }
+ return true;
+ });
+ // Dedup: skip if latest lesson has same anomaly types
+ var anomalyTypes = anomalies.map(function(a) { return a.type; }).sort().join(',');
+ var isDuplicate = autoSkills.length > 0 && autoSkills[autoSkills.length - 1][1].anomalyTypes === anomalyTypes;
+ // FIFO rotation: if at cap, delete oldest before creating
+ if (!isDuplicate && autoSkills.length >= 3) {
+ var oldest = autoSkills[0];
+ try { fs.unlink('/skills/' + oldest[0] + '.md'); } catch (ex) {}
+ delete index[oldest[0]];
+ autoSkills.shift();
+ }
+ if (!isDuplicate) {
+ var skillName = 'loop-lesson-' + Date.now();
+ var content = '---\nname: ' + skillName + '\ndescription: Auto-saved from agent loop anomaly detection\ntags: [auto-improvement, agent-loop, auto]\ncategory: system\n---\n## Anomalies Detected\n\n' + JSON.stringify(anomalies, null, 2) + '\n\n## Suggestions\n\n' + suggestion + '\n\n## Context\n- Profile: ' + (this._activeLoopProfile ? this._activeLoopProfile.name : 'none') + '\n- Turns: ' + result.toolTurns + '\n- Tools used: ' + toolLog.map(function(t) { return t.name; }).join(', ');
+ fs.mkdirRecursive('/skills');
+ fs.writeFile('/skills/' + skillName + '.md', content);
+ index[skillName] = { name: skillName, description: 'Auto-saved loop lesson', tags: ['auto-improvement', 'agent-loop', 'auto'], category: 'system', created: new Date().toISOString(), updated: new Date().toISOString(), anomalyTypes: anomalyTypes };
+ fs.fsWriteJSON('/skills/index.json', index);
+ }
+ } catch (e) { /* VFS not available */ }
+ var msg = '💡 Self-improvement: ' + anomalies.length + ' anomaly(ies)';
+ if (profileAdjusted) msg += ' — profile auto-adjusted';
+ this.terminal.print(msg, 'info');
}
- _vaultmindStatusCmd() {
- const token = localStorage.getItem('vaultmind_token');
- if (token) {
- this.terminal.print('✅ VaultMind session active (token: ' + token.substring(0, 12) + '...)', 'success');
- } else {
- this.terminal.print('❌ No VaultMind session. Use: vaultmind-connect [testnet|mainnet]', 'warning');
+ async _collectDashboardData() {
+ try {
+ const terminal = this.terminal;
+ const mm = terminal.moduleManager;
+ const om = terminal.ollamaManager;
+ const fs = terminal.fs;
+ const data = {};
+
+ // Version
+ data.version = window.NEWOS4_VERSION || 'v?';
+
+ // Modules
+ const mods = mm ? Object.keys(mm.modules) : [];
+ const registry = mm ? (mm.moduleRegistry || []) : [];
+ data.modules = {
+ loaded: mods.length,
+ total: registry.length || mods.length,
+ modifiedCount: 0
+ };
+ try {
+ const modsDir = fs ? fs.readDir('/_mods/') : null;
+ if (modsDir) data.modules.modifiedCount = modsDir.filter(function(e) { return e.type === 'file' && e.name.endsWith('.js') && !e.name.endsWith('.orig.js'); }).length;
+ } catch (e) {}
+
+ // Agents
+ const orch = mm ? mm.modules['OrchestratorModule'] : null;
+ if (orch && orch._agents) {
+ var agentList = Object.values(orch._agents);
+ data.agents = {
+ total: agentList.length,
+ busy: agentList.filter(function(a) { return a.status === 'busy'; }).length,
+ idle: agentList.filter(function(a) { return a.status === 'idle'; }).length,
+ sleeping: agentList.filter(function(a) { return a.status === 'sleeping'; }).length,
+ failed: agentList.filter(function(a) { return a.status === 'failed' || a.status === 'restarting'; }).length,
+ list: agentList.map(function(a) { return { id: a.id, role: a.role, status: a.status, caps: (a.capabilities || []).length }; })
+ };
+ } else {
+ data.agents = { total: 0, busy: 0, idle: 0, sleeping: 0, failed: 0, list: [] };
+ }
+
+ // Memory
+ var heapStr = 'N/A';
+ try { if (performance.memory) heapStr = (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB'; } catch (e) {}
+ var graphNodes = 0;
+ var mg = mm ? mm.modules['MemoryGraphModule'] : null;
+ if (mg && mg._nodes) graphNodes = Object.keys(mg._nodes).length;
+ data.memory = { heap: heapStr, graphNodes: graphNodes };
+
+ // VFS
+ try {
+ if (fs) {
+ var fileCount = 0, dirCount = 0;
+ var walk = function(path) {
+ var entries = fs.ls(path);
+ if (!entries) return;
+ for (var i = 0; i < entries.length; i++) {
+ if (entries[i].type === 'file') fileCount++;
+ else { dirCount++; walk(path === '/' ? '/' + entries[i].name : path + '/' + entries[i].name); }
+ }
+ };
+ walk('/');
+ data.vfs = { files: fileCount, dirs: dirCount, size: (JSON.stringify(fs.root || {}).length / 1024).toFixed(1) + 'KB' };
+ }
+ } catch (e) { data.vfs = { files: 0, dirs: 0, size: '0KB' }; }
+
+ // Ollama
+ data.ollama = {
+ connected: om ? om.connected : false,
+ model: om ? om.currentModel : null,
+ provider: om && om.currentProvider ? om.currentProvider.name : null
+ };
+
+ // Skills
+ try {
+ var skillIdx = fs ? fs.fsReadJSON('/skills/index.json') : null;
+ data.skills = { count: skillIdx ? Object.keys(skillIdx).length : 0 };
+ } catch (e) { data.skills = { count: 0 }; }
+
+ // Token usage
+ try {
+ if (om && typeof om.getUsageStats === 'function') {
+ var usage = om.getUsageStats({ since_hours: 24 });
+ data.usage = usage;
+ }
+ } catch (e) { data.usage = { total: 0 }; }
+
+ // Tool latency
+ var sm = mm ? mm.modules['ServerMonitorModule'] : null;
+ if (sm && sm._latencyLog) {
+ var toolStats = {};
+ for (var i = 0; i < sm._latencyLog.length; i++) {
+ var entry = sm._latencyLog[i];
+ if (!toolStats[entry.name]) toolStats[entry.name] = { calls: 0, totalMs: 0, errors: 0, maxMs: 0 };
+ toolStats[entry.name].calls++;
+ toolStats[entry.name].totalMs += entry.duration;
+ toolStats[entry.name].errors += entry.error ? 1 : 0;
+ toolStats[entry.name].maxMs = Math.max(toolStats[entry.name].maxMs, entry.duration);
+ }
+ data.latency = Object.entries(toolStats)
+ .map(function(e) { return { name: e[0], avg: Math.round(e[1].totalMs / e[1].calls) + 'ms', max: e[1].maxMs + 'ms', calls: e[1].calls, errors: e[1].errors }; })
+ .sort(function(a, b) { return parseInt(b.avg) - parseInt(a.avg); })
+ .slice(0, 10);
+ } else {
+ data.latency = [];
+ }
+
+ // System health
+ var totalErrors = 0;
+ try {
+ var ds = mm ? mm.modules['DebugSupervisorModule'] : null;
+ if (ds && ds._errorDB) totalErrors += Object.keys(ds._errorDB.errors).length;
+ if (sm && sm._latencyLog) totalErrors += sm._latencyLog.filter(function(e) { return e.error; }).length;
+ } catch (e) {}
+ var predictor = mm ? mm.modules['PredictorModule'] : null;
+ var predictorAcc = 'N/A';
+ if (predictor && predictor._accuracy && predictor._accuracy.total > 0) {
+ predictorAcc = Math.round(predictor._accuracy.correct / predictor._accuracy.total * 100) + '%';
+ }
+ data.health = {
+ modules: data.modules.loaded + '/' + data.modules.total,
+ vfsSize: data.vfs ? data.vfs.size : '?',
+ online: typeof navigator !== 'undefined' ? navigator.onLine : true,
+ errors: totalErrors,
+ predictorAccuracy: predictorAcc
+ };
+
+ data.timestamp = Date.now();
+
+ // Write to localStorage for the dashboard HTML page to read
+ try { localStorage.setItem('webx11_dashboard_data', JSON.stringify(data)); } catch (e) {}
+
+ return data;
+ } catch (e) {
+ console.error('[Dashboard] Collection error:', e.message);
+ return { error: e.message, timestamp: Date.now() };
}
}
}
@@ -12021,8 +13267,7 @@ class ChatStudioModule {
var shortNames = {
'llama3.2': 'Llama3.2',
'llama3': 'Llama3',
- 'gemma3:4b': 'Gemma3-4B',
- 'gemma3:1b': 'Gemma3-1B',
+ 'gemma4:31b-cloud': 'Gemma4-31B',
'gemma2:2b': 'Gemma2',
'qwen2.5:7b-instruct': 'Qwen2.5',
'qwen3:8b': 'Qwen3',
@@ -13173,10 +14418,15 @@ class LeaseModule {
this.terminal.print('📜 Lease system loaded', 'info');
}
createLease(args) {
+ // Parse --write flag
+ const writeIdx = args.indexOf('--write');
+ const writeAccess = writeIdx !== -1;
+ if (writeIdx !== -1) args.splice(writeIdx, 1);
+
const workerId = args[1];
const duration = parseInt(args[2]) || 30;
const task = args.slice(3).join(' ') || 'generic task';
- if (!workerId) return this.terminal.print('Usage: lease-create <agent-id> [duration_sec] [task]', 'error');
+ if (!workerId) return this.terminal.print('Usage: lease-create <agent-id> [duration_sec] [task] [--write]', 'error');
const lease = {
id: `lease-${Date.now()}`,
@@ -13186,7 +14436,8 @@ class LeaseModule {
createdAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + duration * 1000).toISOString(),
status: 'active',
- duration
+ duration,
+ write_access: writeAccess
};
this._leases[lease.id] = lease;
this.terminal.fs.writeFile(`leases/${lease.id}.json`, JSON.stringify(lease));
@@ -13214,9 +14465,14 @@ class LeaseModule {
this.terminal.print(`📜 Leases: ${active.length} active, ${expired.length} expired, ${revoked.length} revoked`, 'header');
active.forEach(l => {
const remaining = Math.max(0, Math.round((new Date(l.expiresAt) - Date.now()) / 1000));
- this.terminal.print(` ${l.id} → ${l.workerId} (${remaining}s remain) — ${l.task.substring(0, 40)}`, 'info');
+ const writeTag = l.write_access ? ' ✏️write' : ' 🔍read';
+ this.terminal.print(` ${l.id} → ${l.workerId} (${remaining}s remain)${writeTag} — ${l.task.substring(0, 40)}`, 'info');
});
}
+ hasWriteAccess(agentId) {
+ // Check if any active lease grants write access for this agent
+ return Object.values(this._leases).some(l => l.workerId === agentId && l.status === 'active' && l.write_access === true);
+ }
revokeLease(id) {
if (!this._leases[id]) return this.terminal.print(`❌ Lease ${id} not found`, 'error');
this._leases[id].status = 'revoked';
@@ -13296,6 +14552,8 @@ class OrchestratorModule {
this._activeConsensus = null;
this._pipelineHistory = [];
this._PIPELINE_CONFIG = { autoFix: false, stageTimeout: 30000 };
+ this._schedules = [];
+ this._scheduleTimer = null;
this._loadConfig();
}
_loadConfig() {
@@ -13348,6 +14606,9 @@ class OrchestratorModule {
this.commands['consensus-status'] = () => this.consensusStatus();
this.commands['consensus-history'] = () => this.consensusHistory();
this.commands['pipeline'] = (args) => this.runPipeline(args);
+ this.commands['orchestrator-schedule'] = (args) => this.addSchedule(args);
+ this.commands['orchestrator-schedules'] = () => this.listSchedules();
+ this.commands['orchestrator-unschedule'] = (args) => this.removeSchedule(args[0]);
}
// ─── Task Decomposition Engine ───
_decomposeGoal(goal) {
@@ -13410,6 +14671,9 @@ class OrchestratorModule {
'consensus-status': 'consensus-status — Show current/active consensus round status',
'consensus-history': 'consensus-history — Show past consensus rounds and their resolutions',
'pipeline': 'pipeline <test|deploy|verify|history|config> — Autonomous CI/CD: test agents/queue/modules, deploy (test→auto-fix→verify), history (show runs), config (show config)',
+ 'orchestrator-schedule': 'orchestrator-schedule <role> <interval_seconds> <task...> — Schedule a recurring task delegation',
+ 'orchestrator-schedules': 'orchestrator-schedules — List all active recurring schedules',
+ 'orchestrator-unschedule': 'orchestrator-unschedule <id> — Remove a recurring schedule',
'orchestrator-status': 'orchestrator-status — Show agent pool status and config',
'orchestrator-spawn': 'orchestrator-spawn [role] [capabilities...] — Manually spawn a new agent',
'orchestrator-kill': 'orchestrator-kill <agent-id> — Terminate an agent and revoke leases',
@@ -13434,13 +14698,17 @@ class OrchestratorModule {
this._reanimateFromVFS();
// A2A mailbox polling loop (500ms)
if (this._A2A_POLLING) {
- this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
+ this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
}
// Heartbeat watchdog
- this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), this._HEARTBEAT_CHECK);
+ this._heartbeatInterval = this.terminal.moduleManager._setInterval(() => this._checkHeartbeats(), this._HEARTBEAT_CHECK);
// Idle timeout sweep
- this._idleSweepInterval = setInterval(() => this._sweepIdle(), this._IDLE_SWEEP_INTERVAL);
+ this._idleSweepInterval = this.terminal.moduleManager._setInterval(() => this._sweepIdle(), this._IDLE_SWEEP_INTERVAL);
this.terminal.print('🧠 Orchestrator loaded — A2A mailbox transport active', 'success');
+ // Load recurring schedules from VFS
+ this._loadSchedules();
+ // Pre-spawn default agents after a short delay (let other modules init first)
+ setTimeout(() => this._preSpawnDefaultAgents(), 2000);
// Subscribe to event bus for in-memory A2A delivery (instant, no VFS poll)
this._on('a2a:message', (msg) => {
if (msg.type === 'heartbeat' && msg.from && this._agents[msg.from]) {
@@ -13825,16 +15093,16 @@ class OrchestratorModule {
'a2a-polling': () => {
this._A2A_POLLING = val === 'on' || val === 'true';
if (this._A2A_POLLING && !this._a2aPollTimer) {
- this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
+ this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
} else if (!this._A2A_POLLING && this._a2aPollTimer) {
- clearInterval(this._a2aPollTimer);
+ this.terminal.moduleManager._clearTimer(this._a2aPollTimer);
this._a2aPollTimer = null;
}
return `a2a-polling ${this._A2A_POLLING ? 'on' : 'off'}`;
},
- 'a2a-poll-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 50 || n > 30000) return 'Must be 50-30000ms'; clearInterval(this._a2aPollTimer); this._a2aPollTimer = null; this._A2A_POLL_INTERVAL = n; if (this._A2A_POLLING) { this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), n); } return `a2a-poll-interval set to ${n}ms`; },
- 'heartbeat-check': () => { const n = parseInt(val); if (isNaN(n) || n < 500 || n > 60000) return 'Must be 500-60000ms'; clearInterval(this._heartbeatInterval); this._HEARTBEAT_CHECK = n; this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), n); return `heartbeat-check set to ${n}ms`; },
- 'idle-sweep-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; clearInterval(this._idleSweepInterval); this._IDLE_SWEEP_INTERVAL = n; this._idleSweepInterval = setInterval(() => this._sweepIdle(), n); return `idle-sweep-interval set to ${n}ms`; },
+ 'a2a-poll-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 50 || n > 30000) return 'Must be 50-30000ms'; this.terminal.moduleManager._clearTimer(this._a2aPollTimer); this._a2aPollTimer = null; this._A2A_POLL_INTERVAL = n; if (this._A2A_POLLING) { this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), n); } return `a2a-poll-interval set to ${n}ms`; },
+ 'heartbeat-check': () => { const n = parseInt(val); if (isNaN(n) || n < 500 || n > 60000) return 'Must be 500-60000ms'; this.terminal.moduleManager._clearTimer(this._heartbeatInterval); this._HEARTBEAT_CHECK = n; this._heartbeatInterval = this.terminal.moduleManager._setInterval(() => this._checkHeartbeats(), n); return `heartbeat-check set to ${n}ms`; },
+ 'idle-sweep-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; this.terminal.moduleManager._clearTimer(this._idleSweepInterval); this._IDLE_SWEEP_INTERVAL = n; this._idleSweepInterval = this.terminal.moduleManager._setInterval(() => this._sweepIdle(), n); return `idle-sweep-interval set to ${n}ms`; },
'dead-purge-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; this._DEAD_PURGE_TIMEOUT = n; return `dead-purge-timeout set to ${n}ms`; },
'consensus-size': () => { const n = parseInt(val); if (isNaN(n) || n < 2 || n > 10) return 'Must be 2-10'; this._CONSENSUS_SIZE = n; return `consensus-size set to ${n}`; },
'consensus-strategy': () => { if (!['majority', 'unanimous', 'weighted'].includes(val)) return 'Must be majority, unanimous, or weighted'; this._CONSENSUS_STRATEGY = val; return `consensus-strategy set to ${val}`; },
@@ -14247,7 +15515,7 @@ class OrchestratorModule {
// Fix 2: Restart A2A poller if stopped
if (!this._A2A_POLLING) {
this._A2A_POLLING = true;
- this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
+ this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
this.terminal.print(' Restarted A2A polling', 'warning');
fixed++;
}
@@ -14377,9 +15645,12 @@ class OrchestratorModule {
}
}
- // Create a lease
+ // Create a lease — with --write if agent has write capabilities
const leaseModule = this.terminal.moduleManager.modules['LeaseModule'];
- const lease = leaseModule ? leaseModule.createLease(['lease-create', agent.id, '60', task]) : null;
+ const hasWriteCap = (agent.capabilities || []).some(function(c) { return c === 'write_file' || c === 'edit_module' || c === 'delete_file'; });
+ const leaseArgs = ['lease-create', agent.id, '60', task];
+ if (hasWriteCap) leaseArgs.push('--write');
+ const lease = leaseModule ? leaseModule.createLease(leaseArgs) : null;
// Mark agent as busy
agent.status = 'busy';
@@ -14448,6 +15719,109 @@ class OrchestratorModule {
return outboxResult || 'completed';
}
+ // ─── Recurring Scheduling ───
+
+ addSchedule(args) {
+ if (args.length < 2) return this.terminal.print('Usage: orchestrator-schedule <role> <interval_seconds> <task...>', 'error');
+ const role = args[0];
+ const interval = parseInt(args[1]);
+ if (isNaN(interval) || interval < 10) return this.terminal.print('Interval must be >= 10 seconds', 'error');
+ const task = args.slice(2).join(' ');
+ if (!task) return this.terminal.print('Task description required', 'error');
+
+ const schedule = {
+ id: 'sched-' + Date.now(),
+ role: role,
+ interval: interval * 1000,
+ task: task,
+ createdAt: new Date().toISOString(),
+ lastRun: null,
+ nextRun: Date.now() + interval * 1000,
+ runs: 0
+ };
+ this._schedules.push(schedule);
+ this._saveSchedules();
+ this._startScheduleTimer();
+ this.terminal.print('📅 Schedule created: "' + task.substring(0, 40) + '" every ' + interval + 's (role: ' + role + ')', 'success');
+ return schedule;
+ }
+
+ listSchedules() {
+ if (this._schedules.length === 0) return this.terminal.print('No active schedules', 'info');
+ this.terminal.print('📅 Recurring Schedules (' + this._schedules.length + '):', 'header');
+ for (const s of this._schedules) {
+ const nextIn = Math.max(0, Math.round((s.nextRun - Date.now()) / 1000));
+ this.terminal.print(' ' + s.id + ' — "' + s.task.substring(0, 40) + '" every ' + (s.interval / 1000) + 's [' + s.role + '] (next: ' + nextIn + 's, runs: ' + s.runs + ')', 'info');
+ }
+ }
+
+ removeSchedule(id) {
+ const idx = this._schedules.findIndex(function(s) { return s.id === id; });
+ if (idx === -1) return this.terminal.print('Schedule ' + id + ' not found', 'error');
+ this._schedules.splice(idx, 1);
+ this._saveSchedules();
+ if (this._schedules.length === 0 && this._scheduleTimer) {
+ clearInterval(this._scheduleTimer);
+ this._scheduleTimer = null;
+ }
+ this.terminal.print('Schedule ' + id + ' removed', 'success');
+ }
+
+ _startScheduleTimer() {
+ if (this._scheduleTimer) return;
+ this._scheduleTimer = setInterval(function() {
+ var now = Date.now();
+ for (var i = 0; i < this._schedules.length; i++) {
+ var s = this._schedules[i];
+ if (now >= s.nextRun) {
+ s.lastRun = new Date().toISOString();
+ s.nextRun = now + s.interval;
+ s.runs++;
+ this.terminal.print('📅 Schedule trigger: "' + s.task.substring(0, 40) + '"', 'info');
+ this.delegateTask(s.task);
+ }
+ }
+ this._saveSchedules();
+ }.bind(this), 5000);
+ }
+
+ _saveSchedules() {
+ try {
+ this.terminal.fs.fsWriteJSON('/orchestrator/schedules.json', this._schedules);
+ } catch (e) { console.warn('[Orchestrator] Schedule save error:', e.message); }
+ }
+
+ _loadSchedules() {
+ try {
+ const data = this.terminal.fs.fsReadJSON('/orchestrator/schedules.json');
+ if (data && Array.isArray(data)) {
+ this._schedules = data;
+ if (this._schedules.length > 0) this._startScheduleTimer();
+ }
+ } catch (e) { this._schedules = []; }
+ }
+
+ _preSpawnDefaultAgents() {
+ // Spawn 3 role-specific agents at boot
+ var roles = [
+ { role: 'auto-learner', caps: ['list_files', 'read_file', 'write_file', 'view_module'], task: 'audit skills' },
+ { role: 'kernel-architect', caps: ['list_files', 'read_file', 'write_file', 'run_command'], task: 'index VFS' },
+ { role: 'searcher', caps: ['list_files', 'read_file', 'web_search'], task: 'check stale data' }
+ ];
+ for (var i = 0; i < roles.length; i++) {
+ var r = roles[i];
+ // Check if an agent with this role already exists
+ var existing = Object.values(this._agents).find(function(a) { return a.role === r.role; });
+ if (!existing) {
+ var agent = this.spawnAgent(r.role, r.caps);
+ if (agent) {
+ agent.restartPolicy = 'always';
+ this.terminal.print('🤖 Pre-spawned agent: ' + r.role + ' (' + r.caps.length + ' caps)', 'info');
+ }
+ }
+ }
+ }
+
// ─── Monitoring ───
_checkHeartbeats() {
@@ -14914,6 +16288,11 @@ class MemoryGraphModule {
this._debounceTimer = null;
this._initialized = false;
this._totalNodeAccesses = 0;
+ // v4.5.89: Decay configuration
+ this._decayDays = 30;
+ this._decayMinUses = 3;
+ this._decayRate = 0.01; // Daily decay rate
+ this._lastDecayRun = 0;
}
getHelpInfo() {
@@ -14923,6 +16302,10 @@ class MemoryGraphModule {
'memgraph-stats': 'memgraph-stats — Most connected nodes, strongest edges, cluster info',
'memgraph-export': 'memgraph-export — Export graph as JSON',
'memgraph-prune': 'memgraph-prune — Remove stale low-weight edges (>30d, <3 accesses)',
+ 'memgraph-learn': 'memgraph-learn <entity> <fact> — Add a fact to the memory graph',
+ 'memgraph-session': 'memgraph-session [date] — Show graph nodes for a session (default: today)',
+ 'memgraph-agent': 'memgraph-agent <agentId> — Show namespaced nodes for an agent',
+ 'memgraph-decay-config': 'memgraph-decay-config <days> <min_uses> — Configure decay policy (default: 30d, 3 uses)',
};
}
@@ -14932,6 +16315,11 @@ class MemoryGraphModule {
this.commands['memgraph-stats'] = () => this.showStats();
this.commands['memgraph-export'] = () => this.exportGraph();
this.commands['memgraph-prune'] = () => this.prune();
+ this.commands['memgraph-learn'] = (args) => this.learn(args);
+ this.commands['memgraph-session'] = (args) => this.showSession(args.join(' '));
+ this.commands['memgraph-agent'] = (args) => this.showAgent(args.join(' '));
+ // v4.5.89: Decay configuration
+ this.commands['memgraph-decay-config'] = (args) => this.decayConfig(args);
}
init(terminal) {
@@ -14940,13 +16328,16 @@ class MemoryGraphModule {
this._initialized = true;
this.terminal.print('🧠 Memory Graph loaded — cross-session context active', 'info');
// Log startup for graph
- this.addNode('session', `session-${Date.now()}`, `Session ${new Date().toLocaleDateString()}`);
+ // v4.5.89 fix: Use day-floored key to match edge source keys (P0-1 from review)
+ this.addNode('session', `session-${Math.floor(Date.now() / 86400000)}`, `Session ${new Date().toLocaleDateString()}`);
}
// ─── Node API ───
- addNode(type, id, label, metadata = {}) {
- const key = `${type}:${id}`;
+ addNode(type, id, label, metadata = {}, agentId = null) {
+ // v4.5.88: Support agent namespacing with dual-lookup
+ const ns = agentId ? `agent:${agentId}` : 'global';
+ const key = agentId ? `${ns}:${type}:${id}` : `${type}:${id}`;
if (!this._nodes[key]) {
this._nodes[key] = {
id: key,
@@ -14955,7 +16346,9 @@ class MemoryGraphModule {
metadata: metadata || {},
created: new Date().toISOString(),
updated: new Date().toISOString(),
- access_count: 1
+ access_count: 1,
+ namespace: ns,
+ source_session: `session-${Math.floor(Date.now() / 86400000)}`
};
} else {
this._nodes[key].updated = new Date().toISOString();
@@ -14969,8 +16362,14 @@ class MemoryGraphModule {
return key;
}
- getNode(type, id) {
- return this._nodes[`${type}:${id}`] || null;
+ // v4.5.88: Dual-lookup — try namespaced key first, fall back to global
+ getNode(type, id, agentId = null) {
+ if (agentId) {
+ const nsKey = `agent:${agentId}:${type}:${id}`;
+ if (this._nodes[nsKey]) return this._nodes[nsKey];
+ }
+ const globalKey = `${type}:${id}`;
+ return this._nodes[globalKey] || null;
}
findNodes(query) {
@@ -14984,7 +16383,8 @@ class MemoryGraphModule {
// ─── Edge API ───
- addEdge(sourceKey, targetKey, type, weight = 1) {
+ // v4.5.88: addEdge tracks source_agents for multi-agent attribution
+ addEdge(sourceKey, targetKey, type, weight = 1, agentId = null) {
// Check if edge already exists
let edge = this._edges.find(e =>
e.source === sourceKey && e.target === targetKey && e.type === type
@@ -14993,6 +16393,7 @@ class MemoryGraphModule {
edge.weight = (edge.weight + weight) / 2;
edge.last_used = new Date().toISOString();
edge.use_count++;
+ if (agentId) edge.source_agents = [...new Set([...(edge.source_agents || []), agentId])];
} else {
this._edges.push({
source: sourceKey,
@@ -15001,7 +16402,8 @@ class MemoryGraphModule {
weight,
created: new Date().toISOString(),
last_used: new Date().toISOString(),
- use_count: 1
+ use_count: 1,
+ source_agents: agentId ? [agentId] : []
});
}
this._debounceSave();
@@ -15009,11 +16411,12 @@ class MemoryGraphModule {
// ─── Event-based Learning ───
- recordFileAccess(path) {
+ // v4.5.89 fix: Thread agentId through record* methods (P1-3 from review)
+ recordFileAccess(path, agentId = null) {
const name = path.split('/').pop() || path;
- const nodeKey = this.addNode('file', path, name);
+ const nodeKey = this.addNode('file', path, name, {}, agentId);
const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
- this.addEdge(sessionKey, nodeKey, 'accessed', 0.5);
+ this.addEdge(sessionKey, nodeKey, 'accessed', 0.5, agentId);
// Extract file extension edge
const ext = path.split('.').pop();
@@ -15023,10 +16426,10 @@ class MemoryGraphModule {
}
}
- recordToolCall(toolName, success) {
- const nodeKey = this.addNode('tool', toolName, toolName);
+ recordToolCall(toolName, success, agentId = null) {
+ const nodeKey = this.addNode('tool', toolName, toolName, {}, agentId);
const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
- this.addEdge(sessionKey, nodeKey, success ? 'used' : 'failed', success ? 0.5 : 0.8);
+ this.addEdge(sessionKey, nodeKey, success ? 'used' : 'failed', success ? 0.5 : 0.8, agentId);
}
recordConcept(concept, filePath) {
@@ -15036,15 +16439,16 @@ class MemoryGraphModule {
this.addEdge(fileKey, conceptKey, 'tagged_as', 0.7);
}
+ // v4.5.88: recordTask now passes agentId for namespacing + adds source_session link
recordTask(task, result, agentId) {
const taskId = `task-${Date.now()}`;
const taskKey = this.addNode('task', taskId, task.substring(0, 60), {
full: task,
result: result?.substring(0, 100),
agent: agentId
- });
+ }, agentId);
const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
- this.addEdge(sessionKey, taskKey, 'executed', 0.6);
+ this.addEdge(sessionKey, taskKey, 'executed', 0.6, agentId);
// Extract keywords as concepts
const words = task.toLowerCase().split(/\s+/).filter(w => w.length > 3);
@@ -15103,18 +16507,38 @@ class MemoryGraphModule {
}
}
- return lines.join('\\n');
+ return lines.join('\n');
}
getContextForPrompt() {
const summary = this.getContextSummary();
if (!summary) return '';
- return `[Memory Graph Context]\\n${summary}\\n`;
+ // Suppress noise: don't inject when graph is nearly empty
+ const totalNodes = Object.keys(this._nodes).length;
+ const totalEdges = this._edges.length;
+ if (totalNodes <= 2 && totalEdges === 0) return '';
+ return `[Memory Graph Context]\n${summary}\n`;
+ }
+
+ // Alias for orchestrator compatibility (orchestrator calls getStats, not showStats)
+ getStats() {
+ const types = {};
+ for (const n of Object.values(this._nodes)) {
+ types[n.type] = (types[n.type] || 0) + 1;
+ }
+ return {
+ nodes: Object.keys(this._nodes).length,
+ edges: this._edges.length,
+ types: types,
+ totalAccesses: this._totalNodeAccesses
+ };
}
// ─── Commands ───
showStatus() {
+ // v4.5.89: Lazy decay — run applyDecay() on status check
+ this.applyDecay();
const types = {};
for (const n of Object.values(this._nodes)) {
types[n.type] = (types[n.type] || 0) + 1;
@@ -15129,6 +16553,8 @@ class MemoryGraphModule {
this.terminal.print(` Edges: ${this._edges.length}`, 'info');
this.terminal.print(` Total accesses: ${this._totalNodeAccesses}`, 'info');
this.terminal.print(` Path: ${this._indexPath}`, 'info');
+ // v4.5.89: Show decay config
+ this.terminal.print(` Decay: ${this._decayDays}d threshold, ${this._decayMinUses} min uses, rate ${this._decayRate}/day`, 'text-muted');
}
search(query) {
@@ -15248,6 +16674,181 @@ class MemoryGraphModule {
this.terminal.print(`🧹 Pruned ${pruned} edges and ${nodesPruned} orphan nodes`, nodesPruned > 0 ? 'warning' : 'info');
}
+ // v4.5.89: Apply decay scores to nodes based on age and access patterns
+ applyDecay() {
+ const now = Date.now();
+ if (now - this._lastDecayRun < 3600000) return;
+ this._lastDecayRun = now;
+
+ // v4.5.89b fix: Compute decay rate from _decayDays (P0 from review)
+ const effectiveRate = 0.9 / this._decayDays;
+ let decayed = 0;
+ let removed = 0;
+
+ for (const key of Object.keys(this._nodes)) {
+ const node = this._nodes[key];
+ const ageDays = (now - new Date(node.updated || node.created).getTime()) / 86400000;
+ node.decay_score = Math.max(0, 1.0 - (ageDays * effectiveRate));
+ if (node.decay_score < 0.1 && node.access_count < this._decayMinUses) {
+ if (node.type === 'session') continue;
+ if (node.namespace && node.namespace.startsWith('agent:')) continue;
+ delete this._nodes[key];
+ removed++;
+ continue;
+ }
+ decayed++;
+ }
+
+ this._edges = this._edges.filter(e => {
+ const ageDays = (now - new Date(e.last_used).getTime()) / 86400000;
+ e.decay_score = Math.max(0, 1.0 - (ageDays * effectiveRate));
+ return !(e.decay_score < 0.1 && (e.use_count || 1) < this._decayMinUses);
+ });
+
+ if (removed > 0) {
+ this._debounceSave();
+ console.log('[MemoryGraph] Decay: ' + decayed + ' scored, ' + removed + ' auto-removed');
+ }
+
+ // v4.5.90: Check for stale skills
+ var staleSkills = this.getStaleSkills();
+ if (staleSkills.length > 0) {
+ this.terminal.print('🧠 Stale skills: ' + staleSkills.length + ' (' + staleSkills.map(function(s) { return s.name; }).join(', ') + ')', 'warning');
+ }
+ }
+
+ // v4.5.90: Record skill usage for decay tracking
+ recordSkillUse(name, success) {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ if (!index[name]) index[name] = { useCount: 0, successCount: 0 };
+ index[name].lastUsed = new Date().toISOString();
+ index[name].useCount = (index[name].useCount || 0) + 1;
+ if (success !== false) {
+ index[name].successCount = (index[name].successCount || 0) + 1;
+ }
+ fs.fsWriteJSON('/skills/index.json', index);
+ } catch (e) {
+ console.warn('[MemoryGraph] Skill use tracking error:', e.message);
+ }
+ }
+
+ // v4.5.90: Get stale skills (unused >_decayDays, useCount == 0, not immune)
+ getStaleSkills() {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return [];
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ const now = Date.now();
+ // v4.5.90b fix: Use configurable _decayDays (P1 from review)
+ const staleThreshold = this._decayDays * 24 * 60 * 60 * 1000;
+ var stale = [];
+ for (var name of Object.keys(index)) {
+ var stats = index[name] || {};
+ if (stats.immune) continue;
+ var useCount = stats.useCount || 0;
+ var lastUsed = stats.lastUsed ? new Date(stats.lastUsed).getTime() : 0;
+ if (useCount === 0 && (now - lastUsed > staleThreshold || lastUsed === 0)) {
+ var daysUnused = lastUsed > 0 ? Math.floor((now - lastUsed) / 86400000) : 999;
+ stale.push({ name: name, daysUnused: daysUnused });
+ }
+ }
+ return stale;
+ } catch (e) {
+ return [];
+ }
+ }
+
+ // v4.5.89: Configure decay policy
+ decayConfig(args) {
+ if (args.length < 2) {
+ this.terminal.print('Usage: memgraph-decay-config <days> <min_uses>', 'error');
+ this.terminal.print(' Current: ' + this._decayDays + 'd threshold, ' + this._decayMinUses + ' min uses', 'info');
+ this.terminal.print(' Decay rate: ' + this._decayRate + '/day (0.01 = 1% per day)', 'info');
+ return;
+ }
+ const days = parseInt(args[0]);
+ const minUses = parseInt(args[1]);
+ if (isNaN(days) || days < 1) return this.terminal.print('Days must be >= 1', 'error');
+ if (isNaN(minUses) || minUses < 0) return this.terminal.print('Min uses must be >= 0', 'error');
+ this._decayDays = days;
+ this._decayMinUses = minUses;
+ this._debounceSave();
+ this.terminal.print('✅ Decay config: ' + days + 'd threshold, ' + minUses + ' min uses', 'success');
+ }
+
+ learn(args) {
+ if (!args || args.length < 2) return this.terminal.print('Usage: memgraph-learn <entity> <fact>', 'error');
+ const entity = args[0];
+ const fact = args.slice(1).join(' ');
+ this.addNode('concept', entity, entity);
+ const factKey = this.addNode('fact', entity + '-' + Date.now(), fact);
+ const conceptKey = `concept:${entity}`;
+ this.addEdge(conceptKey, factKey, 'has_fact', 0.9);
+ this.terminal.print(`✅ Learned: ${entity} → ${fact}`, 'success');
+ // v4.5.89b fix: Index only the new fact, not all facts (P2 from review — O(n) → O(1))
+ const nvfs = this.terminal.moduleManager.modules['NeuralVFSModule'];
+ if (nvfs && typeof nvfs.indexGraphFacts === 'function') {
+ nvfs.indexSingleFact(factKey, entity, fact);
+ }
+ }
+
+ // v4.5.88: Show graph nodes for a specific session
+ showSession(dateStr) {
+ let sessionKey;
+ if (dateStr) {
+ const d = new Date(dateStr);
+ if (isNaN(d.getTime())) return this.terminal.print('Usage: memgraph-session [YYYY-MM-DD]', 'error');
+ sessionKey = `session-${Math.floor(d.getTime() / 86400000)}`;
+ } else {
+ sessionKey = `session-${Math.floor(Date.now() / 86400000)}`;
+ }
+
+ const sessionNodes = Object.values(this._nodes).filter(function(n) {
+ return n.source_session === sessionKey || n.type === 'session' && n.id.includes(sessionKey);
+ });
+
+ this.terminal.print('📅 Session Graph: ' + sessionKey, 'header');
+ if (sessionNodes.length === 0) {
+ this.terminal.print(' No nodes found for this session', 'info');
+ return;
+ }
+ this.terminal.print(' ' + sessionNodes.length + ' nodes:', 'info');
+ for (var n of sessionNodes.slice(0, 15)) {
+ var ns = n.namespace || 'global';
+ this.terminal.print(' ' + n.type + ':' + n.label + ' (' + ns + ', ' + n.access_count + 'x)', 'info');
+ }
+ var sessionEdges = this._edges.filter(function(e) {
+ return sessionNodes.some(function(n) { return n.id === e.source || n.id === e.target; });
+ });
+ this.terminal.print(' ' + sessionEdges.length + ' edges', 'info');
+ }
+
+ // v4.5.88: Show namespaced nodes for a specific agent
+ showAgent(agentId) {
+ if (!agentId) return this.terminal.print('Usage: memgraph-agent <agentId>', 'error');
+ var ns = 'agent:' + agentId;
+ var agentNodes = Object.values(this._nodes).filter(function(n) {
+ return n.namespace === ns;
+ });
+ var agentEdges = this._edges.filter(function(e) {
+ return (e.source_agents || []).includes(agentId);
+ });
+
+ this.terminal.print('🤖 Agent Memory: ' + agentId, 'header');
+ if (agentNodes.length === 0) {
+ this.terminal.print(' No namespaced nodes for this agent', 'info');
+ this.terminal.print(' (Agent may have used global namespace)', 'text-muted');
+ return;
+ }
+ this.terminal.print(' ' + agentNodes.length + ' nodes, ' + agentEdges.length + ' edges:', 'info');
+ for (var n of agentNodes.slice(0, 15)) {
+ this.terminal.print(' ' + n.type + ':' + n.label + ' (' + n.access_count + 'x)', 'info');
+ }
+ }
+
// ─── Persistence ───
_loadFromVFS() {
@@ -15259,18 +16860,48 @@ class MemoryGraphModule {
const exportData = fs.fsReadJSON(`${this._indexPath}export.json`);
if (exportData && exportData.nodes) {
for (const n of exportData.nodes) {
+ // v4.5.89 fix: Migrate old-format nodes (P1-1 from review)
+ if (!n.namespace) n.namespace = 'global';
+ if (!n.source_session) {
+ const createdMs = new Date(n.created).getTime();
+ n.source_session = isNaN(createdMs) ? null : `session-${Math.floor(createdMs / 86400000)}`;
+ }
this._nodes[n.id] = n;
}
this._edges = exportData.edges || [];
+ for (const e of this._edges) {
+ if (!e.source_agents) e.source_agents = [];
+ }
this._totalNodeAccesses = (exportData.stats?.totalAccesses) || 0;
return;
}
// Fallback: load nodes and edges separately
const nodes = fs.fsReadJSON(`${this._indexPath}nodes.json`);
- if (nodes) this._nodes = nodes;
+ if (nodes) {
+ this._nodes = nodes;
+ for (const n of Object.values(this._nodes)) {
+ if (!n.namespace) n.namespace = 'global';
+ if (!n.source_session) {
+ const createdMs = new Date(n.created).getTime();
+ n.source_session = isNaN(createdMs) ? null : `session-${Math.floor(createdMs / 86400000)}`;
+ }
+ }
+ }
const edges = fs.fsReadJSON(`${this._indexPath}edges.json`);
- if (edges) this._edges = edges;
+ if (edges) {
+ this._edges = edges;
+ for (const e of this._edges) {
+ if (!e.source_agents) e.source_agents = [];
+ }
+ }
+ // v4.5.89b fix: Load persisted decay config (P1 from review)
+ const config = fs.fsReadJSON(`${this._indexPath}config.json`);
+ if (config) {
+ if (typeof config.decayDays === 'number') this._decayDays = config.decayDays;
+ if (typeof config.decayMinUses === 'number') this._decayMinUses = config.decayMinUses;
+ if (typeof config.lastDecayRun === 'number') this._lastDecayRun = config.lastDecayRun;
+ }
} catch (e) {
console.warn('[MemoryGraph] Load error (fresh start):', e.message);
}
@@ -15285,6 +16916,12 @@ class MemoryGraphModule {
}
fs.fsWriteJSON(`${this._indexPath}nodes.json`, this._nodes);
fs.fsWriteJSON(`${this._indexPath}edges.json`, this._edges);
+ // v4.5.89b fix: Persist decay config (P1 from review)
+ fs.fsWriteJSON(`${this._indexPath}config.json`, {
+ decayDays: this._decayDays,
+ decayMinUses: this._decayMinUses,
+ lastDecayRun: this._lastDecayRun
+ });
} catch (e) {
console.error('[MemoryGraph] Save error:', e.message);
}
@@ -15595,9 +17232,14 @@ class WorldModelModule {
this.terminal = null;
this._model = null;
this._lastRefresh = null;
- this._REFRESH_INTERVAL = 300000; // 5 minutes
+ this._REFRESH_INTERVAL = 300000; // 5 minutes (fallback)
this._MAX_PROJECTS = 10;
this._refreshTimer = null;
+ // v4.5.89: Event-driven sync state
+ this._syncDebounce = null;
+ this._syncing = false;
+ this._lastEventSync = 0;
+ this._EVENT_DEBOUNCE_MS = 500;
}
getHelpInfo() {
@@ -15639,6 +17281,25 @@ class WorldModelModule {
this.terminal.print('🌐 World Model loaded — system self-awareness active', 'info');
}
+ // v4.5.89: Event-driven sync — debounced, recursion-guarded
+ _scheduleSync() {
+ if (this._syncing) return; // Prevent recursion during active synthesis
+ if (this._syncDebounce) clearTimeout(this._syncDebounce);
+ const now = Date.now();
+ // Skip if we synced < 2s ago (rate limit beyond debounce)
+ if (now - this._lastEventSync < 2000) return;
+ this._syncDebounce = setTimeout(() => {
+ this._syncDebounce = null;
+ this._syncing = true;
+ try {
+ this._synthesize();
+ this._lastEventSync = Date.now();
+ } finally {
+ this._syncing = false;
+ }
+ }, this._EVENT_DEBOUNCE_MS);
+ }
+
// ─── Synthesis Engine ───
_synthesize() {
@@ -15707,9 +17368,10 @@ class WorldModelModule {
try {
const mm = this.terminal?.moduleManager;
if (!mm) return 0;
- // Count registry entries (all known modules) vs loaded
- const registry = mm.moduleRegistry || [];
- return registry.length || Object.keys(mm.modules || {}).length;
+ // Use max of registry vs loaded so the ratio is never >1 (e.g. 43/7 → 43/43)
+ const loadedCount = Object.keys(mm.modules || {}).length;
+ const registryCount = (mm.moduleRegistry || []).length;
+ return Math.max(loadedCount, registryCount);
} catch (e) {
console.warn('[WorldModel] Total modules gather error:', e.message);
return Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0;
@@ -15717,10 +17379,10 @@ class WorldModelModule {
}
_gatherSystemInfo(fs, moduleCount, version) {
- // Get real health data from ServerMonitorModule's _latencyLog and AI assistant's _errorLog
+ // Get real health data from ServerMonitorModule's _latencyLog and DebugSupervisorModule
const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule'];
- const ai = this.terminal?.moduleManager?.modules?.['WebX11AIAssistantModule'];
- const errorCount = ai?._errorLog?.length || 0;
+ const ds = this.terminal?.moduleManager?.modules?.['DebugSupervisorModule'];
+ 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;
return {
@@ -15816,11 +17478,11 @@ class WorldModelModule {
_gatherTelemetry(fs) {
try {
- // Get real latency data from ServerMonitorModule and errors from AI assistant
+ // Get real latency data from ServerMonitorModule and errors from DebugSupervisorModule
const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule'];
- const ai = this.terminal?.moduleManager?.modules?.['WebX11AIAssistantModule'];
+ const ds = this.terminal?.moduleManager?.modules?.['DebugSupervisorModule'];
const latencyDataPoints = sm?._latencyLog ? Object.keys(sm._latencyLog).length : 0;
- const recentErrors = ai?._errorLog?.length || 0;
+ const recentErrors = ds ? Object.keys(ds._errorDB?.errors || {}).length : 0;
return {
toolLatencyDataPoints: latencyDataPoints,
recentErrors: recentErrors,
@@ -15940,41 +17602,44 @@ class WorldModelModule {
if (!question) return this.terminal.print('Usage: world-model-ask <question>', 'error');
if (!this._model) return this.terminal.print('No model — run world-model-refresh first', 'warning');
- const q = question.toLowerCase();
+ const answer = this._answerQuery(question);
+ this.terminal.print(`❓ ${question}`, 'info');
+ this.terminal.print(`💡 ${answer}`, 'success');
+ }
+
+ // Shared query routing — used by both ask() (terminal) and world_model_query (AI tool)
+ _answerQuery(question) {
+ if (!this._model) return 'World model not synthesized yet — run world-model-refresh';
+ const q = (question || '').toLowerCase();
const m = this._model;
- let answer = '';
if (q.includes('module') || q.includes('loaded')) {
- answer = `Loaded ${m.modules.loaded} modules (${m.modules.modifiedCount} modified: ${m.modules.modified.join(', ') || 'none'})`;
+ return `Loaded ${m.modules.loaded} modules (${m.modules.modifiedCount} modified: ${m.modules.modified.join(', ') || 'none'})`;
} else if (q.includes('skill')) {
- answer = `Installed ${m.skills.count} skills: ${m.skills.skills.join(', ') || 'none'}`;
+ return `Installed ${m.skills.count} skills: ${m.skills.skills.join(', ') || 'none'}`;
} else if (q.includes('file') || q.includes('vfs')) {
- answer = `${m.files.total} files in ${m.files.directories} directories, total ${m.files.size}`;
+ return `${m.files.total} files in ${m.files.directories} directories, total ${m.files.size}`;
} else if (q.includes('agent') || q.includes('orchestrat')) {
- answer = `${m.agents.agents} agents (${m.agents.active} active)`;
+ return `${m.agents.agents} agents (${m.agents.active} active)`;
} else if (q.includes('health') || q.includes('status')) {
- answer = `Health: ${m.system.health} · Memory: ${m.system.memory} · ${m.telemetry.recentErrors > 0 ? `${m.telemetry.recentErrors} recent errors` : 'No errors'}`;
+ return `Health: ${m.system.health} · Memory: ${m.system.memory} · ${m.telemetry.recentErrors > 0 ? `${m.telemetry.recentErrors} recent errors` : 'No errors'}`;
} else if (q.includes('memory') || q.includes('graph')) {
- answer = `Memory Graph: ${m.memory.nodes} nodes, ${m.memory.edges} edges (${m.memory.active ? 'active' : 'inactive'})`;
+ return `Memory Graph: ${m.memory.nodes} nodes, ${m.memory.edges} edges (${m.memory.active ? 'active' : 'inactive'})`;
} else if (q.includes('change') || q.includes('yesterday') || q.includes('recent')) {
const recent = m.recent;
- answer = recent.length > 0 ? `Recent changes: ${recent.join('; ')}` : 'No recent changes detected';
+ return recent.length > 0 ? `Recent changes: ${recent.join('; ')}` : 'No recent changes detected';
} else if (q.includes('project') || q.includes('work')) {
const projects = m.projects.activeProjects;
- answer = projects.length > 0
+ return projects.length > 0
? `Active projects: ${projects.map(p => `${p.name} (${p.accesses}x)`).join(', ')}`
: 'No active projects detected';
} else if (q.includes('telemetry') || q.includes('latency')) {
- answer = `${m.telemetry.toolLatencyDataPoints} tool latency data points tracked`;
+ return `${m.telemetry.toolLatencyDataPoints} tool latency data points tracked, ${m.telemetry.recentErrors} recent errors`;
} else if (q.includes('summary') || q.includes('overview') || q.includes('all')) {
- answer = this.showSummary();
- return;
+ return `newOS4 ${m.version} · ${m.modules.loaded}/${m.modules.total} modules · ${m.skills.count} skills · ${m.files.total} files · ${m.agents.agents} agents · ${m.memory.nodes} graph nodes · ${m.telemetry.recentErrors > 0 ? `⚠️ ${m.telemetry.recentErrors} errors` : '✅ OK'}`;
} else {
- answer = `I'm not sure how to answer "${question}". Try: modules, skills, files, agents, health, memory, changes, projects, telemetry, or summary.`;
+ return `Unknown query. Try: modules, skills, files, agents, health, memory, changes, projects, telemetry, or summary.`;
}
-
- this.terminal.print(`❓ ${question}`, 'info');
- this.terminal.print(`💡 ${answer}`, 'success');
}
refresh() {
@@ -16017,7 +17682,7 @@ class WorldModelModule {
const agentStr = m.agents.error
? 'agents: N/A'
: `${m.agents.agents} agents`;
- return `[World Model Context]\\nSystem: newOS4 ${m.version} · ${m.modules.loaded} modules · ${m.skills.count} skills · ${m.files.total} files · ${agentStr}${m.telemetry.recentErrors > 0 ? ` · ⚠️ ${m.telemetry.recentErrors} errors` : ' · ✅ OK'}${m.modules.modifiedCount > 0 ? `\\nModified modules: ${m.modules.modified.slice(0, 3).join(', ')}${m.modules.modifiedCount > 3 ? ` (+${m.modules.modifiedCount - 3} more)` : ''}` : ''}`;
+ return `[World Model Context]\nSystem: newOS4 ${m.version} · ${m.modules.loaded} modules · ${m.skills.count} skills · ${m.files.total} files · ${agentStr}${m.telemetry.recentErrors > 0 ? ` · ⚠️ ${m.telemetry.recentErrors} errors` : ' · ✅ OK'}${m.modules.modifiedCount > 0 ? `\nModified modules: ${m.modules.modified.slice(0, 3).join(', ')}${m.modules.modifiedCount > 3 ? ` (+${m.modules.modifiedCount - 3} more)` : ''}` : ''}`;
}
// ─── Persistence ───
@@ -16036,10 +17701,20 @@ class WorldModelModule {
}
}
+ // v4.5.89: Atomic write — temp file + rename to prevent corruption
_saveToVFS(fs) {
try {
if (!fs.readDir('/system')) fs.mkdir('system');
- fs.fsWriteJSON('/system/world-model.json', this._model);
+ // v4.5.89b fix: Clean stale temp from previous crash (P1 from review)
+ try { fs.deleteFile('/system/world-model.tmp.json'); } catch (_) {}
+ fs.fsWriteJSON('/system/world-model.tmp.json', this._model);
+ const tmp = fs.fsReadJSON('/system/world-model.tmp.json');
+ if (tmp) {
+ fs.fsWriteJSON('/system/world-model.json', tmp);
+ fs.deleteFile('/system/world-model.tmp.json');
+ } else {
+ console.error('[WorldModel] Save failed: temp file corrupt after write');
+ }
} catch (e) {
console.error('[WorldModel] Save error:', e.message);
}
@@ -16087,7 +17762,7 @@ class NeuralVFSModule {
this._loadAliases();
// Deep Indexing toggle
this._deepIndexEnabled = localStorage.getItem('webx11_nvfs_deep') === 'true';
- this._deepModel = localStorage.getItem('webx11_nvfs_deep_model') || 'ministral-3:3b-cloud';
+ this._deepModel = localStorage.getItem('webx11_nvfs_deep_model') || 'gemma4:31b-cloud';
this._deepIndexing = false;
this._deepQueue = [];
this._deepProcessed = 0;
@@ -16134,6 +17809,7 @@ class NeuralVFSModule {
this.commands['nvfs-sync'] = async (args) => this.nvfsSync(args);
this.commands['nvfs-save'] = async (args) => this.nvfsSave();
this.commands['nvfs-cancel'] = (args) => this.cancelIndex();
+ this.commands['nvfs-index-graph'] = () => this.indexGraphFacts();
}
// ── Core Index Builder (async, chunked) ──
@@ -16306,12 +17982,6 @@ class NeuralVFSModule {
resolve(args) {
if (!args[0]) return this.terminal.print('Usage: nvfs-resolve <concept>', 'warning');
- // Auto-sync dirty paths before resolving
- if (this._dirtyPaths.size > 0) {
- this.terminal.print('🔄 Auto-syncing ' + this._dirtyPaths.size + ' dirty file(s) before resolve...', 'info');
- this._reindexDirtyPaths();
- }
-
const concept = args.join(' ').toLowerCase();
// Check aliases first
@@ -17104,6 +18774,61 @@ Concepts:`;
}
}
}
+
+ // v4.5.89: Semantic bridge — mirror graph facts into VFS for unified search
+ indexGraphFacts() {
+ const mg = this.terminal.moduleManager.modules['MemoryGraphModule'];
+ if (!mg) return this.terminal.print('❌ Memory Graph module not loaded', 'error');
+ const fs = this.terminal.fs;
+ if (!fs) return this.terminal.print('❌ VFS not available', 'error');
+
+ const factNodes = Object.values(mg._nodes).filter(function(n) {
+ return n.type === 'concept' || n.type === 'fact' || n.type === 'entity' || n.type === 'task';
+ });
+
+ if (factNodes.length === 0) {
+ return this.terminal.print('No graph fact nodes to index. Use memgraph-learn first.', 'info');
+ }
+
+ if (!fs.readDir('/_graph_facts')) fs.mkdir('_graph_facts');
+
+ // v4.5.89b fix: Clean stale files for deleted nodes (P2 from review)
+ const validKeys = new Set(factNodes.map(function(n) { return n.id.replace(/[^a-zA-Z0-9_-]/g, '_') + '.txt'; }));
+ try {
+ const existing = fs.readDir('/_graph_facts') || [];
+ for (const f of existing) {
+ if (!validKeys.has(f)) fs.deleteFile('/_graph_facts/' + f);
+ }
+ } catch (_) {}
+
+ let indexed = 0;
+ for (const node of factNodes) {
+ let content = node.label + '\n';
+ if (node.metadata) {
+ if (node.metadata.full) content += node.metadata.full + '\n';
+ if (node.metadata.result) content += 'Result: ' + node.metadata.result + '\n';
+ }
+ content += 'Type: ' + node.type + '\n';
+ content += 'Namespace: ' + (node.namespace || 'global') + '\n';
+ if (node.source_session) content += 'Session: ' + node.source_session + '\n';
+ const safeName = node.id.replace(/[^a-zA-Z0-9_-]/g, '_');
+ fs.writeFile('/_graph_facts/' + safeName + '.txt', content);
+ indexed++;
+ }
+
+ this.terminal.print('🔗 Indexed ' + indexed + ' graph facts into /_graph_facts/', 'success');
+ this.terminal.print(' Run nvfs-index to include them in semantic search', 'info');
+ }
+
+ // v4.5.89b: Index a single fact — O(1) instead of O(n) re-index (P2 from review)
+ indexSingleFact(factKey, entity, fact) {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+ if (!fs.readDir('/_graph_facts')) fs.mkdir('_graph_facts');
+ const safeName = factKey.replace(/[^a-zA-Z0-9_-]/g, '_');
+ const content = entity + '\n' + fact + '\nType: fact\nNamespace: global\n';
+ fs.writeFile('/_graph_facts/' + safeName + '.txt', content);
+ }
}
// Required for module system
@@ -17124,6 +18849,7 @@ if (typeof window !== 'undefined') {
'contexts': 'contexts — List all available context profiles',
'context-new': 'context-new <name> <displayName> <systemPrompt> — Create a new context profile',
'context-delete': 'context-delete <name> — Delete a context profile',
+ 'context-auto': 'context-auto [on|off] — Toggle auto-routing (detects intent from user input)',
'smart-ask': 'smart-ask <question> — Ask a question using the active context'
};
}
@@ -17169,6 +18895,7 @@ if (typeof window !== 'undefined') {
this.commands['contexts'] = () => this.listContexts();
this.commands['context-new'] = (args) => this.createContext(args);
this.commands['context-delete'] = (args) => this.deleteContext(args);
+ this.commands['context-auto'] = (args) => this.toggleAutoRouting(args);
this.commands['smart-ask'] = (args) => this.smartAsk(args);
}
setContext(args) {
@@ -17238,6 +18965,116 @@ if (typeof window !== 'undefined') {
this.terminal.print(`Profile '${args[0]}' not found`, "error");
}
}
+ // ─── Intent Detection Engine ───
+ _getIntentPatterns() {
+ return {
+ 'coding': [
+ /^code\b/i, /\b(edit|fix|write|create|implement|refactor|build)\s+(module|function|class|file|code|script)/i,
+ /\b(debug|bug|error|fix)\s+(code|module|function|script)/i, /\b(implement|write|create)\s+(a\s+)?(function|class|module|command)/i,
+ /\b(refactor|rewrite|optimize)\s+(code|module|function)/i, /\b(add\s+)(feature|command|functionality)/i
+ ],
+ 'debug': [
+ /^debug\b/i, /\b(error|crash|bug|fail|traceback|exception|broken|not working)\b/i,
+ /\b(why|what happened|diagnose|investigate|find out why)\b/i, /\b(check|inspect|examine)\s+(log|error|issue|problem)/i
+ ],
+ 'research': [
+ /^research\b/i, /\b(search|find|look up|research|investigate|analyze)\b/i,
+ /\b(what is|how does|tell me about|explain)\b.*\?$/i, /\b(find|search)\s+(for\s+)?(information|docs|documentation|article)/i
+ ],
+ 'creative': [
+ /^brainstorm\b/i, /\b(create|design|imagine|generate|compose|draft)\s+(story|poem|art|design|idea|concept|mindmap)/i,
+ /\b(creative|inspire|innovative|novel)\b/i
+ ],
+ 'system-admin': [
+ /^(config|setup|install|deploy|backup|restore|security|monitor|maintenance)\b/i,
+ /\b(configure|setup|install|deploy|backup|restore)\s+(system|server|module|service)/i,
+ /\b(security|permission|access|block|allow)\b/i
+ ],
+ 'chat': [
+ /^(hi|hello|hey|how are you|what's up|good morning|good evening)\b/i,
+ /^.{0,30}\?$/ // short questions
+ ]
+ };
+ }
+ autoDetectIntent(text) {
+ if (!text || typeof text !== 'string') return { profile: null, confidence: 0 };
+ const patterns = this._getIntentPatterns();
+ const scores = {};
+ const lower = text.toLowerCase();
+
+ for (const [profile, regexes] of Object.entries(patterns)) {
+ let matches = 0;
+ for (const re of regexes) {
+ if (re.test(text) || re.test(lower)) matches++;
+ }
+ if (matches > 0) scores[profile] = matches;
+ }
+
+ // Code snippet detection (bias toward coding)
+ if (/[{([\s]*['"`]/.test(text) || /\b(function|class|const|let|var|import|export|=>)\b/.test(text)) {
+ scores['coding'] = (scores['coding'] || 0) + 2;
+ }
+
+ // Very short text → chat
+ if (text.length < 15 && !scores['coding'] && !scores['debug']) {
+ scores['chat'] = (scores['chat'] || 0) + 1;
+ }
+
+ // Find best match
+ let bestProfile = null;
+ let bestScore = 0;
+ let totalScore = 0;
+ for (const [profile, score] of Object.entries(scores)) {
+ totalScore += score;
+ if (score > bestScore) { bestScore = score; bestProfile = profile; }
+ }
+
+ // Calculate confidence (0-100)
+ const confidence = totalScore > 0 ? Math.round((bestScore / totalScore) * 100) : 0;
+
+ return { profile: bestProfile, confidence };
+ }
+ autoRouteContext(text) {
+ if (!text) return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: 0, reason: 'no input' };
+
+ // Check if auto-routing is enabled
+ const enabled = localStorage.getItem('webx11_context_auto') !== 'off';
+ if (!enabled) return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: 0, reason: 'auto-routing disabled' };
+
+ const result = this.autoDetectIntent(text);
+ if (!result.profile || result.confidence < 70) {
+ return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: result.confidence, reason: 'confidence too low (<70%)' };
+ }
+
+ // Don't switch if already on the best profile
+ if (result.profile === this.activeProfile) {
+ return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: result.confidence, reason: 'already on best profile' };
+ }
+
+ // Check if the target profile exists
+ if (!this.contextProfiles[result.profile]) {
+ return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: result.confidence, reason: 'profile "' + result.profile + '" not found' };
+ }
+
+ // Switch
+ const from = this.activeProfile;
+ this.setContext([result.profile]);
+ return { switched: true, from: from, to: result.profile, confidence: result.confidence, reason: 'intent detected' };
+ }
+ toggleAutoRouting(args) {
+ const val = args && args[0] ? args[0].toLowerCase() : null;
+ if (val === 'on' || val === 'true' || val === '1') {
+ localStorage.setItem('webx11_context_auto', 'on');
+ this.terminal.print('✅ Auto-routing enabled — context will switch based on intent', 'success');
+ } else if (val === 'off' || val === 'false' || val === '0') {
+ localStorage.setItem('webx11_context_auto', 'off');
+ this.terminal.print('⛔ Auto-routing disabled — use context <name> to switch manually', 'warning');
+ } else {
+ const current = localStorage.getItem('webx11_context_auto') !== 'off';
+ this.terminal.print('Auto-routing: ' + (current ? 'ON' : 'OFF'), 'info');
+ this.terminal.print('Usage: context-auto on|off', 'info');
+ }
+ }
async smartAsk(args) {
if (!args[0]) {
this.terminal.print("Usage: smart-ask <your-question>", "warning"); return;
@@ -17285,4 +19122,707 @@ if (typeof window !== 'undefined') {
}
}
+/* === module-debug-supervisor.js === */
+// module-debug-supervisor.js — Persistent error capture, clustering, root-cause analysis, and auto-fix
+// Commands: debug-stats, debug-history, debug-analyze, debug-trends, debug-fix, debug-health
+class DebugSupervisorModule {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ this._healthy = true;
+ this._errorDB = { errors: {}, clusters: {}, patterns: {}, sessionId: null };
+ this._maxErrors = 500;
+ this._ttlDays = 7;
+ this._writeDebounce = null;
+ this._pendingWrites = 0;
+ this._originalConsole = {};
+ this._originalOnError = null;
+ this._originalOnRejection = null;
+ this._originalFetch = null;
+ this._interceptorInstalled = false;
+ this._sessionId = this._generateSessionId();
+ }
+
+ _generateSessionId() {
+ try {
+ let sid = localStorage.getItem('webx11_debug_session');
+ if (!sid) {
+ sid = Date.now().toString(36) + '-' + Math.random().toString(36).substring(2, 8);
+ localStorage.setItem('webx11_debug_session', sid);
+ }
+ return sid;
+ } catch (e) {
+ return Date.now().toString(36);
+ }
+ }
+
+ getHelpInfo() {
+ return {
+ 'debug-stats': 'debug-stats — Show error summary (total, unique, clusters, top issues)',
+ 'debug-history': 'debug-history [n] — Show last N errors (default: 20)',
+ 'debug-analyze': 'debug-analyze <hash> — Deep analysis of a specific error',
+ 'debug-trends': 'debug-trends [days] — Error frequency over time (default: 7)',
+ 'debug-fix': 'debug-fix <hash> — Suggest and optionally apply fix for an error',
+ 'debug-health': 'debug-health — Check supervisor module health status',
+ };
+ }
+
+ registerCommands() {
+ this.commands['debug-stats'] = () => this._cmdStats();
+ this.commands['debug-history'] = (args) => this._cmdHistory(args);
+ this.commands['debug-analyze'] = (args) => this._cmdAnalyze(args);
+ this.commands['debug-trends'] = (args) => this._cmdTrends(args);
+ this.commands['debug-fix'] = (args) => this._cmdFix(args);
+ this.commands['debug-health'] = () => this._cmdHealth();
+ }
+
+ seed() {
+ // seed() is not called automatically by module manager — interceptors go in init()
+ }
+
+ init() {
+ this._loadDB();
+ this._installInterceptors();
+ }
+
+ // ── Error Database ──
+
+ _loadDB() {
+ try {
+ const raw = localStorage.getItem('webx11_debug_db');
+ if (raw) {
+ const parsed = JSON.parse(raw);
+ this._errorDB = parsed;
+ this._pruneExpired();
+ }
+ } catch (e) {
+ // Corrupted DB — start fresh
+ this._errorDB = { errors: {}, clusters: {}, patterns: {}, sessionId: this._sessionId };
+ }
+ }
+
+ _saveDB() {
+ try {
+ localStorage.setItem('webx11_debug_db', JSON.stringify(this._errorDB));
+ } catch (e) {
+ // Quota exceeded — evict oldest 20% and retry
+ this._evictLRU(0.2);
+ try {
+ localStorage.setItem('webx11_debug_db', JSON.stringify(this._errorDB));
+ } catch (e2) {
+ // Still failing — supervisor degrades gracefully
+ console.warn('[DebugSupervisor] DB write failed, errors may be lost');
+ }
+ }
+ }
+
+ _scheduleSave() {
+ this._pendingWrites++;
+ if (this._writeDebounce) return;
+ this._writeDebounce = setTimeout(() => {
+ this._writeDebounce = null;
+ if (this._pendingWrites > 0) {
+ this._saveDB();
+ this._pendingWrites = 0;
+ }
+ }, 2000);
+ }
+
+ _pruneExpired() {
+ const now = Date.now();
+ const maxAge = this._ttlDays * 86400000;
+ let changed = false;
+ for (const [hash, entry] of Object.entries(this._errorDB.errors)) {
+ if (now - entry.lastSeen > maxAge) {
+ delete this._errorDB.errors[hash];
+ changed = true;
+ }
+ }
+ if (changed) this._saveDB();
+ }
+
+ _evictLRU(fraction) {
+ const entries = Object.entries(this._errorDB.errors);
+ const sorted = entries.sort((a, b) => a[1].lastSeen - b[1].lastSeen);
+ const toRemove = Math.ceil(sorted.length * fraction);
+ for (let i = 0; i < toRemove; i++) {
+ delete this._errorDB.errors[sorted[i][0]];
+ }
+ }
+
+ _checkQuota() {
+ try {
+ const total = 0;
+ for (const key in localStorage) {
+ // Estimate: key + value length
+ }
+ // Simple check: try writing a test key
+ const testKey = '__quota_test__';
+ localStorage.setItem(testKey, '1');
+ localStorage.removeItem(testKey);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ // ── Error Classification ──
+
+ _classifyError(message, source, stack) {
+ const type = this._detectType(message, stack);
+ const severity = this._detectSeverity(type, message);
+ const category = this._detectCategory(message, source, type);
+ return { type, severity, category };
+ }
+
+ _detectType(message, stack) {
+ if (!message) return 'unknown';
+ if (message.includes('ReferenceError') || message.includes('is not defined')) return 'ReferenceError';
+ if (message.includes('TypeError') || message.includes('is not a function') || message.includes('cannot read property')) return 'TypeError';
+ if (message.includes('SyntaxError') || message.includes('Unexpected token')) return 'SyntaxError';
+ if (message.includes('HTTP') || message.includes('NetworkError') || message.includes('Failed to fetch')) return 'HTTP';
+ if (message.includes('timeout') || message.includes('Timeout') || message.includes('AbortError')) return 'timeout';
+ if (message.includes('QuotaExceededError') || message.includes('quota')) return 'quota';
+ if (message.includes('SecurityError') || message.includes('permission')) return 'security';
+ if (stack && stack.includes('tool_')) return 'tool';
+ return 'runtime';
+ }
+
+ _detectSeverity(type, message) {
+ if (type === 'SyntaxError' || type === 'ReferenceError' || type === 'quota') return 'fatal';
+ if (type === 'HTTP' && message.includes('5')) return 'error';
+ if (type === 'timeout' || type === 'security') return 'error';
+ if (type === 'tool') return 'warning';
+ return 'error';
+ }
+
+ _detectCategory(message, source, type) {
+ if (type === 'HTTP' || type === 'timeout') return 'network';
+ if (type === 'quota') return 'vfs';
+ if (type === 'security') return 'security';
+ if (source && (source.includes('module-') || source.includes('ext/'))) return 'module';
+ if (type === 'tool') return 'tool';
+ return 'runtime';
+ }
+
+ _extractModule(source) {
+ if (!source) return null;
+ const m = source.match(/(module-[a-z-]+)\.js/);
+ return m ? m[1] : null;
+ }
+
+ _computeHash(message, source, type) {
+ const raw = (message || '') + '|' + (source || '') + '|' + (type || '');
+ // Simple hash (Fowler-Noll-Vo)
+ let hash = 0x811c9dc5;
+ for (let i = 0; i < raw.length; i++) {
+ hash ^= raw.charCodeAt(i);
+ hash = Math.imul(hash, 0x01000193);
+ }
+ return (hash >>> 0).toString(16);
+ }
+
+ // ── Error Interception ──
+
+ _installInterceptors() {
+ if (this._interceptorInstalled) return;
+ const self = this;
+
+ // Console.error
+ this._originalConsole.error = console.error;
+ console.error = function() {
+ const args = Array.prototype.slice.call(arguments);
+ const msg = args.map(a => typeof a === 'object' ? (a.message || JSON.stringify(a)) : String(a)).join(' ');
+ self._recordError(msg, 'console.error', null);
+ return self._originalConsole.error.apply(console, args);
+ };
+
+ // window.onerror
+ this._originalOnError = window.onerror;
+ window.onerror = function(msg, src, line, col, err) {
+ const fullMsg = msg + (err ? ' — ' + err.message : '') + ' (' + src + ':' + line + ':' + col + ')';
+ self._recordError(fullMsg, src || 'window.onerror', err ? err.stack : null);
+ if (self._originalOnError) return self._originalOnError.call(window, msg, src, line, col, err);
+ };
+
+ // Unhandled rejections
+ this._originalOnRejection = window.onunhandledrejection;
+ window.addEventListener('unhandledrejection', function(e) {
+ const msg = e.reason ? (e.reason.message || String(e.reason)) : 'Unhandled Promise rejection';
+ self._recordError(msg, 'unhandledrejection', e.reason ? e.reason.stack : null);
+ });
+
+ // Fetch failures
+ this._originalFetch = window.fetch;
+ window.fetch = function() {
+ const args = arguments;
+ const url = typeof args[0] === 'string' ? args[0] : (args[0]?.url || '?');
+ return self._originalFetch.apply(window, args).catch(function(err) {
+ self._recordError('Fetch failed: ' + url + ' — ' + (err.message || err), url, err.stack);
+ throw err;
+ });
+ };
+
+ this._interceptorInstalled = true;
+ }
+
+ // ── Record Error ──
+
+ recordToolError(toolName, args, errorMessage) {
+ this._recordError(errorMessage, 'tool:' + toolName, null);
+ }
+
+ _recordError(message, source, stack) {
+ if (!this._healthy) return;
+ const classification = this._classifyError(message, source, stack);
+ const hash = this._computeHash(message, source, classification.type);
+ const now = Date.now();
+
+ if (this._errorDB.errors[hash]) {
+ this._errorDB.errors[hash].count++;
+ this._errorDB.errors[hash].lastSeen = now;
+ if (this._errorDB.errors[hash].sessions.indexOf(this._sessionId) === -1) {
+ this._errorDB.errors[hash].sessions.push(this._sessionId);
+ }
+ } else {
+ // LRU eviction if at cap
+ const keys = Object.keys(this._errorDB.errors);
+ if (keys.length >= this._maxErrors) {
+ this._evictLRU(0.1);
+ }
+ this._errorDB.errors[hash] = {
+ count: 1,
+ firstSeen: now,
+ lastSeen: now,
+ message: message,
+ type: classification.type,
+ severity: classification.severity,
+ category: classification.category,
+ source: source,
+ stack: stack ? stack.split('\n').slice(0, 10).join('\n') : null,
+ module: this._extractModule(source),
+ sessions: [this._sessionId],
+ };
+ }
+
+ this._scheduleSave();
+ }
+
+ // ── Clustering (Jaccard similarity) ──
+
+ _normalizeMessage(msg) {
+ if (!msg) return '';
+ return msg
+ .toLowerCase()
+ .replace(/\b0x[a-f0-9]+\b/g, '0xHEX')
+ .replace(/\b\d+\b/g, 'N')
+ .replace(/["'].*?["']/g, '"STR"')
+ .replace(/[^\w\s]/g, ' ')
+ .replace(/\s+/g, ' ')
+ .trim();
+ }
+
+ _jaccardSimilarity(a, b) {
+ const setA = new Set(a.split(' ').filter(Boolean));
+ const setB = new Set(b.split(' ').filter(Boolean));
+ if (setA.size === 0 && setB.size === 0) return 1;
+ let intersection = 0;
+ for (const token of setA) {
+ if (setB.has(token)) intersection++;
+ }
+ const union = setA.size + setB.size - intersection;
+ return union === 0 ? 0 : intersection / union;
+ }
+
+ _clusterErrors() {
+ const errors = Object.entries(this._errorDB.errors);
+ if (errors.length === 0) return [];
+
+ const clusters = [];
+ const assigned = new Set();
+
+ for (let i = 0; i < errors.length; i++) {
+ if (assigned.has(i)) continue;
+ const [hashI, entryI] = errors[i];
+ const normI = this._normalizeMessage(entryI.message);
+ const cluster = { hashes: [hashI], messages: [entryI.message], count: entryI.count, types: new Set([entryI.type]), categories: new Set([entryI.category]) };
+ assigned.add(i);
+
+ for (let j = i + 1; j < errors.length; j++) {
+ if (assigned.has(j)) continue;
+ const [hashJ, entryJ] = errors[j];
+ const normJ = this._normalizeMessage(entryJ.message);
+ if (this._jaccardSimilarity(normI, normJ) >= 0.35) {
+ cluster.hashes.push(hashJ);
+ cluster.messages.push(entryJ.message);
+ cluster.count += entryJ.count;
+ cluster.types.add(entryJ.type);
+ cluster.categories.add(entryJ.category);
+ assigned.add(j);
+ }
+ }
+
+ cluster.label = this._mostFrequent(cluster.messages);
+ cluster.typeCount = cluster.types.size;
+ cluster.categoryCount = cluster.categories.size;
+ delete cluster.types;
+ delete cluster.categories;
+ clusters.push(cluster);
+ }
+
+ clusters.sort((a, b) => b.count - a.count);
+ return clusters;
+ }
+
+ _mostFrequent(arr) {
+ const freq = {};
+ let best = arr[0], bestCount = 1;
+ for (const item of arr) {
+ freq[item] = (freq[item] || 0) + 1;
+ if (freq[item] > bestCount) {
+ best = item;
+ bestCount = freq[item];
+ }
+ }
+ return best;
+ }
+
+ // ── Root-Cause Analysis ──
+
+ _analyzeRootCause(hash) {
+ const entry = this._errorDB.errors[hash];
+ if (!entry) return { found: false };
+
+ const findings = [];
+ const fixes = [];
+
+ // Tool failure → check module
+ if (entry.source && entry.source.startsWith('tool:')) {
+ const toolName = entry.source.replace('tool:', '');
+ const mm = this.terminal?.moduleManager;
+ if (mm) {
+ // Check if any module has this command
+ let found = false;
+ for (const [modName, mod] of Object.entries(mm.modules)) {
+ if (mod.commands && mod.commands[toolName]) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ findings.push({ type: 'module_not_loaded', detail: 'No module provides command "' + toolName + '"' });
+ fixes.push({ action: 'loadmod', params: toolName, description: 'Load module providing ' + toolName });
+ }
+ }
+ }
+
+ // HTTP error → check network
+ if (entry.type === 'HTTP' || entry.type === 'timeout') {
+ findings.push({ type: 'network_issue', detail: 'Network request failed: ' + entry.message.substring(0, 100) });
+ fixes.push({ action: 'test-net', params: null, description: 'Run network diagnostics' });
+ }
+
+ // ReferenceError → check bundle
+ if (entry.type === 'ReferenceError') {
+ findings.push({ type: 'missing_class', detail: 'ReferenceError suggests a class or variable is not defined' });
+ fixes.push({ action: 'reload', params: null, description: 'Reload the page to re-initialize all modules' });
+ }
+
+ // Module error → check module loader
+ if (entry.category === 'module') {
+ findings.push({ type: 'module_error', detail: 'Error in module: ' + (entry.module || 'unknown') });
+ fixes.push({ action: 'loadmod', params: entry.module, description: 'Reload module: ' + entry.module });
+ }
+
+ return { found: findings.length > 0, findings, fixes };
+ }
+
+ // ── Fix Registry ──
+
+ _buildFixSuggestion(hash) {
+ const entry = this._errorDB.errors[hash];
+ if (!entry) return null;
+
+ const rca = this._analyzeRootCause(hash);
+ if (!rca.found) {
+ return { confidence: 0, suggestion: 'No known fix pattern for this error', autoApply: false };
+ }
+
+ // Calculate confidence based on frequency
+ let confidence = Math.min(0.5, entry.count / 10);
+ if (entry.count >= 3) confidence = 0.7;
+ if (entry.count >= 10) confidence = 0.9;
+
+ return {
+ confidence: confidence,
+ suggestion: rca.findings.map(f => f.detail).join('; '),
+ fixes: rca.fixes,
+ autoApply: confidence >= 0.9,
+ };
+ }
+
+ // ── Terminal Commands ──
+
+ _cmdStats() {
+ const db = this._errorDB.errors;
+ const keys = Object.keys(db);
+ if (keys.length === 0) {
+ this.terminal.print('📊 Debug Stats: No errors recorded', 'success');
+ return;
+ }
+
+ const totalCount = keys.reduce((sum, k) => sum + db[k].count, 0);
+ const bySeverity = {};
+ const byCategory = {};
+ for (const k of keys) {
+ const e = db[k];
+ bySeverity[e.severity] = (bySeverity[e.severity] || 0) + e.count;
+ byCategory[e.category] = (byCategory[e.category] || 0) + e.count;
+ }
+
+ const clusters = this._clusterErrors();
+ const topClusters = clusters.slice(0, 5);
+
+ this.terminal.print('📊 Debug Stats', 'header');
+ this.terminal.print(' Total errors: ' + totalCount, 'info');
+ this.terminal.print(' Unique errors: ' + keys.length, 'info');
+ this.terminal.print(' Clusters: ' + clusters.length, 'info');
+ this.terminal.print(' Session: ' + this._sessionId, 'text-muted');
+ this.terminal.print(' Supervisor: ' + (this._healthy ? '✅ healthy' : '🔴 degraded'), this._healthy ? 'success' : 'error');
+
+ this.terminal.print('── By Severity ──', 'info');
+ for (const [sev, count] of Object.entries(bySeverity)) {
+ const icon = sev === 'fatal' ? '🔴' : sev === 'error' ? '🟡' : '🟢';
+ this.terminal.print(' ' + icon + ' ' + sev + ': ' + count, sev === 'fatal' ? 'error' : 'warning');
+ }
+
+ this.terminal.print('── By Category ──', 'info');
+ for (const [cat, count] of Object.entries(byCategory)) {
+ this.terminal.print(' • ' + cat + ': ' + count, 'text-muted');
+ }
+
+ if (topClusters.length > 0) {
+ this.terminal.print('── Top Clusters ──', 'info');
+ for (const c of topClusters) {
+ this.terminal.print(' [' + c.hashes.length + ' errors ×' + c.count + '] ' + c.label.substring(0, 80), 'warning');
+ }
+ this.terminal.print(' Run debug-analyze <hash> for details', 'text-muted');
+ }
+ }
+
+ _cmdHistory(args) {
+ const n = (args && args[0]) ? parseInt(args[0]) : 20;
+ const entries = Object.entries(this._errorDB.errors)
+ .sort((a, b) => b[1].lastSeen - a[1].lastSeen)
+ .slice(0, n);
+
+ if (entries.length === 0) {
+ this.terminal.print('No errors recorded', 'success');
+ return;
+ }
+
+ this.terminal.print('── Error History (last ' + entries.length + ') ──', 'header');
+ for (const [hash, e] of entries) {
+ const time = new Date(e.lastSeen).toLocaleTimeString();
+ const icon = e.severity === 'fatal' ? '🔴' : e.severity === 'error' ? '🟡' : '🟢';
+ const msg = e.message.substring(0, 80);
+ this.terminal.print(' ' + icon + ' [' + time + '] ' + msg, e.severity === 'fatal' ? 'error' : 'text-muted');
+ this.terminal.print(' hash=' + hash + ' type=' + e.type + ' cat=' + e.category + ' ×' + e.count, 'text-muted');
+ }
+ this.terminal.print('── End ──', 'info');
+ this.terminal.print('Run debug-analyze <hash> for details', 'text-muted');
+ }
+
+ _cmdAnalyze(args) {
+ const hash = (args && args[0]) || '';
+ if (!hash) {
+ this.terminal.print('Usage: debug-analyze <hash>', 'warning');
+ return;
+ }
+
+ const entry = this._errorDB.errors[hash];
+ if (!entry) {
+ this.terminal.print('Error not found: ' + hash, 'error');
+ return;
+ }
+
+ this.terminal.print('── Error Analysis: ' + hash + ' ──', 'header');
+ this.terminal.print(' Message: ' + entry.message, 'info');
+ this.terminal.print(' Type: ' + entry.type + ' | Severity: ' + entry.severity + ' | Category: ' + entry.category, 'info');
+ this.terminal.print(' Source: ' + (entry.source || '?'), 'text-muted');
+ this.terminal.print(' Module: ' + (entry.module || 'unknown'), 'text-muted');
+ this.terminal.print(' Count: ' + entry.count + ' | First: ' + new Date(entry.firstSeen).toLocaleString() + ' | Last: ' + new Date(entry.lastSeen).toLocaleString(), 'info');
+ this.terminal.print(' Sessions: ' + (entry.sessions || []).length, 'text-muted');
+ if (entry.stack) {
+ this.terminal.print('── Stack Trace (top 10) ──', 'info');
+ this.terminal.print(entry.stack, 'text-muted');
+ }
+
+ // Root-cause analysis
+ const rca = this._analyzeRootCause(hash);
+ if (rca.found) {
+ this.terminal.print('── Root Cause Analysis ──', 'warning');
+ for (const f of rca.findings) {
+ this.terminal.print(' • [' + f.type + '] ' + f.detail, 'warning');
+ }
+ if (rca.fixes.length > 0) {
+ this.terminal.print('── Suggested Fixes ──', 'success');
+ for (const fix of rca.fixes) {
+ this.terminal.print(' → ' + fix.action + (fix.params ? ' ' + fix.params : '') + ' — ' + fix.description, 'success');
+ }
+ }
+ } else {
+ this.terminal.print(' No root cause identified', 'text-muted');
+ }
+
+ // Fix suggestion
+ const fix = this._buildFixSuggestion(hash);
+ if (fix && fix.confidence > 0) {
+ this.terminal.print('── Auto-Fix Confidence: ' + Math.round(fix.confidence * 100) + '% ──', fix.autoApply ? 'success' : 'warning');
+ this.terminal.print(' ' + fix.suggestion, 'info');
+ if (fix.autoApply) {
+ this.terminal.print(' ⚡ Confidence ≥ 90% — fix can be auto-applied with debug-fix ' + hash, 'success');
+ }
+ }
+ }
+
+ _cmdTrends(args) {
+ const days = (args && args[0]) ? parseInt(args[0]) : 7;
+ const cutoff = Date.now() - days * 86400000;
+
+ const entries = Object.values(this._errorDB.errors).filter(e => e.lastSeen >= cutoff);
+ if (entries.length === 0) {
+ this.terminal.print('No errors in the last ' + days + ' days', 'success');
+ return;
+ }
+
+ // Group by day
+ const byDay = {};
+ for (const e of entries) {
+ const day = new Date(e.lastSeen).toISOString().split('T')[0];
+ if (!byDay[day]) byDay[day] = { total: 0, byType: {} };
+ byDay[day].total += e.count;
+ byDay[day].byType[e.type] = (byDay[day].byType[e.type] || 0) + e.count;
+ }
+
+ const sortedDays = Object.keys(byDay).sort();
+ this.terminal.print('── Error Trends (last ' + days + ' days) ──', 'header');
+ for (const day of sortedDays) {
+ const d = byDay[day];
+ const types = Object.entries(d.byType).map(([t, c]) => t + ':' + c).join(', ');
+ this.terminal.print(' ' + day + ': ' + d.total + ' errors (' + types + ')', d.total > 5 ? 'warning' : 'text-muted');
+ }
+
+ // Top error types
+ const typeTotals = {};
+ for (const e of entries) {
+ typeTotals[e.type] = (typeTotals[e.type] || 0) + e.count;
+ }
+ this.terminal.print('── By Type ──', 'info');
+ for (const [type, count] of Object.entries(typeTotals).sort((a, b) => b[1] - a[1])) {
+ this.terminal.print(' • ' + type + ': ' + count, 'text-muted');
+ }
+ }
+
+ _cmdFix(args) {
+ const hash = (args && args[0]) || '';
+ if (!hash) {
+ this.terminal.print('Usage: debug-fix <hash>', 'warning');
+ return;
+ }
+
+ const fix = this._buildFixSuggestion(hash);
+ if (!fix || fix.confidence === 0) {
+ this.terminal.print('No fix available for error: ' + hash, 'warning');
+ return;
+ }
+
+ this.terminal.print('── Fix Suggestion for ' + hash + ' ──', 'header');
+ this.terminal.print(' Confidence: ' + Math.round(fix.confidence * 100) + '%', fix.autoApply ? 'success' : 'warning');
+ this.terminal.print(' Suggestion: ' + fix.suggestion, 'info');
+
+ if (fix.fixes && fix.fixes.length > 0) {
+ this.terminal.print('── Available Fixes ──', 'info');
+ for (const f of fix.fixes) {
+ this.terminal.print(' → ' + f.action + (f.params ? ' ' + f.params : ''), 'success');
+ this.terminal.print(' ' + f.description, 'text-muted');
+ }
+ }
+
+ if (fix.autoApply) {
+ this.terminal.print('⚡ Auto-applying fix...', 'info');
+ this._applyFix(hash, fix);
+ } else {
+ this.terminal.print('Confidence below auto-apply threshold. Run the suggested command manually.', 'warning');
+ }
+ }
+
+ _applyFix(hash, fix) {
+ if (!fix.fixes || fix.fixes.length === 0) return;
+ const firstFix = fix.fixes[0];
+ try {
+ if (firstFix.action === 'loadmod' && this.terminal) {
+ this.terminal.executeCommand('loadmod ' + (firstFix.params || ''));
+ this.terminal.print(' ✅ Applied: loadmod ' + (firstFix.params || ''), 'success');
+ } else if (firstFix.action === 'test-net' && this.terminal) {
+ this.terminal.executeCommand('test-net');
+ this.terminal.print(' ✅ Applied: test-net', 'success');
+ } else if (firstFix.action === 'reload') {
+ this.terminal.print(' ℹ️ Manual action needed: reload the page', 'warning');
+ } else {
+ this.terminal.print(' ℹ️ Cannot auto-apply: ' + firstFix.action, 'warning');
+ }
+ } catch (e) {
+ this.terminal.print(' ❌ Fix failed: ' + e.message, 'error');
+ }
+ }
+
+ _cmdHealth() {
+ this.terminal.print('── DebugSupervisor Health ──', 'header');
+ this.terminal.print(' Status: ' + (this._healthy ? '✅ Healthy' : '🔴 Degraded'), this._healthy ? 'success' : 'error');
+ this.terminal.print(' Session: ' + this._sessionId, 'text-muted');
+ this.terminal.print(' Errors tracked: ' + Object.keys(this._errorDB.errors).length + ' unique', 'info');
+ this.terminal.print(' Max errors: ' + this._maxErrors, 'text-muted');
+ this.terminal.print(' TTL: ' + this._ttlDays + ' days', 'text-muted');
+ this.terminal.print(' Interceptors: ' + (this._interceptorInstalled ? '✅ active' : '🔴 inactive'), this._interceptorInstalled ? 'success' : 'error');
+ this.terminal.print(' Quota: ' + (this._checkQuota() ? '✅ available' : '🔴 low'), this._checkQuota() ? 'success' : 'error');
+ }
+
+ // ── Dashboard Data ──
+
+ getDashboardData() {
+ const db = this._errorDB.errors;
+ const keys = Object.keys(db);
+ const totalCount = keys.reduce((sum, k) => sum + db[k].count, 0);
+ const clusters = this._clusterErrors().slice(0, 5);
+
+ return {
+ totalUnique: keys.length,
+ totalCount: totalCount,
+ clusters: clusters.map(c => ({ label: c.label.substring(0, 60), count: c.count, hashes: c.hashes.length })),
+ healthy: this._healthy,
+ topType: this._getTopType(db),
+ topCategory: this._getTopCategory(db),
+ };
+ }
+
+ _getTopType(db) {
+ const freq = {};
+ for (const e of Object.values(db)) {
+ freq[e.type] = (freq[e.type] || 0) + e.count;
+ }
+ const sorted = Object.entries(freq).sort((a, b) => b[1] - a[1]);
+ return sorted.length > 0 ? sorted[0][0] : null;
+ }
+
+ _getTopCategory(db) {
+ const freq = {};
+ for (const e of Object.values(db)) {
+ freq[e.category] = (freq[e.category] || 0) + e.count;
+ }
+ const sorted = Object.entries(freq).sort((a, b) => b[1] - a[1]);
+ return sorted.length > 0 ? sorted[0][0] : null;
+ }
+}
+
/* === end core module bundle === */
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index 8a0a7c8..73078d0 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -86,10 +86,10 @@ buildSystemPrompt() {
} catch (e) {}
let base = `Current date: ${dateStr} (${dayStr}) ${timeStr}${nvfsNote}\n`
+ 'VFS→Modules→AI. nvfs-index for commands. Tool→Observe→Answer. nvfs-resolve. Destructive cmds need confirm.\n'
- + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind: vaultmind_connect(network) — connect wallet, vaultmind_status — check session, vaultmind_query(ep,msg,agent) — Hyperliquid prices/portfolio/chat | wallet_lookup(address) — read-only portfolio for any wallet | get_usage_stats(model,provider,since_hours) — LLM token/latency stats\n'
- + 'web_fetch tool: GET for normal pages (returns title+content+word_count), POST for JSON APIs like Hyperliquid /info (pass method="POST" and body as JSON string).\n'
- + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, guardrail, wget, tar, secret-* — discoverable via help. '
- + 'JS: node <file.js> executes JS from VFS (alias for js-run), node -e "expr" for inline eval. No real Node.js runtime — browser JS only.';
+ + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind | wallet-lookup | get_usage_stats\\n'
+ + 'web_fetch: GET for pages, POST with method+body for JSON APIs. JS: node <file.js> or node -e "expr" (browser JS only).\n'
+ + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, world-model, predict, guardrail, tar, secret-* — via help.\n'
+ + 'predict_task: estimate duration before complex tasks. world_model_query: ask about system state, changes, health.';
if (profilePrompt) {
base = profilePrompt + '\n\n' + base;
}
@@ -99,6 +99,18 @@ buildSystemPrompt() {
if (lp.systemPrompt.prepend) base = lp.systemPrompt.prepend + '\n\n' + base;
if (lp.systemPrompt.append) base = base + '\n\n' + lp.systemPrompt.append;
}
+ // Inject auto-fix suggestions from DebugSupervisorModule
+ try {
+ var ds = this.terminal?.moduleManager?.modules?.DebugSupervisorModule;
+ if (ds && this._autoFixSuggestions && this._autoFixSuggestions.length > 0) {
+ base += '\n\n[Auto-Fix Suggestions]\n';
+ for (var fi = 0; fi < this._autoFixSuggestions.length; fi++) {
+ var s = this._autoFixSuggestions[fi];
+ base += '- ' + s.module + ': ' + s.suggestion + ' (' + s.reason + ')\n';
+ }
+ base += '[/Auto-Fix Suggestions]';
+ }
+ } catch (e) {}
return base;
}
getToolDefinitions() {
@@ -165,6 +177,13 @@ buildSystemPrompt() {
}, {
type: 'function',
function: {
+ name: 'get_dashboard_data',
+ description: 'Full dashboard telemetry: modules, agents, memory, VFS, Ollama, token usage, tool latency, system health',
+ parameters: { type: 'object', properties: {}, required: [] }
+ }
+ }, {
+ type: 'function',
+ function: {
name: 'get_help',
description: 'Command reference',
parameters: { type: 'object', properties: {}, required: [] }
@@ -231,15 +250,18 @@ buildSystemPrompt() {
type: 'function',
function: {
name: 'edit_module',
- description: 'Edit module source (DESTRUCTIVE)',
- parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] }
+ description: 'Edit module source (DESTRUCTIVE). If code is provided, writes directly to VFS and auto-tests. If no code, opens the visual editor (requires manual Save).',
+ parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' }, code: { type: 'string', description: 'Full new source code. When provided, writes directly and auto-tests without opening the editor.' } }, required: ['name'] }
}
}, {
type: 'function',
function: {
name: 'test_module',
- description: 'Test edited module from /_mods/',
- parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] }
+ description: 'Test an edited module: loads it, checks JS errors, verifies commands registered, runs optional test assertions. Auto-rolls-back on failure.',
+ parameters: { type: 'object', properties: {
+ name: { type: 'string', description: 'Module class name' },
+ tests: { type: 'array', description: 'Optional test assertions: [{command, expect}]', items: { type: 'object', properties: { command: { type: 'string' }, expect: { type: 'string' } } } }
+ }, required: ['name'] }
}
}, {
type: 'function',
@@ -440,7 +462,7 @@ buildSystemPrompt() {
required: []
}
}
-}, {
+ }, {
type: 'function',
function: {
name: 'smart_context_set',
@@ -471,7 +493,7 @@ buildSystemPrompt() {
type: 'function',
function: {
name: 'vaultmind_query',
- description: "Query VaultMind AI for Hyperliquid market data (prices, portfolio, transactions, copy-trade) or ask an AI agent. Uses same-origin /vmai/ API.",
+ description: "Query VaultMind AI for Hyperliquid market data (prices, portfolio, transactions, copy-trade) or ask an AI agent. Uses same-origin /vmai-api/ API.",
parameters: {
type: 'object',
properties: {
@@ -533,11 +555,11 @@ buildSystemPrompt() {
},
required: []
}
- },
- {
- type: 'function',
- function: {
- name: 'agent_loop_list',
+ }
+ }, {
+ type: 'function',
+ function: {
+ name: 'agent_loop_list',
description: 'List all available agent loop profiles with their settings and benchmark results.',
parameters: { type: 'object', properties: {}, required: [] }
}
@@ -589,9 +611,77 @@ buildSystemPrompt() {
required: []
}
}
- }
- ];
- }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'predict_task',
+ description: 'Predict task duration, tool turns, and confidence based on historical execution data. Use before starting complex tasks to estimate resource needs.',
+ parameters: {
+ type: 'object',
+ properties: {
+ task: { type: 'string', description: 'Task description to predict' }
+ },
+ required: ['task']
+ }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'world_model_query',
+ description: 'Query the system world model for self-awareness: modules, skills, files, agents, health, memory graph, recent changes, active projects, telemetry. Ask questions like "what changed" or "how many modules".',
+ parameters: {
+ type: 'object',
+ properties: {
+ question: { type: 'string', description: 'What to ask: modules, skills, files, agents, health, memory, changes, projects, telemetry, summary' }
+ },
+ required: ['question']
+ }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'health_check',
+ description: 'Run a system health check after a module edit. Verifies: no JS errors, all core commands work, world model is valid, agents are alive.',
+ parameters: { type: 'object', properties: {} }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'diff_module',
+ description: 'Show a diff between the original module source and the edited version in _mods/. Returns line-by-line changes.',
+ parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'ship_module',
+ description: 'Full pipeline: edit → test → health_check → regression suite. Auto-rolls-back on any failure. Set commit:true to also persist to server (requires human approval). Without commit, runs as dry-run.',
+ parameters: { type: 'object', properties: {
+ name: { type: 'string', description: 'Module class name' },
+ code: { type: 'string', description: 'Full new source code' },
+ tests: { type: 'array', description: 'Test assertions [{command, expect}]', items: { type: 'object', properties: { command: { type: 'string' }, expect: { type: 'string' } } } },
+ commit: { type: 'boolean', description: 'If true, request server persistence (needs human approval). Default: false.' }
+ }, required: ['name', 'code'] }
+ }
+ },
+ {
+ type: 'function',
+ function: {
+ name: 'commit_module',
+ description: 'Request to persist an edited module from VFS _mods/ to the server file system via WebDAV. Creates a pending commit — user must type commit-approve in the terminal to execute. Includes bundle sync and version bump.',
+ parameters: { type: 'object', properties: {
+ name: { type: 'string', description: 'Module class name' },
+ file: { type: 'string', description: 'Target server path (e.g. modules/ext/shell.js). Auto-detect from module registry if omitted.' }
+ }, required: ['name'] }
+ }
+ }
+ ];
+ }
getHelpInfo() {
return {
'ai': 'ai <question> — Ask the AI assistant anything',
@@ -614,6 +704,8 @@ buildSystemPrompt() {
'reset-mem': 'reset-mem — Reset AI memory (logs, context, profile, guardrails) without losing VFS or config',
'vaultmind-connect': 'vaultmind-connect [testnet|mainnet] — Connect VaultMind wallet using hyperliquid_key from SecretVault',
'vaultmind-status': 'vaultmind-status — Check VaultMind session status',
+ 'vaultmind-query': 'vaultmind-query <prices|portfolio|transactions|copy-trade|chat> [message] — Query VaultMind API',
+ 'wallet-lookup': 'wallet-lookup <address> — Look up Hyperliquid wallet summary',
};
}
registerCommands() {
@@ -629,6 +721,21 @@ buildSystemPrompt() {
this.commands['ai-model'] = (args) => this.showModel(args);
this.commands['ai-compress'] = () => this._manualCompress();
this.commands['ai-config'] = (args) => this.aiConfig(args);
+ this.commands['commit-approve'] = (args) => this._executeCommit(args.join(' '));
+ this.commands['commit-deny'] = () => {
+ if (!this._pendingCommit) return this.terminal.print('No pending commit.', 'warning');
+ const name = this._pendingCommit.name;
+ this._pendingCommit = null;
+ this.terminal.print('❌ Commit denied for ' + name, 'warning');
+ };
+ this.commands['commit-status'] = () => {
+ if (!this._pendingCommit) return this.terminal.print('No pending commit.', 'info');
+ const pc = this._pendingCommit;
+ this.terminal.print('📋 Pending commit: ' + pc.name, 'info');
+ this.terminal.print(' Target: ' + pc.serverPath, 'info');
+ if (pc.bundlePath) this.terminal.print(' Bundle: ' + pc.bundlePath, 'info');
+ this.terminal.print(' Changes: ' + pc.changes + ' line(s)', 'info');
+ };
this.commands['agent-loops'] = (args) => this.agentLoopsCmd(args);
this.commands['bench'] = (args) => this.benchCmd(args);
this.commands['bench-compare'] = (args) => this.benchCompareCmd(args);
@@ -649,6 +756,8 @@ buildSystemPrompt() {
this.commands['reset-mem'] = () => this.resetMem();
this.commands['vaultmind-connect'] = (args) => this._vaultmindConnectCmd(args);
this.commands['vaultmind-status'] = () => this._vaultmindStatusCmd();
+ this.commands['vaultmind-query'] = (args) => this._vaultmindQueryCmd(args);
+ this.commands['wallet-lookup'] = (args) => this._walletLookupCmd(args);
}
async chat(args) {
const userMessage = args.join(' ');
@@ -668,6 +777,18 @@ buildSystemPrompt() {
}
this._chatLock = true;
this.terminal.markDirty();
+ // Auto-route context based on intent
+ try {
+ const scm = this.terminal?.moduleManager?.modules?.SmartContextModule;
+ if (scm && typeof scm.autoRouteContext === 'function') {
+ const routeResult = scm.autoRouteContext(userMessage);
+ if (routeResult.switched) {
+ this.terminal.print('🔄 Context auto-switched: ' + routeResult.from + ' → ' + routeResult.to + ' (' + routeResult.confidence + '% confidence)', 'info');
+ // Rebuild system prompt after context switch
+ this.systemPrompt = this.buildSystemPrompt();
+ }
+ }
+ } catch (e) { console.warn('[AI] Auto-routing error:', e.message); }
// Re-read context from localStorage (auto-restore may have written it after init)
this._loadContext();
// Compress context if it exceeds the token threshold
@@ -751,6 +872,24 @@ buildSystemPrompt() {
if (loopResult.toolTurns >= 2 && !this._cancelled) {
await this.afterActionReflection(userMessage, loopResult.toolTurns);
}
+ // Feed result to predictor for learning (cold module activation)
+ if (loopResult && !this._cancelled && loopResult.toolTurns >= 1) {
+ try {
+ const predictor = this.terminal.moduleManager.modules['PredictorModule'];
+ if (predictor && typeof predictor.recordResult === 'function') {
+ const features = predictor._extractFeatures(userMessage);
+ const duration = (Date.now() - lockStart) / 1000;
+ predictor.recordResult(features, duration, loopResult.toolTurns, !this._cancelled);
+ }
+ } catch (e) { /* predictor learning is best-effort */ }
+ // Record task in memory graph for direct AI conversations (not just orchestrator)
+ try {
+ const mg = this.terminal.moduleManager.modules['MemoryGraphModule'];
+ if (mg && typeof mg.recordTask === 'function') {
+ mg.recordTask(userMessage, loopResult.finalAnswer || '', 'ai-assistant');
+ }
+ } catch (e) { /* memory graph is best-effort */ }
+ }
// Self-improvement: post-run anomaly detection
if (loopResult && !this._cancelled) {
try {
@@ -804,6 +943,280 @@ buildSystemPrompt() {
});
}
}
+ // ─── Self-Improvement Helpers ───
+ async _captureCommandOutput(cmd) {
+ const originalPrint = this.terminal.print.bind(this.terminal);
+ let output = '';
+ this.terminal.print = (text, type) => { output += text + '\n'; };
+ try { await this.terminal.security.checkAndRun(cmd); }
+ finally { this.terminal.print = originalPrint; }
+ return output.trim();
+ }
+ async _captureShellCommand(cmd) {
+ // Routes through executeCommand (shell parser) for >, |, &&, export, etc.
+ const originalPrint = this.terminal.print.bind(this.terminal);
+ let output = '';
+ this.terminal.print = (text, type) => { output += text + '\n'; };
+ try { await this.terminal.executeCommand(cmd); }
+ finally { this.terminal.print = originalPrint; }
+ return output.trim();
+ }
+ async _captureConsoleErrors(durationMs) {
+ durationMs = durationMs || 500;
+ var errors = [];
+ var origError = console.error;
+ var origOnError = window.onerror;
+ console.error = function() {
+ var args = Array.prototype.slice.call(arguments);
+ errors.push(args.join(' '));
+ };
+ window.onerror = function(msg, src, line, col, err) {
+ errors.push(msg + (err ? ' — ' + err.message : ''));
+ };
+ await new Promise(function(r) { setTimeout(r, durationMs); });
+ console.error = origError;
+ window.onerror = origOnError;
+ return errors;
+ }
+ _regressionTasks() {
+ return [
+ { id: 'shell-export', module: 'ShellModule', command: 'export REGTEST=ok', expect: 'REGTEST', useShell: true },
+ { id: 'shell-redirect', module: 'ShellModule', command: 'echo hi > _regtest.txt', expect: 'Redirected', useShell: true },
+ { id: 'shell-chain', module: 'ShellModule', command: 'echo a && echo b', expect: 'b', useShell: true },
+ { id: 'shell-pipe', module: 'ShellModule', command: 'echo hello | grep hello', expect: 'hello', useShell: true },
+ { id: 'fs-ls', module: 'SystemModule', command: 'ls', expect: '' },
+ { id: 'fs-pwd', module: 'SystemModule', command: 'pwd', expect: '/' },
+ { id: 'orch-agents', module: 'OrchestratorModule', command: 'list-agents', expect: 'agent' },
+ { id: 'ai-tools', module: 'WebX11AIAssistantModule', command: 'list_modules', expect: 'Module' },
+ { id: 'calc-basic', module: 'CalcModule', command: 'calc 1+1', expect: '2' },
+ { id: 'calc-advanced', module: 'CalcModule', command: 'calc 2*3', expect: '6' },
+ { id: 'webdav-ls', module: 'WebDAVModule', command: 'webdav-ls', expect: '' },
+ { id: 'cron-list', module: 'CronModule', command: 'cron-list', expect: '' },
+ { id: 'theme-list', module: 'ThemeModule', command: 'theme-list', expect: '' },
+ { id: 'mindmap-list', module: 'MindmapModule', command: 'mindmap-list', expect: '' },
+ { id: 'media-status', module: 'MediaModule', command: 'media-status', expect: '' },
+ { id: 'skill-list', module: 'SkillPackCoreModule', command: 'skill-list', expect: '' },
+ { id: 'crypto-encode', module: 'CryptoGzipModule', command: 'crypto-encode test', expect: '' },
+ { id: 'debug-toggle', module: 'DebugPanelModule', command: 'debug', expect: '' },
+ { id: 'voice-status', module: 'VoiceModule', command: 'voice-status', expect: '' },
+ { id: 'wpm-status', module: 'WPMModule', command: 'wpm-status', expect: '' },
+ { id: 'console-help', module: 'ConsoleModule', command: 'console-help', expect: '' },
+ { id: 'tutorial-status', module: 'TutorialModule', command: 'tutorial-status', expect: '' },
+ ];
+ }
+ async _runRegressionSuite(moduleName) {
+ const tests = this._regressionTasks().filter(t => t.module === moduleName);
+ if (tests.length === 0) return { module: moduleName, total: 0, passed: 0, failed: 0, results: [], allPassed: true, message: 'No regression tests for ' + moduleName };
+ const results = [];
+ for (const t of tests) {
+ try {
+ const out = t.useShell ? await this._captureShellCommand(t.command) : await this._captureCommandOutput(t.command);
+ const pass = t.expect ? out.includes(t.expect) : out.length > 0;
+ results.push({ id: t.id, command: t.command, pass, output: out.substring(0, 100) });
+ } catch (e) {
+ results.push({ id: t.id, command: t.command, pass: false, output: 'Error: ' + e.message });
+ }
+ }
+ const passed = results.filter(r => r.pass).length;
+ return { module: moduleName, total: results.length, passed, failed: results.length - passed, results, allPassed: passed === results.length };
+ }
+ // ─── Phase D: Server Persistence ───
+ async _webdavPush(remotePath, content) {
+ const webdav = this.terminal.moduleManager.modules['WebDAVModule'];
+ if (!webdav) throw new Error('WebDAV module not loaded');
+ // Use the webdav module's internal request method
+ const resp = await webdav._webdavRequest('PUT', remotePath, {
+ body: content,
+ headers: { 'Content-Type': 'text/plain' }
+ });
+ if (!resp.ok && resp.status !== 201 && resp.status !== 204) {
+ throw new Error('WebDAV PUT failed: HTTP ' + resp.status);
+ }
+ return true;
+ }
+ async _webdavGet(remotePath) {
+ const webdav = this.terminal.moduleManager.modules['WebDAVModule'];
+ if (!webdav) throw new Error('WebDAV module not loaded');
+ const resp = await webdav._webdavRequest('GET', remotePath);
+ if (!resp.ok) throw new Error('WebDAV GET failed: HTTP ' + resp.status);
+ return await resp.text();
+ }
+ _extractClassName(code) {
+ const m = code.match(/^class\s+(\w+)/m);
+ return m ? m[1] : null;
+ }
+ _extractClassBlock(code, className) {
+ // Extract the class definition from standalone code (without self-registration block)
+ const classStart = code.indexOf('class ' + className);
+ if (classStart === -1) return null;
+ // Find matching closing brace by counting
+ let depth = 0;
+ let end = classStart;
+ for (let i = classStart; i < code.length; i++) {
+ if (code[i] === '{') depth++;
+ else if (code[i] === '}') {
+ depth--;
+ if (depth === 0) { end = i + 1; break; }
+ }
+ }
+ return code.substring(classStart, end);
+ }
+ async _syncBundle(bundlePath, modName, newCode) {
+ try {
+ // Download current bundle
+ const bundle = await this._webdavGet(bundlePath);
+ // Find the class block in the bundle
+ const className = modName;
+ const classStart = bundle.indexOf('class ' + className);
+ if (classStart === -1) return { synced: false, reason: 'Class not found in bundle' };
+ // Find matching closing brace
+ let depth = 0;
+ let classEnd = classStart;
+ for (let i = classStart; i < bundle.length; i++) {
+ if (bundle[i] === '{') depth++;
+ else if (bundle[i] === '}') {
+ depth--;
+ if (depth === 0) { classEnd = i + 1; break; }
+ }
+ }
+ // Extract new class block from the edited code
+ const newClassBlock = this._extractClassBlock(newCode, className);
+ if (!newClassBlock) return { synced: false, reason: 'Could not extract class from new code' };
+ // Replace in bundle
+ const newBundle = bundle.substring(0, classStart) + newClassBlock + bundle.substring(classEnd);
+ // Validate with vm.Script equivalent (Function constructor)
+ try { new Function(newBundle); } catch (e) {
+ return { synced: false, reason: 'Bundle syntax error after replace: ' + e.message };
+ }
+ // Upload
+ await this._webdavPush(bundlePath, newBundle);
+ return { synced: true, bytes: newBundle.length };
+ } catch (e) {
+ return { synced: false, reason: e.message };
+ }
+ }
+ _parseVersion(ver) {
+ // Handle both old format (2026-07-06-v4.6.01) and new format (v4.6.01)
+ const m = ver.match(/v(\d+)\.(\d+)\.(\d+)/);
+ if (!m) return { major: 4, minor: 6, patch: 0 };
+ return { major: parseInt(m[1]), minor: parseInt(m[2]), patch: parseInt(m[3]) };
+ }
+ _bumpVersionString(ver) {
+ const v = this._parseVersion(ver);
+ return 'v' + v.major + '.' + v.minor + '.' + (v.patch + 1);
+ }
+ async _bumpVersion() {
+ // Read current version from the page
+ const currentVer = window.NEWOS4_VERSION || 'v4.6.01';
+ const newVer = this._bumpVersionString(currentVer);
+ // 1. Update main.html — replace all ?v= occurrences
+ const mainHtml = await this._webdavGet('main.html');
+ const updatedHtml = mainHtml.replace(/\?v=[^"']+/g, '?v=' + newVer);
+ await this._webdavPush('main.html', updatedHtml);
+ // 2. Update module-loader.js — replace LATEST_VER
+ const loader = await this._webdavGet('modules/core/module-loader.js');
+ const updatedLoader = loader.replace(/const LATEST_VER = '[^']+'/, "const LATEST_VER = '" + newVer + "'");
+ await this._webdavPush('modules/core/module-loader.js', updatedLoader);
+ return { from: currentVer, to: newVer };
+ }
+ _calculateConfidence(modName, testResult, healthResult, regResult, changeCount) {
+ var score = 100;
+ if (changeCount > 50) score -= 20;
+ if (changeCount > 100) score -= 20;
+ if (regResult.total === 0) score -= 30;
+ var critical = ['ShellModule', 'SystemModule', 'OrchestratorModule', 'WebX11AIAssistantModule'];
+ if (critical.indexOf(modName) !== -1) score -= 15;
+ if (testResult.tests && testResult.tests.length >= 3) score += 10;
+ if (healthResult.jsErrors && healthResult.jsErrors.length > 0) score -= 50;
+ return Math.max(0, Math.min(100, score));
+ }
+
+ async _executeCommit(comment) {
+ const pc = this._pendingCommit;
+ if (!pc) return this.terminal.print('No pending commit. Use commit_module first.', 'warning');
+ // Rate limit check
+ const today = new Date().toISOString().split('T')[0];
+ const commitLog = this.terminal.fs.fsReadJSON('/_mods/commit-log.json') || {};
+ if (commitLog[today] && commitLog[today] >= 5) {
+ return this.terminal.print('⚠️ Rate limit: 5 commits today. Try tomorrow.', 'warning');
+ }
+ // Cooldown check (10 min)
+ if (commitLog._lastCommitTime && Date.now() - commitLog._lastCommitTime < 600000) {
+ const wait = Math.ceil((600000 - (Date.now() - commitLog._lastCommitTime)) / 60000);
+ return this.terminal.print('⏱ Cooldown: ' + wait + ' min until next commit.', 'warning');
+ }
+ this.terminal.print('🚀 Executing commit for ' + pc.name + '...', 'info');
+ const results = { module: pc.name, steps: [] };
+ // Step 1: Push standalone file
+ try {
+ await this._webdavPush(pc.serverPath, pc.code);
+ results.steps.push({ step: 'standalone', status: 'ok', path: pc.serverPath });
+ this.terminal.print(' ✅ Standalone: ' + pc.serverPath, 'success');
+ } catch (e) {
+ results.steps.push({ step: 'standalone', status: 'fail', error: e.message });
+ this.terminal.print(' ❌ Standalone failed: ' + e.message, 'error');
+ return results;
+ }
+ // Step 1b: Post-commit syntax verification
+ try {
+ const verifyResp = await fetch(pc.serverPath + '?v=' + Date.now());
+ const committedCode = await verifyResp.text();
+ new Function(committedCode);
+ results.steps.push({ step: 'verify', status: 'ok' });
+ this.terminal.print(' ✅ Post-commit syntax check passed', 'success');
+ } catch (e) {
+ results.steps.push({ step: 'verify', status: 'fail', error: e.message });
+ this.terminal.print(' ❌ Post-commit syntax check failed: ' + e.message, 'error');
+ // Auto-revert
+ try {
+ await this._webdavPush(pc.serverPath, pc.origCode || '');
+ this.terminal.print(' ⏪ Auto-reverted to original code', 'warning');
+ results.reverted = true;
+ results.syntaxError = e.message;
+ } catch (revertErr) {
+ this.terminal.print(' ❌ Auto-revert FAILED: ' + revertErr.message, 'error');
+ results.revertFailed = true;
+ }
+ this._pendingCommit = null;
+ return results;
+ }
+ // Step 2: Sync bundle
+ if (pc.bundlePath) {
+ try {
+ const syncResult = await this._syncBundle(pc.bundlePath, pc.name, pc.code);
+ results.steps.push({ step: 'bundle', status: syncResult.synced ? 'ok' : 'skip', detail: syncResult });
+ if (syncResult.synced) this.terminal.print(' ✅ Bundle: ' + pc.bundlePath, 'success');
+ else this.terminal.print(' ⚠️ Bundle sync skipped: ' + syncResult.reason, 'warning');
+ } catch (e) {
+ results.steps.push({ step: 'bundle', status: 'fail', error: e.message });
+ this.terminal.print(' ❌ Bundle sync failed: ' + e.message, 'error');
+ }
+ }
+ // Step 3: Bump version
+ try {
+ const verResult = await this._bumpVersion();
+ results.steps.push({ step: 'version', status: 'ok', from: verResult.from, to: verResult.to });
+ this.terminal.print(' ✅ Version: ' + verResult.from + ' → ' + verResult.to, 'success');
+ results.newVersion = verResult.to;
+ } catch (e) {
+ results.steps.push({ step: 'version', status: 'fail', error: e.message });
+ this.terminal.print(' ⚠️ Version bump failed: ' + e.message, 'warning');
+ }
+ // Step 4: Cleanup _mods/
+ this.terminal.fs.deleteFile('_mods/' + pc.name + '.js');
+ this.terminal.fs.deleteFile('_mods/' + pc.name + '.orig.js');
+ this.terminal.fs.deleteFile('_mods/' + pc.name + '.meta.json');
+ // Step 5: Rate limit update
+ commitLog[today] = (commitLog[today] || 0) + 1;
+ commitLog._lastCommitTime = Date.now();
+ this.terminal.fs.fsWriteJSON('/_mods/commit-log.json', commitLog);
+ // Clear pending
+ this._pendingCommit = null;
+ this.terminal.print('🎉 Commit complete! Refresh to load ' + (results.newVersion || 'new version'), 'success');
+ if (comment) this.terminal.print(' Comment: ' + comment, 'info');
+ results.success = true;
+ return results;
+ }
_summarizeObservation(result, maxLen) {
if (!result || typeof result !== 'string' || result.length <= maxLen) return result || '';
const lines = result.split('\n');
@@ -1630,7 +2043,7 @@ buildSystemPrompt() {
}
try {
this.terminal.fs.root = JSON.parse(JSON.stringify(this._shadowSnapshot));
- this.terminal.fs.saveToLocalStorage();
+ this.terminal.fs.saveToStorage();
this._shadowSnapshot = null;
this.terminal.print('↩️ VFS rolled back to pre-shadow state', 'success');
} catch (e) {
@@ -1651,8 +2064,6 @@ buildSystemPrompt() {
'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',
'debug','debug-ai','session-new','mem-debug','reset-mem',
- // Node bridge — alias for js-run
- 'node',
// Synced from terminal whitelist — read-only orchestration/A2A/agent commands
'orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result',
'orchestrator-status','orchestrator-list','orchestrator-config','orchestrator-restart-policy',
@@ -1769,6 +2180,9 @@ buildSystemPrompt() {
memory: performance.memory ? (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB' : 'N/A'
});
}
+ case 'get_dashboard_data': {
+ return JSON.stringify(await this._collectDashboardData());
+ }
case 'get_help': {
const cmds = this.terminal.moduleManager.getAllCommands();
return JSON.stringify(Object.keys(cmds).sort());
@@ -1982,8 +2396,12 @@ buildSystemPrompt() {
if (!task) return JSON.stringify({ error: 'No task provided' });
const orch = this.terminal.moduleManager.modules['OrchestratorModule'];
if (!orch) return JSON.stringify({ error: 'Orchestrator not available' });
- orch.delegateTask(task);
- return JSON.stringify({ message: `Task delegated: ${task.substring(0, 80)}` });
+ try {
+ const result = await orch.delegateTask(task);
+ return JSON.stringify({ success: true, task: task.substring(0, 80), result: typeof result === 'string' ? result.substring(0, 500) : 'completed' });
+ } catch (e) {
+ return JSON.stringify({ error: 'Delegation failed: ' + e.message });
+ }
}
case 'spawn_agent': {
const role = a.role || 'worker';
@@ -2236,7 +2654,7 @@ buildSystemPrompt() {
return JSON.stringify({ error: 'Invalid endpoint. Valid: ' + validEps.join(', ') });
}
try {
- const epMap = { prices: '/vmai/market/prices', portfolio: '/vmai/portfolio', transactions: '/vmai/transactions/summary', 'copy-trade': '/vmai/copy-trading/traders', chat: '/vmai/chat' };
+ const epMap = { prices: '/vmai-api/market/prices', portfolio: '/vmai-api/portfolio', transactions: '/vmai-api/transactions/summary', 'copy-trade': '/vmai-api/copy-trading/traders', chat: '/vmai-api/chat' };
let url = epMap[ep];
let opts = { method: 'GET', headers: { 'Accept': 'application/json' } };
// Portfolio, transactions, copy-trade need auth token
@@ -2322,12 +2740,48 @@ buildSystemPrompt() {
}.bind(this));
return JSON.stringify({ success: true, message: "Benchmark started on '" + profileName + "'", tasks: taskIds || 'all' });
}
+ case 'predict_task': {
+ var predictor = this.terminal.moduleManager.modules['PredictorModule'];
+ if (!predictor) return JSON.stringify({ error: 'Predictor module not loaded' });
+ var taskDesc = a.task || '';
+ if (!taskDesc) return JSON.stringify({ error: 'task parameter required' });
+ var pFeatures = predictor._extractFeatures(taskDesc);
+ var pSimilar = predictor._findSimilar(pFeatures);
+ var pResult = predictor._computePrediction(pSimilar, pFeatures);
+ return JSON.stringify({
+ task: taskDesc.substring(0, 80),
+ estimatedDuration: pResult.duration + 's',
+ estimatedToolTurns: pResult.toolTurns,
+ confidence: Math.round(pResult.confidence * 100) + '%',
+ similarTasks: pResult.similarCount,
+ recommendation: pResult.confidence >= predictor._CONFIDENCE_THRESHOLD && pResult.duration > 30
+ ? 'Set TTL to ' + Math.ceil(pResult.duration * 1.5) + 's'
+ : 'No special action needed'
+ });
+ }
+ case 'world_model_query': {
+ var wm = this.terminal.moduleManager.modules['WorldModelModule'];
+ if (!wm) return JSON.stringify({ error: 'WorldModelModule not loaded' });
+ if (!wm._model) return JSON.stringify({ error: 'World model not synthesized yet — run world-model-refresh' });
+ var wmAnswer = wm._answerQuery(a.question || '');
+ return JSON.stringify({ question: a.question, answer: wmAnswer, version: wm._model.version, synthesized: wm._model.synthesized });
+ }
+ case 'health_check':
+ case 'diff_module':
+ case 'ship_module':
+ case 'commit_module':
+ return await this.runModuleTool(name, argsStr);
default:
return JSON.stringify({ error: `Unknown tool: ${name}` });
}
} catch (e) {
this._errorLog.push({ tool: name, args: a, error: e.message, timestamp: Date.now() });
if (this._errorLog.length > 20) this._errorLog.shift();
+ // Also feed into DebugSupervisorModule
+ try {
+ var ds = this.terminal?.moduleManager?.modules?.DebugSupervisorModule;
+ if (ds) ds.recordToolError(name, a, e.message);
+ } catch (dsErr) {}
return JSON.stringify({ error: e.message, stack: e.stack?.substring(0, 200) });
}
}
@@ -2421,10 +2875,61 @@ buildSystemPrompt() {
}
case 'test_module': {
const modName = a.name || '';
+ const tests = a.tests || null;
const code = this.terminal.fs.readFile(`_mods/${modName}.js`);
if (!code) return JSON.stringify({ error: `No local edit for ${modName}` });
- this.terminal.moduleManager.loadModuleFromCode(code, `blob://_mods/${modName}.js`);
- return `Module "${modName}" loaded from local edit for testing`;
+ // Save current module state for rollback
+ const oldInstance = this.terminal.moduleManager.modules[modName];
+ const oldCommands = oldInstance ? Object.keys(oldInstance.commands) : [];
+ // Load new version
+ const ok = await this.terminal.moduleManager.loadModuleFromCode(code, `blob://_mods/${modName}.js`);
+ if (!ok) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: 'Module failed to load', rolledBack: true });
+ }
+ // Check for JS errors after load
+ const jsErrors = await this._captureConsoleErrors(300);
+ if (jsErrors.length > 0) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: 'JS errors after load', errors: jsErrors.slice(0, 5), rolledBack: true });
+ }
+ // Verify commands registered
+ const newInstance = this.terminal.moduleManager.modules[modName];
+ const newCommands = newInstance ? Object.keys(newInstance.commands) : [];
+ const lostCommands = oldCommands.filter(c => !newCommands.includes(c));
+ if (lostCommands.length > 0) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: 'Commands lost after edit', lost: lostCommands, rolledBack: true });
+ }
+ // Run test assertions if provided
+ if (tests && Array.isArray(tests) && tests.length > 0) {
+ const results = [];
+ for (const t of tests) {
+ const cmd = t.command || '';
+ // Use shell parser for commands with >, |, &&, ||, ;, export, set
+ const useShell = /[>|&;]/.test(cmd) || cmd.startsWith('export ') || cmd.startsWith('set ');
+ const output = useShell ? await this._captureShellCommand(cmd) : await this._captureCommandOutput(cmd);
+ const passed = t.expect ? output.includes(t.expect) : output.length > 0;
+ results.push({ command: cmd, expected: t.expect || '(any output)', actual: output.substring(0, 200), pass: passed });
+ }
+ const failed = results.filter(r => !r.pass).length;
+ if (failed > 0) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: failed + '/' + results.length + ' tests failed', results, rolledBack: true });
+ }
+ return JSON.stringify({ success: true, module: modName, commands: newCommands.length, tests: results });
+ }
+ // Smoke test: run first command if available
+ if (newCommands.length > 0) {
+ try {
+ const smokeOut = await this._captureCommandOutput(newCommands[0]);
+ return JSON.stringify({ success: true, module: modName, commands: newCommands.length, smokeTest: { command: newCommands[0], output: smokeOut.substring(0, 200) } });
+ } catch (e) {
+ if (oldInstance) this.terminal.moduleManager.modules[modName] = oldInstance;
+ return JSON.stringify({ error: 'Smoke test failed: ' + e.message, rolledBack: true });
+ }
+ }
+ return JSON.stringify({ success: true, module: modName, commands: newCommands.length });
}
case 'reload_module': {
const modName = a.name || '';
@@ -2488,11 +2993,181 @@ buildSystemPrompt() {
}
return JSON.stringify({ modules, total: modules.length, hasLocalEdits: hasMods });
}
+ case 'health_check': {
+ const results = {};
+ const coreCmds = ['ls', 'pwd', 'echo test', 'help', 'grep', 'find', 'tree', 'cat'];
+ results.coreCommands = {};
+ for (const cmd of coreCmds) {
+ try {
+ const out = await this._captureCommandOutput(cmd);
+ results.coreCommands[cmd] = (out.length > 0 || cmd === 'ls') ? 'ok' : 'empty output';
+ } catch (e) {
+ results.coreCommands[cmd] = 'FAIL: ' + e.message;
+ }
+ }
+ const mm = this.terminal.moduleManager;
+ results.moduleCount = Object.keys(mm.modules).length;
+ const wm = this.terminal.fs.fsReadJSON('system/world-model.json');
+ results.worldModelVersion = wm ? wm.version : 'not found';
+ results.versionMatch = wm && wm.version === (typeof window !== 'undefined' ? window.NEWOS4_VERSION : '');
+ const orch = mm.modules['OrchestratorModule'];
+ if (orch) {
+ const agents = Object.values(orch._agents);
+ results.agents = { total: agents.length, healthy: agents.filter(a => a.status !== 'failed').length, failed: agents.filter(a => a.status === 'failed').length };
+ }
+ results.jsErrors = await this._captureConsoleErrors(500);
+ const allCoreOk = Object.values(results.coreCommands).every(v => v === 'ok');
+ results.healthy = allCoreOk && results.versionMatch && results.jsErrors.length === 0;
+ return JSON.stringify(results);
+ }
+ case 'diff_module': {
+ const modName = a.name || '';
+ const newCode = this.terminal.fs.readFile(`_mods/${modName}.js`);
+ if (!newCode) return JSON.stringify({ error: `No edit for ${modName}` });
+ let origCode = this.terminal.fs.readFile(`_mods/${modName}.orig.js`);
+ if (!origCode || origCode === 'placeholder') {
+ const instance = this.terminal.moduleManager.modules[modName];
+ if (instance) origCode = instance.constructor.toString();
+ }
+ if (!origCode) return JSON.stringify({ error: 'No original source found' });
+ const origLines = origCode.split('\n');
+ const newLines = newCode.split('\n');
+ const changes = [];
+ const maxLines = Math.max(origLines.length, newLines.length);
+ for (let i = 0; i < maxLines; i++) {
+ if (origLines[i] !== newLines[i]) {
+ if (origLines[i] && !newLines[i]) changes.push({ line: i+1, type: 'removed', content: origLines[i].substring(0, 120) });
+ else if (!origLines[i] && newLines[i]) changes.push({ line: i+1, type: 'added', content: newLines[i].substring(0, 120) });
+ else changes.push({ line: i+1, type: 'changed', old: (origLines[i]||'').substring(0, 80), new: (newLines[i]||'').substring(0, 80) });
+ }
+ }
+ return JSON.stringify({ module: modName, originalLines: origLines.length, newLines: newLines.length, changes: changes.length, diff: changes.slice(0, 50) });
+ }
+ case 'ship_module': {
+ const modName = a.name || '';
+ const newCode = a.code || '';
+ const tests = a.tests || [];
+ const shouldCommit = a.commit === true;
+ if (!modName || !newCode) return JSON.stringify({ error: 'name and code required' });
+ // Step 1: Write to _mods/
+ this.terminal.fs.writeFile(`_mods/${modName}.js`, newCode);
+ this.terminal.fs.writeFile(`_mods/${modName}.meta.json`, JSON.stringify({ editedAt: new Date().toISOString(), editedBy: 'ship_module' }));
+ // Step 2: Test
+ const testResult = await this.runModuleTool('test_module', JSON.stringify({ name: modName, tests }));
+ const testParsed = JSON.parse(testResult);
+ if (testParsed.error) {
+ return JSON.stringify({ step: 'test', error: testParsed.error, rolledBack: testParsed.rolledBack, pipeline: 'aborted at test' });
+ }
+ // Step 3: Health check
+ const healthResult = await this.runModuleTool('health_check', '{}');
+ const healthParsed = JSON.parse(healthResult);
+ if (!healthParsed.healthy) {
+ await this.runModuleTool('reload_module', JSON.stringify({ name: modName }));
+ return JSON.stringify({ step: 'health_check', error: 'System unhealthy after edit', health: healthParsed, pipeline: 'aborted at health_check, rolled back' });
+ }
+ // Step 4: Regression suite
+ const regResult = await this._runRegressionSuite(modName);
+ if (!regResult.allPassed) {
+ await this.runModuleTool('reload_module', JSON.stringify({ name: modName }));
+ return JSON.stringify({ step: 'regression', error: 'Regression tests failed', regression: regResult, pipeline: 'aborted at regression, rolled back' });
+ }
+ // Step 5: Calculate confidence score
+ const changeCount = newCode.split('\n').length;
+ const confidence = this._calculateConfidence(modName, testParsed, healthParsed, regResult, changeCount);
+ // Step 6: Commit (if requested)
+ if (shouldCommit) {
+ if (confidence >= 70) {
+ // Auto-commit: execute directly
+ const commitResult = await this.runModuleTool('commit_module', JSON.stringify({ name: modName }));
+ const commitParsed = JSON.parse(commitResult);
+ if (commitParsed.pendingCommit) {
+ // Execute the pending commit immediately
+ const execResult = await this._executeCommit('auto: confidence=' + confidence);
+ return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression → AUTO-COMMIT (confidence=' + confidence + ')', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, autoCommitted: true, commitResult: execResult });
+ }
+ return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression → auto-commit', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, commit: commitParsed });
+ } else {
+ // Confidence too low — human approval required
+ const commitResult = await this.runModuleTool('commit_module', JSON.stringify({ name: modName }));
+ return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression → HUMAN APPROVAL REQUIRED (confidence=' + confidence + ' < 70)', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, commit: JSON.parse(commitResult), humanApprovalRequired: true });
+ }
+ }
+ return JSON.stringify({ success: true, pipeline: 'edit → test → health → regression (dry-run)', module: modName, confidence: confidence, testResult: testParsed, health: healthParsed, regression: regResult, next: 'Call commit_module or ship_module with commit:true to persist' });
+ }
+ case 'commit_module': {
+ const modName = a.name || '';
+ const targetFile = a.file || '';
+ const newCode = this.terminal.fs.readFile(`_mods/${modName}.js`);
+ if (!newCode) return JSON.stringify({ error: `No local edit for ${modName}. Run edit_module or ship_module first.` });
+
+ // Auto-detect target file from module registry
+ let serverPath = targetFile;
+ if (!serverPath) {
+ const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName);
+ if (entry && entry.url) {
+ serverPath = entry.url.replace(/^https?:\/\/[^/]+/, '').replace(/\?v=.*/, '');
+ }
+ }
+ if (!serverPath) return JSON.stringify({ error: 'Cannot determine server path. Provide file= parameter (e.g. modules/ext/shell.js).' });
+
+ // Get diff
+ let origCode = this.terminal.fs.readFile(`_mods/${modName}.orig.js`);
+ if (!origCode || origCode === 'placeholder') {
+ const instance = this.terminal.moduleManager.modules[modName];
+ if (instance) origCode = instance.constructor.toString();
+ }
+ const origLines = origCode ? origCode.split('\n') : [];
+ const newLines = newCode.split('\n');
+ let changeCount = 0;
+ for (let i = 0; i < Math.max(origLines.length, newLines.length); i++) {
+ if (origLines[i] !== newLines[i]) changeCount++;
+ }
+
+ // Detect bundle path
+ let bundlePath = null;
+ if (serverPath.includes('/modules/core/') || serverPath.includes('module-aiassist')) {
+ bundlePath = 'modules/core/core-bundle.js';
+ } else if (serverPath.includes('/modules/ext/')) {
+ bundlePath = 'modules/ext/critical-ext-bundle.js';
+ }
+
+ // Store pending commit
+ this._pendingCommit = {
+ name: modName,
+ code: newCode,
+ origCode: origCode || '',
+ serverPath: serverPath,
+ bundlePath: bundlePath,
+ changes: changeCount,
+ createdAt: Date.now()
+ };
+
+ this.terminal.print('📋 Commit request created for ' + modName, 'info');
+ this.terminal.print(' Target: ' + serverPath, 'info');
+ if (bundlePath) this.terminal.print(' Bundle: ' + bundlePath, 'info');
+ this.terminal.print(' Changes: ' + changeCount + ' line(s) different', 'info');
+ this.terminal.print(' Type \'commit-approve\' to persist or \'commit-deny\' to cancel', 'warning');
+
+ return JSON.stringify({
+ pendingCommit: true,
+ module: modName,
+ serverPath: serverPath,
+ bundlePath: bundlePath,
+ changes: changeCount,
+ newLines: newLines.length,
+ message: 'Commit request created. User must type commit-approve to execute.'
+ });
+ }
default: return JSON.stringify({ error: `Unknown module tool: ${name}` });
}
} catch (e) {
this._errorLog.push({ tool: name, args: a, error: e.message, timestamp: Date.now() });
if (this._errorLog.length > 20) this._errorLog.shift();
+ // Also feed into DebugSupervisorModule
+ try {
+ var ds = this.terminal?.moduleManager?.modules?.DebugSupervisorModule;
+ if (ds) ds.recordToolError(name, a, e.message);
+ } catch (dsErr) {}
return JSON.stringify({ error: e.message, stack: e.stack?.substring(0, 200) });
}
}
@@ -2827,6 +3502,19 @@ buildSystemPrompt() {
}
try { localStorage.setItem('webx11_ai_patterns', JSON.stringify(this._commandFreq)); } catch (e) {}
}
+ _buildToolToModuleMap() {
+ this._toolToModuleMap = {};
+ const mm = this.terminal.moduleManager;
+ if (!mm || !mm.modules) return;
+ for (const [modName, mod] of Object.entries(mm.modules)) {
+ if (mod.commands) {
+ for (const cmdName of Object.keys(mod.commands)) {
+ this._toolToModuleMap[cmdName] = modName;
+ }
+ }
+ }
+ }
+
async _autoCreateErrorSkill() {
const grouped = {};
for (const e of this._errorLog) {
@@ -3106,6 +3794,49 @@ buildSystemPrompt() {
this.terminal.print('❌ No VaultMind session. Use: vaultmind-connect [testnet|mainnet]', 'warning');
}
}
+ async _vaultmindQueryCmd(args) {
+ const ep = args[0];
+ const validEps = ['prices', 'portfolio', 'transactions', 'copy-trade', 'chat'];
+ if (!validEps.includes(ep)) {
+ this.terminal.print('Usage: vaultmind-query <prices|portfolio|transactions|copy-trade|chat> [message|agent]', 'warning'); return;
+ }
+ try {
+ const epMap = { prices: '/vmai-api/market/prices', portfolio: '/vmai-api/portfolio', transactions: '/vmai-api/transactions/summary', 'copy-trade': '/vmai-api/copy-trading/traders', chat: '/vmai-api/chat' };
+ let url = epMap[ep];
+ let opts = { method: 'GET', headers: { 'Accept': 'application/json' } };
+ const authEps = ['portfolio', 'transactions', 'copy-trade'];
+ if (authEps.includes(ep)) {
+ const token = localStorage.getItem('vaultmind_token');
+ if (token) opts.headers['Authorization'] = 'Bearer ' + token;
+ }
+ if (ep === 'chat') {
+ opts.method = 'POST';
+ opts.headers['Content-Type'] = 'application/json';
+ const token = localStorage.getItem('vaultmind_token');
+ if (token) opts.headers['Authorization'] = 'Bearer ' + token;
+ opts.body = JSON.stringify({
+ message: args.slice(1).join(' ') || 'What is the current market status?',
+ agent: 'market'
+ });
+ }
+ const resp = await fetch(url, opts);
+ const data = await resp.json();
+ this.terminal.print(JSON.stringify(data, null, 2), 'success');
+ } catch (e) {
+ this.terminal.print('VaultMind query error: ' + e.message, 'error');
+ }
+ }
+ async _walletLookupCmd(args) {
+ const addr = args[0];
+ if (!addr) { this.terminal.print('Usage: wallet-lookup <address>', 'warning'); return; }
+ try {
+ const resp = await fetch('/wallet/' + encodeURIComponent(addr));
+ const data = await resp.json();
+ this.terminal.print(JSON.stringify(data, null, 2), 'success');
+ } catch (e) {
+ this.terminal.print('Wallet lookup error: ' + e.message, 'error');
+ }
+ }
// ── Agent Loop Profile System ──
_defaultLoopProfile(name, desc) {
return {
@@ -3411,6 +4142,20 @@ buildSystemPrompt() {
else if (a.type === 'high_token_usage') suggestions.push('High token usage (' + (a.value/1000).toFixed(0) + 'K) — consider a minimal-tools profile for simpler tasks');
else if (a.type === 'tool_failures') {
suggestions.push(a.count + ' tool failures (' + a.tools.join(', ') + ') — check if modules are loaded or paths correct');
+ // Auto-fix: map failed tools to modules and suggest fix
+ if (!this._autoFixSuggestions) this._autoFixSuggestions = [];
+ if (!this._toolToModuleMap) this._buildToolToModuleMap();
+ for (var fi = 0; fi < a.tools.length; fi++) {
+ var failedMod = this._toolToModuleMap[a.tools[fi]];
+ if (failedMod && this._autoFixSuggestions.length < 3) {
+ this._autoFixSuggestions.push({
+ module: failedMod,
+ tool: a.tools[fi],
+ reason: 'Repeated tool failure in agent loop',
+ suggestion: 'Consider running ship_module to fix ' + failedMod
+ });
+ }
+ }
// Auto-correct: inject negative constraint into profile append
if (this._activeLoopProfile && this._activeLoopProfile.systemPrompt) {
var constraint = '[Constraint] Avoid tools that failed: ' + a.tools.join(', ') + '. Verify module state before calling these tools.';
@@ -3476,4 +4221,141 @@ buildSystemPrompt() {
if (profileAdjusted) msg += ' — profile auto-adjusted';
this.terminal.print(msg, 'info');
}
+ async _collectDashboardData() {
+ try {
+ const terminal = this.terminal;
+ const mm = terminal.moduleManager;
+ const om = terminal.ollamaManager;
+ const fs = terminal.fs;
+ const data = {};
+
+ // Version
+ data.version = window.NEWOS4_VERSION || 'v?';
+
+ // Modules
+ const mods = mm ? Object.keys(mm.modules) : [];
+ const registry = mm ? (mm.moduleRegistry || []) : [];
+ data.modules = {
+ loaded: mods.length,
+ total: registry.length || mods.length,
+ modifiedCount: 0
+ };
+ try {
+ const modsDir = fs ? fs.readDir('/_mods/') : null;
+ if (modsDir) data.modules.modifiedCount = modsDir.filter(function(e) { return e.type === 'file' && e.name.endsWith('.js') && !e.name.endsWith('.orig.js'); }).length;
+ } catch (e) {}
+
+ // Agents
+ const orch = mm ? mm.modules['OrchestratorModule'] : null;
+ if (orch && orch._agents) {
+ var agentList = Object.values(orch._agents);
+ data.agents = {
+ total: agentList.length,
+ busy: agentList.filter(function(a) { return a.status === 'busy'; }).length,
+ idle: agentList.filter(function(a) { return a.status === 'idle'; }).length,
+ sleeping: agentList.filter(function(a) { return a.status === 'sleeping'; }).length,
+ failed: agentList.filter(function(a) { return a.status === 'failed' || a.status === 'restarting'; }).length,
+ list: agentList.map(function(a) { return { id: a.id, role: a.role, status: a.status, caps: (a.capabilities || []).length }; })
+ };
+ } else {
+ data.agents = { total: 0, busy: 0, idle: 0, sleeping: 0, failed: 0, list: [] };
+ }
+
+ // Memory
+ var heapStr = 'N/A';
+ try { if (performance.memory) heapStr = (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB'; } catch (e) {}
+ var graphNodes = 0;
+ var mg = mm ? mm.modules['MemoryGraphModule'] : null;
+ if (mg && mg._nodes) graphNodes = Object.keys(mg._nodes).length;
+ data.memory = { heap: heapStr, graphNodes: graphNodes };
+
+ // VFS
+ try {
+ if (fs) {
+ var fileCount = 0, dirCount = 0;
+ var walk = function(path) {
+ var entries = fs.ls(path);
+ if (!entries) return;
+ for (var i = 0; i < entries.length; i++) {
+ if (entries[i].type === 'file') fileCount++;
+ else { dirCount++; walk(path === '/' ? '/' + entries[i].name : path + '/' + entries[i].name); }
+ }
+ };
+ walk('/');
+ data.vfs = { files: fileCount, dirs: dirCount, size: (JSON.stringify(fs.root || {}).length / 1024).toFixed(1) + 'KB' };
+ }
+ } catch (e) { data.vfs = { files: 0, dirs: 0, size: '0KB' }; }
+
+ // Ollama
+ data.ollama = {
+ connected: om ? om.connected : false,
+ model: om ? om.currentModel : null,
+ provider: om && om.currentProvider ? om.currentProvider.name : null
+ };
+
+ // Skills
+ try {
+ var skillIdx = fs ? fs.fsReadJSON('/skills/index.json') : null;
+ data.skills = { count: skillIdx ? Object.keys(skillIdx).length : 0 };
+ } catch (e) { data.skills = { count: 0 }; }
+
+ // Token usage
+ try {
+ if (om && typeof om.getUsageStats === 'function') {
+ var usage = om.getUsageStats({ since_hours: 24 });
+ data.usage = usage;
+ }
+ } catch (e) { data.usage = { total: 0 }; }
+
+ // Tool latency
+ var sm = mm ? mm.modules['ServerMonitorModule'] : null;
+ if (sm && sm._latencyLog) {
+ var toolStats = {};
+ for (var i = 0; i < sm._latencyLog.length; i++) {
+ var entry = sm._latencyLog[i];
+ if (!toolStats[entry.name]) toolStats[entry.name] = { calls: 0, totalMs: 0, errors: 0, maxMs: 0 };
+ toolStats[entry.name].calls++;
+ toolStats[entry.name].totalMs += entry.duration;
+ toolStats[entry.name].errors += entry.error ? 1 : 0;
+ toolStats[entry.name].maxMs = Math.max(toolStats[entry.name].maxMs, entry.duration);
+ }
+ data.latency = Object.entries(toolStats)
+ .map(function(e) { return { name: e[0], avg: Math.round(e[1].totalMs / e[1].calls) + 'ms', max: e[1].maxMs + 'ms', calls: e[1].calls, errors: e[1].errors }; })
+ .sort(function(a, b) { return parseInt(b.avg) - parseInt(a.avg); })
+ .slice(0, 10);
+ } else {
+ data.latency = [];
+ }
+
+ // System health
+ var totalErrors = 0;
+ try {
+ var ds = mm ? mm.modules['DebugSupervisorModule'] : null;
+ if (ds && ds._errorDB) totalErrors += Object.keys(ds._errorDB.errors).length;
+ if (sm && sm._latencyLog) totalErrors += sm._latencyLog.filter(function(e) { return e.error; }).length;
+ } catch (e) {}
+ var predictor = mm ? mm.modules['PredictorModule'] : null;
+ var predictorAcc = 'N/A';
+ if (predictor && predictor._accuracy && predictor._accuracy.total > 0) {
+ predictorAcc = Math.round(predictor._accuracy.correct / predictor._accuracy.total * 100) + '%';
+ }
+ data.health = {
+ modules: data.modules.loaded + '/' + data.modules.total,
+ vfsSize: data.vfs ? data.vfs.size : '?',
+ online: typeof navigator !== 'undefined' ? navigator.onLine : true,
+ errors: totalErrors,
+ predictorAccuracy: predictorAcc
+ };
+
+ data.timestamp = Date.now();
+
+ // Write to localStorage for the dashboard HTML page to read
+ try { localStorage.setItem('webx11_dashboard_data', JSON.stringify(data)); } catch (e) {}
+
+ return data;
+ } catch (e) {
+ console.error('[Dashboard] Collection error:', e.message);
+ return { error: e.message, timestamp: Date.now() };
+ }
+ }
}
diff --git a/modules/core/module-chatstudio.js b/modules/core/module-chatstudio.js
index 8e9d208..dd5ec65 100644
--- a/modules/core/module-chatstudio.js
+++ b/modules/core/module-chatstudio.js
@@ -501,8 +501,7 @@ class ChatStudioModule {
var shortNames = {
'llama3.2': 'Llama3.2',
'llama3': 'Llama3',
- 'gemma3:4b': 'Gemma3-4B',
- 'gemma3:1b': 'Gemma3-1B',
+ 'gemma4:31b-cloud': 'Gemma4-31B',
'gemma2:2b': 'Gemma2',
'qwen2.5:7b-instruct': 'Qwen2.5',
'qwen3:8b': 'Qwen3',
diff --git a/modules/core/module-debug-supervisor.js b/modules/core/module-debug-supervisor.js
new file mode 100644
index 0000000..15fe5d4
--- /dev/null
+++ b/modules/core/module-debug-supervisor.js
@@ -0,0 +1,701 @@
+// module-debug-supervisor.js — Persistent error capture, clustering, root-cause analysis, and auto-fix
+// Commands: debug-stats, debug-history, debug-analyze, debug-trends, debug-fix, debug-health
+class DebugSupervisorModule {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ this._healthy = true;
+ this._errorDB = { errors: {}, clusters: {}, patterns: {}, sessionId: null };
+ this._maxErrors = 500;
+ this._ttlDays = 7;
+ this._writeDebounce = null;
+ this._pendingWrites = 0;
+ this._originalConsole = {};
+ this._originalOnError = null;
+ this._originalOnRejection = null;
+ this._originalFetch = null;
+ this._interceptorInstalled = false;
+ this._sessionId = this._generateSessionId();
+ }
+
+ _generateSessionId() {
+ try {
+ let sid = localStorage.getItem('webx11_debug_session');
+ if (!sid) {
+ sid = Date.now().toString(36) + '-' + Math.random().toString(36).substring(2, 8);
+ localStorage.setItem('webx11_debug_session', sid);
+ }
+ return sid;
+ } catch (e) {
+ return Date.now().toString(36);
+ }
+ }
+
+ getHelpInfo() {
+ return {
+ 'debug-stats': 'debug-stats — Show error summary (total, unique, clusters, top issues)',
+ 'debug-history': 'debug-history [n] — Show last N errors (default: 20)',
+ 'debug-analyze': 'debug-analyze <hash> — Deep analysis of a specific error',
+ 'debug-trends': 'debug-trends [days] — Error frequency over time (default: 7)',
+ 'debug-fix': 'debug-fix <hash> — Suggest and optionally apply fix for an error',
+ 'debug-health': 'debug-health — Check supervisor module health status',
+ };
+ }
+
+ registerCommands() {
+ this.commands['debug-stats'] = () => this._cmdStats();
+ this.commands['debug-history'] = (args) => this._cmdHistory(args);
+ this.commands['debug-analyze'] = (args) => this._cmdAnalyze(args);
+ this.commands['debug-trends'] = (args) => this._cmdTrends(args);
+ this.commands['debug-fix'] = (args) => this._cmdFix(args);
+ this.commands['debug-health'] = () => this._cmdHealth();
+ }
+
+ seed() {
+ // seed() is not called automatically by module manager — interceptors go in init()
+ }
+
+ init() {
+ this._loadDB();
+ this._installInterceptors();
+ }
+
+ // ── Error Database ──
+
+ _loadDB() {
+ try {
+ const raw = localStorage.getItem('webx11_debug_db');
+ if (raw) {
+ const parsed = JSON.parse(raw);
+ this._errorDB = parsed;
+ this._pruneExpired();
+ }
+ } catch (e) {
+ // Corrupted DB — start fresh
+ this._errorDB = { errors: {}, clusters: {}, patterns: {}, sessionId: this._sessionId };
+ }
+ }
+
+ _saveDB() {
+ try {
+ localStorage.setItem('webx11_debug_db', JSON.stringify(this._errorDB));
+ } catch (e) {
+ // Quota exceeded — evict oldest 20% and retry
+ this._evictLRU(0.2);
+ try {
+ localStorage.setItem('webx11_debug_db', JSON.stringify(this._errorDB));
+ } catch (e2) {
+ // Still failing — supervisor degrades gracefully
+ console.warn('[DebugSupervisor] DB write failed, errors may be lost');
+ }
+ }
+ }
+
+ _scheduleSave() {
+ this._pendingWrites++;
+ if (this._writeDebounce) return;
+ this._writeDebounce = setTimeout(() => {
+ this._writeDebounce = null;
+ if (this._pendingWrites > 0) {
+ this._saveDB();
+ this._pendingWrites = 0;
+ }
+ }, 2000);
+ }
+
+ _pruneExpired() {
+ const now = Date.now();
+ const maxAge = this._ttlDays * 86400000;
+ let changed = false;
+ for (const [hash, entry] of Object.entries(this._errorDB.errors)) {
+ if (now - entry.lastSeen > maxAge) {
+ delete this._errorDB.errors[hash];
+ changed = true;
+ }
+ }
+ if (changed) this._saveDB();
+ }
+
+ _evictLRU(fraction) {
+ const entries = Object.entries(this._errorDB.errors);
+ const sorted = entries.sort((a, b) => a[1].lastSeen - b[1].lastSeen);
+ const toRemove = Math.ceil(sorted.length * fraction);
+ for (let i = 0; i < toRemove; i++) {
+ delete this._errorDB.errors[sorted[i][0]];
+ }
+ }
+
+ _checkQuota() {
+ try {
+ const total = 0;
+ for (const key in localStorage) {
+ // Estimate: key + value length
+ }
+ // Simple check: try writing a test key
+ const testKey = '__quota_test__';
+ localStorage.setItem(testKey, '1');
+ localStorage.removeItem(testKey);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ // ── Error Classification ──
+
+ _classifyError(message, source, stack) {
+ const type = this._detectType(message, stack);
+ const severity = this._detectSeverity(type, message);
+ const category = this._detectCategory(message, source, type);
+ return { type, severity, category };
+ }
+
+ _detectType(message, stack) {
+ if (!message) return 'unknown';
+ if (message.includes('ReferenceError') || message.includes('is not defined')) return 'ReferenceError';
+ if (message.includes('TypeError') || message.includes('is not a function') || message.includes('cannot read property')) return 'TypeError';
+ if (message.includes('SyntaxError') || message.includes('Unexpected token')) return 'SyntaxError';
+ if (message.includes('HTTP') || message.includes('NetworkError') || message.includes('Failed to fetch')) return 'HTTP';
+ if (message.includes('timeout') || message.includes('Timeout') || message.includes('AbortError')) return 'timeout';
+ if (message.includes('QuotaExceededError') || message.includes('quota')) return 'quota';
+ if (message.includes('SecurityError') || message.includes('permission')) return 'security';
+ if (stack && stack.includes('tool_')) return 'tool';
+ return 'runtime';
+ }
+
+ _detectSeverity(type, message) {
+ if (type === 'SyntaxError' || type === 'ReferenceError' || type === 'quota') return 'fatal';
+ if (type === 'HTTP' && message.includes('5')) return 'error';
+ if (type === 'timeout' || type === 'security') return 'error';
+ if (type === 'tool') return 'warning';
+ return 'error';
+ }
+
+ _detectCategory(message, source, type) {
+ if (type === 'HTTP' || type === 'timeout') return 'network';
+ if (type === 'quota') return 'vfs';
+ if (type === 'security') return 'security';
+ if (source && (source.includes('module-') || source.includes('ext/'))) return 'module';
+ if (type === 'tool') return 'tool';
+ return 'runtime';
+ }
+
+ _extractModule(source) {
+ if (!source) return null;
+ const m = source.match(/(module-[a-z-]+)\.js/);
+ return m ? m[1] : null;
+ }
+
+ _computeHash(message, source, type) {
+ const raw = (message || '') + '|' + (source || '') + '|' + (type || '');
+ // Simple hash (Fowler-Noll-Vo)
+ let hash = 0x811c9dc5;
+ for (let i = 0; i < raw.length; i++) {
+ hash ^= raw.charCodeAt(i);
+ hash = Math.imul(hash, 0x01000193);
+ }
+ return (hash >>> 0).toString(16);
+ }
+
+ // ── Error Interception ──
+
+ _installInterceptors() {
+ if (this._interceptorInstalled) return;
+ const self = this;
+
+ // Console.error
+ this._originalConsole.error = console.error;
+ console.error = function() {
+ const args = Array.prototype.slice.call(arguments);
+ const msg = args.map(a => typeof a === 'object' ? (a.message || JSON.stringify(a)) : String(a)).join(' ');
+ self._recordError(msg, 'console.error', null);
+ return self._originalConsole.error.apply(console, args);
+ };
+
+ // window.onerror
+ this._originalOnError = window.onerror;
+ window.onerror = function(msg, src, line, col, err) {
+ const fullMsg = msg + (err ? ' — ' + err.message : '') + ' (' + src + ':' + line + ':' + col + ')';
+ self._recordError(fullMsg, src || 'window.onerror', err ? err.stack : null);
+ if (self._originalOnError) return self._originalOnError.call(window, msg, src, line, col, err);
+ };
+
+ // Unhandled rejections
+ this._originalOnRejection = window.onunhandledrejection;
+ window.addEventListener('unhandledrejection', function(e) {
+ const msg = e.reason ? (e.reason.message || String(e.reason)) : 'Unhandled Promise rejection';
+ self._recordError(msg, 'unhandledrejection', e.reason ? e.reason.stack : null);
+ });
+
+ // Fetch failures
+ this._originalFetch = window.fetch;
+ window.fetch = function() {
+ const args = arguments;
+ const url = typeof args[0] === 'string' ? args[0] : (args[0]?.url || '?');
+ return self._originalFetch.apply(window, args).catch(function(err) {
+ self._recordError('Fetch failed: ' + url + ' — ' + (err.message || err), url, err.stack);
+ throw err;
+ });
+ };
+
+ this._interceptorInstalled = true;
+ }
+
+ // ── Record Error ──
+
+ recordToolError(toolName, args, errorMessage) {
+ this._recordError(errorMessage, 'tool:' + toolName, null);
+ }
+
+ _recordError(message, source, stack) {
+ if (!this._healthy) return;
+ const classification = this._classifyError(message, source, stack);
+ const hash = this._computeHash(message, source, classification.type);
+ const now = Date.now();
+
+ if (this._errorDB.errors[hash]) {
+ this._errorDB.errors[hash].count++;
+ this._errorDB.errors[hash].lastSeen = now;
+ if (this._errorDB.errors[hash].sessions.indexOf(this._sessionId) === -1) {
+ this._errorDB.errors[hash].sessions.push(this._sessionId);
+ }
+ } else {
+ // LRU eviction if at cap
+ const keys = Object.keys(this._errorDB.errors);
+ if (keys.length >= this._maxErrors) {
+ this._evictLRU(0.1);
+ }
+ this._errorDB.errors[hash] = {
+ count: 1,
+ firstSeen: now,
+ lastSeen: now,
+ message: message,
+ type: classification.type,
+ severity: classification.severity,
+ category: classification.category,
+ source: source,
+ stack: stack ? stack.split('\n').slice(0, 10).join('\n') : null,
+ module: this._extractModule(source),
+ sessions: [this._sessionId],
+ };
+ }
+
+ this._scheduleSave();
+ }
+
+ // ── Clustering (Jaccard similarity) ──
+
+ _normalizeMessage(msg) {
+ if (!msg) return '';
+ return msg
+ .toLowerCase()
+ .replace(/\b0x[a-f0-9]+\b/g, '0xHEX')
+ .replace(/\b\d+\b/g, 'N')
+ .replace(/["'].*?["']/g, '"STR"')
+ .replace(/[^\w\s]/g, ' ')
+ .replace(/\s+/g, ' ')
+ .trim();
+ }
+
+ _jaccardSimilarity(a, b) {
+ const setA = new Set(a.split(' ').filter(Boolean));
+ const setB = new Set(b.split(' ').filter(Boolean));
+ if (setA.size === 0 && setB.size === 0) return 1;
+ let intersection = 0;
+ for (const token of setA) {
+ if (setB.has(token)) intersection++;
+ }
+ const union = setA.size + setB.size - intersection;
+ return union === 0 ? 0 : intersection / union;
+ }
+
+ _clusterErrors() {
+ const errors = Object.entries(this._errorDB.errors);
+ if (errors.length === 0) return [];
+
+ const clusters = [];
+ const assigned = new Set();
+
+ for (let i = 0; i < errors.length; i++) {
+ if (assigned.has(i)) continue;
+ const [hashI, entryI] = errors[i];
+ const normI = this._normalizeMessage(entryI.message);
+ const cluster = { hashes: [hashI], messages: [entryI.message], count: entryI.count, types: new Set([entryI.type]), categories: new Set([entryI.category]) };
+ assigned.add(i);
+
+ for (let j = i + 1; j < errors.length; j++) {
+ if (assigned.has(j)) continue;
+ const [hashJ, entryJ] = errors[j];
+ const normJ = this._normalizeMessage(entryJ.message);
+ if (this._jaccardSimilarity(normI, normJ) >= 0.35) {
+ cluster.hashes.push(hashJ);
+ cluster.messages.push(entryJ.message);
+ cluster.count += entryJ.count;
+ cluster.types.add(entryJ.type);
+ cluster.categories.add(entryJ.category);
+ assigned.add(j);
+ }
+ }
+
+ cluster.label = this._mostFrequent(cluster.messages);
+ cluster.typeCount = cluster.types.size;
+ cluster.categoryCount = cluster.categories.size;
+ delete cluster.types;
+ delete cluster.categories;
+ clusters.push(cluster);
+ }
+
+ clusters.sort((a, b) => b.count - a.count);
+ return clusters;
+ }
+
+ _mostFrequent(arr) {
+ const freq = {};
+ let best = arr[0], bestCount = 1;
+ for (const item of arr) {
+ freq[item] = (freq[item] || 0) + 1;
+ if (freq[item] > bestCount) {
+ best = item;
+ bestCount = freq[item];
+ }
+ }
+ return best;
+ }
+
+ // ── Root-Cause Analysis ──
+
+ _analyzeRootCause(hash) {
+ const entry = this._errorDB.errors[hash];
+ if (!entry) return { found: false };
+
+ const findings = [];
+ const fixes = [];
+
+ // Tool failure → check module
+ if (entry.source && entry.source.startsWith('tool:')) {
+ const toolName = entry.source.replace('tool:', '');
+ const mm = this.terminal?.moduleManager;
+ if (mm) {
+ // Check if any module has this command
+ let found = false;
+ for (const [modName, mod] of Object.entries(mm.modules)) {
+ if (mod.commands && mod.commands[toolName]) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ findings.push({ type: 'module_not_loaded', detail: 'No module provides command "' + toolName + '"' });
+ fixes.push({ action: 'loadmod', params: toolName, description: 'Load module providing ' + toolName });
+ }
+ }
+ }
+
+ // HTTP error → check network
+ if (entry.type === 'HTTP' || entry.type === 'timeout') {
+ findings.push({ type: 'network_issue', detail: 'Network request failed: ' + entry.message.substring(0, 100) });
+ fixes.push({ action: 'test-net', params: null, description: 'Run network diagnostics' });
+ }
+
+ // ReferenceError → check bundle
+ if (entry.type === 'ReferenceError') {
+ findings.push({ type: 'missing_class', detail: 'ReferenceError suggests a class or variable is not defined' });
+ fixes.push({ action: 'reload', params: null, description: 'Reload the page to re-initialize all modules' });
+ }
+
+ // Module error → check module loader
+ if (entry.category === 'module') {
+ findings.push({ type: 'module_error', detail: 'Error in module: ' + (entry.module || 'unknown') });
+ fixes.push({ action: 'loadmod', params: entry.module, description: 'Reload module: ' + entry.module });
+ }
+
+ return { found: findings.length > 0, findings, fixes };
+ }
+
+ // ── Fix Registry ──
+
+ _buildFixSuggestion(hash) {
+ const entry = this._errorDB.errors[hash];
+ if (!entry) return null;
+
+ const rca = this._analyzeRootCause(hash);
+ if (!rca.found) {
+ return { confidence: 0, suggestion: 'No known fix pattern for this error', autoApply: false };
+ }
+
+ // Calculate confidence based on frequency
+ let confidence = Math.min(0.5, entry.count / 10);
+ if (entry.count >= 3) confidence = 0.7;
+ if (entry.count >= 10) confidence = 0.9;
+
+ return {
+ confidence: confidence,
+ suggestion: rca.findings.map(f => f.detail).join('; '),
+ fixes: rca.fixes,
+ autoApply: confidence >= 0.9,
+ };
+ }
+
+ // ── Terminal Commands ──
+
+ _cmdStats() {
+ const db = this._errorDB.errors;
+ const keys = Object.keys(db);
+ if (keys.length === 0) {
+ this.terminal.print('📊 Debug Stats: No errors recorded', 'success');
+ return;
+ }
+
+ const totalCount = keys.reduce((sum, k) => sum + db[k].count, 0);
+ const bySeverity = {};
+ const byCategory = {};
+ for (const k of keys) {
+ const e = db[k];
+ bySeverity[e.severity] = (bySeverity[e.severity] || 0) + e.count;
+ byCategory[e.category] = (byCategory[e.category] || 0) + e.count;
+ }
+
+ const clusters = this._clusterErrors();
+ const topClusters = clusters.slice(0, 5);
+
+ this.terminal.print('📊 Debug Stats', 'header');
+ this.terminal.print(' Total errors: ' + totalCount, 'info');
+ this.terminal.print(' Unique errors: ' + keys.length, 'info');
+ this.terminal.print(' Clusters: ' + clusters.length, 'info');
+ this.terminal.print(' Session: ' + this._sessionId, 'text-muted');
+ this.terminal.print(' Supervisor: ' + (this._healthy ? '✅ healthy' : '🔴 degraded'), this._healthy ? 'success' : 'error');
+
+ this.terminal.print('── By Severity ──', 'info');
+ for (const [sev, count] of Object.entries(bySeverity)) {
+ const icon = sev === 'fatal' ? '🔴' : sev === 'error' ? '🟡' : '🟢';
+ this.terminal.print(' ' + icon + ' ' + sev + ': ' + count, sev === 'fatal' ? 'error' : 'warning');
+ }
+
+ this.terminal.print('── By Category ──', 'info');
+ for (const [cat, count] of Object.entries(byCategory)) {
+ this.terminal.print(' • ' + cat + ': ' + count, 'text-muted');
+ }
+
+ if (topClusters.length > 0) {
+ this.terminal.print('── Top Clusters ──', 'info');
+ for (const c of topClusters) {
+ this.terminal.print(' [' + c.hashes.length + ' errors ×' + c.count + '] ' + c.label.substring(0, 80), 'warning');
+ }
+ this.terminal.print(' Run debug-analyze <hash> for details', 'text-muted');
+ }
+ }
+
+ _cmdHistory(args) {
+ const n = (args && args[0]) ? parseInt(args[0]) : 20;
+ const entries = Object.entries(this._errorDB.errors)
+ .sort((a, b) => b[1].lastSeen - a[1].lastSeen)
+ .slice(0, n);
+
+ if (entries.length === 0) {
+ this.terminal.print('No errors recorded', 'success');
+ return;
+ }
+
+ this.terminal.print('── Error History (last ' + entries.length + ') ──', 'header');
+ for (const [hash, e] of entries) {
+ const time = new Date(e.lastSeen).toLocaleTimeString();
+ const icon = e.severity === 'fatal' ? '🔴' : e.severity === 'error' ? '🟡' : '🟢';
+ const msg = e.message.substring(0, 80);
+ this.terminal.print(' ' + icon + ' [' + time + '] ' + msg, e.severity === 'fatal' ? 'error' : 'text-muted');
+ this.terminal.print(' hash=' + hash + ' type=' + e.type + ' cat=' + e.category + ' ×' + e.count, 'text-muted');
+ }
+ this.terminal.print('── End ──', 'info');
+ this.terminal.print('Run debug-analyze <hash> for details', 'text-muted');
+ }
+
+ _cmdAnalyze(args) {
+ const hash = (args && args[0]) || '';
+ if (!hash) {
+ this.terminal.print('Usage: debug-analyze <hash>', 'warning');
+ return;
+ }
+
+ const entry = this._errorDB.errors[hash];
+ if (!entry) {
+ this.terminal.print('Error not found: ' + hash, 'error');
+ return;
+ }
+
+ this.terminal.print('── Error Analysis: ' + hash + ' ──', 'header');
+ this.terminal.print(' Message: ' + entry.message, 'info');
+ this.terminal.print(' Type: ' + entry.type + ' | Severity: ' + entry.severity + ' | Category: ' + entry.category, 'info');
+ this.terminal.print(' Source: ' + (entry.source || '?'), 'text-muted');
+ this.terminal.print(' Module: ' + (entry.module || 'unknown'), 'text-muted');
+ this.terminal.print(' Count: ' + entry.count + ' | First: ' + new Date(entry.firstSeen).toLocaleString() + ' | Last: ' + new Date(entry.lastSeen).toLocaleString(), 'info');
+ this.terminal.print(' Sessions: ' + (entry.sessions || []).length, 'text-muted');
+ if (entry.stack) {
+ this.terminal.print('── Stack Trace (top 10) ──', 'info');
+ this.terminal.print(entry.stack, 'text-muted');
+ }
+
+ // Root-cause analysis
+ const rca = this._analyzeRootCause(hash);
+ if (rca.found) {
+ this.terminal.print('── Root Cause Analysis ──', 'warning');
+ for (const f of rca.findings) {
+ this.terminal.print(' • [' + f.type + '] ' + f.detail, 'warning');
+ }
+ if (rca.fixes.length > 0) {
+ this.terminal.print('── Suggested Fixes ──', 'success');
+ for (const fix of rca.fixes) {
+ this.terminal.print(' → ' + fix.action + (fix.params ? ' ' + fix.params : '') + ' — ' + fix.description, 'success');
+ }
+ }
+ } else {
+ this.terminal.print(' No root cause identified', 'text-muted');
+ }
+
+ // Fix suggestion
+ const fix = this._buildFixSuggestion(hash);
+ if (fix && fix.confidence > 0) {
+ this.terminal.print('── Auto-Fix Confidence: ' + Math.round(fix.confidence * 100) + '% ──', fix.autoApply ? 'success' : 'warning');
+ this.terminal.print(' ' + fix.suggestion, 'info');
+ if (fix.autoApply) {
+ this.terminal.print(' ⚡ Confidence ≥ 90% — fix can be auto-applied with debug-fix ' + hash, 'success');
+ }
+ }
+ }
+
+ _cmdTrends(args) {
+ const days = (args && args[0]) ? parseInt(args[0]) : 7;
+ const cutoff = Date.now() - days * 86400000;
+
+ const entries = Object.values(this._errorDB.errors).filter(e => e.lastSeen >= cutoff);
+ if (entries.length === 0) {
+ this.terminal.print('No errors in the last ' + days + ' days', 'success');
+ return;
+ }
+
+ // Group by day
+ const byDay = {};
+ for (const e of entries) {
+ const day = new Date(e.lastSeen).toISOString().split('T')[0];
+ if (!byDay[day]) byDay[day] = { total: 0, byType: {} };
+ byDay[day].total += e.count;
+ byDay[day].byType[e.type] = (byDay[day].byType[e.type] || 0) + e.count;
+ }
+
+ const sortedDays = Object.keys(byDay).sort();
+ this.terminal.print('── Error Trends (last ' + days + ' days) ──', 'header');
+ for (const day of sortedDays) {
+ const d = byDay[day];
+ const types = Object.entries(d.byType).map(([t, c]) => t + ':' + c).join(', ');
+ this.terminal.print(' ' + day + ': ' + d.total + ' errors (' + types + ')', d.total > 5 ? 'warning' : 'text-muted');
+ }
+
+ // Top error types
+ const typeTotals = {};
+ for (const e of entries) {
+ typeTotals[e.type] = (typeTotals[e.type] || 0) + e.count;
+ }
+ this.terminal.print('── By Type ──', 'info');
+ for (const [type, count] of Object.entries(typeTotals).sort((a, b) => b[1] - a[1])) {
+ this.terminal.print(' • ' + type + ': ' + count, 'text-muted');
+ }
+ }
+
+ _cmdFix(args) {
+ const hash = (args && args[0]) || '';
+ if (!hash) {
+ this.terminal.print('Usage: debug-fix <hash>', 'warning');
+ return;
+ }
+
+ const fix = this._buildFixSuggestion(hash);
+ if (!fix || fix.confidence === 0) {
+ this.terminal.print('No fix available for error: ' + hash, 'warning');
+ return;
+ }
+
+ this.terminal.print('── Fix Suggestion for ' + hash + ' ──', 'header');
+ this.terminal.print(' Confidence: ' + Math.round(fix.confidence * 100) + '%', fix.autoApply ? 'success' : 'warning');
+ this.terminal.print(' Suggestion: ' + fix.suggestion, 'info');
+
+ if (fix.fixes && fix.fixes.length > 0) {
+ this.terminal.print('── Available Fixes ──', 'info');
+ for (const f of fix.fixes) {
+ this.terminal.print(' → ' + f.action + (f.params ? ' ' + f.params : ''), 'success');
+ this.terminal.print(' ' + f.description, 'text-muted');
+ }
+ }
+
+ if (fix.autoApply) {
+ this.terminal.print('⚡ Auto-applying fix...', 'info');
+ this._applyFix(hash, fix);
+ } else {
+ this.terminal.print('Confidence below auto-apply threshold. Run the suggested command manually.', 'warning');
+ }
+ }
+
+ _applyFix(hash, fix) {
+ if (!fix.fixes || fix.fixes.length === 0) return;
+ const firstFix = fix.fixes[0];
+ try {
+ if (firstFix.action === 'loadmod' && this.terminal) {
+ this.terminal.executeCommand('loadmod ' + (firstFix.params || ''));
+ this.terminal.print(' ✅ Applied: loadmod ' + (firstFix.params || ''), 'success');
+ } else if (firstFix.action === 'test-net' && this.terminal) {
+ this.terminal.executeCommand('test-net');
+ this.terminal.print(' ✅ Applied: test-net', 'success');
+ } else if (firstFix.action === 'reload') {
+ this.terminal.print(' ℹ️ Manual action needed: reload the page', 'warning');
+ } else {
+ this.terminal.print(' ℹ️ Cannot auto-apply: ' + firstFix.action, 'warning');
+ }
+ } catch (e) {
+ this.terminal.print(' ❌ Fix failed: ' + e.message, 'error');
+ }
+ }
+
+ _cmdHealth() {
+ this.terminal.print('── DebugSupervisor Health ──', 'header');
+ this.terminal.print(' Status: ' + (this._healthy ? '✅ Healthy' : '🔴 Degraded'), this._healthy ? 'success' : 'error');
+ this.terminal.print(' Session: ' + this._sessionId, 'text-muted');
+ this.terminal.print(' Errors tracked: ' + Object.keys(this._errorDB.errors).length + ' unique', 'info');
+ this.terminal.print(' Max errors: ' + this._maxErrors, 'text-muted');
+ this.terminal.print(' TTL: ' + this._ttlDays + ' days', 'text-muted');
+ this.terminal.print(' Interceptors: ' + (this._interceptorInstalled ? '✅ active' : '🔴 inactive'), this._interceptorInstalled ? 'success' : 'error');
+ this.terminal.print(' Quota: ' + (this._checkQuota() ? '✅ available' : '🔴 low'), this._checkQuota() ? 'success' : 'error');
+ }
+
+ // ── Dashboard Data ──
+
+ getDashboardData() {
+ const db = this._errorDB.errors;
+ const keys = Object.keys(db);
+ const totalCount = keys.reduce((sum, k) => sum + db[k].count, 0);
+ const clusters = this._clusterErrors().slice(0, 5);
+
+ return {
+ totalUnique: keys.length,
+ totalCount: totalCount,
+ clusters: clusters.map(c => ({ label: c.label.substring(0, 60), count: c.count, hashes: c.hashes.length })),
+ healthy: this._healthy,
+ topType: this._getTopType(db),
+ topCategory: this._getTopCategory(db),
+ };
+ }
+
+ _getTopType(db) {
+ const freq = {};
+ for (const e of Object.values(db)) {
+ freq[e.type] = (freq[e.type] || 0) + e.count;
+ }
+ const sorted = Object.entries(freq).sort((a, b) => b[1] - a[1]);
+ return sorted.length > 0 ? sorted[0][0] : null;
+ }
+
+ _getTopCategory(db) {
+ const freq = {};
+ for (const e of Object.values(db)) {
+ freq[e.category] = (freq[e.category] || 0) + e.count;
+ }
+ const sorted = Object.entries(freq).sort((a, b) => b[1] - a[1]);
+ return sorted.length > 0 ? sorted[0][0] : null;
+ }
+}
diff --git a/modules/core/module-duckduckgo.js b/modules/core/module-duckduckgo.js
deleted file mode 100644
index c443b72..0000000
--- a/modules/core/module-duckduckgo.js
+++ /dev/null
@@ -1,38 +0,0 @@
-// Extracted from newOS4 index.html — DuckDuckGoModule
- class DuckDuckGoModule {
- constructor() {
- this.commands = {};
- this.terminal = null;
- }
- getHelpInfo() {
- return {
- 'ddg': 'ddg <query> — Search DuckDuckGo (opens query in new tab)',
- 'duck': 'duck <query> — Alias for ddg',
- };
- }
- registerCommands() {
- this.commands['ddg'] = (args) => this.search(args);
- this.commands['duck'] = (args) => this.search(args);
- }
- async search(args) {
- if (!args[0]) {
- this.terminal.print("Usage: ddg <search-query>", "warning");
- this.terminal.print("Example: ddg javascript tutorials", "info");
- this.terminal.print("Note: Due to CORS, results may be limited. Try 'search' command for better results.", "warning");
- return;
- }
- const query = args.join(' ');
- this.terminal.print(`🦆 DuckDuckGo Search: ${query}`, "info");
-
- // DuckDuckGo doesn't have a public API, so we provide alternatives
- this.terminal.print("⚠️ DuckDuckGo doesn't provide a public API for direct access.", "warning");
- this.terminal.print("Opening search in new tab...", "info");
-
- // Open in new tab as fallback
- const ddgUrl = `https://duckduckgo.com/?q=${encodeURIComponent(query)}`;
- window.open(ddgUrl, '_blank');
-
- this.terminal.print(`\n💡 Alternative: Use 'search <query>' for SearXNG (privacy-focused)`, "info");
- this.terminal.print(`🔗 Direct link: ${ddgUrl}`, "info");
- }
- }
diff --git a/modules/core/module-dynamicprompts.js b/modules/core/module-dynamicprompts.js
deleted file mode 100644
index ff8c5ea..0000000
--- a/modules/core/module-dynamicprompts.js
+++ /dev/null
@@ -1,102 +0,0 @@
-// Extracted from newOS4 index.html — DynamicPromptsModule
- class DynamicPromptsModule {
- constructor() {
- this.commands = {}; this.terminal = null;
- this.prompts = this.loadPrompts(); this.variables = {};
- }
- getHelpInfo() {
- return {
- 'prompt': 'prompt <name> [additional-text] — Use a saved prompt template',
- 'prompt-save': 'prompt-save <name> <template> — Save a new prompt template',
- 'prompt-list': 'prompt-list — List all saved prompt templates',
- 'prompt-delete': 'prompt-delete <name> — Delete a saved prompt',
- 'prompt-var': 'prompt-var <name> <value> — Set a template variable',
- 'prompt-vars': 'prompt-vars — List all template variables'
- };
- }
- loadPrompts() {
- try {
- const saved = localStorage.getItem('webx11_prompts');
- return saved ? JSON.parse(saved) : {};
- } catch (e) { return {}; }
- }
- savePrompts() {
- localStorage.setItem('webx11_prompts', JSON.stringify(this.prompts));
- }
- registerCommands() {
- this.commands['prompt'] = (args) => this.usePrompt(args);
- this.commands['prompt-save'] = (args) => this.savePrompt(args);
- this.commands['prompt-list'] = () => this.listPrompts();
- this.commands['prompt-delete'] = (args) => this.deletePrompt(args);
- this.commands['prompt-var'] = (args) => this.setVariable(args);
- this.commands['prompt-vars'] = () => this.listVariables();
- }
- usePrompt(args) {
- if (!args[0]) {
- this.terminal.print("Usage: prompt <name> [additional-text]", "warning"); return;
- }
- const name = args[0], additional = args.slice(1).join(' ');
- if (!this.prompts[name]) {
- this.terminal.print(`Prompt '${name}' not found`, "error"); return;
- }
- let template = this.prompts[name].template;
- Object.entries(this.variables).forEach(([k, v]) => {
- template = template.replace(new RegExp(`\\{${k}\\}`, 'g'), v);
- });
- const finalPrompt = additional ? `${template} ${additional}` : template;
- this.terminal.print("=== Generated Prompt ===", "info");
- this.terminal.printHTML(`<div class="output-line ollama" style="margin-top: 5px;">
- <button class="copy-btn" onclick="navigator.clipboard.writeText(this.parentElement.innerText.replace('Copy','').trim())">Copy</button>
- ${finalPrompt}
- </div>`);
- this.terminal.print("\nUse with: ai <generated-prompt>", "info");
- }
- savePrompt(args) {
- if (args.length < 2) {
- this.terminal.print("Usage: prompt-save <name> <template>", "warning"); return;
- }
- const name = args[0], template = args.slice(1).join(' ');
- this.prompts[name] = { template, created: new Date().toISOString() };
- this.savePrompts();
- this.terminal.print(`Prompt '${name}' saved`, "success");
- }
- listPrompts() {
- const names = Object.keys(this.prompts);
- if (names.length === 0) {
- this.terminal.print("No saved prompts", "info"); return;
- }
- this.terminal.print("=== Saved Prompts ===", "info");
- names.forEach(name => {
- const p = this.prompts[name];
- this.terminal.print(`📝 ${name}: ${p.template.substring(0, 50)}...`);
- });
- }
- deletePrompt(args) {
- if (!args[0]) {
- this.terminal.print("Usage: prompt-delete <name>", "warning"); return;
- }
- if (this.prompts[args[0]]) {
- delete this.prompts[args[0]];
- this.savePrompts();
- this.terminal.print(`Prompt '${args[0]}' deleted`, "success");
- } else {
- this.terminal.print(`Prompt '${args[0]}' not found`, "error");
- }
- }
- setVariable(args) {
- if (args.length < 2) {
- this.terminal.print("Usage: prompt-var <name> <value>", "warning"); return;
- }
- const name = args[0], value = args.slice(1).join(' ');
- this.variables[name] = value;
- this.terminal.print(`Variable set: ${name} = ${value}`, "success");
- }
- listVariables() {
- const names = Object.keys(this.variables);
- if (names.length === 0) {
- this.terminal.print("No variables set", "info"); return;
- }
- this.terminal.print("=== Variables ===", "info");
- names.forEach(name => { this.terminal.print(`${name}: ${this.variables[name]}`); });
- }
- }
diff --git a/modules/core/module-io.js b/modules/core/module-io.js
deleted file mode 100644
index d7ff996..0000000
--- a/modules/core/module-io.js
+++ /dev/null
@@ -1,80 +0,0 @@
-// Extracted from newOS4 index.html — ModuleIOModule
- class ModuleIOModule {
- constructor() { this.commands = {}; this.terminal = null; }
- getHelpInfo() {
- return {
- 'export-mods': 'export-mods — Export all loaded modules to a JSON file',
- 'import-mods': 'import-mods <filename> — Import modules from a JSON file',
- 'backup': 'backup — Create a full system backup',
- 'restore': 'restore <filename> — Restore system from a backup file',
- 'share-mod': 'share-mod <module-name> — Share a module as JSON to clipboard'
- };
- }
- registerCommands() {
- this.commands['export-mods'] = () => this.exportModules();
- this.commands['import-mods'] = (args) => this.importModules(args);
- this.commands['backup'] = () => this.fullBackup();
- this.commands['restore'] = (args) => this.restoreBackup(args);
- this.commands['share-mod'] = (args) => this.shareModule(args);
- }
- exportModules() {
- const registry = this.terminal.moduleManager.moduleRegistry;
- if (registry.length === 0) {
- this.terminal.print("No modules to export", "warning"); return;
- }
- const exportData = { version: '4.1', exported: new Date().toISOString(), modules: registry };
- const json = JSON.stringify(exportData, null, 2);
- const blob = new Blob([json], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url; a.download = `webx11-modules-${Date.now()}.json`; a.click();
- URL.revokeObjectURL(url);
- this.terminal.print(`Exported ${registry.length} module(s)`, "success");
- }
- importModules(args) {
- this.terminal.print("⚠️ To import modules:", "warning");
- this.terminal.print("1. Upload the JSON file using 'upload'", "info");
- this.terminal.print("2. Use: import-mods <filename>", "info");
- }
- async fullBackup() {
- const backup = {
- version: '4.1', timestamp: new Date().toISOString(),
- filesystem: localStorage.getItem('webx11_filesystem'),
- modules: localStorage.getItem('webx11_modules'),
- ollama: {
- endpoint: localStorage.getItem('webx11_ollama_endpoint'),
- model: localStorage.getItem('webx11_ollama_model')
- }, config: {}
- };
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i);
- if (key.startsWith('webx11_')) { backup.config[key] = localStorage.getItem(key); }
- }
- const json = JSON.stringify(backup, null, 2);
- const blob = new Blob([json], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url; a.download = `webx11-backup-${Date.now()}.json`; a.click();
- URL.revokeObjectURL(url);
- this.terminal.print("Full backup created", "success");
- }
- restoreBackup(args) {
- this.terminal.print("⚠️ To restore backup:", "warning");
- this.terminal.print("1. Upload the backup JSON file", "info");
- this.terminal.print("2. Use: restore <filename>", "info");
- }
- shareModule(args) {
- if (!args[0]) {
- this.terminal.print("Usage: share-mod <module-name>", "warning"); return;
- }
- const mod = this.terminal.moduleManager.moduleRegistry.find(m => m.name === args[0]);
- if (!mod) {
- this.terminal.print(`Module '${args[0]}' not found`, "error"); return;
- }
- const shareData = { name: mod.name, url: mod.url, shared: new Date().toISOString() };
- const json = JSON.stringify(shareData, null, 2);
- navigator.clipboard.writeText(json).then(() => {
- this.terminal.print("Module info copied to clipboard", "success");
- }).catch(() => { this.terminal.print(json, "info"); });
- }
- }
diff --git a/modules/core/module-leases.js b/modules/core/module-leases.js
index 7489c92..60e8d98 100644
--- a/modules/core/module-leases.js
+++ b/modules/core/module-leases.js
@@ -38,10 +38,15 @@ class LeaseModule {
this.terminal.print('📜 Lease system loaded', 'info');
}
createLease(args) {
+ // Parse --write flag
+ const writeIdx = args.indexOf('--write');
+ const writeAccess = writeIdx !== -1;
+ if (writeIdx !== -1) args.splice(writeIdx, 1);
+
const workerId = args[1];
const duration = parseInt(args[2]) || 30;
const task = args.slice(3).join(' ') || 'generic task';
- if (!workerId) return this.terminal.print('Usage: lease-create <agent-id> [duration_sec] [task]', 'error');
+ if (!workerId) return this.terminal.print('Usage: lease-create <agent-id> [duration_sec] [task] [--write]', 'error');
const lease = {
id: `lease-${Date.now()}`,
@@ -51,7 +56,8 @@ class LeaseModule {
createdAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + duration * 1000).toISOString(),
status: 'active',
- duration
+ duration,
+ write_access: writeAccess
};
this._leases[lease.id] = lease;
this.terminal.fs.writeFile(`leases/${lease.id}.json`, JSON.stringify(lease));
@@ -79,9 +85,14 @@ class LeaseModule {
this.terminal.print(`📜 Leases: ${active.length} active, ${expired.length} expired, ${revoked.length} revoked`, 'header');
active.forEach(l => {
const remaining = Math.max(0, Math.round((new Date(l.expiresAt) - Date.now()) / 1000));
- this.terminal.print(` ${l.id} → ${l.workerId} (${remaining}s remain) — ${l.task.substring(0, 40)}`, 'info');
+ const writeTag = l.write_access ? ' ✏️write' : ' 🔍read';
+ this.terminal.print(` ${l.id} → ${l.workerId} (${remaining}s remain)${writeTag} — ${l.task.substring(0, 40)}`, 'info');
});
}
+ hasWriteAccess(agentId) {
+ // Check if any active lease grants write access for this agent
+ return Object.values(this._leases).some(l => l.workerId === agentId && l.status === 'active' && l.write_access === true);
+ }
revokeLease(id) {
if (!this._leases[id]) return this.terminal.print(`❌ Lease ${id} not found`, 'error');
this._leases[id].status = 'revoked';
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index 7bb7c89..52af62a 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 = '2026-06-30-v4.5.83';
+ const LATEST_VER = 'v4.6.10';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
diff --git a/modules/core/module-memory-graph.js b/modules/core/module-memory-graph.js
index 2382a4a..ffc79e0 100644
--- a/modules/core/module-memory-graph.js
+++ b/modules/core/module-memory-graph.js
@@ -12,6 +12,11 @@ class MemoryGraphModule {
this._debounceTimer = null;
this._initialized = false;
this._totalNodeAccesses = 0;
+ // v4.5.89: Decay configuration
+ this._decayDays = 30;
+ this._decayMinUses = 3;
+ this._decayRate = 0.01; // Daily decay rate
+ this._lastDecayRun = 0;
}
getHelpInfo() {
@@ -21,6 +26,10 @@ class MemoryGraphModule {
'memgraph-stats': 'memgraph-stats — Most connected nodes, strongest edges, cluster info',
'memgraph-export': 'memgraph-export — Export graph as JSON',
'memgraph-prune': 'memgraph-prune — Remove stale low-weight edges (>30d, <3 accesses)',
+ 'memgraph-learn': 'memgraph-learn <entity> <fact> — Add a fact to the memory graph',
+ 'memgraph-session': 'memgraph-session [date] — Show graph nodes for a session (default: today)',
+ 'memgraph-agent': 'memgraph-agent <agentId> — Show namespaced nodes for an agent',
+ 'memgraph-decay-config': 'memgraph-decay-config <days> <min_uses> — Configure decay policy (default: 30d, 3 uses)',
};
}
@@ -30,6 +39,11 @@ class MemoryGraphModule {
this.commands['memgraph-stats'] = () => this.showStats();
this.commands['memgraph-export'] = () => this.exportGraph();
this.commands['memgraph-prune'] = () => this.prune();
+ this.commands['memgraph-learn'] = (args) => this.learn(args);
+ this.commands['memgraph-session'] = (args) => this.showSession(args.join(' '));
+ this.commands['memgraph-agent'] = (args) => this.showAgent(args.join(' '));
+ // v4.5.89: Decay configuration
+ this.commands['memgraph-decay-config'] = (args) => this.decayConfig(args);
}
init(terminal) {
@@ -38,13 +52,16 @@ class MemoryGraphModule {
this._initialized = true;
this.terminal.print('🧠 Memory Graph loaded — cross-session context active', 'info');
// Log startup for graph
- this.addNode('session', `session-${Date.now()}`, `Session ${new Date().toLocaleDateString()}`);
+ // v4.5.89 fix: Use day-floored key to match edge source keys (P0-1 from review)
+ this.addNode('session', `session-${Math.floor(Date.now() / 86400000)}`, `Session ${new Date().toLocaleDateString()}`);
}
// ─── Node API ───
- addNode(type, id, label, metadata = {}) {
- const key = `${type}:${id}`;
+ addNode(type, id, label, metadata = {}, agentId = null) {
+ // v4.5.88: Support agent namespacing with dual-lookup
+ const ns = agentId ? `agent:${agentId}` : 'global';
+ const key = agentId ? `${ns}:${type}:${id}` : `${type}:${id}`;
if (!this._nodes[key]) {
this._nodes[key] = {
id: key,
@@ -53,7 +70,9 @@ class MemoryGraphModule {
metadata: metadata || {},
created: new Date().toISOString(),
updated: new Date().toISOString(),
- access_count: 1
+ access_count: 1,
+ namespace: ns,
+ source_session: `session-${Math.floor(Date.now() / 86400000)}`
};
} else {
this._nodes[key].updated = new Date().toISOString();
@@ -67,8 +86,14 @@ class MemoryGraphModule {
return key;
}
- getNode(type, id) {
- return this._nodes[`${type}:${id}`] || null;
+ // v4.5.88: Dual-lookup — try namespaced key first, fall back to global
+ getNode(type, id, agentId = null) {
+ if (agentId) {
+ const nsKey = `agent:${agentId}:${type}:${id}`;
+ if (this._nodes[nsKey]) return this._nodes[nsKey];
+ }
+ const globalKey = `${type}:${id}`;
+ return this._nodes[globalKey] || null;
}
findNodes(query) {
@@ -82,7 +107,8 @@ class MemoryGraphModule {
// ─── Edge API ───
- addEdge(sourceKey, targetKey, type, weight = 1) {
+ // v4.5.88: addEdge tracks source_agents for multi-agent attribution
+ addEdge(sourceKey, targetKey, type, weight = 1, agentId = null) {
// Check if edge already exists
let edge = this._edges.find(e =>
e.source === sourceKey && e.target === targetKey && e.type === type
@@ -91,6 +117,7 @@ class MemoryGraphModule {
edge.weight = (edge.weight + weight) / 2;
edge.last_used = new Date().toISOString();
edge.use_count++;
+ if (agentId) edge.source_agents = [...new Set([...(edge.source_agents || []), agentId])];
} else {
this._edges.push({
source: sourceKey,
@@ -99,7 +126,8 @@ class MemoryGraphModule {
weight,
created: new Date().toISOString(),
last_used: new Date().toISOString(),
- use_count: 1
+ use_count: 1,
+ source_agents: agentId ? [agentId] : []
});
}
this._debounceSave();
@@ -107,11 +135,12 @@ class MemoryGraphModule {
// ─── Event-based Learning ───
- recordFileAccess(path) {
+ // v4.5.89 fix: Thread agentId through record* methods (P1-3 from review)
+ recordFileAccess(path, agentId = null) {
const name = path.split('/').pop() || path;
- const nodeKey = this.addNode('file', path, name);
+ const nodeKey = this.addNode('file', path, name, {}, agentId);
const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
- this.addEdge(sessionKey, nodeKey, 'accessed', 0.5);
+ this.addEdge(sessionKey, nodeKey, 'accessed', 0.5, agentId);
// Extract file extension edge
const ext = path.split('.').pop();
@@ -121,10 +150,10 @@ class MemoryGraphModule {
}
}
- recordToolCall(toolName, success) {
- const nodeKey = this.addNode('tool', toolName, toolName);
+ recordToolCall(toolName, success, agentId = null) {
+ const nodeKey = this.addNode('tool', toolName, toolName, {}, agentId);
const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
- this.addEdge(sessionKey, nodeKey, success ? 'used' : 'failed', success ? 0.5 : 0.8);
+ this.addEdge(sessionKey, nodeKey, success ? 'used' : 'failed', success ? 0.5 : 0.8, agentId);
}
recordConcept(concept, filePath) {
@@ -134,15 +163,16 @@ class MemoryGraphModule {
this.addEdge(fileKey, conceptKey, 'tagged_as', 0.7);
}
+ // v4.5.88: recordTask now passes agentId for namespacing + adds source_session link
recordTask(task, result, agentId) {
const taskId = `task-${Date.now()}`;
const taskKey = this.addNode('task', taskId, task.substring(0, 60), {
full: task,
result: result?.substring(0, 100),
agent: agentId
- });
+ }, agentId);
const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
- this.addEdge(sessionKey, taskKey, 'executed', 0.6);
+ this.addEdge(sessionKey, taskKey, 'executed', 0.6, agentId);
// Extract keywords as concepts
const words = task.toLowerCase().split(/\s+/).filter(w => w.length > 3);
@@ -201,18 +231,38 @@ class MemoryGraphModule {
}
}
- return lines.join('\\n');
+ return lines.join('\n');
}
getContextForPrompt() {
const summary = this.getContextSummary();
if (!summary) return '';
- return `[Memory Graph Context]\\n${summary}\\n`;
+ // Suppress noise: don't inject when graph is nearly empty
+ const totalNodes = Object.keys(this._nodes).length;
+ const totalEdges = this._edges.length;
+ if (totalNodes <= 2 && totalEdges === 0) return '';
+ return `[Memory Graph Context]\n${summary}\n`;
+ }
+
+ // Alias for orchestrator compatibility (orchestrator calls getStats, not showStats)
+ getStats() {
+ const types = {};
+ for (const n of Object.values(this._nodes)) {
+ types[n.type] = (types[n.type] || 0) + 1;
+ }
+ return {
+ nodes: Object.keys(this._nodes).length,
+ edges: this._edges.length,
+ types: types,
+ totalAccesses: this._totalNodeAccesses
+ };
}
// ─── Commands ───
showStatus() {
+ // v4.5.89: Lazy decay — run applyDecay() on status check
+ this.applyDecay();
const types = {};
for (const n of Object.values(this._nodes)) {
types[n.type] = (types[n.type] || 0) + 1;
@@ -227,6 +277,8 @@ class MemoryGraphModule {
this.terminal.print(` Edges: ${this._edges.length}`, 'info');
this.terminal.print(` Total accesses: ${this._totalNodeAccesses}`, 'info');
this.terminal.print(` Path: ${this._indexPath}`, 'info');
+ // v4.5.89: Show decay config
+ this.terminal.print(` Decay: ${this._decayDays}d threshold, ${this._decayMinUses} min uses, rate ${this._decayRate}/day`, 'text-muted');
}
search(query) {
@@ -346,6 +398,181 @@ class MemoryGraphModule {
this.terminal.print(`🧹 Pruned ${pruned} edges and ${nodesPruned} orphan nodes`, nodesPruned > 0 ? 'warning' : 'info');
}
+ // v4.5.89: Apply decay scores to nodes based on age and access patterns
+ applyDecay() {
+ const now = Date.now();
+ if (now - this._lastDecayRun < 3600000) return;
+ this._lastDecayRun = now;
+
+ // v4.5.89b fix: Compute decay rate from _decayDays (P0 from review)
+ const effectiveRate = 0.9 / this._decayDays;
+ let decayed = 0;
+ let removed = 0;
+
+ for (const key of Object.keys(this._nodes)) {
+ const node = this._nodes[key];
+ const ageDays = (now - new Date(node.updated || node.created).getTime()) / 86400000;
+ node.decay_score = Math.max(0, 1.0 - (ageDays * effectiveRate));
+ if (node.decay_score < 0.1 && node.access_count < this._decayMinUses) {
+ if (node.type === 'session') continue;
+ if (node.namespace && node.namespace.startsWith('agent:')) continue;
+ delete this._nodes[key];
+ removed++;
+ continue;
+ }
+ decayed++;
+ }
+
+ this._edges = this._edges.filter(e => {
+ const ageDays = (now - new Date(e.last_used).getTime()) / 86400000;
+ e.decay_score = Math.max(0, 1.0 - (ageDays * effectiveRate));
+ return !(e.decay_score < 0.1 && (e.use_count || 1) < this._decayMinUses);
+ });
+
+ if (removed > 0) {
+ this._debounceSave();
+ console.log('[MemoryGraph] Decay: ' + decayed + ' scored, ' + removed + ' auto-removed');
+ }
+
+ // v4.5.90: Check for stale skills
+ var staleSkills = this.getStaleSkills();
+ if (staleSkills.length > 0) {
+ this.terminal.print('🧠 Stale skills: ' + staleSkills.length + ' (' + staleSkills.map(function(s) { return s.name; }).join(', ') + ')', 'warning');
+ }
+ }
+
+ // v4.5.90: Record skill usage for decay tracking
+ recordSkillUse(name, success) {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ if (!index[name]) index[name] = { useCount: 0, successCount: 0 };
+ index[name].lastUsed = new Date().toISOString();
+ index[name].useCount = (index[name].useCount || 0) + 1;
+ if (success !== false) {
+ index[name].successCount = (index[name].successCount || 0) + 1;
+ }
+ fs.fsWriteJSON('/skills/index.json', index);
+ } catch (e) {
+ console.warn('[MemoryGraph] Skill use tracking error:', e.message);
+ }
+ }
+
+ // v4.5.90: Get stale skills (unused >_decayDays, useCount == 0, not immune)
+ getStaleSkills() {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return [];
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ const now = Date.now();
+ // v4.5.90b fix: Use configurable _decayDays (P1 from review)
+ const staleThreshold = this._decayDays * 24 * 60 * 60 * 1000;
+ var stale = [];
+ for (var name of Object.keys(index)) {
+ var stats = index[name] || {};
+ if (stats.immune) continue;
+ var useCount = stats.useCount || 0;
+ var lastUsed = stats.lastUsed ? new Date(stats.lastUsed).getTime() : 0;
+ if (useCount === 0 && (now - lastUsed > staleThreshold || lastUsed === 0)) {
+ var daysUnused = lastUsed > 0 ? Math.floor((now - lastUsed) / 86400000) : 999;
+ stale.push({ name: name, daysUnused: daysUnused });
+ }
+ }
+ return stale;
+ } catch (e) {
+ return [];
+ }
+ }
+
+ // v4.5.89: Configure decay policy
+ decayConfig(args) {
+ if (args.length < 2) {
+ this.terminal.print('Usage: memgraph-decay-config <days> <min_uses>', 'error');
+ this.terminal.print(' Current: ' + this._decayDays + 'd threshold, ' + this._decayMinUses + ' min uses', 'info');
+ this.terminal.print(' Decay rate: ' + this._decayRate + '/day (0.01 = 1% per day)', 'info');
+ return;
+ }
+ const days = parseInt(args[0]);
+ const minUses = parseInt(args[1]);
+ if (isNaN(days) || days < 1) return this.terminal.print('Days must be >= 1', 'error');
+ if (isNaN(minUses) || minUses < 0) return this.terminal.print('Min uses must be >= 0', 'error');
+ this._decayDays = days;
+ this._decayMinUses = minUses;
+ this._debounceSave();
+ this.terminal.print('✅ Decay config: ' + days + 'd threshold, ' + minUses + ' min uses', 'success');
+ }
+
+ learn(args) {
+ if (!args || args.length < 2) return this.terminal.print('Usage: memgraph-learn <entity> <fact>', 'error');
+ const entity = args[0];
+ const fact = args.slice(1).join(' ');
+ this.addNode('concept', entity, entity);
+ const factKey = this.addNode('fact', entity + '-' + Date.now(), fact);
+ const conceptKey = `concept:${entity}`;
+ this.addEdge(conceptKey, factKey, 'has_fact', 0.9);
+ this.terminal.print(`✅ Learned: ${entity} → ${fact}`, 'success');
+ // v4.5.89b fix: Index only the new fact, not all facts (P2 from review — O(n) → O(1))
+ const nvfs = this.terminal.moduleManager.modules['NeuralVFSModule'];
+ if (nvfs && typeof nvfs.indexGraphFacts === 'function') {
+ nvfs.indexSingleFact(factKey, entity, fact);
+ }
+ }
+
+ // v4.5.88: Show graph nodes for a specific session
+ showSession(dateStr) {
+ let sessionKey;
+ if (dateStr) {
+ const d = new Date(dateStr);
+ if (isNaN(d.getTime())) return this.terminal.print('Usage: memgraph-session [YYYY-MM-DD]', 'error');
+ sessionKey = `session-${Math.floor(d.getTime() / 86400000)}`;
+ } else {
+ sessionKey = `session-${Math.floor(Date.now() / 86400000)}`;
+ }
+
+ const sessionNodes = Object.values(this._nodes).filter(function(n) {
+ return n.source_session === sessionKey || n.type === 'session' && n.id.includes(sessionKey);
+ });
+
+ this.terminal.print('📅 Session Graph: ' + sessionKey, 'header');
+ if (sessionNodes.length === 0) {
+ this.terminal.print(' No nodes found for this session', 'info');
+ return;
+ }
+ this.terminal.print(' ' + sessionNodes.length + ' nodes:', 'info');
+ for (var n of sessionNodes.slice(0, 15)) {
+ var ns = n.namespace || 'global';
+ this.terminal.print(' ' + n.type + ':' + n.label + ' (' + ns + ', ' + n.access_count + 'x)', 'info');
+ }
+ var sessionEdges = this._edges.filter(function(e) {
+ return sessionNodes.some(function(n) { return n.id === e.source || n.id === e.target; });
+ });
+ this.terminal.print(' ' + sessionEdges.length + ' edges', 'info');
+ }
+
+ // v4.5.88: Show namespaced nodes for a specific agent
+ showAgent(agentId) {
+ if (!agentId) return this.terminal.print('Usage: memgraph-agent <agentId>', 'error');
+ var ns = 'agent:' + agentId;
+ var agentNodes = Object.values(this._nodes).filter(function(n) {
+ return n.namespace === ns;
+ });
+ var agentEdges = this._edges.filter(function(e) {
+ return (e.source_agents || []).includes(agentId);
+ });
+
+ this.terminal.print('🤖 Agent Memory: ' + agentId, 'header');
+ if (agentNodes.length === 0) {
+ this.terminal.print(' No namespaced nodes for this agent', 'info');
+ this.terminal.print(' (Agent may have used global namespace)', 'text-muted');
+ return;
+ }
+ this.terminal.print(' ' + agentNodes.length + ' nodes, ' + agentEdges.length + ' edges:', 'info');
+ for (var n of agentNodes.slice(0, 15)) {
+ this.terminal.print(' ' + n.type + ':' + n.label + ' (' + n.access_count + 'x)', 'info');
+ }
+ }
+
// ─── Persistence ───
_loadFromVFS() {
@@ -357,18 +584,48 @@ class MemoryGraphModule {
const exportData = fs.fsReadJSON(`${this._indexPath}export.json`);
if (exportData && exportData.nodes) {
for (const n of exportData.nodes) {
+ // v4.5.89 fix: Migrate old-format nodes (P1-1 from review)
+ if (!n.namespace) n.namespace = 'global';
+ if (!n.source_session) {
+ const createdMs = new Date(n.created).getTime();
+ n.source_session = isNaN(createdMs) ? null : `session-${Math.floor(createdMs / 86400000)}`;
+ }
this._nodes[n.id] = n;
}
this._edges = exportData.edges || [];
+ for (const e of this._edges) {
+ if (!e.source_agents) e.source_agents = [];
+ }
this._totalNodeAccesses = (exportData.stats?.totalAccesses) || 0;
return;
}
// Fallback: load nodes and edges separately
const nodes = fs.fsReadJSON(`${this._indexPath}nodes.json`);
- if (nodes) this._nodes = nodes;
+ if (nodes) {
+ this._nodes = nodes;
+ for (const n of Object.values(this._nodes)) {
+ if (!n.namespace) n.namespace = 'global';
+ if (!n.source_session) {
+ const createdMs = new Date(n.created).getTime();
+ n.source_session = isNaN(createdMs) ? null : `session-${Math.floor(createdMs / 86400000)}`;
+ }
+ }
+ }
const edges = fs.fsReadJSON(`${this._indexPath}edges.json`);
- if (edges) this._edges = edges;
+ if (edges) {
+ this._edges = edges;
+ for (const e of this._edges) {
+ if (!e.source_agents) e.source_agents = [];
+ }
+ }
+ // v4.5.89b fix: Load persisted decay config (P1 from review)
+ const config = fs.fsReadJSON(`${this._indexPath}config.json`);
+ if (config) {
+ if (typeof config.decayDays === 'number') this._decayDays = config.decayDays;
+ if (typeof config.decayMinUses === 'number') this._decayMinUses = config.decayMinUses;
+ if (typeof config.lastDecayRun === 'number') this._lastDecayRun = config.lastDecayRun;
+ }
} catch (e) {
console.warn('[MemoryGraph] Load error (fresh start):', e.message);
}
@@ -383,6 +640,12 @@ class MemoryGraphModule {
}
fs.fsWriteJSON(`${this._indexPath}nodes.json`, this._nodes);
fs.fsWriteJSON(`${this._indexPath}edges.json`, this._edges);
+ // v4.5.89b fix: Persist decay config (P1 from review)
+ fs.fsWriteJSON(`${this._indexPath}config.json`, {
+ decayDays: this._decayDays,
+ decayMinUses: this._decayMinUses,
+ lastDecayRun: this._lastDecayRun
+ });
} catch (e) {
console.error('[MemoryGraph] Save error:', e.message);
}
diff --git a/modules/core/module-my.js b/modules/core/module-my.js
deleted file mode 100644
index 5fc13ca..0000000
--- a/modules/core/module-my.js
+++ /dev/null
@@ -1,82 +0,0 @@
-class MyModule {
- constructor() {
- this.commands = {}; this.terminal = null; this.userData = {};
- try { const d = localStorage.getItem('webx11_my_data'); if (d) this.userData = JSON.parse(d); } catch (e) {}
- }
- getHelpInfo() {
- return {
- 'my': 'my — Show module info and stored key count',
- 'my-set': 'my-set <key> <value> — Store a key-value pair',
- 'my-get': 'my-get <key> — Retrieve a stored value by key',
- 'my-list': 'my-list — List all stored keys',
- 'my-export': 'my-export — Export all stored data as JSON',
- 'my-import': 'my-import <json> — Import data from JSON string',
- 'my-clear': 'my-clear — Clear all stored data'
- };
- }
- _save() {
- try { localStorage.setItem('webx11_my_data', JSON.stringify(this.userData)); } catch (e) {}
- }
- registerCommands() {
- this.commands['my'] = () => this.showInfo();
- this.commands['my-set'] = (args) => this.setData(args);
- this.commands['my-get'] = (args) => this.getData(args);
- this.commands['my-list'] = () => this.listData();
- this.commands['my-export'] = () => this.exportData();
- this.commands['my-import'] = (args) => this.importData(args);
- this.commands['my-clear'] = () => this.clearData();
- }
- showInfo() {
- this.terminal.print("=== My Module v2.0 ===", "info");
- this.terminal.print("Personal workspace for custom commands");
- this.terminal.print(`Stored keys: ${Object.keys(this.userData).length}`, "success");
- }
- setData(args) {
- if (args.length < 2) {
- this.terminal.print("Usage: my-set <key> <value>", "warning"); return;
- }
- const key = args[0], value = args.slice(1).join(' ');
- this.userData[key] = { value, timestamp: new Date().toISOString() };
- this._save();
- this.terminal.print(`Saved: ${key} = ${value}`, "success");
- }
- getData(args) {
- if (!args[0]) {
- this.terminal.print("Usage: my-get <key>", "warning"); return;
- }
- if (this.userData[args[0]]) {
- this.terminal.print(`${args[0]}: ${this.userData[args[0]].value}`, "info");
- } else {
- this.terminal.print(`Key '${args[0]}' not found`, "error");
- }
- }
- listData() {
- const keys = Object.keys(this.userData);
- if (!keys.length) return this.terminal.print('No data stored.', 'info');
- this.terminal.print('=== My Data ===', 'info');
- keys.forEach(k => {
- const v = this.userData[k];
- const type = typeof v.value;
- this.terminal.print(` ${k} (${type}): ${String(v.value).substring(0, 40)}`);
- });
- }
- exportData() {
- this.terminal.print(JSON.stringify(this.userData, null, 2));
- }
- importData(args) {
- if (!args[0]) return this.terminal.print('Usage: my-import <json>', 'warning');
- try {
- const data = JSON.parse(args.join(' '));
- Object.assign(this.userData, data);
- this._save();
- this.terminal.print(`Imported ${Object.keys(data).length} keys`, 'success');
- } catch (e) {
- this.terminal.print('Invalid JSON: ' + e.message, 'error');
- }
- }
- clearData() {
- this.userData = {};
- localStorage.removeItem('webx11_my_data');
- this.terminal.print("Personal data cleared", "success");
- }
-}
diff --git a/modules/core/module-neural-vfs.js b/modules/core/module-neural-vfs.js
index 32fb0eb..1019299 100644
--- a/modules/core/module-neural-vfs.js
+++ b/modules/core/module-neural-vfs.js
@@ -33,7 +33,7 @@ class NeuralVFSModule {
this._loadAliases();
// Deep Indexing toggle
this._deepIndexEnabled = localStorage.getItem('webx11_nvfs_deep') === 'true';
- this._deepModel = localStorage.getItem('webx11_nvfs_deep_model') || 'ministral-3:3b-cloud';
+ this._deepModel = localStorage.getItem('webx11_nvfs_deep_model') || 'gemma4:31b-cloud';
this._deepIndexing = false;
this._deepQueue = [];
this._deepProcessed = 0;
@@ -80,6 +80,7 @@ class NeuralVFSModule {
this.commands['nvfs-sync'] = async (args) => this.nvfsSync(args);
this.commands['nvfs-save'] = async (args) => this.nvfsSave();
this.commands['nvfs-cancel'] = (args) => this.cancelIndex();
+ this.commands['nvfs-index-graph'] = () => this.indexGraphFacts();
}
// ── Core Index Builder (async, chunked) ──
@@ -1044,6 +1045,61 @@ Concepts:`;
}
}
}
+
+ // v4.5.89: Semantic bridge — mirror graph facts into VFS for unified search
+ indexGraphFacts() {
+ const mg = this.terminal.moduleManager.modules['MemoryGraphModule'];
+ if (!mg) return this.terminal.print('❌ Memory Graph module not loaded', 'error');
+ const fs = this.terminal.fs;
+ if (!fs) return this.terminal.print('❌ VFS not available', 'error');
+
+ const factNodes = Object.values(mg._nodes).filter(function(n) {
+ return n.type === 'concept' || n.type === 'fact' || n.type === 'entity' || n.type === 'task';
+ });
+
+ if (factNodes.length === 0) {
+ return this.terminal.print('No graph fact nodes to index. Use memgraph-learn first.', 'info');
+ }
+
+ if (!fs.readDir('/_graph_facts')) fs.mkdir('_graph_facts');
+
+ // v4.5.89b fix: Clean stale files for deleted nodes (P2 from review)
+ const validKeys = new Set(factNodes.map(function(n) { return n.id.replace(/[^a-zA-Z0-9_-]/g, '_') + '.txt'; }));
+ try {
+ const existing = fs.readDir('/_graph_facts') || [];
+ for (const f of existing) {
+ if (!validKeys.has(f)) fs.deleteFile('/_graph_facts/' + f);
+ }
+ } catch (_) {}
+
+ let indexed = 0;
+ for (const node of factNodes) {
+ let content = node.label + '\n';
+ if (node.metadata) {
+ if (node.metadata.full) content += node.metadata.full + '\n';
+ if (node.metadata.result) content += 'Result: ' + node.metadata.result + '\n';
+ }
+ content += 'Type: ' + node.type + '\n';
+ content += 'Namespace: ' + (node.namespace || 'global') + '\n';
+ if (node.source_session) content += 'Session: ' + node.source_session + '\n';
+ const safeName = node.id.replace(/[^a-zA-Z0-9_-]/g, '_');
+ fs.writeFile('/_graph_facts/' + safeName + '.txt', content);
+ indexed++;
+ }
+
+ this.terminal.print('🔗 Indexed ' + indexed + ' graph facts into /_graph_facts/', 'success');
+ this.terminal.print(' Run nvfs-index to include them in semantic search', 'info');
+ }
+
+ // v4.5.89b: Index a single fact — O(1) instead of O(n) re-index (P2 from review)
+ indexSingleFact(factKey, entity, fact) {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+ if (!fs.readDir('/_graph_facts')) fs.mkdir('_graph_facts');
+ const safeName = factKey.replace(/[^a-zA-Z0-9_-]/g, '_');
+ const content = entity + '\n' + fact + '\nType: fact\nNamespace: global\n';
+ fs.writeFile('/_graph_facts/' + safeName + '.txt', content);
+ }
}
// Required for module system
diff --git a/modules/core/module-orchestrator.js b/modules/core/module-orchestrator.js
index ba43dfd..59d9136 100644
--- a/modules/core/module-orchestrator.js
+++ b/modules/core/module-orchestrator.js
@@ -32,6 +32,8 @@ class OrchestratorModule {
this._activeConsensus = null;
this._pipelineHistory = [];
this._PIPELINE_CONFIG = { autoFix: false, stageTimeout: 30000 };
+ this._schedules = [];
+ this._scheduleTimer = null;
this._loadConfig();
}
_loadConfig() {
@@ -84,6 +86,9 @@ class OrchestratorModule {
this.commands['consensus-status'] = () => this.consensusStatus();
this.commands['consensus-history'] = () => this.consensusHistory();
this.commands['pipeline'] = (args) => this.runPipeline(args);
+ this.commands['orchestrator-schedule'] = (args) => this.addSchedule(args);
+ this.commands['orchestrator-schedules'] = () => this.listSchedules();
+ this.commands['orchestrator-unschedule'] = (args) => this.removeSchedule(args[0]);
}
// ─── Task Decomposition Engine ───
_decomposeGoal(goal) {
@@ -146,6 +151,9 @@ class OrchestratorModule {
'consensus-status': 'consensus-status — Show current/active consensus round status',
'consensus-history': 'consensus-history — Show past consensus rounds and their resolutions',
'pipeline': 'pipeline <test|deploy|verify|history|config> — Autonomous CI/CD: test agents/queue/modules, deploy (test→auto-fix→verify), history (show runs), config (show config)',
+ 'orchestrator-schedule': 'orchestrator-schedule <role> <interval_seconds> <task...> — Schedule a recurring task delegation',
+ 'orchestrator-schedules': 'orchestrator-schedules — List all active recurring schedules',
+ 'orchestrator-unschedule': 'orchestrator-unschedule <id> — Remove a recurring schedule',
'orchestrator-status': 'orchestrator-status — Show agent pool status and config',
'orchestrator-spawn': 'orchestrator-spawn [role] [capabilities...] — Manually spawn a new agent',
'orchestrator-kill': 'orchestrator-kill <agent-id> — Terminate an agent and revoke leases',
@@ -170,13 +178,17 @@ class OrchestratorModule {
this._reanimateFromVFS();
// A2A mailbox polling loop (500ms)
if (this._A2A_POLLING) {
- this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
+ this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
}
// Heartbeat watchdog
- this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), this._HEARTBEAT_CHECK);
+ this._heartbeatInterval = this.terminal.moduleManager._setInterval(() => this._checkHeartbeats(), this._HEARTBEAT_CHECK);
// Idle timeout sweep
- this._idleSweepInterval = setInterval(() => this._sweepIdle(), this._IDLE_SWEEP_INTERVAL);
+ this._idleSweepInterval = this.terminal.moduleManager._setInterval(() => this._sweepIdle(), this._IDLE_SWEEP_INTERVAL);
this.terminal.print('🧠 Orchestrator loaded — A2A mailbox transport active', 'success');
+ // Load recurring schedules from VFS
+ this._loadSchedules();
+ // Pre-spawn default agents after a short delay (let other modules init first)
+ setTimeout(() => this._preSpawnDefaultAgents(), 2000);
// Subscribe to event bus for in-memory A2A delivery (instant, no VFS poll)
this._on('a2a:message', (msg) => {
if (msg.type === 'heartbeat' && msg.from && this._agents[msg.from]) {
@@ -561,16 +573,16 @@ class OrchestratorModule {
'a2a-polling': () => {
this._A2A_POLLING = val === 'on' || val === 'true';
if (this._A2A_POLLING && !this._a2aPollTimer) {
- this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
+ this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
} else if (!this._A2A_POLLING && this._a2aPollTimer) {
- clearInterval(this._a2aPollTimer);
+ this.terminal.moduleManager._clearTimer(this._a2aPollTimer);
this._a2aPollTimer = null;
}
return `a2a-polling ${this._A2A_POLLING ? 'on' : 'off'}`;
},
- 'a2a-poll-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 50 || n > 30000) return 'Must be 50-30000ms'; clearInterval(this._a2aPollTimer); this._a2aPollTimer = null; this._A2A_POLL_INTERVAL = n; if (this._A2A_POLLING) { this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), n); } return `a2a-poll-interval set to ${n}ms`; },
- 'heartbeat-check': () => { const n = parseInt(val); if (isNaN(n) || n < 500 || n > 60000) return 'Must be 500-60000ms'; clearInterval(this._heartbeatInterval); this._HEARTBEAT_CHECK = n; this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), n); return `heartbeat-check set to ${n}ms`; },
- 'idle-sweep-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; clearInterval(this._idleSweepInterval); this._IDLE_SWEEP_INTERVAL = n; this._idleSweepInterval = setInterval(() => this._sweepIdle(), n); return `idle-sweep-interval set to ${n}ms`; },
+ 'a2a-poll-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 50 || n > 30000) return 'Must be 50-30000ms'; this.terminal.moduleManager._clearTimer(this._a2aPollTimer); this._a2aPollTimer = null; this._A2A_POLL_INTERVAL = n; if (this._A2A_POLLING) { this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), n); } return `a2a-poll-interval set to ${n}ms`; },
+ 'heartbeat-check': () => { const n = parseInt(val); if (isNaN(n) || n < 500 || n > 60000) return 'Must be 500-60000ms'; this.terminal.moduleManager._clearTimer(this._heartbeatInterval); this._HEARTBEAT_CHECK = n; this._heartbeatInterval = this.terminal.moduleManager._setInterval(() => this._checkHeartbeats(), n); return `heartbeat-check set to ${n}ms`; },
+ 'idle-sweep-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; this.terminal.moduleManager._clearTimer(this._idleSweepInterval); this._IDLE_SWEEP_INTERVAL = n; this._idleSweepInterval = this.terminal.moduleManager._setInterval(() => this._sweepIdle(), n); return `idle-sweep-interval set to ${n}ms`; },
'dead-purge-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; this._DEAD_PURGE_TIMEOUT = n; return `dead-purge-timeout set to ${n}ms`; },
'consensus-size': () => { const n = parseInt(val); if (isNaN(n) || n < 2 || n > 10) return 'Must be 2-10'; this._CONSENSUS_SIZE = n; return `consensus-size set to ${n}`; },
'consensus-strategy': () => { if (!['majority', 'unanimous', 'weighted'].includes(val)) return 'Must be majority, unanimous, or weighted'; this._CONSENSUS_STRATEGY = val; return `consensus-strategy set to ${val}`; },
@@ -983,7 +995,7 @@ class OrchestratorModule {
// Fix 2: Restart A2A poller if stopped
if (!this._A2A_POLLING) {
this._A2A_POLLING = true;
- this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
+ this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
this.terminal.print(' Restarted A2A polling', 'warning');
fixed++;
}
@@ -1113,9 +1125,12 @@ class OrchestratorModule {
}
}
- // Create a lease
+ // Create a lease — with --write if agent has write capabilities
const leaseModule = this.terminal.moduleManager.modules['LeaseModule'];
- const lease = leaseModule ? leaseModule.createLease(['lease-create', agent.id, '60', task]) : null;
+ const hasWriteCap = (agent.capabilities || []).some(function(c) { return c === 'write_file' || c === 'edit_module' || c === 'delete_file'; });
+ const leaseArgs = ['lease-create', agent.id, '60', task];
+ if (hasWriteCap) leaseArgs.push('--write');
+ const lease = leaseModule ? leaseModule.createLease(leaseArgs) : null;
// Mark agent as busy
agent.status = 'busy';
@@ -1184,6 +1199,109 @@ class OrchestratorModule {
return outboxResult || 'completed';
}
+ // ─── Recurring Scheduling ───
+
+ addSchedule(args) {
+ if (args.length < 2) return this.terminal.print('Usage: orchestrator-schedule <role> <interval_seconds> <task...>', 'error');
+ const role = args[0];
+ const interval = parseInt(args[1]);
+ if (isNaN(interval) || interval < 10) return this.terminal.print('Interval must be >= 10 seconds', 'error');
+ const task = args.slice(2).join(' ');
+ if (!task) return this.terminal.print('Task description required', 'error');
+
+ const schedule = {
+ id: 'sched-' + Date.now(),
+ role: role,
+ interval: interval * 1000,
+ task: task,
+ createdAt: new Date().toISOString(),
+ lastRun: null,
+ nextRun: Date.now() + interval * 1000,
+ runs: 0
+ };
+ this._schedules.push(schedule);
+ this._saveSchedules();
+ this._startScheduleTimer();
+ this.terminal.print('📅 Schedule created: "' + task.substring(0, 40) + '" every ' + interval + 's (role: ' + role + ')', 'success');
+ return schedule;
+ }
+
+ listSchedules() {
+ if (this._schedules.length === 0) return this.terminal.print('No active schedules', 'info');
+ this.terminal.print('📅 Recurring Schedules (' + this._schedules.length + '):', 'header');
+ for (const s of this._schedules) {
+ const nextIn = Math.max(0, Math.round((s.nextRun - Date.now()) / 1000));
+ this.terminal.print(' ' + s.id + ' — "' + s.task.substring(0, 40) + '" every ' + (s.interval / 1000) + 's [' + s.role + '] (next: ' + nextIn + 's, runs: ' + s.runs + ')', 'info');
+ }
+ }
+
+ removeSchedule(id) {
+ const idx = this._schedules.findIndex(function(s) { return s.id === id; });
+ if (idx === -1) return this.terminal.print('Schedule ' + id + ' not found', 'error');
+ this._schedules.splice(idx, 1);
+ this._saveSchedules();
+ if (this._schedules.length === 0 && this._scheduleTimer) {
+ clearInterval(this._scheduleTimer);
+ this._scheduleTimer = null;
+ }
+ this.terminal.print('Schedule ' + id + ' removed', 'success');
+ }
+
+ _startScheduleTimer() {
+ if (this._scheduleTimer) return;
+ this._scheduleTimer = setInterval(function() {
+ var now = Date.now();
+ for (var i = 0; i < this._schedules.length; i++) {
+ var s = this._schedules[i];
+ if (now >= s.nextRun) {
+ s.lastRun = new Date().toISOString();
+ s.nextRun = now + s.interval;
+ s.runs++;
+ this.terminal.print('📅 Schedule trigger: "' + s.task.substring(0, 40) + '"', 'info');
+ this.delegateTask(s.task);
+ }
+ }
+ this._saveSchedules();
+ }.bind(this), 5000);
+ }
+
+ _saveSchedules() {
+ try {
+ this.terminal.fs.fsWriteJSON('/orchestrator/schedules.json', this._schedules);
+ } catch (e) { console.warn('[Orchestrator] Schedule save error:', e.message); }
+ }
+
+ _loadSchedules() {
+ try {
+ const data = this.terminal.fs.fsReadJSON('/orchestrator/schedules.json');
+ if (data && Array.isArray(data)) {
+ this._schedules = data;
+ if (this._schedules.length > 0) this._startScheduleTimer();
+ }
+ } catch (e) { this._schedules = []; }
+ }
+
+ _preSpawnDefaultAgents() {
+ // Spawn 3 role-specific agents at boot
+ var roles = [
+ { role: 'auto-learner', caps: ['list_files', 'read_file', 'write_file', 'view_module'], task: 'audit skills' },
+ { role: 'kernel-architect', caps: ['list_files', 'read_file', 'write_file', 'run_command'], task: 'index VFS' },
+ { role: 'searcher', caps: ['list_files', 'read_file', 'web_search'], task: 'check stale data' }
+ ];
+ for (var i = 0; i < roles.length; i++) {
+ var r = roles[i];
+ // Check if an agent with this role already exists
+ var existing = Object.values(this._agents).find(function(a) { return a.role === r.role; });
+ if (!existing) {
+ var agent = this.spawnAgent(r.role, r.caps);
+ if (agent) {
+ agent.restartPolicy = 'always';
+ this.terminal.print('🤖 Pre-spawned agent: ' + r.role + ' (' + r.caps.length + ' caps)', 'info');
+ }
+ }
+ }
+ }
+
// ─── Monitoring ───
_checkHeartbeats() {
diff --git a/modules/core/module-smartctx.js b/modules/core/module-smartctx.js
index 0bfc91a..fc3882d 100644
--- a/modules/core/module-smartctx.js
+++ b/modules/core/module-smartctx.js
@@ -10,6 +10,7 @@
'contexts': 'contexts — List all available context profiles',
'context-new': 'context-new <name> <displayName> <systemPrompt> — Create a new context profile',
'context-delete': 'context-delete <name> — Delete a context profile',
+ 'context-auto': 'context-auto [on|off] — Toggle auto-routing (detects intent from user input)',
'smart-ask': 'smart-ask <question> — Ask a question using the active context'
};
}
@@ -55,6 +56,7 @@
this.commands['contexts'] = () => this.listContexts();
this.commands['context-new'] = (args) => this.createContext(args);
this.commands['context-delete'] = (args) => this.deleteContext(args);
+ this.commands['context-auto'] = (args) => this.toggleAutoRouting(args);
this.commands['smart-ask'] = (args) => this.smartAsk(args);
}
setContext(args) {
@@ -124,6 +126,116 @@
this.terminal.print(`Profile '${args[0]}' not found`, "error");
}
}
+ // ─── Intent Detection Engine ───
+ _getIntentPatterns() {
+ return {
+ 'coding': [
+ /^code\b/i, /\b(edit|fix|write|create|implement|refactor|build)\s+(module|function|class|file|code|script)/i,
+ /\b(debug|bug|error|fix)\s+(code|module|function|script)/i, /\b(implement|write|create)\s+(a\s+)?(function|class|module|command)/i,
+ /\b(refactor|rewrite|optimize)\s+(code|module|function)/i, /\b(add\s+)(feature|command|functionality)/i
+ ],
+ 'debug': [
+ /^debug\b/i, /\b(error|crash|bug|fail|traceback|exception|broken|not working)\b/i,
+ /\b(why|what happened|diagnose|investigate|find out why)\b/i, /\b(check|inspect|examine)\s+(log|error|issue|problem)/i
+ ],
+ 'research': [
+ /^research\b/i, /\b(search|find|look up|research|investigate|analyze)\b/i,
+ /\b(what is|how does|tell me about|explain)\b.*\?$/i, /\b(find|search)\s+(for\s+)?(information|docs|documentation|article)/i
+ ],
+ 'creative': [
+ /^brainstorm\b/i, /\b(create|design|imagine|generate|compose|draft)\s+(story|poem|art|design|idea|concept|mindmap)/i,
+ /\b(creative|inspire|innovative|novel)\b/i
+ ],
+ 'system-admin': [
+ /^(config|setup|install|deploy|backup|restore|security|monitor|maintenance)\b/i,
+ /\b(configure|setup|install|deploy|backup|restore)\s+(system|server|module|service)/i,
+ /\b(security|permission|access|block|allow)\b/i
+ ],
+ 'chat': [
+ /^(hi|hello|hey|how are you|what's up|good morning|good evening)\b/i,
+ /^.{0,30}\?$/ // short questions
+ ]
+ };
+ }
+ autoDetectIntent(text) {
+ if (!text || typeof text !== 'string') return { profile: null, confidence: 0 };
+ const patterns = this._getIntentPatterns();
+ const scores = {};
+ const lower = text.toLowerCase();
+
+ for (const [profile, regexes] of Object.entries(patterns)) {
+ let matches = 0;
+ for (const re of regexes) {
+ if (re.test(text) || re.test(lower)) matches++;
+ }
+ if (matches > 0) scores[profile] = matches;
+ }
+
+ // Code snippet detection (bias toward coding)
+ if (/[{([\s]*['"`]/.test(text) || /\b(function|class|const|let|var|import|export|=>)\b/.test(text)) {
+ scores['coding'] = (scores['coding'] || 0) + 2;
+ }
+
+ // Very short text → chat
+ if (text.length < 15 && !scores['coding'] && !scores['debug']) {
+ scores['chat'] = (scores['chat'] || 0) + 1;
+ }
+
+ // Find best match
+ let bestProfile = null;
+ let bestScore = 0;
+ let totalScore = 0;
+ for (const [profile, score] of Object.entries(scores)) {
+ totalScore += score;
+ if (score > bestScore) { bestScore = score; bestProfile = profile; }
+ }
+
+ // Calculate confidence (0-100)
+ const confidence = totalScore > 0 ? Math.round((bestScore / totalScore) * 100) : 0;
+
+ return { profile: bestProfile, confidence };
+ }
+ autoRouteContext(text) {
+ if (!text) return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: 0, reason: 'no input' };
+
+ // Check if auto-routing is enabled
+ const enabled = localStorage.getItem('webx11_context_auto') !== 'off';
+ if (!enabled) return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: 0, reason: 'auto-routing disabled' };
+
+ const result = this.autoDetectIntent(text);
+ if (!result.profile || result.confidence < 70) {
+ return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: result.confidence, reason: 'confidence too low (<70%)' };
+ }
+
+ // Don't switch if already on the best profile
+ if (result.profile === this.activeProfile) {
+ return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: result.confidence, reason: 'already on best profile' };
+ }
+
+ // Check if the target profile exists
+ if (!this.contextProfiles[result.profile]) {
+ return { switched: false, from: this.activeProfile, to: this.activeProfile, confidence: result.confidence, reason: 'profile "' + result.profile + '" not found' };
+ }
+
+ // Switch
+ const from = this.activeProfile;
+ this.setContext([result.profile]);
+ return { switched: true, from: from, to: result.profile, confidence: result.confidence, reason: 'intent detected' };
+ }
+ toggleAutoRouting(args) {
+ const val = args && args[0] ? args[0].toLowerCase() : null;
+ if (val === 'on' || val === 'true' || val === '1') {
+ localStorage.setItem('webx11_context_auto', 'on');
+ this.terminal.print('✅ Auto-routing enabled — context will switch based on intent', 'success');
+ } else if (val === 'off' || val === 'false' || val === '0') {
+ localStorage.setItem('webx11_context_auto', 'off');
+ this.terminal.print('⛔ Auto-routing disabled — use context <name> to switch manually', 'warning');
+ } else {
+ const current = localStorage.getItem('webx11_context_auto') !== 'off';
+ this.terminal.print('Auto-routing: ' + (current ? 'ON' : 'OFF'), 'info');
+ this.terminal.print('Usage: context-auto on|off', 'info');
+ }
+ }
async smartAsk(args) {
if (!args[0]) {
this.terminal.print("Usage: smart-ask <your-question>", "warning"); return;
diff --git a/modules/core/module-world-model.js b/modules/core/module-world-model.js
index 6f0c4e5..61eecfc 100644
--- a/modules/core/module-world-model.js
+++ b/modules/core/module-world-model.js
@@ -7,9 +7,14 @@ class WorldModelModule {
this.terminal = null;
this._model = null;
this._lastRefresh = null;
- this._REFRESH_INTERVAL = 300000; // 5 minutes
+ this._REFRESH_INTERVAL = 300000; // 5 minutes (fallback)
this._MAX_PROJECTS = 10;
this._refreshTimer = null;
+ // v4.5.89: Event-driven sync state
+ this._syncDebounce = null;
+ this._syncing = false;
+ this._lastEventSync = 0;
+ this._EVENT_DEBOUNCE_MS = 500;
}
getHelpInfo() {
@@ -51,6 +56,25 @@ class WorldModelModule {
this.terminal.print('🌐 World Model loaded — system self-awareness active', 'info');
}
+ // v4.5.89: Event-driven sync — debounced, recursion-guarded
+ _scheduleSync() {
+ if (this._syncing) return; // Prevent recursion during active synthesis
+ if (this._syncDebounce) clearTimeout(this._syncDebounce);
+ const now = Date.now();
+ // Skip if we synced < 2s ago (rate limit beyond debounce)
+ if (now - this._lastEventSync < 2000) return;
+ this._syncDebounce = setTimeout(() => {
+ this._syncDebounce = null;
+ this._syncing = true;
+ try {
+ this._synthesize();
+ this._lastEventSync = Date.now();
+ } finally {
+ this._syncing = false;
+ }
+ }, this._EVENT_DEBOUNCE_MS);
+ }
+
// ─── Synthesis Engine ───
_synthesize() {
@@ -119,9 +143,10 @@ class WorldModelModule {
try {
const mm = this.terminal?.moduleManager;
if (!mm) return 0;
- // Count registry entries (all known modules) vs loaded
- const registry = mm.moduleRegistry || [];
- return registry.length || Object.keys(mm.modules || {}).length;
+ // Use max of registry vs loaded so the ratio is never >1 (e.g. 43/7 → 43/43)
+ const loadedCount = Object.keys(mm.modules || {}).length;
+ const registryCount = (mm.moduleRegistry || []).length;
+ return Math.max(loadedCount, registryCount);
} catch (e) {
console.warn('[WorldModel] Total modules gather error:', e.message);
return Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0;
@@ -129,10 +154,10 @@ class WorldModelModule {
}
_gatherSystemInfo(fs, moduleCount, version) {
- // Get real health data from ServerMonitorModule's _latencyLog and AI assistant's _errorLog
+ // Get real health data from ServerMonitorModule's _latencyLog and DebugSupervisorModule
const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule'];
- const ai = this.terminal?.moduleManager?.modules?.['WebX11AIAssistantModule'];
- const errorCount = ai?._errorLog?.length || 0;
+ const ds = this.terminal?.moduleManager?.modules?.['DebugSupervisorModule'];
+ 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;
return {
@@ -228,11 +253,11 @@ class WorldModelModule {
_gatherTelemetry(fs) {
try {
- // Get real latency data from ServerMonitorModule and errors from AI assistant
+ // Get real latency data from ServerMonitorModule and errors from DebugSupervisorModule
const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule'];
- const ai = this.terminal?.moduleManager?.modules?.['WebX11AIAssistantModule'];
+ const ds = this.terminal?.moduleManager?.modules?.['DebugSupervisorModule'];
const latencyDataPoints = sm?._latencyLog ? Object.keys(sm._latencyLog).length : 0;
- const recentErrors = ai?._errorLog?.length || 0;
+ const recentErrors = ds ? Object.keys(ds._errorDB?.errors || {}).length : 0;
return {
toolLatencyDataPoints: latencyDataPoints,
recentErrors: recentErrors,
@@ -352,41 +377,44 @@ class WorldModelModule {
if (!question) return this.terminal.print('Usage: world-model-ask <question>', 'error');
if (!this._model) return this.terminal.print('No model — run world-model-refresh first', 'warning');
- const q = question.toLowerCase();
+ const answer = this._answerQuery(question);
+ this.terminal.print(`❓ ${question}`, 'info');
+ this.terminal.print(`💡 ${answer}`, 'success');
+ }
+
+ // Shared query routing — used by both ask() (terminal) and world_model_query (AI tool)
+ _answerQuery(question) {
+ if (!this._model) return 'World model not synthesized yet — run world-model-refresh';
+ const q = (question || '').toLowerCase();
const m = this._model;
- let answer = '';
if (q.includes('module') || q.includes('loaded')) {
- answer = `Loaded ${m.modules.loaded} modules (${m.modules.modifiedCount} modified: ${m.modules.modified.join(', ') || 'none'})`;
+ return `Loaded ${m.modules.loaded} modules (${m.modules.modifiedCount} modified: ${m.modules.modified.join(', ') || 'none'})`;
} else if (q.includes('skill')) {
- answer = `Installed ${m.skills.count} skills: ${m.skills.skills.join(', ') || 'none'}`;
+ return `Installed ${m.skills.count} skills: ${m.skills.skills.join(', ') || 'none'}`;
} else if (q.includes('file') || q.includes('vfs')) {
- answer = `${m.files.total} files in ${m.files.directories} directories, total ${m.files.size}`;
+ return `${m.files.total} files in ${m.files.directories} directories, total ${m.files.size}`;
} else if (q.includes('agent') || q.includes('orchestrat')) {
- answer = `${m.agents.agents} agents (${m.agents.active} active)`;
+ return `${m.agents.agents} agents (${m.agents.active} active)`;
} else if (q.includes('health') || q.includes('status')) {
- answer = `Health: ${m.system.health} · Memory: ${m.system.memory} · ${m.telemetry.recentErrors > 0 ? `${m.telemetry.recentErrors} recent errors` : 'No errors'}`;
+ return `Health: ${m.system.health} · Memory: ${m.system.memory} · ${m.telemetry.recentErrors > 0 ? `${m.telemetry.recentErrors} recent errors` : 'No errors'}`;
} else if (q.includes('memory') || q.includes('graph')) {
- answer = `Memory Graph: ${m.memory.nodes} nodes, ${m.memory.edges} edges (${m.memory.active ? 'active' : 'inactive'})`;
+ return `Memory Graph: ${m.memory.nodes} nodes, ${m.memory.edges} edges (${m.memory.active ? 'active' : 'inactive'})`;
} else if (q.includes('change') || q.includes('yesterday') || q.includes('recent')) {
const recent = m.recent;
- answer = recent.length > 0 ? `Recent changes: ${recent.join('; ')}` : 'No recent changes detected';
+ return recent.length > 0 ? `Recent changes: ${recent.join('; ')}` : 'No recent changes detected';
} else if (q.includes('project') || q.includes('work')) {
const projects = m.projects.activeProjects;
- answer = projects.length > 0
+ return projects.length > 0
? `Active projects: ${projects.map(p => `${p.name} (${p.accesses}x)`).join(', ')}`
: 'No active projects detected';
} else if (q.includes('telemetry') || q.includes('latency')) {
- answer = `${m.telemetry.toolLatencyDataPoints} tool latency data points tracked`;
+ return `${m.telemetry.toolLatencyDataPoints} tool latency data points tracked, ${m.telemetry.recentErrors} recent errors`;
} else if (q.includes('summary') || q.includes('overview') || q.includes('all')) {
- answer = this.showSummary();
- return;
+ return `newOS4 ${m.version} · ${m.modules.loaded}/${m.modules.total} modules · ${m.skills.count} skills · ${m.files.total} files · ${m.agents.agents} agents · ${m.memory.nodes} graph nodes · ${m.telemetry.recentErrors > 0 ? `⚠️ ${m.telemetry.recentErrors} errors` : '✅ OK'}`;
} else {
- answer = `I'm not sure how to answer "${question}". Try: modules, skills, files, agents, health, memory, changes, projects, telemetry, or summary.`;
+ return `Unknown query. Try: modules, skills, files, agents, health, memory, changes, projects, telemetry, or summary.`;
}
-
- this.terminal.print(`❓ ${question}`, 'info');
- this.terminal.print(`💡 ${answer}`, 'success');
}
refresh() {
@@ -429,7 +457,7 @@ class WorldModelModule {
const agentStr = m.agents.error
? 'agents: N/A'
: `${m.agents.agents} agents`;
- return `[World Model Context]\\nSystem: newOS4 ${m.version} · ${m.modules.loaded} modules · ${m.skills.count} skills · ${m.files.total} files · ${agentStr}${m.telemetry.recentErrors > 0 ? ` · ⚠️ ${m.telemetry.recentErrors} errors` : ' · ✅ OK'}${m.modules.modifiedCount > 0 ? `\\nModified modules: ${m.modules.modified.slice(0, 3).join(', ')}${m.modules.modifiedCount > 3 ? ` (+${m.modules.modifiedCount - 3} more)` : ''}` : ''}`;
+ return `[World Model Context]\nSystem: newOS4 ${m.version} · ${m.modules.loaded} modules · ${m.skills.count} skills · ${m.files.total} files · ${agentStr}${m.telemetry.recentErrors > 0 ? ` · ⚠️ ${m.telemetry.recentErrors} errors` : ' · ✅ OK'}${m.modules.modifiedCount > 0 ? `\nModified modules: ${m.modules.modified.slice(0, 3).join(', ')}${m.modules.modifiedCount > 3 ? ` (+${m.modules.modifiedCount - 3} more)` : ''}` : ''}`;
}
// ─── Persistence ───
@@ -448,10 +476,20 @@ class WorldModelModule {
}
}
+ // v4.5.89: Atomic write — temp file + rename to prevent corruption
_saveToVFS(fs) {
try {
if (!fs.readDir('/system')) fs.mkdir('system');
- fs.fsWriteJSON('/system/world-model.json', this._model);
+ // v4.5.89b fix: Clean stale temp from previous crash (P1 from review)
+ try { fs.deleteFile('/system/world-model.tmp.json'); } catch (_) {}
+ fs.fsWriteJSON('/system/world-model.tmp.json', this._model);
+ const tmp = fs.fsReadJSON('/system/world-model.tmp.json');
+ if (tmp) {
+ fs.fsWriteJSON('/system/world-model.json', tmp);
+ fs.deleteFile('/system/world-model.tmp.json');
+ } else {
+ console.error('[WorldModel] Save failed: temp file corrupt after write');
+ }
} catch (e) {
console.error('[WorldModel] Save error:', e.message);
}
diff --git a/modules/core/ollama-manager.js b/modules/core/ollama-manager.js
index 13472fc..4f47db2 100644
--- a/modules/core/ollama-manager.js
+++ b/modules/core/ollama-manager.js
@@ -26,7 +26,7 @@ class OllamaManager {
this.streamEnabled = localStorage.getItem('webx11_stream_enabled') === 'true';
// --- Validator Agent Config ---
this.validatorEnabled = localStorage.getItem('webx11_validator_enabled') === 'true';
- this.validatorModel = localStorage.getItem('webx11_validator_model') || 'ministral-3:3b-cloud';
+ this.validatorModel = localStorage.getItem('webx11_validator_model') || 'gemma4:31b-cloud';
this.validatorEndpoint = localStorage.getItem('webx11_validator_endpoint') || '';
// --- Model Parameters (temperature, top_p, max_tokens, thinking) ---
this.modelParams = this._loadModelParams();
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index 3abb074..9896bee 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -7,19 +7,71 @@
this.lastRouteTime = 0;
this.lastRouteCmd = '';
this.blockedCount = 0;
- 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','mem-debug','reset-mem','reset','factory-reset','reset-vfs','js','js-run','node','debug','debug-ai','session-new','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze']);
- // 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){}
- try { const w = localStorage.getItem('webx11_security_whitelist'); if(w){JSON.parse(w).forEach(c=>this.whitelist.add(c));} }catch(e){}
- try { const f = localStorage.getItem('webx11_security_flags'); if(f){JSON.parse(f).forEach(c=>this.suspiciousFlags.add(c));} }catch(e){}
+ this._baseDestructive = ['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot'];
+ this._baseSystem = ['reboot','shutdown','format','kill','halt','poweroff'];
+ this._baseSuspiciousFlags = ['--dump','--execute','--eval','--exec','--shell','--script'];
+ this._baseWhitelist = ['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','reset','factory-reset','reset-vfs','js','js-run','node','debug','debug-ai','debug-stats','debug-history','debug-analyze','debug-trends','debug-fix','debug-health','session-new','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','dashboard','context-auto'];
+ this.destructiveCmds = new Set(this._baseDestructive);
+ this.systemCmds = new Set(this._baseSystem);
+ this.suspiciousFlags = new Set(this._baseSuspiciousFlags);
+ this.whitelist = new Set(this._baseWhitelist);
+ // Load persisted user deltas (add/remove) so hardcoded defaults can be demoted.
+ this._loadSecurityDeltas();
this._registerBuiltIn();
}
+ _loadSecurityDeltas() {
+ try {
+ const schema = this.terminal._getDataSchemaVersion ? this.terminal._getDataSchemaVersion() : 1;
+ localStorage.setItem('webx11_data_schema_version', String(schema));
+ const raw = localStorage.getItem('webx11_security_deltas');
+ if (!raw) {
+ // One-time migration from legacy flat keys to delta format.
+ const legacy = {};
+ try { legacy.destructive = JSON.parse(localStorage.getItem('webx11_security_destructive') || '[]'); } catch (e) {}
+ try { legacy.system = JSON.parse(localStorage.getItem('webx11_security_system') || '[]'); } catch (e) {}
+ try { legacy.whitelist = JSON.parse(localStorage.getItem('webx11_security_whitelist') || '[]'); } catch (e) {}
+ try { legacy.flags = JSON.parse(localStorage.getItem('webx11_security_flags') || '[]'); } catch (e) {}
+ const delta = { add: {}, remove: {} };
+ for (const c of (legacy.destructive || [])) if (!this._baseDestructive.includes(c)) { if (!delta.add.destructive) delta.add.destructive = []; delta.add.destructive.push(c); }
+ for (const c of (legacy.system || [])) if (!this._baseSystem.includes(c)) { if (!delta.add.system) delta.add.system = []; delta.add.system.push(c); }
+ for (const c of (legacy.whitelist || [])) if (!this._baseWhitelist.includes(c)) { if (!delta.add.whitelist) delta.add.whitelist = []; delta.add.whitelist.push(c); }
+ for (const f of (legacy.flags || [])) if (!this._baseSuspiciousFlags.includes(f)) { if (!delta.add.flags) delta.add.flags = []; delta.add.flags.push(f); }
+ localStorage.setItem('webx11_security_deltas', JSON.stringify(delta));
+ localStorage.removeItem('webx11_security_destructive');
+ localStorage.removeItem('webx11_security_system');
+ localStorage.removeItem('webx11_security_whitelist');
+ localStorage.removeItem('webx11_security_flags');
+ return;
+ }
+ const d = JSON.parse(raw);
+ if (d.remove) {
+ for (const c of (d.remove.destructive || [])) this.destructiveCmds.delete(c);
+ for (const c of (d.remove.system || [])) this.systemCmds.delete(c);
+ for (const c of (d.remove.whitelist || [])) this.whitelist.delete(c);
+ for (const f of (d.remove.flags || [])) this.suspiciousFlags.delete(f);
+ }
+ if (d.add) {
+ for (const c of (d.add.destructive || [])) this.destructiveCmds.add(c);
+ for (const c of (d.add.system || [])) this.systemCmds.add(c);
+ for (const c of (d.add.whitelist || [])) this.whitelist.add(c);
+ for (const f of (d.add.flags || [])) this.suspiciousFlags.add(f);
+ }
+ } catch (e) {}
+ }
+ _saveSecurityDeltas() {
+ const delta = { add: {}, remove: {} };
+ for (const c of this.destructiveCmds) if (!this._baseDestructive.includes(c)) { if (!delta.add.destructive) delta.add.destructive = []; delta.add.destructive.push(c); }
+ for (const c of this._baseDestructive) if (!this.destructiveCmds.has(c)) { if (!delta.remove.destructive) delta.remove.destructive = []; delta.remove.destructive.push(c); }
+ for (const c of this.systemCmds) if (!this._baseSystem.includes(c)) { if (!delta.add.system) delta.add.system = []; delta.add.system.push(c); }
+ for (const c of this._baseSystem) if (!this.systemCmds.has(c)) { if (!delta.remove.system) delta.remove.system = []; delta.remove.system.push(c); }
+ for (const c of this.whitelist) if (!this._baseWhitelist.includes(c)) { if (!delta.add.whitelist) delta.add.whitelist = []; delta.add.whitelist.push(c); }
+ for (const c of this._baseWhitelist) if (!this.whitelist.has(c)) { if (!delta.remove.whitelist) delta.remove.whitelist = []; delta.remove.whitelist.push(c); }
+ for (const f of this.suspiciousFlags) if (!this._baseSuspiciousFlags.includes(f)) { if (!delta.add.flags) delta.add.flags = []; delta.add.flags.push(f); }
+ for (const f of this._baseSuspiciousFlags) if (!this.suspiciousFlags.has(f)) { if (!delta.remove.flags) delta.remove.flags = []; delta.remove.flags.push(f); }
+ localStorage.setItem('webx11_security_deltas', JSON.stringify(delta));
+ }
_registerBuiltIn() {
- const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze'];
+ const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','dashboard'];
internals.forEach(c => this.registerCommand(c, { category: 'builtin' }));
}
setLevel(n) {
@@ -38,8 +90,13 @@
}
registerCommand(name, meta = {}) {
this.commandMeta.set(name, { destructive: false, category: 'user', ...meta });
+ this.invalidateCommandCache();
+ }
+ invalidateCommandCache() {
+ this._commandCacheDirty = true;
}
_collectAllCommands() {
+ if (!this._commandCacheDirty && this._commandCache) return this._commandCache;
const cmds = new Set([...this.commandMeta.keys()]);
for (const modName in this.terminal.moduleManager.modules) {
const mod = this.terminal.moduleManager.modules[modName];
@@ -49,7 +106,9 @@
}
const builtins = ['clear','help','ls','df','security','security-level'];
builtins.forEach(c => cmds.add(c));
- return [...cmds];
+ this._commandCache = [...cmds];
+ this._commandCacheDirty = false;
+ return this._commandCache;
}
_levenshtein(a, b) {
a = a.toLowerCase(); b = b.toLowerCase();
@@ -278,6 +337,19 @@
// Apply config overrides after everything is initialized
this._applyTerminalConfig();
}
+ /* ---------- helpers ---------- */
+ static _escapeHtml(str) {
+ if (str == null) return '';
+ return String(str).replace(/[&<>'"]/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[m]));
+ }
+ _escapeHtml(str) { return WebX11Terminal._escapeHtml(str); }
+ _getDataSchemaVersion() { return 1; }
+ _escapeYamlString(str) {
+ return '"' + String(str == null ? '' : str).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
+ }
+ _invalidateCommandCache() {
+ if (this.security) this.security.invalidateCommandCache();
+ }
initEventListeners() {
this.inputField.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
@@ -1195,6 +1267,7 @@
this._safeRegister('ServerMonitorModule', 'server monitor');
this._safeRegister('SmartContextModule', 'smart context');
this._safeRegister('SimpleGUIModule', 'simple GUI builder');
+ this._safeRegister('DebugSupervisorModule', 'debug supervisor');
// v4.5.47: Critical ext modules promoted to boot path
this._safeRegister('EditorModule', 'editor');
this._safeRegister('ShellModule', 'shell');
@@ -1242,6 +1315,7 @@
DiffModule: typeof DiffModule !== 'undefined' ? DiffModule : undefined,
BusyBoxModule: typeof BusyBoxModule !== 'undefined' ? BusyBoxModule : undefined,
SmartContextModule: typeof SmartContextModule !== 'undefined' ? SmartContextModule : undefined,
+ DebugSupervisorModule: typeof DebugSupervisorModule !== 'undefined' ? DebugSupervisorModule : undefined,
};
const cls = registry[className];
if (typeof cls === 'undefined') {
@@ -1371,10 +1445,12 @@
splitPipe(input) {
const parts = [];
let current = '';
- let inQuote = false;
+ let inSingle = false;
+ let inDouble = false;
for (const ch of input) {
- if (ch === '"' || ch === "'") { inQuote = !inQuote; current += ch; }
- else if (ch === '|' && !inQuote) { parts.push(current.trim()); current = ''; }
+ if (ch === '"' && !inSingle) { inDouble = !inDouble; current += ch; }
+ else if (ch === "'" && !inDouble) { inSingle = !inSingle; current += ch; }
+ else if (ch === '|' && !inSingle && !inDouble) { parts.push(current.trim()); current = ''; }
else { current += ch; }
}
parts.push(current.trim());
@@ -1414,8 +1490,11 @@
const originalPrint = this.print.bind(this);
let output = '';
this.print = (text, type) => { output += text + '\n'; };
- await this.dispatchCommand(cmd, args);
- this.print = originalPrint;
+ try {
+ await this.dispatchCommand(cmd, args);
+ } finally {
+ this.print = originalPrint;
+ }
const trimmed = output.trim();
if (trimmed.length > this._pipeBufferSize) {
return trimmed.slice(0, this._pipeBufferSize);
@@ -1431,6 +1510,21 @@
if (cmd === 'load-local' || cmd === 'loadlocal') { this.loadLocalModule(args[0]); return; }
if (cmd === 'smart-prompt') { const val = args[0] && args[0] === 'off' ? 'off' : 'on'; localStorage.setItem('webx11_smart_prompt', val); this.print('Smart prompt ' + val, 'success'); this.updatePrompt(); return; }
if (cmd === 'mem') { this.printMemoryStats(); return; }
+ if (cmd === 'dashboard') {
+ // Collect dashboard data and open the dashboard page
+ const aiMod = this.moduleManager?.modules?.WebX11AIAssistantModule;
+ if (aiMod && typeof aiMod._collectDashboardData === 'function') {
+ aiMod._collectDashboardData().then(function() {
+ this.print('📊 Dashboard data collected — opening dashboard...', 'success');
+ window.open('/spaces/newos4/downloads/dashboard.html', 'newOS4 Dashboard', 'width=1200,height=800');
+ }.bind(this));
+ } else {
+ // Fallback: just open the page (it will try localStorage)
+ this.print('📊 Opening dashboard...', 'info');
+ window.open('/spaces/newos4/downloads/dashboard.html', 'newOS4 Dashboard', 'width=1200,height=800');
+ }
+ return;
+ }
if (cmd === 'config') {
if (args.length === 0) {
this.print('=== Terminal Config ===', 'info');
@@ -1493,7 +1587,7 @@
if (action === 'add') { this.security.destructiveCmds.add(args[1]); }
else if (action === 'remove') { this.security.destructiveCmds.delete(args[1]); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_destructive', JSON.stringify([...this.security.destructiveCmds]));
+ this.security._saveSecurityDeltas();
this.print(`Updated destructive set (${this.security.destructiveCmds.size} commands)`, 'success');
return;
}
@@ -1509,7 +1603,7 @@
if (action === 'add') { this.security.systemCmds.add(args[1]); }
else if (action === 'remove') { this.security.systemCmds.delete(args[1]); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_system', JSON.stringify([...this.security.systemCmds]));
+ this.security._saveSecurityDeltas();
this.print(`Updated system set (${this.security.systemCmds.size} commands)`, 'success');
return;
}
@@ -1525,7 +1619,7 @@
if (action === 'add') { this.security.whitelist.add(args[1]); }
else if (action === 'remove') { this.security.whitelist.delete(args[1]); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_whitelist', JSON.stringify([...this.security.whitelist]));
+ this.security._saveSecurityDeltas();
this.print(`Updated whitelist (${this.security.whitelist.size} commands)`, 'success');
return;
}
@@ -1542,15 +1636,15 @@
if (action === 'add') { this.security.suspiciousFlags.add(flag); }
else if (action === 'remove') { this.security.suspiciousFlags.delete(flag); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_flags', JSON.stringify([...this.security.suspiciousFlags]));
+ this.security._saveSecurityDeltas();
this.print(`Updated suspicious flags (${this.security.suspiciousFlags.size} flags)`, 'success');
return;
}
if (cmd === 'cmd-allow') { this._resolveCmdConfirm(true); return; }
if (cmd === 'cmd-deny') { this._resolveCmdConfirm(false); return; }
if (cmd === 'vfs' && args[0] === 'reset') {
- if (args[1] !== '--force') {
- this.print('⚠ This will permanently delete all files! Use vfs reset --force to confirm.', 'warning');
+ if (args[1] !== '--confirm' && args[1] !== '--force') {
+ this.print('⚠ This will permanently delete all files! Use vfs reset --confirm to confirm.', 'warning');
return;
}
this.fs.reset();
@@ -1592,8 +1686,8 @@
return;
}
if (cmd === 'empty-trash') {
- if (args[0] !== '--force') {
- this.print('⚠ This will permanently delete all trashed files! Use empty-trash --force to confirm.', 'warning');
+ if (args[0] !== '--confirm' && args[0] !== '--force') {
+ this.print('⚠ This will permanently delete all trashed files! Use empty-trash --confirm to confirm.', 'warning');
return;
}
if (this.fs.emptyTrash()) {
@@ -1710,7 +1804,11 @@
} catch (_e) {}
}
return; }
- catch (err) { this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error'); return; }
+ catch (err) {
+ this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error');
+ console.error(`[Terminal] Error in ${modName}.${cmd}:`, err);
+ return;
+ }
}
}
// Enhanced: check fuzzy suggestions before showing "not found"
@@ -1747,7 +1845,17 @@
files.forEach(f => {
const div = document.createElement('div');
div.className = `file-item ${f.type === 'dir' ? 'dir' : ''}`;
- div.innerHTML = `<span>${f.type === 'dir' ? '📁' : '📄'} ${f.name}</span> <span style="font-size:0.8em">${f.size}b</span>`;
+ const icon = document.createElement('span');
+ icon.textContent = f.type === 'dir' ? '📁' : '📄';
+ const name = document.createElement('span');
+ name.textContent = ' ' + f.name;
+ const size = document.createElement('span');
+ size.style.fontSize = '0.8em';
+ size.textContent = ' ' + f.size + 'b';
+ div.appendChild(icon);
+ div.appendChild(name);
+ div.appendChild(document.createTextNode(' '));
+ div.appendChild(size);
div.onclick = () => {
if (f.type === 'file') {
const content = this.fs.readFile(f.name);
@@ -1786,14 +1894,29 @@
modules.forEach(mod => {
const div = document.createElement('div');
div.className = `module-item ${mod.loaded ? '' : 'disabled'}`;
- const badge = mod.url ? '' : '<span class="module-badge builtin">Built-in</span>';
- div.innerHTML = `
- <div>
- <strong>${mod.name}</strong>${badge}
- <div style="font-size:0.8em; color:#666">${mod.url || 'Built-in'}</div>
- </div>
- ${mod.url ? `<button class="btn btn-danger" onclick="webx11.removeModule('${mod.name}')" style="padding:4px 8px;">✕</button>` : ''}
- `;
+ const info = document.createElement('div');
+ const strong = document.createElement('strong');
+ strong.textContent = mod.name;
+ info.appendChild(strong);
+ if (!mod.url) {
+ const badge = document.createElement('span');
+ badge.className = 'module-badge builtin';
+ badge.textContent = 'Built-in';
+ info.appendChild(badge);
+ }
+ const sub = document.createElement('div');
+ sub.style.cssText = 'font-size:0.8em; color:#666';
+ sub.textContent = mod.url || 'Built-in';
+ info.appendChild(sub);
+ div.appendChild(info);
+ if (mod.url) {
+ const btn = document.createElement('button');
+ btn.className = 'btn btn-danger';
+ btn.style.padding = '4px 8px';
+ btn.textContent = '✕';
+ btn.onclick = () => this.removeModule(mod.name);
+ div.appendChild(btn);
+ }
list.appendChild(div);
});
}
@@ -1905,6 +2028,7 @@
renderConfigList() {
const list = document.getElementById('config-list');
if (!list) return;
+ list.innerHTML = '';
const prefix = 'webx11_config_';
const entries = [];
for (let i = 0; i < localStorage.length; i++) {
@@ -1917,14 +2041,28 @@
list.innerHTML = '<div style="padding:20px; text-align:center; color:#666">No configuration saved.</div>';
return;
}
- list.innerHTML = entries.map(e => {
- const displayVal = e.key.toLowerCase().includes('password') || e.key.toLowerCase().includes('token') || e.key.toLowerCase().includes('secret')
- ? '****' : e.value;
- return `<div class="module-item">
- <div><strong>${e.key}</strong> <span style="color:var(--text-muted)">${displayVal}</span></div>
- <button class="btn btn-danger" onclick="webx11.deleteConfigFromUI('${e.key.replace(/'/g, "\\'")}')" style="padding:4px 8px; font-size:0.8em">✕</button>
- </div>`;
- }).join('');
+ entries.forEach(e => {
+ const item = document.createElement('div');
+ item.className = 'module-item';
+ const info = document.createElement('div');
+ const strong = document.createElement('strong');
+ strong.textContent = e.key;
+ info.appendChild(strong);
+ const val = document.createElement('span');
+ val.style.color = 'var(--text-muted)';
+ const sensitive = /password|token|secret|key|private|credential|auth/i.test(e.key);
+ val.textContent = sensitive ? '****' : e.value;
+ info.appendChild(document.createTextNode(' '));
+ info.appendChild(val);
+ const btn = document.createElement('button');
+ btn.className = 'btn btn-danger';
+ btn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ btn.textContent = '✕';
+ btn.onclick = () => this.deleteConfigFromUI(e.key);
+ item.appendChild(info);
+ item.appendChild(btn);
+ list.appendChild(item);
+ });
}
addConfigFromUI() {
const key = document.getElementById('config-key-input').value.trim();
@@ -1977,10 +2115,24 @@
// Stats
if (statsEl) {
const cats = new Set(Object.values(index).map(s => s.category || 'general'));
- statsEl.innerHTML = `<span>📚 ${names.length} skills</span><span>📂 ${cats.size} categories</span>${query ? `<span>🔍 ${filtered.length} match '${query}'</span>` : ''}`;
+ statsEl.innerHTML = '';
+ const totalSpan = document.createElement('span');
+ totalSpan.textContent = '📚 ' + names.length + ' skills';
+ statsEl.appendChild(totalSpan);
+ const catSpan = document.createElement('span');
+ catSpan.textContent = '📂 ' + cats.size + ' categories';
+ statsEl.appendChild(catSpan);
+ if (query) {
+ const matchSpan = document.createElement('span');
+ matchSpan.textContent = '🔍 ' + filtered.length + ' match';
+ statsEl.appendChild(matchSpan);
+ }
}
if (filtered.length === 0) {
- list.innerHTML = '<div style="padding:20px; text-align:center; color:#666">' + (query ? 'No skills match your search.' : 'No skills yet. Create one below!') + '</div>';
+ const empty = document.createElement('div');
+ empty.style.cssText = 'padding:20px; text-align:center; color:#666';
+ empty.textContent = query ? 'No skills match your search.' : 'No skills yet. Create one below!';
+ list.appendChild(empty);
return;
}
// Group by category
@@ -2006,18 +2158,41 @@
const tags = (s.tags || []).join(', ');
const div = document.createElement('div');
div.className = 'module-item';
- div.innerHTML = `
- <div>
- <strong style="color:var(--success-color)">${name}</strong>
- <div style="font-size:0.85em; color:#aaa">${s.description || ''}</div>
- <div style="font-size:0.75em; color:#666">${tags ? '🏷 ' + tags : ''}</div>
- </div>
- <div style="display:flex;gap:4px">
- <button class="btn" onclick="webx11.viewSkillUI('${name}')" style="padding:4px 8px; font-size:0.8em">👁</button>
- <button class="btn" onclick="webx11.editSkillUI('${name}')" style="padding:4px 8px; font-size:0.8em">✏️</button>
- <button class="btn btn-danger" onclick="webx11.deleteSkillUI('${name}')" style="padding:4px 8px; font-size:0.8em">✕</button>
- </div>
- `;
+ const info = document.createElement('div');
+ const strong = document.createElement('strong');
+ strong.style.color = 'var(--success-color)';
+ strong.textContent = name;
+ info.appendChild(strong);
+ const desc = document.createElement('div');
+ desc.style.cssText = 'font-size:0.85em; color:#aaa';
+ desc.textContent = s.description || '';
+ info.appendChild(desc);
+ const tagsDiv = document.createElement('div');
+ tagsDiv.style.cssText = 'font-size:0.75em; color:#666';
+ tagsDiv.textContent = tags ? '🏷 ' + tags : '';
+ info.appendChild(tagsDiv);
+ const actions = document.createElement('div');
+ actions.style.cssText = 'display:flex;gap:4px';
+ const viewBtn = document.createElement('button');
+ viewBtn.className = 'btn';
+ viewBtn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ viewBtn.textContent = '👁';
+ viewBtn.onclick = () => this.viewSkillUI(name);
+ const editBtn = document.createElement('button');
+ editBtn.className = 'btn';
+ editBtn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ editBtn.textContent = '✏️';
+ editBtn.onclick = () => this.editSkillUI(name);
+ const delBtn = document.createElement('button');
+ delBtn.className = 'btn btn-danger';
+ delBtn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ delBtn.textContent = '✕';
+ delBtn.onclick = () => this.deleteSkillUI(name);
+ actions.appendChild(viewBtn);
+ actions.appendChild(editBtn);
+ actions.appendChild(delBtn);
+ div.appendChild(info);
+ div.appendChild(actions);
body.appendChild(div);
}
section.appendChild(body);
@@ -2081,9 +2256,9 @@
alert(`Skill '${name}' already exists. Delete it first or use skill-patch command.`);
return;
}
- let content = contentInput || `---\nname: ${name}\ndescription: ${description}\ntags: [${tags.join(', ')}]\ncategory: ${category}\n---\n## Steps\n1. (to be filled)\n`;
+ let content = contentInput || `---\nname: ${this._escapeYamlString(name)}\ndescription: ${this._escapeYamlString(description)}\ntags: [${tags.map(t => this._escapeYamlString(t)).join(', ')}]\ncategory: ${this._escapeYamlString(category)}\n---\n## Steps\n1. (to be filled)\n`;
if (!content.startsWith('---')) {
- content = `---\nname: ${name}\ndescription: ${description}\ntags: [${tags.join(', ')}]\ncategory: ${category}\n---\n${content}`;
+ content = `---\nname: ${this._escapeYamlString(name)}\ndescription: ${this._escapeYamlString(description)}\ntags: [${tags.map(t => this._escapeYamlString(t)).join(', ')}]\ncategory: ${this._escapeYamlString(category)}\n---\n${content}`;
}
fs.writeFile(`/skills/${name}.md`, content);
const index = fs.fsReadJSON('/skills/index.json') || {};
diff --git a/modules/core/virtual-fs.js b/modules/core/virtual-fs.js
index 7f6b441..d8b5098 100644
--- a/modules/core/virtual-fs.js
+++ b/modules/core/virtual-fs.js
@@ -37,6 +37,7 @@ class VirtualFileSystem {
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 ds = () => mm()?.DebugSupervisorModule || null;
const heap = () => typeof performance !== 'undefined' && performance.memory ? performance.memory : null;
const fmt = (bytes) => {
if (!bytes && bytes !== 0) return 'N/A';
@@ -112,7 +113,7 @@ class VirtualFileSystem {
const a = ai();
const cCount = a?.context?.length || 0;
const lCount = a?._toolCallLog?.length || 0;
- const eCount = a?._errorLog?.length || 0;
+ const eCount = ds() ? Object.keys(ds()._errorDB?.errors || {}).length : 0;
const modules = mm() ? Object.keys(mm()).length : 0;
const domNodes = typeof document !== 'undefined' ? document.querySelectorAll('*').length : 0;
return [
@@ -166,9 +167,12 @@ class VirtualFileSystem {
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');
+ const d = ds();
+ if (!d || !d._errorDB || Object.keys(d._errorDB.errors).length === 0) return '(empty)';
+ return Object.entries(d._errorDB.errors)
+ .sort((a, b) => b[1].lastSeen - a[1].lastSeen)
+ .map(([hash, e]) => `[${hash}] ${e.type}/${e.severity} ×${e.count}: ${e.message.substring(0, 100)}`)
+ .join('\n');
}),
'commands': vm('commands', () => {
const a = ai();