diff options
| author | Agando <roduccomm@gmail.com> | 2026-07-09 06:00:32 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-07-09 06:00:32 +0200 |
| commit | cf7461ffeb4f53f3f50c9c692a2e4fdcecf17e82 (patch) | |
| tree | e7780cd41271381cf108c9c81d71a3cb65cfd555 /modules/core | |
| parent | ea4d09fb5ccba12ed774fcf7c913edfa1e6a851a (diff) | |
auto: 2026-07-09_06:00 changes
Diffstat (limited to 'modules/core')
| -rw-r--r-- | modules/core/core-bundle.js | 3424 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 934 | ||||
| -rw-r--r-- | modules/core/module-chatstudio.js | 3 | ||||
| -rw-r--r-- | modules/core/module-debug-supervisor.js | 701 | ||||
| -rw-r--r-- | modules/core/module-duckduckgo.js | 38 | ||||
| -rw-r--r-- | modules/core/module-dynamicprompts.js | 102 | ||||
| -rw-r--r-- | modules/core/module-io.js | 80 | ||||
| -rw-r--r-- | modules/core/module-leases.js | 17 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/core/module-memory-graph.js | 303 | ||||
| -rw-r--r-- | modules/core/module-my.js | 82 | ||||
| -rw-r--r-- | modules/core/module-neural-vfs.js | 58 | ||||
| -rw-r--r-- | modules/core/module-orchestrator.js | 140 | ||||
| -rw-r--r-- | modules/core/module-smartctx.js | 112 | ||||
| -rw-r--r-- | modules/core/module-world-model.js | 96 | ||||
| -rw-r--r-- | modules/core/ollama-manager.js | 2 | ||||
| -rw-r--r-- | modules/core/terminal.js | 291 | ||||
| -rw-r--r-- | modules/core/virtual-fs.js | 12 |
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 => ({'&':'&','<':'<','>':'>',"'":''','"':'"'}[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 => ({'&':'&','<':'<','>':'>',"'":''','"':'"'}[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(); |