// Extracted from newOS4 index.html β€” WebX11AIAssistantModule (Full Agent Loop) class WebX11AIAssistantModule { constructor() { this.commands = {}; this.commandsMeta = { 'ai-config': { argType: 'enum', enumFn: '_getAiConfigKeys' } }; this.terminal = null; this.systemPrompt = this.buildSystemPrompt(); this.context = []; this.maxContext = 10; this._sessions = new Map(); // agentTag -> {context, profile}; 'main' aliases this.context this._toolCache = new Map(); // D2: key -> {result, ts, epoch} this._toolEpoch = 0; this._mutatingTools = new Set(['write_file', 'edit_module', 'run_command', 'delete_file', 'make_directory', 'commit_module', 'ship_module', 'patch_skill', 'delegate_task', 'reload_module', 'create_skill', 'create_mindmap']); // Only deterministic reads are cached β€” never status/log/polling tools. this._cacheableTools = new Set(['read_file', 'list_files', 'list_modules', 'view_module', 'nvfs_resolve', 'get_help', 'list_skills', 'diff_module', 'get_module_status']); this.maxContextTokens = 12000; this.MAX_TOOL_TURNS = 8; this._pendingConfirmation = null; this._confirmResolve = null; this._errorLog = []; this._commandFreq = {}; this._messageCount = 0; this._userProfile = {}; this._sessionMemoryLoaded = false; this._sessionSaved = false; this._thinkingTimer = null; this._thinkingStartTime = 0; this._validatorSuppressed = false; this._toolDefs = null; this._toolCallLog = []; this._debugPanelEnabled = false; this._lastRawResponse = null; this._lastRawMessages = null; // πŸ”’ Guardrail Layer this._guardrailPolicy = null; this._shadowSnapshot = null; this._guardrailViolations = {}; this._confirmTimeoutMs = 120000; this._skillDebounceMs = 1800000; this._chatLockTimeoutMs = 10000; this._loadGuardrailPolicy(); // ── Agent Loop Profile System ── this._activeLoopProfile = null; this._loopProfiles = {}; this._initLoopProfiles(); // Restore AI config from localStorage (maxContext, MAX_TOOL_TURNS, etc.) try { const cfg = localStorage.getItem('webx11_ai_config'); if (cfg) { const p = JSON.parse(cfg); if (p.maxTurns) this.MAX_TOOL_TURNS = p.maxTurns; if (p.maxContext) this.maxContext = p.maxContext; if (p.maxContextTokens) this.maxContextTokens = p.maxContextTokens; if (p.confirmTimeoutMs) this._confirmTimeoutMs = p.confirmTimeoutMs; if (p.skillDebounceMs) this._skillDebounceMs = p.skillDebounceMs; if (p.chatLockTimeoutMs) this._chatLockTimeoutMs = p.chatLockTimeoutMs; } } catch (e) {} try { const s = localStorage.getItem('webx11_ai_patterns'); if (s) this._commandFreq = JSON.parse(s); } catch (e) {} try { const p = localStorage.getItem('webx11_user_profile'); if (p) this._userProfile = JSON.parse(p); } catch (e) {} // Restore AI conversation context from localStorage try { const saved = localStorage.getItem('webx11_ai_context'); if (saved) { const parsed = JSON.parse(saved); if (parsed && Array.isArray(parsed.context)) { this.context = parsed.context; this._messageCount = parsed.messageCount || 0; } } } catch (e) {} } buildSystemPrompt() { // Check for user-overridden prompt in localStorage try { const saved = localStorage.getItem('webx11_ai_system_prompt'); if (saved) { const now = new Date(); const dateStr = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0'); const dayStr = now.toLocaleDateString('en-US', { weekday: 'long' }); const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); return `Current date: ${dateStr} (${dayStr}) ${timeStr}\n\n${saved}`; } } catch (e) {} // Check SmartContextModule for active profile system prompt let profilePrompt = ''; try { const scm = this.terminal?.moduleManager?.modules?.SmartContextModule; if (scm && scm.activeProfile && scm.contextProfiles[scm.activeProfile]) { profilePrompt = scm.contextProfiles[scm.activeProfile].systemPrompt || ''; } } catch (e) {} const now = new Date(); const dateStr = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0'); const dayStr = now.toLocaleDateString('en-US', { weekday: 'long' }); const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); const nvfs = this.terminal?.moduleManager?.modules?.NeuralVFSModule; let nvfsNote = ''; try { if (nvfs && nvfs.lastScan && nvfs.lastScan !== '(loaded from disk)') { const ageMs = Date.now() - new Date(nvfs.lastScan).getTime(); if (ageMs > 14400000) nvfsNote = ' |nvfs ' + Math.floor(ageMs/3600000) + 'h stale'; } } 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 | wallet-lookup | get_usage_stats\\n' + 'web_fetch: GET for pages, POST with method+body for JSON APIs. JS: node 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: read the system snapshot (modules, files, agents, telemetry, recent errors) and answer from it.'; 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) {} base += '\n\n[File reads, tools, verification]\n' + '- read_file returns 8000 chars by default (limit up to 16000). If the reply says truncated=true, call read_file again with offset= before concluding anything about the file.\n' + '- Prefer targeted reads over whole files: run_command with grep "" (prints line: text), sed -n \'10,40p\' , head -40 , tail -n 40 , wc -l . Flags are tolerated, but plain `cat ` and read_file offset/limit always work.\n' + '- sed without -i is read-only and allowed; sed -i, rm, mv and writes need user confirmation.\n' + '- After ANY write or edit: re-read the changed lines (cat or sed -n) and run a check that exists in THIS terminal β€” `test` for the suite, `grep -n` to confirm the new line, `wc -l` for size. There is no shell, no node and no curl here.\n' + '- If a tool fails twice, change approach (different tool, smaller scope) instead of retrying the same call.'; return base; } getToolDefinitions() { if (!this._toolDefs) this._toolDefs = this._buildToolDefs(); return this._filterToolsByProfile(this._toolDefs); } _filterToolsByProfile(allTools) { var p = this._activeLoopProfile; if (!p || !p.tools) return allTools; 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) { return allTools.filter(function(t) { return p.tools.enabled.indexOf(t.function.name) !== -1; }); } return allTools; } _buildToolDefs() { return [{ type: 'function', function: { name: 'list_files', description: 'List VFS directory contents', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Dir path ("" for root)' } }, required: [] } } }, { type: 'function', function: { name: 'read_file', description: 'Read a VFS file. Large files come back in pages: if the reply has truncated=true, call again with offset= instead of guessing or giving up. Returns JSON {path, offset, returned, total_chars, truncated, next_offset, content}.', parameters: { type: 'object', properties: { path: { type: 'string', description: 'File path' }, offset: { type: 'integer', description: 'Start character offset (default 0)' }, limit: { type: 'integer', description: 'Max characters to return (default 8000, max 32000)' } }, required: ['path'] } } }, { type: 'function', function: { name: 'write_file', description: 'Write file (DESTRUCTIVE)', parameters: { type: 'object', properties: { path: { type: 'string', description: 'File path' }, content: { type: 'string', description: 'Content' } }, required: ['path', 'content'] } } }, { type: 'function', function: { name: 'make_directory', description: 'Create VFS directory', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Dir name' } }, required: ['path'] } } }, { type: 'function', function: { name: 'delete_file', description: 'Delete file or empty dir (DESTRUCTIVE)', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Path to delete' } }, required: ['path'] } } }, { type: 'function', function: { name: 'get_status', description: 'System status: FS, modules, memory', parameters: { type: 'object', properties: {}, required: [] } } }, { 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: [] } } }, { type: 'function', function: { name: 'web_search', description: 'Search web via SearXNG + DDG', parameters: { type: 'object', properties: { query: { type: 'string', description: 'Search query' } }, required: ['query'] } } }, { type: 'function', function: { name: 'web_fetch', description: 'Fetch & extract URL content. Supports GET (default) and POST (for APIs like Hyperliquid /info).', parameters: { type: 'object', properties: { url: { type: 'string', description: 'Full URL' }, method: { type: 'string', enum: ['GET', 'POST'], description: 'HTTP method (default GET)' }, body: { type: 'string', description: 'Request body for POST (JSON string, max 10KB)' }, content_type: { type: 'string', description: 'Content-Type header (default application/json for POST)' } }, required: ['url'] } } }, { type: 'function', function: { name: 'wpm_install', description: 'Install WPM package', parameters: { type: 'object', properties: { package: { type: 'string', description: 'Package name' } }, required: ['package'] } } }, { type: 'function', function: { name: 'wpm_search', description: 'Search WPM packages', parameters: { type: 'object', properties: { keyword: { type: 'string', description: 'Keyword' } }, required: ['keyword'] } } }, { type: 'function', function: { name: 'run_command', description: 'Run terminal command (safe-read commands auto-approved; mutating ones require confirmation)', parameters: { type: 'object', properties: { command: { type: 'string', description: 'Command to run' } }, required: ['command'] } } }, { type: 'function', function: { name: 'get_current_dir', description: 'Current VFS directory', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'view_module', description: 'View module source code', parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] } } }, { type: 'function', function: { name: 'edit_module', 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 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', function: { name: 'reload_module', description: 'Reload module from original URL', parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] } } }, { type: 'function', function: { name: 'list_modified_modules', description: 'List modules with local edits', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'get_module_status', description: 'Module status: loaded, modified, URLs', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'load_skills', description: 'Search skills relevant to task', parameters: { type: 'object', properties: { query: { type: 'string', description: 'Task description' } }, required: ['query'] } } }, { type: 'function', function: { name: 'create_skill', description: 'Save a new skill', parameters: { type: 'object', properties: { name: { type: 'string', description: 'kebab-case name' }, description: { type: 'string', description: 'One-line description' }, content: { type: 'string', description: 'Markdown with YAML frontmatter' }, tags: { type: 'string', description: 'Comma-separated' }, category: { type: 'string', description: 'e.g. research, system, dev' } }, required: ['name', 'description', 'content'] } } }, { type: 'function', function: { name: 'patch_skill', description: 'Replace skill content', parameters: { type: 'object', properties: { name: { type: 'string', description: 'Skill name' }, content: { type: 'string', description: 'New markdown content' } }, required: ['name', 'content'] } } }, { type: 'function', function: { name: 'list_skills', description: 'Browse skills (optional filter)', parameters: { type: 'object', properties: { category: { type: 'string', description: 'Filter by category' } }, required: [] } } }, { type: 'function', function: { name: 'check_updates', description: 'Check module URLs are reachable', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'user_get', description: 'Read user profile key', parameters: { type: 'object', properties: { key: { type: 'string', description: 'e.g. preferredModel' } }, required: ['key'] } } }, { type: 'function', function: { name: 'user_set', description: 'Write user profile key', parameters: { type: 'object', properties: { key: { type: 'string', description: 'Profile key' }, value: { type: 'string', description: 'Value' } }, required: ['key', 'value'] } } }, { type: 'function', function: { name: 'user_list', description: 'List stored profile keys', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'user_forget', description: 'Delete profile key', parameters: { type: 'object', properties: { key: { type: 'string', description: 'Key to delete' } }, required: ['key'] } } }, { type: 'function', function: { name: 'session_save', description: 'Save session summary', parameters: { type: 'object', properties: { summary: { type: 'string', description: 'What was accomplished' } }, required: ['summary'] } } }, { type: 'function', function: { name: 'session_load', description: 'Load past session by date', parameters: { type: 'object', properties: { date: { type: 'string', description: 'YYYY-MM-DD' } }, required: ['date'] } } }, { type: 'function', function: { name: 'session_recent', description: 'Show recent sessions', parameters: { type: 'object', properties: { count: { type: 'number', description: 'Count (default 3)' } }, required: [] } } }, { type: 'function', function: { name: 'session_terminal', description: 'Read last terminal output', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'session_stats', description: 'Session aggregate stats', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'list_agents', description: 'List registered agent IDs, status, and capabilities', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'delegate_task', description: '(DESTRUCTIVE) Delegate a task to a worker agent. Auto-spawns if needed.', parameters: { type: 'object', properties: { task: { type: 'string', description: 'The task description to delegate' } }, required: ['task'] } } }, { type: 'function', function: { name: 'chat_hermes', description: 'Send a message to Hermes, the platform operator agent, and read replies. Use when a task needs a decision only the operator can make: policy, a deploy approval, scope. Replies are asynchronous β€” call with action "read" to check for an answer.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['send', 'read'], description: 'send a message, or read replies' }, text: { type: 'string', description: 'message body, required for send' } }, required: ['action'] } } }, { type: 'function', function: { name: 'spawn_agent', description: '(DESTRUCTIVE) Spawn a new worker agent with a role and capabilities', parameters: { type: 'object', properties: { role: { type: 'string', description: 'Agent role (e.g. researcher, writer)' }, capabilities: { type: 'string', description: 'Comma-separated capabilities (optional)' } }, required: ['role'] } } }, { type: 'function', function: { name: 'nvfs_resolve', description: 'Search VFS files by semantic concept using Neural VFS index. Call nvfs-index first to build the index.', parameters: { type: 'object', properties: { concept: { type: 'string', description: 'Concept to search (e.g. skill, memory, module, webdav, security)' } }, required: ['concept'] } } }, { type: 'function', function: { name: 'list_available_modules', description: 'List all available modules with metadata (core/ext, commands, class name, layer). Filter by type with type=core or type=ext.', parameters: { type: 'object', properties: { type: { type: 'string', description: 'Optional: filter to "core" or "ext"' } }, required: [] } } }, { type: 'function', function: { name: 'smart_context_list', description: 'List available AI context profiles (e.g. coding, debug, creative, technical, chat, system-admin, research, security). Each profile sets the AI system prompt, temperature, and behavior for a specific task type. Also switches the agent loop profile (max turns, tool set) to match. Returns profile names, descriptions, and system prompts. Use this when the user task has a clear domain (coding, debugging, research, etc.) to optimize AI behavior.', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'smart_context_set', description: "Set the active AI context profile. Changes the AI system prompt, temperature, and behavior for subsequent responses. Also switches the agent loop profile (max turns, tool set) to match. Available profiles: coding (code editing), debug (bug fixing), creative (brainstorming), technical (documentation), chat (casual), system-admin (maintenance), research (information gathering), security (threat assessment). Use this when the user task changes domain.", parameters: { type: 'object', properties: { name: { type: 'string', description: 'Profile name (e.g. "coding", "debug", "creative", "technical", "chat", "system-admin", "research", "security")' } }, required: ['name'] } } }, { type: 'function', function: { name: 'create_mindmap', description: 'Create a mindmap with the given topic and child nodes, then render it as inline SVG in the terminal. Creates root and all children in one call. Use this when the user asks for a mindmap or visual architecture.', parameters: { type: 'object', properties: { topic: { type: 'string', description: 'Root topic of the mindmap (e.g. "WebX11 Architecture")' }, children: { type: 'string', description: 'Comma-separated list of child node names (e.g. "AI Engine, VFS System, Modules")' } }, required: ['topic'] } } }, { 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/ API.", parameters: { type: 'object', properties: { endpoint: { type: 'string', description: 'prices, portfolio, transactions, copy-trade, or chat' }, message: { type: 'string', description: 'Question for the AI agent (chat endpoint only)' }, agent: { type: 'string', enum: ['market', 'trading', 'transaction', 'copy'], description: 'Which AI agent to query (chat endpoint only)' } }, required: ['endpoint'] } } }, { type: 'function', function: { name: 'vaultmind_connect', description: 'Connect to VaultMind AI by reading Hyperliquid private key from SecretVault (stored as "hyperliquid_key") and authenticating with the backend. Stores session token in localStorage.', parameters: { type: 'object', properties: { network: { type: 'string', enum: ['testnet', 'mainnet'], description: 'Hyperliquid network (default testnet)' } }, required: [] } } }, { type: 'function', function: { name: 'vaultmind_status', description: 'Check if VaultMind AI session token exists and is valid in localStorage.', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'wallet_lookup', description: 'Read-only Hyperliquid wallet lookup β€” returns portfolio, positions, and account value for any wallet address. No auth needed.', parameters: { type: 'object', properties: { address: { type: 'string', description: 'Hyperliquid wallet address (0x...)' } }, required: ['address'] } } }, { type: 'function', function: { name: 'get_usage_stats', description: 'Get LLM usage statistics: total requests, tokens, latency, per-model breakdown. Use this to report token consumption or latency to the user.', parameters: { type: 'object', properties: { model: { type: 'string', description: 'Filter by model name (optional)' }, provider: { type: 'string', description: 'Filter by provider name (optional)' }, since_hours: { type: 'number', description: 'Only last N hours (optional)' } }, 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: 'Read the synthesized system world model (modules, files, skills, agents, memory graph, telemetry, recent changes). Returns a JSON snapshot you can reason over; `facts` additionally carries the instant keyword answer. Ask detail:"full" for the complete model (~3 KB).', parameters: { type: 'object', properties: { question: { type: 'string', description: 'What you want to know (used for the fast facts line)' }, detail: { type: 'string', description: '"digest" (default) or "full"' } }, 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 β€” Ask the AI assistant anything', 'ask': 'ask β€” Alias for ai command', 'ai-clear': 'ai-clear β€” Clear conversation context', 'ai-system': 'ai-system β€” Set system prompt', 'ai-context': 'ai-context β€” Show current conversation context', 'ai-allow': 'ai-allow β€” Confirm pending AI action', 'ai-deny': 'ai-deny β€” Deny pending AI action', 'ai-ignore': 'ai-ignore β€” Suppress validator warnings for this session', 'ai-stop': 'ai-stop β€” Cancel current AI operation', 'ai-model': 'ai-model [model] β€” Show or set AI model', 'ai-config': 'ai-config [key] [value] β€” View or set AI loop config (max-turns, max-context, confirm-timeout(0=disabled))', 'agent-loops': 'agent-loops [name|new|edit|delete|clone|export|reset] β€” Manage agent loop profiles', 'bench': 'bench [profile] [--tasks=ids] [--repeat=N] β€” Run benchmark suite on a profile', 'bench-compare': 'bench-compare β€” Compare benchmark results of two profiles', 'debug': 'debug β€” Toggle system debug console (logs, fetch, errors)', 'debug-ai': 'debug-ai β€” Toggle AI tool-call debug panel', 'mem-debug': 'mem-debug β€” Show memory breakdown by component', '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 [message] β€” Query VaultMind API', 'wallet-lookup': 'wallet-lookup
β€” Look up Hyperliquid wallet summary', }; } registerCommands() { this.commands['ai'] = (args) => this.chat(args); this.commands['ask'] = (args) => this.chat(args); this.commands['ai-clear'] = () => this.clearContext(); this.commands['ai-system'] = (args) => this.setSystemPrompt(args); this.commands['ai-context'] = () => this.showContext(); this.commands['ai-allow'] = () => this.confirmPending(); this.commands['ai-deny'] = () => this.denyPending(); this.commands['ai-ignore'] = () => this.ignoreValidator(); this.commands['ai-stop'] = () => this.cancelAi(); 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); this.commands['debug'] = () => this.toggleDebugPanel(); this.commands['debug-ai'] = () => this.toggleDebug(); this.commands['debug-ai-last'] = () => this._showLastRawResponse(); this.commands['debug-ai-send'] = () => this._showLastRawRequest(); this.commands['debug-ai-log'] = () => this._showToolCallLog(); this.commands['archive-list'] = () => this._archiveList(); this.commands['archive-read'] = (args) => this._archiveRead(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]); this.commands['shadow'] = (args) => this.shadowExec(args); this.commands['shadow-commit'] = () => this.shadowCommit(); this.commands['shadow-rollback'] = () => this.shadowRollback(); this.commands['mem-debug'] = () => this.memDebug(); this.commands['reset-mem'] = () => this.resetMem(); 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); } _cacheGet(name, args) { const key = name + ':' + (typeof args === 'string' ? args : JSON.stringify(args || {})); if (!this._cacheableTools.has(name)) return { hit: false, key: key }; const e = this._toolCache.get(key); if (e && e.epoch === this._toolEpoch && (Date.now() - e.ts) < 30000) { return { hit: true, key: key, result: e.result }; } return { hit: false, key: key }; } _cacheSet(name, key, result) { if (this._mutatingTools.has(name)) { this._toolEpoch++; return; } // invalidate all if (!this._cacheableTools.has(name) || !key || typeof result !== 'string') return; this._toolCache.set(key, { result: result, ts: Date.now(), epoch: this._toolEpoch }); if (this._toolCache.size > 200) { this._toolCache.delete(this._toolCache.keys().next().value); } } _sessionCtx(tag) { const k = tag || 'main'; // main is always the live this.context (commands like `ai-clear`/`load` reassign it) if (k === 'main') return { context: this.context, profile: null }; if (!this._sessions.has(k)) this._sessions.set(k, { context: [], profile: null }); return this._sessions.get(k); } async chat(args, opts = {}) { const userMessage = args.join(' '); if (!userMessage) { this.terminal.print('Usage: ai ', 'warning'); return; } this._cancelled = false; this.terminal.setAiBusy(true); const lockStart = Date.now(); while (this._chatLock) { if (Date.now() - lockStart > this._chatLockTimeoutMs) { this._chatLock = false; this.terminal.print('⚠️ Chat lock timed out β€” force released', 'warning'); break; } await new Promise(r => setTimeout(r, 50)); } this._chatLock = true; const taskStart = Date.now(); // predictor durations exclude chat-lock wait 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); } // Agent runs must not touch the user's main conversation: _loadContext()/ _compressContext() // reassign this.context, which would detach the session captured below. const _isAgentRun = !!opts.agentTag; if (!_isAgentRun) { this._loadContext(); await this._compressContext(); this._toolCache.clear(); // out-of-band VFS edits may have happened since the last chat } // Per-agent session: 'main' aliases this.context, agents get their own history. const sess = this._sessionCtx(opts.agentTag); let loopResult; try { const messages = [ { role: 'system', content: this.systemPrompt } ]; sess.context.slice().forEach(msg => messages.push(msg)); messages.push({ role: 'user', content: userMessage }); this.terminal.print('πŸ€– WebX11AI thinking...', 'info'); // Auto-load skills relevant to the user's query (awaited, not fire-and-forget) // Skip skill loading for trivial queries (< 10 chars, no actionable keywords) const optEnabled = this.terminal?.ollamaManager?.optimizationEnabled !== false; const isTrivial = optEnabled && userMessage.length < 10 && !/\b(search|write|install|edit|create|make|find|delete|remove|change|update|improve|add|fix|build|set|load|save|help)\b/i.test(userMessage); if (!isTrivial) { try { const skillRes = await this.runTool('load_skills', { query: userMessage }); const skillData = JSON.parse(skillRes); if (skillData && skillData.matches && skillData.matches.length > 0) { const skillNotes = skillData.matches.map(m => `[Skill: ${m.name}] ${m.content}`).join('\n\n---\n\n'); messages.push({ role: 'system', content: `Relevant skills for this task:\n\n${skillNotes}\n\nUse the steps above as guidance. If they're wrong or outdated, use patch_skill to fix them after the task.` }); this.terminal.print(`πŸ“š Loaded ${skillData.count} relevant skill(s)`, 'info'); } } catch (e) { /* skills not available yet β€” first run */ } } // Track conversation patternsfor learning this._trackPattern(userMessage); // Extract facts from user message this._extractFacts(userMessage, ''); // On first message of session, auto-load recent memory if (!this._sessionMemoryLoaded) { this._sessionMemoryLoaded = true; this.terminal.fs.mkdirRecursive('/memory'); try { const recentRes = await this.runTool('session_recent', { count: 3 }); const rd = JSON.parse(recentRes); if (rd.sessions && rd.sessions.length > 0) { const memNotes = rd.sessions.map(s => '[' + s.date + '] ' + (s.tasks || 1) + ' tasks: ' + (s.summary || 'No summary')).join('\n'); messages.push({ role: 'system', content: 'Recent sessions:\n' + memNotes + '\nThis provides continuity. Refer to past work when relevant.' }); this.terminal.print('πŸ“– Loaded ' + rd.sessions.length + ' recent session(s)', 'info'); } } catch (e) {} } // Inject memory graph context for cross-session awareness try { const memgraph = this.terminal.moduleManager.modules['MemoryGraphModule']; if (memgraph) { const mctx = memgraph.getContextForPrompt(); if (mctx) { messages.push({ role: 'system', content: mctx }); } } } catch (e) { /* memory graph not available */ } // Inject world model context for system self-awareness try { const wm = this.terminal.moduleManager.modules['WorldModelModule']; if (wm) { const wctx = wm.getContextForPrompt(); if (wctx) { messages.push({ role: 'system', content: wctx }); } } } catch (e) { /* world model not available */ } // v4.6.56: register a prediction for THIS turn so recordResult can score it by id. let turnPredictionId = null; try { const predictor = this.terminal.moduleManager.modules['PredictorModule']; if (predictor && typeof predictor.notePrediction === 'function') { const tp = predictor.notePrediction(userMessage, 'loop'); turnPredictionId = tp && tp.id ? tp.id : null; } } catch (e) { /* predictor is best-effort */ } const _traceTag = opts && opts.agentTag ? opts.agentTag : 'main'; try { loopResult = await this.agenticLoop(messages, opts); } catch (_loopErr) { this._flushToolTrace(sess, _traceTag); // never leave stale notes behind throw _loopErr; } sess.context.push({ role: 'user', content: userMessage }); if (loopResult.finalAnswer) { sess.context.push({ role: 'assistant', content: loopResult.finalAnswer }); } // Durable trace of what was read/run this turn (2026-10-21) β€” without this, // every later turn started blind and the model re-read the same files. this._flushToolTrace(sess, _traceTag); if (sess.context.length > this.maxContext * 2) { // Silently splicing the opening turns is how a long task forgets its own brief: // the model then "remembers" a conversation that never started that way. Drop // the same messages, but leave a marker saying how many and what the first was. const cap = this.maxContext * 2; const dropped = sess.context.splice(0, sess.context.length - (cap - 1)); const firstUser = dropped.find(m => m.role === 'user'); const note = '[context trim: ' + dropped.length + ' earlier message(s) left this window' + (firstUser && firstUser.content ? ' β€” the earliest was: "' + String(firstUser.content).replace(/\s+/g, ' ').slice(0, 90) + '…"' : '') + ']'; sess.context.unshift({ role: 'system', content: note }); this.terminal.print('βœ‚οΈ Context trimmed: ' + dropped.length + ' message(s) β€” the model is told what it lost', 'text-muted'); } // Reflection: after complex tasks, run health scan and auto-skill creation 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() - taskStart) / 1000; predictor.recordResult(features, duration, loopResult.toolTurns, !this._cancelled, { task: userMessage, predictionId: turnPredictionId }); } } 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) { /* self-improvement is best-effort */ } } // Auto-create skills from repeated errors if (this._errorLog.length >= 3) { await this._autoCreateErrorSkill(); } // Skill quality audit every 20 messages this._messageCount++; if (this._messageCount % 20 === 0) { await this._auditSkills(); } // Sound notification on AI response if (this.terminal.playNotificationSound) { setTimeout(() => this.terminal.playNotificationSound(), 100); } } finally { this._chatLock = false; this.terminal.setAiBusy(false); // Persist only the user's conversation β€” agent sessions are ephemeral. if (!_isAgentRun) this._saveContext(); // Log the AI interaction this.terminal.log('INFO', 'AI', 'Query: ' + userMessage.substring(0, 100)); if (loopResult && loopResult.toolTurns) { 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); } } // Auto-archive every 10 messages if (this._messageCount > 0 && this._messageCount % 10 === 0) { this._archiveSession('Auto-archive at message ' + this._messageCount); } // Register session save on page unload (runs once per session) if (!this._sessionSaved) { this._sessionSaved = true; window.addEventListener('beforeunload', () => { this.terminal.fs.mkdirRecursive('/memory'); const today = new Date().toISOString().split('T')[0]; const index = this.terminal.fs.fsReadJSON('/memory/index.json') || {}; index[today] = index[today] || { summary: 'Session in progress', tasks: 0 }; index[today].tasks = (index[today].tasks || 0) + 1; index[today].updated = new Date().toISOString(); this.terminal.fs.fsWriteJSON('/memory/index.json', index); // Also persist AI context on page close this._saveContext(); }); } } // ─── Self-Improvement Helpers ─── async _captureCommandOutput(cmd) { let output = ''; // Loop guard: AI-initiated commands must not route back to AI on unknown command this.terminal.security._aiToolDepth++; this.terminal._pushPrintCapture(buf => { output += buf; }); try { await this.terminal.security.checkAndRun(cmd); } finally { this.terminal._popPrintCapture(); this.terminal.security._aiToolDepth--; } return output.trim(); } async _captureShellCommand(cmd) { // Routes through executeCommand (shell parser) for >, |, &&, export, etc. let output = ''; // Loop guard: AI-initiated commands must not route back to AI on unknown command this.terminal.security._aiToolDepth++; this.terminal._pushPrintCapture(buf => { output += buf; }); try { await this.terminal.executeCommand(cmd); } finally { this.terminal._popPrintCapture(); this.terminal.security._aiToolDepth--; } 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'); if (lines.length > 5) { return lines.slice(0, 2).join('\n') + '\n...(' + (lines.length - 4) + ' more lines)...\n' + lines.slice(-2).join('\n'); } return result.substring(0, maxLen) + '...(truncated from ' + result.length + ' chars)'; } // MAINT-023: one agentic turn used to grow without limit β€” every role:'tool' body was // pushed verbatim and only this.context was trimmed BETWEEN turns, so a measured turn // went 6 β†’ 112 705 tokens across 8 model calls (maxContextTokens is 12 000). Two // limits keep a single turn inside the window: clip each observation as it arrives, // and elide the oldest ones once the running total passes the budget. _clipToolResult(result) { const text = typeof result === 'string' ? result : JSON.stringify(result); const max = this.maxToolResultChars || 6000; if (!text || text.length <= max) return text; return text.slice(0, max) + '\n…[clipped: ' + (text.length - max) + ' of ' + text.length + ' chars withheld β€” re-read with offset/limit if you need the rest]'; } _boundToolContext(messages) { const keepVerbatim = this.toolResultsVerbatim || 4; const budget = this.toolContextBudgetChars || 24000; const idx = []; for (let i = 0; i < messages.length; i++) { if (messages[i] && messages[i].role === 'tool') idx.push(i); } if (!idx.length) return messages; const recent = new Set(idx.slice(-keepVerbatim)); let total = 0; for (const i of idx) total += String(messages[i].content || '').length; // Oldest first, never the recent few: those are what the model is working on. for (const i of idx) { if (total <= budget) break; if (recent.has(i)) continue; const len = String(messages[i].content || '').length; const stub = '[earlier tool result elided β€” ' + len + ' chars; call the tool again if you still need it]'; messages[i].content = stub; total -= len - stub.length; } return messages; } // 2026-10-21: tool results lived only inside the current turn, so the next turn // had no idea what the model had already read or run. Keep a compact per-turn // trace (per agent tag) and persist it into the session context. // Review-hardened: no file bodies, no untrusted web output, no secrets. _noteObservation(name, args, result, tag) { try { if (/^(web_search|web_fetch|scrape|fetch)$/.test(name)) return; // injection surface const key = tag || 'main'; if (!this._turnObservations) this._turnObservations = {}; const buf = this._turnObservations[key] || (this._turnObservations[key] = []); const clean = s => String(s === undefined || s === null ? '' : s) .replace(/\s+/g, ' ') .replace(/(token|key|secret|password|bearer)[^,}\s]*/ig, '$1:[redacted]'); let argStr = (typeof args === 'object' && args !== null) ? JSON.stringify(args) : String(args || ''); argStr = clean(argStr).slice(0, 80); let note; if (name === 'read_file') { let p = {}; try { p = JSON.parse(String(result)) || {}; } catch (e) {} note = 'read_file ' + (p.path || argStr) + ' [' + (p.offset || 0) + '+' + (p.returned || 0) + '/' + (p.total_chars === undefined ? '?' : p.total_chars) + (p.truncated ? ', next=' + p.next_offset : '') + ']'; } else { note = name + '(' + argStr + ') β†’ ' + clean(String(result)).slice(0, 120); } buf.push(note.slice(0, 240)); // Ring buffer: keep the first 2 (the goal) and the last 10 (verification). if (buf.length > 12) buf.splice(2, 1); } catch (_e) {} } // Push this turn's trace into the session context and clear the buffer. _flushToolTrace(sess, tag) { try { const key = tag || 'main'; const buf = (this._turnObservations && this._turnObservations[key]) || []; if (buf.length && sess && sess.context) { sess.context.push({ role: 'system', content: 'βš™οΈ tool trace: ' + buf.join(' | ') }); } if (this._turnObservations) delete this._turnObservations[key]; } catch (_e) {} } _escapeHtml(str) { return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } _updateDebugPanel() { if (!this._debugPanelEnabled) return; const panel = document.getElementById('debug-ai-panel'); if (!panel) return; const activeTab = panel.getAttribute('data-tab') || 'tools'; const recent = this._toolCallLog.slice(-20); let toolHtml = ''; for (const entry of recent) { const statusClass = entry.error ? 'debug-entry-error' : 'debug-entry-ok'; const argsStr = typeof entry.args === 'string' ? entry.args.substring(0, 60) : JSON.stringify(entry.args).substring(0, 60); toolHtml += '
' + '#' + entry.turn + ' ' + '' + this._escapeHtml(entry.name) + ' ' + '' + this._escapeHtml(argsStr) + ' ' + '' + entry.duration + 's' + (entry.error ? ' ⚠️' : '') + '
'; } // Raw I/O tab content let rawHtml = ''; if (this._lastRawResponse) { const r = this._lastRawResponse; const content = (r.message && r.message.content) || ''; const hasTools = r.tool_calls && r.tool_calls.length > 0; rawHtml += '
'; if (r.error) rawHtml += '
⚠️ ' + this._escapeHtml(r.error) + '
'; if (content) { const safe = this._escapeHtml(content.length > 300 ? content.substring(0, 300) + '...' : content); rawHtml += '
Content: ' + safe + '
'; } if (hasTools) { rawHtml += '
Tools: ' + r.tool_calls.map(function(tc) { return tc.function.name; }).join(', ') + '
'; } if (this._lastRawMessages) { rawHtml += '
Messages: ' + this._lastRawMessages.length + '
'; } rawHtml += '
'; } else { rawHtml = '
No AI response yet. Send a message first.
'; } const tabBar = '
' + 'πŸ”§ Tools' + 'πŸ“‘ Raw I/O' + '
'; const bodyHtml = activeTab === 'raw' ? rawHtml : toolHtml; panel.innerHTML = '
' + 'πŸ”§ AI Debug ' + this._toolCallLog.length + ' calls' + '' + '' + '' + '' + 'βœ•' + '
' + tabBar + bodyHtml; } _isToolError(result) { if (!result || typeof result !== 'string') return null; const trimmed = result.trim(); if (trimmed.startsWith('{"error":') || trimmed.startsWith("{'error':")) { try { const parsed = JSON.parse(trimmed); return parsed.error || null; } catch (e) { return null; } } return null; } _trackToolFailure(toolName, error) { try { if (!this._toolFailuresBuffer) this._toolFailuresBuffer = {}; if (!this._toolFailuresBuffer[toolName]) this._toolFailuresBuffer[toolName] = { count: 0, last: '', errors: [] }; const entry = this._toolFailuresBuffer[toolName]; entry.count++; entry.last = new Date().toISOString(); entry.errors.push(error); if (entry.errors.length > 5) entry.errors.shift(); if (entry.count >= 3) { this.terminal.print(`⚠️ Tool '${toolName}' has failed ${entry.count} times. Consider creating a workaround skill.`, 'warning'); } if (this._toolFailureFlushTimer) clearTimeout(this._toolFailureFlushTimer); this._toolFailureFlushTimer = setTimeout(() => this._flushToolFailures(), 2000); } catch (e) { /* ignore */ } } _flushToolFailures() { try { if (!this._toolFailuresBuffer || Object.keys(this._toolFailuresBuffer).length === 0) return; const key = 'webx11_tool_failures'; const raw = localStorage.getItem(key) || '{}'; const map = JSON.parse(raw); for (const [toolName, entry] of Object.entries(this._toolFailuresBuffer)) { if (!map[toolName]) map[toolName] = { count: 0, last: '', errors: [] }; map[toolName].count += entry.count; map[toolName].last = entry.last; map[toolName].errors.push(...entry.errors); if (map[toolName].errors.length > 10) map[toolName].errors = map[toolName].errors.slice(-10); } localStorage.setItem(key, JSON.stringify(map)); this._toolFailuresBuffer = {}; } catch (e) { /* ignore */ } } async agenticLoop(messages, opts = {}) { var profile = this._activeLoopProfile || {}; var loopCfg = profile.loop || {}; var maxTurns = loopCfg.maxTurns || this.MAX_TOOL_TURNS; var baseTurns = maxTurns; let _lastTurnSig = null, _progressStreak = 0, _stallStreak = 0; const _logStart = (this._toolCallLog || []).length; const _loopLog = []; // per-run tool log (the global one self-trims to 50 and breaks slicing) // D3: extend the budget only while turns keep producing NEW output. const _noteProgress = (results) => { try { const arr = (results || []).map(r => (r && typeof r === 'object' && 'toolResult' in r) ? r.toolResult : r); if (!arr.length) return; const h = _sigHash(JSON.stringify(arr)); if (_lastTurnSig !== null && h !== _lastTurnSig) { _progressStreak++; _stallStreak = 0; } else if (_lastTurnSig !== null) { _stallStreak++; _progressStreak = 0; } _lastTurnSig = h; if (_progressStreak >= 2 && maxTurns < baseTurns + 4) { maxTurns += 2; this.terminal.print(`πŸ“ˆ Progress detected β€” extending turn budget to ${maxTurns}`, 'info'); } if (_stallStreak === 3) { messages.push({ role: 'system', content: 'No new information in the last turns β€” stop calling tools and give your final answer now.' }); } } catch (_e) {} }; 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 try { var fs = this.terminal.fs; if (fs) { // MAINT-020: lessons live in /lessons, not in /skills β€” writing them into // the skills directory made auto-generated lessons show up as skills. var index = fs.fsReadJSON('/lessons/index.json') || {}; var lessonKeys = Object.keys(index); if (lessonKeys.length === 0) { // one-release fallback for lessons written by older builds var legacy = fs.fsReadJSON('/skills/index.json') || {}; lessonKeys = Object.keys(legacy).filter(function(k) { return k.indexOf('loop-lesson-') === 0; }); } if (lessonKeys.length > 0) { var recentLessons = lessonKeys.sort().slice(-3); var lessonTexts = []; for (var li = 0; li < recentLessons.length; li++) { var lc = fs.readFile('/lessons/' + recentLessons[li] + '.md') || fs.readFile('/skills/' + recentLessons[li] + '.md'); if (lc) { var excerpt = lc.substring(0, 300); lessonTexts.push('[Lesson: ' + recentLessons[li] + '] ' + excerpt); } } if (lessonTexts.length > 0) { messages.push({ role: 'system', content: 'Prior loop lessons (read before planning):\n' + lessonTexts.join('\n---\n') + '\n\nUse these to avoid repeating past mistakes. If lessons mention tool failures, verify those tools before calling them.' }); } } } } catch (e) { /* VFS not available β€” skip memory-augmented planning */ } let toolTurns = 0; const _seenCalls = new Map(); // sig -> {count, ts, resultHash, blocked, lastResult} const _loopWindow = loopDetect.windowMs || 30000; const _warnAt = loopDetect.warnAt || 3; const _blockAt = loopDetect.blockAt || 6; const _hardStopAt = loopDetect.hardStopAt || 9; const _agentTag = opts.agentTag || 'main'; this._pendingBlockedReplays = null; const _sigHash = (s) => { let h = 5381; const str = (typeof s === 'string') ? s : JSON.stringify(s == null ? '' : s); for (let i = 0; i < str.length; i++) h = ((h << 5) + h + str.charCodeAt(i)) | 0; return h >>> 0; }; while (toolTurns < maxTurns) { if (this._cancelled) { this.terminal.print('AI loop cancelled', 'warning'); return { finalAnswer: null, toolTurns }; } toolTurns++; // D4: refresh the running tool ledger so the model never re-reads what it already has if (toolTurns > 1) { try { for (let mi = messages.length - 1; mi >= 0; mi--) { if (messages[mi] && messages[mi]._ledger) messages.splice(mi, 1); } const _led = _loopLog.slice(-8).map(e => '- ' + e.name + '(' + String(typeof e.args === 'string' ? e.args : JSON.stringify(e.args || {})).slice(0, 60) + ') β†’ ' + (e.error ? 'ERROR' : this._summarizeObservation(e.result, 80)) ).join('\n'); if (_led) { messages.push({ role: 'system', _ledger: true, content: 'Tool ledger so far (do NOT repeat a call whose result you already have):\n' + _led }); } } catch (_e) {} } // On last turn, inject wrap-up prompt 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(); // MAINT-023: keep the in-flight tool context bounded before every model call. this._boundToolContext(messages); const res = await this.terminal.ollamaManager.chatWithTools(messages, this.getToolDefinitions()); const thinkTime = Math.round((Date.now() - thinkStart) / 1000); // Store raw response for debug-ai-last this._lastRawResponse = res; this._lastRawMessages = messages.slice(); if (this._cancelled) return { finalAnswer: null, toolTurns }; if (res.error) { this.terminal.print(`⚠️ AI model error (${thinkTime}s): ${res.error}`, 'error'); return { finalAnswer: null, toolTurns }; } if (res.message && !res.tool_calls) { const text = res.message.content || ''; this.terminal.printAIResponse(text || '(no response)'); if (text) { this.terminal.ollamaManager.fireResponseHooks(text); // Auto-speak if enabled const vm = this.terminal.moduleManager && this.terminal.moduleManager.modules['VoiceModule']; if (vm && vm.speakText) vm.speakText(text); } this.terminal.print(`πŸ’¬ AI answered (turn ${toolTurns}/${maxTurns}, ${thinkTime}s)`, 'info'); return { finalAnswer: text, toolTurns }; } if (res.tool_calls && res.tool_calls.length > 0) { // v4.6.30: progress-aware loop guard β€” counts STAGNATION, not repetition. // A repeated call whose result changed is progress and resets the counter. const now = Date.now(); const blocked = []; for (const tc of res.tool_calls) { // 2026-10-21: arguments can be an object (already parsed upstream). // `+ object` produced "name:[object Object]", collapsing every call to // the same tool into one key β†’ false "called 3x" loop warnings when the // model legitimately batched distinct commands. const _argKey = (typeof tc.function.arguments === 'string') ? tc.function.arguments : JSON.stringify(tc.function.arguments || {}); const sig = tc.function.name + ':' + _argKey; let entry = _seenCalls.get(sig); if (!entry || (now - entry.ts) > _loopWindow) { entry = { count: 0, ts: now, resultHash: null, blocked: false, lastResult: null, epoch: this._toolEpoch }; } else if (entry.epoch !== this._toolEpoch) { // A mutation since the last attempt unblocks the replay, but the // repeat counter must survive: resetting it let an identical // mutating call loop forever (review 2026-10-21). entry.epoch = this._toolEpoch; entry.blocked = false; } entry.count++; entry.ts = now; _seenCalls.set(sig, entry); const argPreview = String(_argKey || '') .replace(/(token|key|secret|password|bearer)[^,}]*/ig, '$1:[redacted]') .slice(0, 60); if (entry.blocked || entry.count >= _blockAt) { entry.blocked = true; blocked.push({ tc, entry }); this.terminal.print(`πŸ”„ Loop guard: ${_agentTag} repeated ${tc.function.name}(${argPreview}) ${entry.count}x with an unchanged result β€” serving cached result, not re-running`, 'warning'); continue; } if (entry.count === _warnAt) { this.terminal.print(`⚠️ Loop warning: ${_agentTag} called ${tc.function.name}(${argPreview}) ${entry.count}x β€” reuse the previous result or change the request`, 'warning'); } } if (blocked.length) { const _blockedIds = new Set(blocked.map(b => b.tc.id)); const _remaining = (res.tool_calls || []).filter(tc => !_blockedIds.has(tc.id)); if (_remaining.length === 0) { messages.push(res.message); for (const { tc, entry } of blocked) { messages.push({ role: 'tool', tool_call_id: tc.id, content: (entry.lastResult != null) ? entry.lastResult : JSON.stringify({ blocked: true, reason: 'identical call repeated with unchanged result', tool: tc.function.name }) }); } this.terminal.print(`πŸ›‘ Loop guard: ${blocked.length} repeated call(s) skipped β€” agent continues with cached data`, 'info'); const _hs = Math.min(_hardStopAt, maxTurns); if (blocked.some(b => b.entry.count >= _hs)) { this.terminal.print(`β›” ${_agentTag}: no progress after ${_hs} identical calls β€” ending loop with partial result`, 'error'); return { finalAnswer: this._loopSummary(toolTurns), toolTurns }; } continue; } // Mixed batch: keep the non-blocked calls, replay blocked ones after the // assistant message is pushed below. res.tool_calls = _remaining; this._pendingBlockedReplays = blocked; // MAINT-006: the hard stop was only reachable when EVERY call in the // batch was blocked, so a turn that mixed one repeated call with one // novel call never ended β€” the reported "agents loop on repetitive // turns". Apply the same ceiling on the mixed path. const _hsMixed = Math.min(_hardStopAt, maxTurns); if (blocked.some(b => b.entry.count >= _hsMixed)) { this.terminal.print(`β›” ${_agentTag}: no progress after ${_hsMixed} identical calls β€” ending loop with partial result`, 'error'); return { finalAnswer: this._loopSummary(toolTurns), toolTurns }; } } this.terminal.print(`πŸ”§ AI wants ${res.tool_calls.length} tool(s) β€” turn ${toolTurns}/${maxTurns} (${thinkTime}s)`, 'info'); messages.push(res.message); if (this._pendingBlockedReplays && this._pendingBlockedReplays.length) { for (const { tc, entry } of this._pendingBlockedReplays) { messages.push({ role: 'tool', tool_call_id: tc.id, content: entry.lastResult != null ? entry.lastResult : JSON.stringify({ blocked: true, tool: tc.function.name }) }); } this._pendingBlockedReplays = null; } let pendingText = res.message?.content || null; const destructiveTools = res.tool_calls.filter(tc => this.isDestructive(tc.function.name, tc.function.arguments)); const safeTools = res.tool_calls.filter(tc => !this.isDestructive(tc.function.name, tc.function.arguments)); // Execute non-destructive tools SEQUENTIALLY β€” parallel fan-out corrupts // print-capture attribution, shadow-VFS snapshot/diff pairing, and telemetry // under interleaved awaits. Inference latency dominates anyway. const safeResults = []; for (const tc of safeTools) { const fn = tc.function; const t0 = Date.now(); const _cg = this._cacheGet(fn.name, fn.arguments); let toolResult; if (_cg.hit) { toolResult = _cg.result; this.terminal.print(`♻️ ${fn.name} β†’ cached (${toolResult.length}b, not re-run)`, 'info'); } else { toolResult = await this.runTool(fn.name, fn.arguments); this._cacheSet(fn.name, _cg.key, toolResult); } const t1 = Math.round((Date.now() - t0) / 1000); this._toolCallLog.push({ turn: toolTurns, name: fn.name, args: fn.arguments, result: toolResult, duration: t1, error: this._isToolError(toolResult) ? true : false }); _loopLog.push(this._toolCallLog[this._toolCallLog.length - 1]); // Auto-trim to 50 β€” prevents unbounded memory growth if (this._toolCallLog.length > 50) { this._toolCallLog = this._toolCallLog.slice(-50); } // Telemetry const sm = this.terminal.moduleManager.modules['ServerMonitorModule']; if (sm && sm.logToolCall) sm.logToolCall(fn.name, (t1 * 1000) || 1, this._isToolError(toolResult)); if (this._debugPanelEnabled) this._updateDebugPanel(); safeResults.push({ tc, fn, toolResult, t1 }); } for (const { tc, fn, toolResult, t1 } of safeResults) { this.terminal.print(`πŸ”§ Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info'); // Memory graph: record tool call + file access try { const _mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule']; if (_mg) { _mg.recordToolCall(fn.name, !this._isToolError(toolResult)); if (fn.name === 'read_file' || fn.name === 'write_file' || fn.name === 'view_module' || fn.name === 'edit_module') { const _path = (typeof fn.arguments === 'object' ? (fn.arguments?.path || '') : ''); if (_path) _mg.recordFileAccess(_path); } } } catch (_e) { /* memory graph not available */ } const obs = this._summarizeObservation(toolResult, 150); const err = this._isToolError(toolResult); if (err) { this.terminal.log('WARN', 'AI', `Tool ${fn.name} failed: ${err} (${t1}s)`); this._trackToolFailure(fn.name, err); this.terminal.print(`πŸ“Š Result (${t1}s): ⚠️ Error: ${err}`, 'warning'); } else if (toolResult && toolResult.length > 150) { this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`); const safeId = 'toolresult-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6); this.terminal.printHTML(`πŸ“Š Result (${t1}s): ${this._escapeHtml(obs)} [+] show full (${toolResult.length} chars)`); } else { this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`); this.terminal.print(`πŸ“Š Result (${t1}s): ${obs}`, 'info'); } try { const _sigR = fn.name + ':' + ((typeof fn.arguments === 'string') ? fn.arguments : JSON.stringify(fn.arguments || {})); const _eR = _seenCalls.get(_sigR); if (_eR) { const _rh = _sigHash(toolResult); if (_eR.resultHash !== null && _eR.resultHash !== _rh) _eR.count = 1; // progress β†’ reset _eR.resultHash = _rh; _eR.lastResult = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult); } } catch (_e) {} // MAINT-023: clip on the way in β€” a single read_file page is 16 000 // chars and several of them used to enter the context verbatim. messages.push({ role: 'tool', tool_call_id: tc.id, content: this._clipToolResult(toolResult) }); this._noteObservation(fn.name, fn.arguments, toolResult, _agentTag); } // Only block on destructive tools if (destructiveTools.length > 0) { // 🎯 CONFIDENCE ESCALATION β€” auto-approve only if confidence >= threshold (from profile) let autoApprove = false; if (confEsc.enabled !== false && pendingText) { const confidence = this._parseConfidence(pendingText); if (confidence !== null && confidence >= (confEsc.threshold || 90)) { autoApprove = true; this.terminal.print(`🎯 AI confidence: ${confidence}% β€” auto-approved (threshold: ${confEsc.threshold || 90}%)`, 'info'); } else if (confidence !== null) { 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'); } } const approved = autoApprove || await this.confirmActionMulti(destructiveTools); if (approved) { for (const tc of destructiveTools) { const fn = tc.function; const t0 = Date.now(); this.terminal.print(`πŸ”§ Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info'); const toolResult = await this.runTool(fn.name, fn.arguments); this._toolEpoch++; // D2: mutating action invalidates every cached read const t1 = Math.round((Date.now() - t0) / 1000); this._toolCallLog.push({ turn: toolTurns, name: fn.name, args: fn.arguments, result: toolResult, duration: t1, error: this._isToolError(toolResult) ? true : false }); _loopLog.push(this._toolCallLog[this._toolCallLog.length - 1]); // Auto-trim to 50 β€” prevents unbounded memory growth if (this._toolCallLog.length > 50) { this._toolCallLog = this._toolCallLog.slice(-50); } // Telemetry const sm2 = this.terminal.moduleManager.modules['ServerMonitorModule']; if (sm2 && sm2.logToolCall) sm2.logToolCall(fn.name, (t1 * 1000) || 1, this._isToolError(toolResult)); if (this._debugPanelEnabled) this._updateDebugPanel(); // Memory graph: record destructive tool call try { const _mg2 = this.terminal?.moduleManager?.modules?.['MemoryGraphModule']; if (_mg2) { _mg2.recordToolCall(fn.name, !this._isToolError(toolResult)); if (fn.name === 'write_file' || fn.name === 'edit_module') { const _path2 = (typeof fn.arguments === 'object' ? (fn.arguments?.path || '') : ''); if (_path2) _mg2.recordFileAccess(_path2); } } } catch (_e2) { /* memory graph not available */ } const dObs = this._summarizeObservation(toolResult, 150); const dErr = this._isToolError(toolResult); if (dErr) { this.terminal.log('WARN', 'AI', `Tool ${fn.name} failed: ${dErr} (${t1}s)`); this._trackToolFailure(fn.name, dErr); this.terminal.print(`πŸ“Š Result (${t1}s): ⚠️ Error: ${dErr}`, 'warning'); } else { this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`); this.terminal.print(`πŸ“Š Result (${t1}s): ${dObs}`, 'info'); } try { const _sigD = fn.name + ':' + ((typeof fn.arguments === 'string') ? fn.arguments : JSON.stringify(fn.arguments || {})); const _eD = _seenCalls.get(_sigD); if (_eD) { const _rhD = _sigHash(toolResult); if (_eD.resultHash !== null && _eD.resultHash !== _rhD) _eD.count = 1; _eD.resultHash = _rhD; _eD.lastResult = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult); } } catch (_e) {} // MAINT-023: clip on the way in (same rule as the main loop). messages.push({ role: 'tool', tool_call_id: tc.id, content: this._clipToolResult(toolResult) }); this._noteObservation(fn.name, fn.arguments, toolResult, _agentTag); } } else { messages.push({ role: 'tool', tool_call_id: (destructiveTools[0] && destructiveTools[0].id) || undefined, content: `Destructive actions (${destructiveTools.map(tc => tc.function.name).join(', ')}) denied by user. Non-destructive actions already executed.` }); } } // Validator: check safe tool results for consistency if (safeResults.length > 0 && pendingText && !this._validatorSuppressed) { const om = this.terminal.ollamaManager; if (om && om.validatorEnabled && om.validatorModel) { let invalidTools = []; for (const { fn, toolResult } of safeResults) { const valid = await om.validateToolResult(fn.name, fn.arguments, toolResult, pendingText || ''); if (!valid && !this._cancelled) { invalidTools.push(fn.name); this.terminal.print('πŸ” Validator: ' + fn.name + ' result β€” AI note may not match (logged)', 'info'); } } if (invalidTools.length > 0) { pendingText += '\n\n⚠️ **Validator flagged** ' + invalidTools.join(', ') + ' β€” the AI analysis may not match what was returned. Use `ai-ignore` to dismiss and proceed.'; } } } _noteProgress(safeResults); if (pendingText) { messages.push({ role: 'assistant', content: pendingText }); } continue; } return { finalAnswer: null, toolTurns }; } // Loop exited without final answer β€” synthesize from what we have instead of returning nothing const toolNames = Object.keys(this._commandFreq).slice(-3).join(', '); this.terminal.print(`πŸ€– AI reached the ${toolTurns}/${maxTurns} tool limit.`, 'warning'); this.terminal.print('Tools were executed but no final answer was produced β€” synthesizing from results.', 'info'); return { finalAnswer: this._loopSummary(toolTurns), toolTurns }; } _loopSummary(toolTurns) { const recent = (this._toolCallLog || []).slice(-5).map(e => '- ' + e.name + '(' + String(e.args || '').slice(0, 60) + '): ' + (e.error ? 'error' : this._summarizeObservation(e.result, 100)) ).join('\n'); return 'Stopped after ' + toolTurns + ' tool turns without a final model answer. Last results:\n' + (recent || '(no tool results)'); } async afterActionReflection(query, toolTurns) { // Debounce: only suggest skill creation once per 30 minutes const now = Date.now(); if (this._lastSkillSuggestion && (now - this._lastSkillSuggestion) < this._skillDebounceMs) { return; } const suggestions = []; const fs = this.terminal.fs; try { // Batch: read all status info in one VFS pass const reg = this.terminal.moduleManager.moduleRegistry || []; const total = reg.length; const loaded = Object.keys(this.terminal.moduleManager.modules).length; if (loaded < total) { suggestions.push(loaded + '/' + total + ' modules loaded β€” use loadmod to restore'); } } catch (e) {} try { const skillIdx = fs.fsReadJSON('/skills/index.json'); const skillCount = skillIdx ? Object.keys(skillIdx).length : 0; if (skillCount === 0 && toolTurns >= 4) { suggestions.push('Save this approach as a skill with create_skill'); this._lastSkillSuggestion = now; } } catch (e) {} try { const modsDir = fs.getCurrentDir(); const mc = modsDir.content['_mods']; const modCount = mc && mc.content ? Object.keys(mc.content).filter(k => k.endsWith('.js') && !k.endsWith('.orig.js')).length : 0; if (modCount > 0) { suggestions.push(modCount + ' pending module edit(s) in /_mods/ β€” use test_module to verify'); } } catch (e) {} try { if (performance.memory && performance.memory.usedJSHeapSize > 100 * 1048576) { suggestions.push('High memory (' + (performance.memory.usedJSHeapSize / 1048576).toFixed(0) + 'MB) β€” consider clearing terminal'); } } catch (e) {} if (toolTurns >= 4 && suggestions.length === 0) { suggestions.push('Used ' + toolTurns + ' tools β€” create a skill to save this workflow'); this._lastSkillSuggestion = now; } if (suggestions.length > 0) { this.terminal.print('πŸ’‘ ' + suggestions.join(' | '), 'info'); } } async confirmActionMulti(toolCalls) { // No-confirm mode: auto-approve if (this._confirmTimeoutMs === 0) { this.terminal.print(`⚑ No-confirm mode: auto-approving ${toolCalls.map(tc => tc.function.name).join(', ')}`, 'info'); return true; } const names = toolCalls.map(tc => tc.function.name).join(', '); const args = toolCalls.map(tc => typeof tc.function.arguments === 'string' ? tc.function.arguments : JSON.stringify(tc.function.arguments)).join(' | '); const truncatedArgs = args.length > 60 ? args.substring(0, 60) + '...' : args; const timeoutLabel = this._confirmTimeoutMs === 0 ? '' : ` (Auto-denies in ${this._confirmTimeoutMs / 1000}s)`; this.terminal.printHTML(`⚠️ AI wants to: ${names}(${truncatedArgs}). ai-allow to confirm, ai-deny to refuse.${timeoutLabel}`); // Show confirm bar OUTSIDE scrollable output (always visible) const area = document.getElementById('ai-confirm-area'); if (!area) { // Fallback to promise resolve return true; } area.innerHTML = ` ⚠️ ${names}(${truncatedArgs}) Type: ai-allow or ai-deny `; area.classList.add('visible'); this._confirmArea = area; this._confirmBar = area; // Attach button handlers via both addEventListener + DOM property (inline onclick fallback) const allowBtn = document.getElementById('ai-confirm-allow'); const denyBtn = document.getElementById('ai-confirm-deny'); const onAllow = () => { this._resolveConfirm(true); }; const onDeny = () => { this._resolveConfirm(false); }; allowBtn.addEventListener('click', onAllow); denyBtn.addEventListener('click', onDeny); allowBtn.__allow = onAllow; denyBtn.__deny = onDeny; const startTime = Date.now(); return new Promise((resolve) => { this._pendingConfirmation = { names, args }; this._confirmResolve = resolve; this._confirmAllowBtn = allowBtn; this._confirmDenyBtn = denyBtn; this._confirmOnAllow = onAllow; this._confirmOnDeny = onDeny; // Countdown timer updates with escalating visibility const timerInterval = setInterval(() => { const elapsed = Math.round((Date.now() - startTime) / 1000); const remaining = Math.max(0, (this._confirmTimeoutMs / 1000) - elapsed); const timerEl = document.getElementById('ai-confirm-timer'); if (timerEl) { timerEl.textContent = `⏱ ${remaining}s`; timerEl.className = 'confirm-timer'; if (remaining > 30) { timerEl.style.color = 'var(--warning-color)'; } else if (remaining > 10) { timerEl.classList.add('urgent'); } else { timerEl.classList.add('critical'); } } if (remaining === 60) { this.terminal.print(`⏱ 60s remaining to respond to AI confirmation for: ${names}`, 'warning'); } else if (remaining === 30) { this.terminal.print(`⏱ 30s β€” ${names} will be auto-denied soon!`, 'warning'); } else if (remaining === 10) { this.terminal.print(`⏱ 10s remaining! Type ai-allow or click Allow now!`, 'warning'); } }, 500); this._confirmTimerInterval = timerInterval; this._confirmTimeout = setTimeout(() => { clearInterval(timerInterval); if (this._confirmResolve) { this._confirmResolve(false); this._cleanupConfirmBar(); this.terminal.print('⏱ Confirmation timed out β€” tool not executed', 'warning'); } }, this._confirmTimeoutMs); }); } _resolveConfirm(approved) { if (this._confirmResolve) { this._confirmResolve(approved); this._cleanupConfirmBar(); if (approved) { this.terminal.print('Action allowed', 'success'); } else { this.terminal.print('Action denied', 'warning'); } } } _cleanupConfirmBar() { clearInterval(this._confirmTimerInterval); clearTimeout(this._confirmTimeout); this._confirmTimeout = null; this._confirmTimerInterval = null; this._confirmResolve = null; this._pendingConfirmation = null; this._confirmAllowBtn = null; this._confirmDenyBtn = null; this._confirmOnAllow = null; this._confirmOnDeny = null; const area = document.getElementById('ai-confirm-area'); if (area) { area.classList.remove('visible'); area.innerHTML = ''; } this._confirmArea = null; this._confirmBar = null; const timerEl = document.getElementById('ai-confirm-timer'); if (timerEl) timerEl.style.animation = ''; } confirmPending() { this._resolveConfirm(true); } denyPending() { this._resolveConfirm(false); } ignoreValidator() { this._validatorSuppressed = true; this.terminal.print('πŸ” Validator suppressed for this session', 'info'); } cancelAi() { this._cancelled = true; this.terminal.print('⏹ Cancelling AI...', 'warning'); // Abort any in-flight HTTP request if (this.terminal.ollamaManager) { this.terminal.ollamaManager.abort(); } // Clear any pending confirmation if (this._confirmResolve) { this._resolveConfirm(false); } // Immediately reset UI so user knows it stopped this.terminal.setAiBusy(false); } toggleDebugPanel() { const mod = this.terminal.moduleManager.modules['DebugPanel']; if (mod) { mod.terminal = this.terminal; if (!mod._seeded) { mod._seeded = true; mod.seed(); } mod.toggle(); } else { this.terminal.print('Debug panel not loaded. Run: module load debug-panel', 'warning'); } } toggleDebug() { this._debugPanelEnabled = !this._debugPanelEnabled; let panel = document.getElementById('debug-ai-panel'); if (!panel) { panel = document.createElement('div'); panel.id = 'debug-ai-panel'; panel.style.cssText = 'position:fixed;right:10px;top:50px;width:340px;max-height:350px;overflow-y:auto;background:#0a0a0a;border:1px solid #333;border-radius:6px;z-index:100;font-family:monospace;font-size:0.75em;display:none;'; document.body.appendChild(panel); } panel.style.display = this._debugPanelEnabled ? 'block' : 'none'; if (this._debugPanelEnabled) { this._updateDebugPanel(); this.terminal.print('πŸ”§ AI debug panel enabled β€” shows tool call log. Close with: debug-ai', 'info'); } else { this.terminal.print('πŸ”§ Debug panel disabled', 'info'); } } // πŸ”’ === GUARDRAIL LAYER === _loadGuardrailPolicy() { try { const raw = localStorage.getItem('webx11_guardrail_policy'); if (raw) { this._guardrailPolicy = JSON.parse(raw); } else { // Default policy β€” seed on first run this._guardrailPolicy = { version: 1, rules: [ { id: 'rm-rf-block', pattern: '/rm\\s+-rf/', description: 'Prevent recursive force deletes outside trash', severity: 'deny', tools: ['run_command', 'write_file'] }, { id: 'curl-pipe-bash', pattern: '/(curl|wget)\\s+.*\\|\\s*(bash|sh)/', description: 'Block curl-to-bash execution', severity: 'deny', tools: ['run_command'] }, { id: 'rm-outer-vfs', pattern: '/delete_file.*\\.(user\\.json|token|session|key)/', description: 'Block deletion of sensitive metadata files', severity: 'deny', tools: ['delete_file'] }, { id: 'mass-delete', pattern: '/rm\\s+-rf\\s+\\/|delete_file.*\\*|\\.*\\/\\*/', description: 'Block mass-delete operations', severity: 'deny', tools: ['run_command', 'delete_file'] }, { id: 'chmod-777', pattern: '/chmod\\s+777/', description: 'Block world-writable permissions', severity: 'deny', tools: ['run_command'] }, { id: 'external-dl', pattern: '/(wget|curl)\\s+-[oO]\\s+/', description: 'External file download β€” requires high confidence', severity: 'warn', tools: ['run_command'] }, { id: 'env-overwrite', pattern: '/export\\s+\\w+\\s*=\\s*/', description: 'Modifying environment variables', severity: 'warn', tools: ['run_command'] }, { id: 'eval-danger', pattern: '/(eval|exec|system)\\s*\\(/', description: 'Dynamic code execution', severity: 'deny', tools: ['run_command'] } ], descriptions: {} }; localStorage.setItem('webx11_guardrail_policy', JSON.stringify(this._guardrailPolicy)); } } catch (e) { this._guardrailPolicy = { version: 1, rules: [] }; } } _saveGuardrailPolicy() { try { localStorage.setItem('webx11_guardrail_policy', JSON.stringify(this._guardrailPolicy)); } catch (e) {} } checkGuardrail(toolName, args) { if (!this._guardrailPolicy || !this._guardrailPolicy.rules) return null; const argsStr = typeof args === 'object' ? JSON.stringify(args) : String(args); for (const rule of this._guardrailPolicy.rules) { if (!rule.tools.includes(toolName)) continue; try { const re = new RegExp(rule.pattern.slice(1, -1), 'i'); if (re.test(argsStr)) { this._guardrailViolations[rule.id] = (this._guardrailViolations[rule.id] || 0) + 1; return rule; // { id, pattern, description, severity, tools } } } catch (e) { /* skip malformed regex */ } } return null; } // 🎯 === CONFIDENCE ESCALATION === _parseConfidence(text) { if (!text || typeof text !== 'string') return null; // Look for patterns like "Confidence: 85%" or "confidence=95%" or "I am 72% confident" const patterns = [ /confidence[:\s]*(\d+)%/i, /confidence[=:]?\s*(\d+)\s*%/i, /(\d+)%\s*confiden/i, /confiden[ts]\s+(\d+)%/i ]; for (const re of patterns) { const m = text.match(re); if (m) return parseInt(m[1], 10); } return null; } // πŸ“Έ === SHADOW VFS (Sandboxed Execution) === _snapshotShadowVFS() { // Deep-clone the VFS root state into a snapshot try { const root = this.terminal.fs.root; this._shadowSnapshot = JSON.parse(JSON.stringify(root)); return true; } catch (e) { this.terminal.print('⚠️ Shadow VFS snapshot failed: ' + e.message, 'error'); return false; } } _diffShadowVFS(consume = true) { if (!this._shadowSnapshot) return null; try { const current = this.terminal.fs.root; const changes = []; this._walkDiff(this._shadowSnapshot, current, '', changes); if (consume) { this._shadowSnapshot = null; } return changes; } catch (e) { this._shadowSnapshot = null; return [{ type: 'diff-error', message: e.message }]; } } _walkDiff(snapshot, live, path, changes) { const snapKeys = snapshot && snapshot.content ? Object.keys(snapshot.content) : []; const liveKeys = live && live.content ? Object.keys(live.content) : []; // New files in live for (const key of liveKeys) { const livePath = path ? path + '/' + key : key; const liveEntry = live.content[key]; const snapEntry = snapshot.content ? snapshot.content[key] : undefined; if (!snapEntry) { const entrySize = liveEntry.type === 'file' ? (liveEntry.content || '').length : 0; changes.push({ type: 'created', path: livePath, entryType: liveEntry.type, size: entrySize }); } else if (liveEntry.type === 'file' && liveEntry.content !== snapEntry.content) { const oldLen = (snapEntry.content || '').length; const newLen = (liveEntry.content || '').length; changes.push({ type: 'modified', path: livePath, sizeBefore: oldLen, sizeAfter: newLen }); } // Recurse into directories if (liveEntry.type === 'dir' && snapEntry && snapEntry.type === 'dir') { this._walkDiff(snapEntry, liveEntry, livePath, changes); } } // Deleted files for (const key of snapKeys) { if (!liveKeys.includes(key)) { changes.push({ type: 'deleted', path: path ? path + '/' + key : key }); } } } // πŸ”’ === GUARDRAIL COMMANDS === guardrailCmd(args) { if (args.length === 0) { this.terminal.print('=== Guardrail Policy ===', 'info'); if (!this._guardrailPolicy) { this.terminal.print('No guardrail policy loaded', 'warning'); return; } this.terminal.print(`Rules: ${this._guardrailPolicy.rules.length}`, 'info'); for (const rule of this._guardrailPolicy.rules) { const icon = rule.severity === 'deny' ? 'πŸ”΄' : '🟑'; this.terminal.print(`${icon} ${rule.id}: ${rule.description}`, 'info'); } const violations = Object.values(this._guardrailViolations).reduce((a, b) => a + b, 0); if (violations > 0) { this.terminal.print(`⚠️ ${violations} violation(s) this session`, 'warning'); } this.terminal.print('Usage: guardrail-add | guardrail-remove ', 'info'); return; } const sub = args[0].toLowerCase(); if (sub === 'add' && args.length >= 4) { const id = args[1]; const severity = args[2]; const pattern = args[3]; const tools = args.slice(4); this._guardrailPolicy.rules.push({ id, severity, pattern, tools, description: 'Custom rule' }); this._saveGuardrailPolicy(); this.terminal.print(`βœ… Guardrail rule '${id}' added`, 'success'); } else if (sub === 'remove' && args.length >= 2) { const idx = this._guardrailPolicy.rules.findIndex(r => r.id === args[1]); if (idx >= 0) { this._guardrailPolicy.rules.splice(idx, 1); this._saveGuardrailPolicy(); this.terminal.print(`βœ… Guardrail rule '${args[1]}' removed`, 'success'); } else { this.terminal.print(`❌ Rule '${args[1]}' not found`, 'error'); } } else { this.terminal.print('Usage: guardrail-add | guardrail-remove ', 'warning'); } } // 🎯 SHADOW COMMAND shadowExec(args) { // Usage: shadow // Snapshot VFS, run command, diff output if (args.length === 0) { this.terminal.print('Usage: shadow β€” Execute command in shadow VFS (sandbox preview)', 'warning'); return; } const cmd = args.join(' '); this._snapshotShadowVFS(); const cmdName = cmd.split(' ')[0].toLowerCase(); const cmdArgs = cmd.split(' ').slice(1); let output = ''; this.terminal._pushPrintCapture(buf => { output += buf; }); this.terminal.dispatchCommand(cmdName, cmdArgs).then(() => { this.terminal._popPrintCapture(); const diff = this._diffShadowVFS(false); if (!diff || diff.length === 0) { this.terminal.print('πŸ“¦ Shadow: No filesystem changes detected.', 'success'); } else { this.terminal.print(`πŸ“¦ Shadow: ${diff.length} change(s) detected:`, 'info'); for (const c of diff) { const icon = c.type === 'created' ? 'βž•' : c.type === 'modified' ? '✏️' : 'πŸ—‘οΈ'; const sizeStr = c.size !== undefined ? c.size + 'B' : ''; const detail = c.type === 'created' ? `(${c.entryType}${sizeStr ? ', ' + sizeStr : ''})` : c.type === 'modified' ? `(${c.sizeBefore}β†’${c.sizeAfter}B)` : ''; this.terminal.print(`${icon} ${c.path} ${detail}`, c.type === 'deleted' ? 'warning' : 'info'); } // Ask to commit or rollback this.terminal.print('πŸ”’ VFS modified β€” type "shadow-commit" to accept or "shadow-rollback" to undo.', 'warning'); } }).catch(e => { this.terminal._popPrintCapture(); this.terminal.print('⚠️ Shadow command error: ' + e.message, 'error'); }); } shadowCommit() { this._shadowSnapshot = null; this.terminal.print('βœ… Shadow changes committed to VFS', 'success'); } shadowRollback() { if (!this._shadowSnapshot) { this.terminal.print('❌ No shadow snapshot to rollback to', 'error'); return; } try { this.terminal.fs.root = JSON.parse(JSON.stringify(this._shadowSnapshot)); this.terminal.fs.saveToStorage(); this._shadowSnapshot = null; this.terminal.print('↩️ VFS rolled back to pre-shadow state', 'success'); } catch (e) { this.terminal.print('⚠️ Rollback failed: ' + e.message, 'error'); } } // πŸ”’ OVERRIDES for isDestructive β€” adds runTool intercept // Now command-aware: run_command is only destructive for mutating commands isDestructive(name, args = {}) { if (name === 'run_command') { const cmdStr = typeof args === 'object' ? (args?.command || '') : String(args); const cmd = cmdStr.split(' ')[0].toLowerCase(); // sed: read-only UNLESS -i (in-place write) or it starts a live tail -f timer if (cmd === 'sed' && /(^|\s)-i(\s|$)/.test(cmdStr)) return true; if (cmd === 'tail' && /(^|\s)-f\b|(^|\s)--follow\b/.test(cmdStr)) return false; // tail -f self-downgrades for AI (module-fs guard) const safeRead = new Set(['ls','cat','pwd','echo','date','env','whoami','which','hostname', 'cal','tree','find','grep','head','tail','sed','sort','wc','du','diff','basename','dirname', 'yes','seq','cut','uniq','expand','printf','base64','help','clear','set','export', 'unalias','history','ll','h','mem','status','df','free','top','ps','health-check', 'system-health','modules','listmods','modules-list','get-help-info','alias', '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', // 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', 'a2a-list','a2a-status','a2a-poll', 'consensus','consensus-status','consensus-history', 'pipeline','agent-list','agent-status','contract-list','voice','speak','analyze']); return !safeRead.has(cmd); } return ['write_file','delete_file','make_directory','edit_module','delegate_task','spawn_agent','create_skill','patch_skill','wpm_install','user_set','session_save'].includes(name); } async runTool(name, argsStr) { let a = {}; try { // πŸ”’ GUARDRAIL CHECK β€” intercept before any tool executes const guardrail = this.checkGuardrail(name, argsStr); if (guardrail && guardrail.severity === 'deny') { const errMsg = `πŸ”’ GUARDRAIL BLOCKED: ${guardrail.description} (rule: ${guardrail.id})`; this.terminal.print(errMsg, 'error'); this.terminal.log('WARN', 'GUARDRAIL', errMsg); return JSON.stringify({ error: errMsg }); } if (guardrail && guardrail.severity === 'warn') { this.terminal.print(`🟑 GUARDRAIL WARNING: ${guardrail.description} (rule: ${guardrail.id})`, 'warning'); } a = typeof argsStr === 'object' ? argsStr : (argsStr ? JSON.parse(argsStr) : {}); switch (name) { case 'list_files': { const originalPath = a.path || ''; const p = originalPath.replace(/^\//, '').replace(/\/+$/, ''); const fsapi = this.terminal.fs; const saved = [...fsapi.currentPath]; if (originalPath.startsWith('/')) fsapi.currentPath = []; // MAINT-011: readDir() resolves the virtual mounts and returns null for a // path that does not exist. The old hand-rolled walk over root.content could // not see /sys, so a directory the UI shows answered "Directory not found". // No argument means "the current directory", which ls() already does. const entries = p ? fsapi.readDir(p) : fsapi.ls(); fsapi.currentPath = saved; if (entries === null || entries === undefined) { return JSON.stringify({ error: `Directory not found: ${originalPath || p}` }); } return JSON.stringify(entries.map(f => ({ name: f.name, type: f.type, size: f.size }))); } case 'read_file': { const rp = a.path || ''; const savedR = [...this.terminal.fs.currentPath]; if (rp.startsWith('/')) this.terminal.fs.currentPath = []; const path = rp.replace(/^\//, ''); const content = this.terminal.fs.readFile(path); this.terminal.fs.currentPath = savedR; if (content === null || content === undefined) return JSON.stringify({ error: `File not found: ${path}` }); // Paged reads (2026-10-21): the old 8000-char hard cap made any // bigger file unreadable β€” the model could neither finish it nor // resume. Now the caller pages with offset/limit and gets next_offset. const total = content.length; let offset = Math.max(0, Math.min(parseInt(a.offset, 10) || 0, total)); const limit = Math.max(500, Math.min(parseInt(a.limit, 10) || 8000, 16000)); // Never split a surrogate pair (emoji/CJK-ext) across pages: // a lone surrogate makes JSON.stringify emit an invalid body. if (offset > 0) { const prev = content.charCodeAt(offset - 1); if (prev >= 0xD800 && prev <= 0xDBFF) offset += 1; } let slice = content.slice(offset, offset + limit); if (slice.length && slice.charCodeAt(slice.length - 1) >= 0xD800 && slice.charCodeAt(slice.length - 1) <= 0xDBFF) { slice = slice.slice(0, -1); } const nextOffset = offset + slice.length < total ? offset + slice.length : null; return JSON.stringify({ path: path, offset: offset, returned: slice.length, total_chars: total, truncated: nextOffset !== null, next_offset: nextOffset, content: slice }); } case 'write_file': { const wp = a.path || ''; const savedW = [...this.terminal.fs.currentPath]; if (wp.startsWith('/')) this.terminal.fs.currentPath = []; const path = wp.replace(/^\//, ''); const content = a.content || ''; // MAINT-012: writeFile() returns false when the parent directory is missing // or the target is a directory node. Ignoring it told the model the write // succeeded, and its follow-up read then found nothing. const wrote = this.terminal.fs.writeFile(path, content); this.terminal.fs.currentPath = savedW; if (!wrote) { const why = (typeof this.terminal.fs._isVirtualPath === 'function' && this.terminal.fs._isVirtualPath(wp)) ? 'read-only virtual path' : 'parent directory missing or target is a directory'; return `Write FAILED: ${wp} β€” ${why}; nothing was written`; } return `File written: ${wp} (${content.length} chars)`; } case 'make_directory': { const mp = a.path || ''; const savedM = [...this.terminal.fs.currentPath]; if (mp.startsWith('/')) this.terminal.fs.currentPath = []; const path = mp.replace(/^\//, ''); const ok = this.terminal.fs.mkdir(path); this.terminal.fs.currentPath = savedM; if (ok) return `Directory created: ${path}`; return JSON.stringify({ error: `Cannot create: ${path}` }); } case 'delete_file': { const dp = a.path || ''; const savedD = [...this.terminal.fs.currentPath]; if (dp.startsWith('/')) this.terminal.fs.currentPath = []; const path = dp.replace(/^\//, ''); const delOk = this.terminal.fs.deleteFile(path); this.terminal.fs.currentPath = savedD; if (delOk) return `Deleted: ${path}`; return JSON.stringify({ error: `Cannot delete: ${path}` }); } case 'get_status': { const fs = this.terminal.fs; const mm = this.terminal.moduleManager; const om = this.terminal.ollamaManager; 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' }); } 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()); } case 'get_current_dir': { return '/' + this.terminal.fs.currentPath.join('/'); } case 'web_search': { const query = a.query || ''; if (!query) return JSON.stringify({ error: 'No query provided' }); try { const resp = await fetch('/api/search?q=' + encodeURIComponent(query)); if (!resp.ok) { const err = await resp.json().catch(() => ({})); return JSON.stringify({ error: 'Search failed: ' + (err.error || `HTTP ${resp.status}`), status: resp.status, details: err.details || '' }); } const data = await resp.json(); const results = data.results || []; if (results.length === 0) return JSON.stringify({ message: 'No results found' }); return JSON.stringify({ count: results.length, source: data.source, results: results.map(r => ({ title: r.title, url: r.url, content: (r.content || '').substring(0, 300) })) }); } catch (e) { return JSON.stringify({ error: 'Search failed: ' + e.message }); } } case 'web_fetch': { const url = a.url || ''; if (!url) return JSON.stringify({ error: 'No URL provided' }); const method = (a.method || 'GET').toUpperCase(); const body = a.body || ''; const contentType = a.content_type || (method === 'POST' ? 'application/json' : ''); try { // Use /api/fetch for POST, /api/extract for GET (extract is cleaner for readable content) const endpoint = method === 'POST' ? '/api/fetch' : '/api/extract'; const params = new URLSearchParams({ url }); if (method === 'POST') { params.set('method', 'POST'); params.set('body', body); if (contentType) params.set('content_type', contentType); } const resp = await fetch(endpoint + '?' + params.toString()); if (resp.status === 403) return JSON.stringify({ error: 'URL blocked β€” internal network' }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); return JSON.stringify({ error: 'Fetch failed: ' + (err.error || `HTTP ${resp.status}`), status: resp.status, details: err.details || '' }); } // For POST/route, the response is raw text; for GET/extract it's JSON if (method === 'POST') { const text = await resp.text(); return JSON.stringify({ method, content: text.substring(0, 4000), content_length: text.length }); } const data = await resp.json(); return JSON.stringify({ method, title: data.title || '', content: (data.content || '').substring(0, 4000), word_count: data.word_count || 0, method_proxy: data.method || 'extract' }); } catch (e) { return JSON.stringify({ error: 'Fetch failed: ' + e.message }); } } case 'wpm_install': { const pkg = a.package || ''; if (!pkg) return JSON.stringify({ error: 'No package name' }); const mod = this.terminal.moduleManager.modules['WpmModule']; if (mod) { await mod.install(pkg); return `Package "${pkg}" installation attempted`; } return JSON.stringify({ error: 'WPM module not available' }); } case 'wpm_search': { const kw = a.keyword || ''; if (!kw) return JSON.stringify({ error: 'No keyword' }); const mod = this.terminal.moduleManager.modules['WpmModule']; if (mod) { let output = ''; this.terminal._pushPrintCapture(buf => { output += buf; }); try { await mod.search(kw); } finally { this.terminal._popPrintCapture(); } return output.trim() || 'Search completed'; } return JSON.stringify({ error: 'WPM module not available' }); } case 'run_command': { const cmd = a.command || ''; if (!cmd) return JSON.stringify({ error: 'No command provided' }); // πŸ“Έ Shadow VFS auto-snapshot before run_command this._snapshotShadowVFS(); let output = ''; // Loop guard (depth counter β€” race-safe under Promise.all tool fan-out): // unknown commands return an error instead of routing back to AI this.terminal.security._aiToolDepth++; this.terminal._pushPrintCapture(buf => { output += buf; }); try { await this.terminal.security.checkAndRun(cmd); } finally { this.terminal._popPrintCapture(); this.terminal.security._aiToolDepth--; } // Parse per-call captured output (race-free) β€” not shared security state const nfMatch = output.match(/Command not found: ([^\s.]+)(?:\. Did you mean: ([^\s.?]+)\?)?/); if (nfMatch) { return JSON.stringify({ error: `Command not found: ${nfMatch[1]}`, suggestion: nfMatch[2] || null }); } // πŸ“Έ Diff and append shadow report const shadowDiff = this._diffShadowVFS(); if (shadowDiff && shadowDiff.length > 0) { const diffSummary = shadowDiff.map(c => `${c.type}:${c.path}`).join(', '); output += `\nπŸ“¦ [Shadow VFS: ${shadowDiff.length} change(s) β€” ${diffSummary}]`; this._shadowSnapshot = null; // Changes considered "committed" since AI explicitly ran command } return output.trim() || '(no output)'; } case 'check_updates': { const reg = this.terminal.moduleManager.moduleRegistry; const results = []; for (const mod of reg) { if (mod.url && mod.url.startsWith('http')) { try { const resp = await fetch(mod.url, { method: 'HEAD' }); results.push({ name: mod.name, url: mod.url, status: resp.ok ? 'reachable' : 'error (' + resp.status + ')' }); } catch (e) { results.push({ name: mod.name, url: mod.url, status: 'unreachable' }); } } } return JSON.stringify({ checked: results.length, modules: results }); } case 'user_get': { const key = a.key || ''; if (!key) return JSON.stringify({ error: 'Key is required' }); const val = key.split('.').reduce((o, k) => (o && typeof o === 'object') ? o[k] : undefined, this._userProfile); return JSON.stringify({ key, value: val !== undefined ? val : null }); } case 'user_set': { const ukey = a.key || ''; if (!ukey) return JSON.stringify({ error: 'Key and value are required' }); const uval = a.value || ''; const keys = ukey.split('.'); let obj = this._userProfile; for (let i = 0; i < keys.length - 1; i++) { if (!obj[keys[i]] || typeof obj[keys[i]] !== 'object') obj[keys[i]] = {}; obj = obj[keys[i]]; } obj[keys[keys.length - 1]] = uval; try { localStorage.setItem('webx11_user_profile', JSON.stringify(this._userProfile)); } catch (e) {} return JSON.stringify({ success: true, key: ukey, value: uval }); } case 'user_list': { return JSON.stringify({ profile: this._userProfile, keys: Object.keys(this._userProfile) }); } case 'user_forget': { const fkey = a.key || ''; if (!fkey) return JSON.stringify({ error: 'Key is required' }); if (fkey.includes('.')) { const parts = fkey.split('.'); let obj = this._userProfile; for (let i = 0; i < parts.length - 1; i++) { if (!obj[parts[i]]) return JSON.stringify({ error: 'Key not found' }); obj = obj[parts[i]]; } delete obj[parts[parts.length - 1]]; } else { delete this._userProfile[fkey]; } try { localStorage.setItem('webx11_user_profile', JSON.stringify(this._userProfile)); } catch (e) {} return JSON.stringify({ success: true, key: fkey }); } case 'session_save': { const summary = a.summary || 'No summary provided'; const fs = this.terminal.fs; fs.mkdirRecursive('/memory'); const today = new Date().toISOString().split('T')[0]; const log = { date: today, summary, toolCallCount: 0, skillsCreated: [], modulesEdited: [], timestamp: new Date().toISOString() }; fs.fsWriteJSON('/memory/' + today + '.json', log); const index = fs.fsReadJSON('/memory/index.json') || {}; index[today] = { summary, tasks: (index[today]?.tasks || 0) + 1, updated: new Date().toISOString() }; fs.fsWriteJSON('/memory/index.json', index); return JSON.stringify({ success: true, date: today }); } case 'session_load': { const sdate = a.date || ''; if (!sdate) return JSON.stringify({ error: 'Date required (YYYY-MM-DD)' }); const sfs = this.terminal.fs; const slog = sfs.fsReadJSON('/memory/' + sdate + '.json'); if (!slog) return JSON.stringify({ error: 'No session log for ' + sdate }); return JSON.stringify(slog); } case 'session_recent': { const count = a.count || 3; const rfs = this.terminal.fs; const rindex = rfs.fsReadJSON('/memory/index.json') || {}; const dates = Object.keys(rindex).sort().reverse().slice(0, count); const sessions = dates.map(d => ({ date: d, ...rindex[d] })); return JSON.stringify({ sessions, count: sessions.length }); } case 'session_terminal': { const tfs = this.terminal.fs; try { const raw = tfs.readFile('/_sessions/_auto.json'); if (!raw) return JSON.stringify({ error: 'No auto-saved session found' }); const data = JSON.parse(raw); const lines = (data.terminalLines || []).slice(-50); const text = lines.map(l => { const ts = l.t ? '[' + l.t + '] ' : ''; return ts + l.l; }).join('\n'); return JSON.stringify({ lines: lines.length, output: text, savedAt: data.savedAt || '' }); } catch (e) { return JSON.stringify({ error: 'Could not read terminal history: ' + e.message }); } } case 'session_stats': { const sfs = this.terminal.fs; const sindex = sfs.fsReadJSON('/memory/index.json') || {}; const entries = Object.entries(sindex); let totalTasks = 0; for (const [, meta] of entries) totalTasks += meta.tasks || 0; return JSON.stringify({ totalSessions: entries.length, totalTasks, dates: entries.map(([d]) => d) }); } case 'list_agents': { const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; if (!registry) return JSON.stringify({ error: 'Agent registry not available' }); const agents = this.terminal.fs.ls('/agents/'); const results = []; if (agents) { agents.forEach(a => { if (a.type === 'dir' && a.name !== 'configured') { const data = this.terminal.fs.readFile(`/agents/${a.name}/status.json`); if (data) results.push(JSON.parse(data)); } }); } return JSON.stringify({ agents: results, count: results.length }); } case 'chat_hermes': { // v4.6.71: one channel, one store. Contract: _docs/chat-contract.md const ds = this.terminal.moduleManager.modules['DebugSupervisorModule']; if (!ds || typeof ds._chatPost !== 'function') { return JSON.stringify({ error: 'chat bridge unavailable' }); } try { if ((a.action || 'read') === 'send') { const text = String(a.text || '').trim(); if (!text) return JSON.stringify({ error: 'text is required for send' }); const j = await ds._chatPost(text, 'agent'); return JSON.stringify({ sent: true, id: j.id, epoch: j.epoch, note: 'asynchronous β€” call chat_hermes with action read for the answer' }); } const st = ds._chatThread(); // v4.6.76: a SEPARATE cursor from the terminal display. The UI poller // runs every 30 s and advances the shared cursor, so a read that used it // returned nothing β€” the answer was printed for the human and the model // never saw it. Two readers, two cursors. First read is from 0. const acKey = 'webx11_chat_agent_cursor_' + (st.thread || 'new'); let ac = 0; try { ac = parseInt(localStorage.getItem(acKey) || '0', 10) || 0; } catch (e) { ac = 0; } const resp = await fetch( '/spaces/newos4/api/chat/poll.php' + (ac ? '?since=' + ac : ''), { headers: { 'X-Chat-Token': st.token } }); const j = await resp.json().catch(() => null); if (!resp.ok || !j || !j.success) { return JSON.stringify({ error: (j && j.error) || ('HTTP ' + resp.status) }); } if (j.cursor) { try { localStorage.setItem(acKey, String(j.cursor)); } catch (e) {} } const msgs = (j.messages || []).slice(-10).map(m => ({ author: m.author, ts: m.ts, text: m.text })); return JSON.stringify({ awaiting: j.awaiting, messages: msgs, note: msgs.length ? undefined : (j.awaiting ? 'no reply yet β€” still awaiting Hermes' : 'no new messages since your last read') }); } catch (e) { return JSON.stringify({ error: e.message }); } } case 'delegate_task': { const task = a.task || ''; 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' }); 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'; const caps = a.capabilities ? a.capabilities.split(',').map(s => s.trim()) : ['list_files', 'read_file']; const orch = this.terminal.moduleManager.modules['OrchestratorModule']; if (!orch) return JSON.stringify({ error: 'Orchestrator not available' }); const agent = orch.spawnAgent(role, caps); if (!agent) return JSON.stringify({ error: 'Agent spawn failed β€” limit reached or concurrent spawn conflict' }); return JSON.stringify({ message: `Agent spawned: ${agent.id}`, agent }); } case 'load_skills': { // v4.6.62: tokenised, body-aware search. The old matcher tested the // WHOLE query as one substring against name+description+tags, so word // order decided the outcome ("telemetry dashboard" = 0 while // "telemetry" = 1) and a sentence could never match anything. Skill // bodies were never searched at all (packet Β§4). const rawQuery = (a && a.query) || ''; const query = String(rawQuery).toLowerCase().trim(); const fs = this.terminal.fs; const index = fs.fsReadJSON('/skills/index.json') || {}; if (!query) { const names = Object.keys(index); return JSON.stringify({ skills: names, count: names.length, message: 'No query provided β€” showing all skill names. Call again with a query to load content.' }); } const tokens = this._skillTokens(query); const matches = []; for (const [name, meta] of Object.entries(index)) { const nameL = name.toLowerCase(); const descL = String(meta.description || '').toLowerCase(); const tagsL = (meta.tags || []).map(x => String(x).toLowerCase()); const bodyRaw = fs.readFile('/skills/' + name + '.md') || ''; const bodyL = bodyRaw.toLowerCase(); let score = 0; let matched = 0; for (const tok of tokens) { let hit = 0; if (nameL.includes(tok)) hit += 3; if (descL.includes(tok)) hit += 2; if (tagsL.some(x => x.includes(tok))) hit += 2; if (bodyL) { const n = bodyL.split(tok).length - 1; if (n > 0) hit += 1 + Math.min(n, 4) * 0.25; } if (hit > 0) { score += hit; matched++; } } if (!matched) continue; // phrase bonus: word order must never be able to produce a zero if ((nameL + ' ' + descL).includes(query)) score += 3; // coverage: matching 1 of 4 tokens is a weak hit, not a strong one score = score * (0.5 + 0.5 * (matched / tokens.length)); matches.push({ name: name, description: meta.description, category: meta.category, score: Math.round(score * 100) / 100, matchedTokens: matched + '/' + tokens.length, content: bodyRaw.substring(0, 2000) }); } matches.sort((x, y) => y.score - x.score || (x.name < y.name ? -1 : 1)); const top = matches.slice(0, 3); if (!top.length) { const names = Object.keys(index); return JSON.stringify({ matches: [], count: 0, tokens: tokens, message: 'No skill matched any of: ' + tokens.join(', ') + '. Closest skill names: ' + names.slice(0, 8).join(', ') + '. You can create one after this task.' }); } return JSON.stringify({ matches: top.map(m => ({ name: m.name, description: m.description, content: m.content, score: m.score, matchedTokens: m.matchedTokens })), count: top.length, tokens: tokens }); } case 'create_skill': { const name = (a.name || '').trim().replace(/\s+/g, '-').toLowerCase(); if (!name) return JSON.stringify({ error: 'Skill name is required' }); const description = a.description || ''; let content = a.content || ''; const tags = (a.tags || '').split(',').map(t => t.trim()).filter(Boolean); const category = a.category || 'general'; if (!content.startsWith('---')) { content = `---\nname: ${name}\ndescription: ${description}\ntags: [${tags.join(', ')}]\ncategory: ${category}\n---\n## Steps\n${content}`; } const fs = this.terminal.fs; fs.mkdirRecursive('/skills'); fs.writeFile(`/skills/${name}.md`, content); const index = fs.fsReadJSON('/skills/index.json') || {}; index[name] = { name, description, tags, category, created: index[name]?.created || new Date().toISOString(), updated: new Date().toISOString() }; fs.fsWriteJSON('/skills/index.json', index); this.terminal.print(`πŸ’‘ Skill saved: ${name}`, 'success'); return JSON.stringify({ success: true, name, message: `Skill "${name}" created (${content.length} chars)` }); } case 'patch_skill': { const pname = (a.name || '').trim(); if (!pname) return JSON.stringify({ error: 'Skill name is required' }); const pcontent = a.content || ''; if (!pcontent) return JSON.stringify({ error: 'Skill content is required' }); const fs = this.terminal.fs; const index = fs.fsReadJSON('/skills/index.json') || {}; if (!index[pname]) return JSON.stringify({ error: `Skill "${pname}" not found` }); fs.writeFile(`/skills/${pname}.md`, pcontent); index[pname].updated = new Date().toISOString(); // Re-parse frontmatter for metadata updates const metaMatch = pcontent.match(/^---\n([\s\S]*?)\n---/); if (metaMatch) { const metaLines = metaMatch[1].split('\n'); metaLines.forEach(line => { const [k, ...v] = line.split(':'); if (k === 'description') index[pname].description = v.join(':').trim(); if (k === 'tags') index[pname].tags = v.join(':').trim().replace(/[\[\]]/g, '').split(',').map(t => t.trim()); if (k === 'category') index[pname].category = v.join(':').trim(); }); } fs.fsWriteJSON('/skills/index.json', index); this.terminal.print(`πŸ’‘ Skill patched: ${pname}`, 'success'); return JSON.stringify({ success: true, name: pname, message: `Skill "${pname}" updated` }); } case 'list_skills': { const filterCat = (a.category || '').toLowerCase(); const fs = this.terminal.fs; const index = fs.fsReadJSON('/skills/index.json') || {}; const list = Object.entries(index).map(([name, meta]) => ({ name, description: meta.description, category: meta.category || 'general', tags: meta.tags || [], updated: meta.updated })) .filter(s => !filterCat || (s.category || '').toLowerCase() === filterCat); return JSON.stringify({ skills: list, count: list.length }); } case 'nvfs_resolve': { const concept = (a.concept || '').toLowerCase(); if (!concept) return JSON.stringify({ error: 'Concept is required' }); const nvfs = this.terminal.moduleManager.modules['NeuralVFSModule']; if (!nvfs) return JSON.stringify({ error: 'Neural VFS module not loaded β€” run nvfs-index first' }); // Build index if empty if (Object.keys(nvfs.index).length === 0) { const saved = this.terminal.fs.fsReadJSON('/.semantic_index/map.json'); if (saved) { nvfs.index = saved; } else { return JSON.stringify({ error: 'No semantic index found β€” run nvfs-index first' }); } } const exact = nvfs.index[concept]; const fuzzy = Object.keys(nvfs.index).filter(k => k.includes(concept) || concept.includes(k) ).filter(k => k !== concept); const results = []; if (exact) results.push(...exact.map(e => ({ concept, path: e.path, summary: e.summary }))); for (const f of fuzzy.slice(0, 5)) { results.push(...nvfs.index[f].slice(0, 3).map(e => ({ concept: f, path: e.path, summary: e.summary }))); } return JSON.stringify({ concept, exact: !!exact, count: results.length, results: results.slice(0, 20) }); } case 'list_available_modules': { try { const resp = await fetch('/spaces/newos4/modules-index.json?v=' + (localStorage.getItem('webx11_cache_ver') || '')); if (!resp.ok) return JSON.stringify({ error: 'Module index not available' }); const idx = await resp.json(); const filterType = (a.type || '').toLowerCase(); let mods = Object.values(idx.modules); if (filterType) mods = mods.filter(m => m.type === filterType); return JSON.stringify({ total: mods.length, available: idx.total, type: filterType || 'all', modules: mods.map(m => ({ name: m.name, type: m.type, class: m.class, commands: m.commands.slice(0, 8), description: (m.description || '').substring(0, 80), auto_load: m.auto_load })) }); } catch (e) { return JSON.stringify({ error: 'Failed to load module index: ' + e.message }); } } case 'smart_context_list': { const scm = this.terminal.moduleManager.modules['SmartContextModule']; if (!scm) return JSON.stringify({ error: 'SmartContextModule not loaded', profiles: [] }); const profiles = scm.contextProfiles || {}; return JSON.stringify({ profiles: Object.entries(profiles).map(([key, p]) => ({ name: key, displayName: p.name || key, systemPrompt: (p.systemPrompt || '').substring(0, 80), temperature: p.temperature || 0.7, active: scm.activeProfile === key })) }); } case 'smart_context_set': { const scm = this.terminal.moduleManager.modules['SmartContextModule']; if (!scm) return JSON.stringify({ error: 'SmartContextModule not loaded' }); const name = (a.name || '').toLowerCase(); if (!scm.contextProfiles[name]) { return JSON.stringify({ error: `Profile '${name}' not found`, available: Object.keys(scm.contextProfiles) }); } scm.activeProfile = name; scm.saveActiveProfile(); const profile = scm.contextProfiles[name]; // Rebuild system prompt to reflect new context profile this.systemPrompt = this.buildSystemPrompt(); // Also switch agent loop profile to match const PROFILE_MAP = { 'coding': 'coder', 'debug': 'minimal', 'creative': 'creative', 'technical': 'research', 'chat': 'default', 'system-admin': 'safe', 'research': 'research', 'security': 'safe' }; const targetLoop = PROFILE_MAP[name]; if (targetLoop && this._switchLoopProfile) { this._switchLoopProfile(targetLoop); } return JSON.stringify({ success: true, name, displayName: profile.name, systemPrompt: profile.systemPrompt, temperature: profile.temperature, agentLoop: targetLoop || null }); } case 'create_mindmap': { const mindmap = this.terminal.moduleManager.modules['MindmapModule']; if (!mindmap) return JSON.stringify({ error: 'MindmapModule not loaded' }); const topic = a.topic || 'Mindmap'; const children = a.children || ''; const args = topic.split(' '); mindmap._new(args); const cur = mindmap._ensureMindmap(); if (!cur) return JSON.stringify({ error: 'Failed to create mindmap' }); const rootId = cur.root.id; const added = []; if (children) { const childNames = children.split(',').map(c => c.trim()).filter(Boolean); for (const child of childNames) { mindmap._add([rootId, child]); added.push(child); } } mindmap._svg([]); return JSON.stringify({ success: true, topic, children: added.length ? added : 'none', nodes: mindmap._countNodes(cur.root), rendered: 'inline SVG in terminal' }); } case 'view_module': case 'edit_module': case 'test_module': case 'reload_module': case 'list_modified_modules': case 'get_module_status': return await this.runModuleTool(name, argsStr); case 'vaultmind_connect': { const network = a.network || 'testnet'; try { // Read Hyperliquid key from SecretVault const vault = window.webx11_secretVault; if (!vault) return JSON.stringify({ error: 'SecretVault not available β€” unlock it first with secret-unlock ' }); const privateKey = await vault.decrypt('hyperliquid_key'); if (!privateKey) return JSON.stringify({ error: 'No hyperliquid_key found in SecretVault. Store it first: secret-set hyperliquid_key 0x...' }); // Authenticate with VaultMind backend const resp = await fetch('/vmai-api/auth/wallet', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ private_key: privateKey, account_address: null, network: network }) }); if (!resp.ok) { const errText = await resp.text(); return JSON.stringify({ error: 'VaultMind auth failed (' + resp.status + '): ' + errText }); } const data = await resp.json(); if (data.token) { localStorage.setItem('vaultmind_token', data.token); return JSON.stringify({ success: true, status: data.status || 'connected', network: data.network || network, message: 'Connected to VaultMind on ' + (data.network || network) }); } return JSON.stringify({ error: 'No token returned from VaultMind', data: data }); } catch (e) { return JSON.stringify({ error: 'VaultMind connect error: ' + e.message }); } } case 'vaultmind_status': { try { const token = localStorage.getItem('vaultmind_token'); if (!token) return JSON.stringify({ connected: false, message: 'No VaultMind session token found. Use vaultmind_connect to authenticate.' }); // Try to verify by calling a lightweight endpoint try { const resp = await fetch('/vmai-api/portfolio', { headers: { 'Authorization': 'Bearer ' + token } }); if (resp.ok) return JSON.stringify({ connected: true, token_exists: true, token_prefix: token.substring(0, 12) + '...', message: 'VaultMind session is active and valid.' }); if (resp.status === 401) return JSON.stringify({ connected: false, token_exists: true, message: 'Token exists but is expired or invalid. Reconnect with vaultmind_connect.' }); return JSON.stringify({ connected: true, token_exists: true, message: 'Token exists (status: ' + resp.status + ').' }); } catch (e) { return JSON.stringify({ connected: true, token_exists: true, message: 'Token exists but backend unreachable (' + e.message + ').' }); } } catch (e) { return JSON.stringify({ error: 'VaultMind status error: ' + e.message }); } } case 'vaultmind_query': { const ep = a.endpoint; const validEps = ['prices', 'portfolio', 'transactions', 'copy-trade', 'chat']; if (!validEps.includes(ep)) { return JSON.stringify({ error: 'Invalid endpoint. Valid: ' + validEps.join(', ') }); } 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' } }; // Portfolio, transactions, copy-trade need auth token 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: a.message || 'What is the current market status?', agent: a.agent || 'market' }); } const resp = await fetch(url, opts); const data = await resp.json(); return JSON.stringify({ success: true, data }); } catch (e) { return JSON.stringify({ error: 'VaultMind API error: ' + e.message }); } } case 'wallet_lookup': { const addr = a.address; if (!addr) return JSON.stringify({ error: 'Wallet address required' }); try { const resp = await fetch('/wallet/' + encodeURIComponent(addr)); const data = await resp.json(); return JSON.stringify({ success: true, data }); } catch (e) { return JSON.stringify({ error: 'Wallet lookup error: ' + e.message }); } } case 'get_usage_stats': { const om = this.terminal.ollamaManager; if (!om) return JSON.stringify({ error: 'Ollama manager not available' }); const filters = {}; if (a.model) filters.model = a.model; if (a.provider) filters.provider = a.provider; if (a.since_hours) filters.since_hours = a.since_hours; 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); // v4.6.56: track the prediction so the outcome can score it. var pTracked; if (typeof predictor.notePrediction === 'function') { pTracked = predictor.notePrediction(taskDesc, 'tool'); } else { var pFallback = predictor._computePrediction(pSimilar, pFeatures); pTracked = { id: null, predictedDuration: pFallback.duration, predictedTurns: pFallback.toolTurns, confidence: pFallback.confidence }; } return JSON.stringify({ task: taskDesc.substring(0, 80), predictionId: pTracked.id || null, estimatedDuration: pTracked.predictedDuration + 's', estimatedToolTurns: pTracked.predictedTurns, confidence: Math.round(pTracked.confidence * 100) + '%', similarTasks: pSimilar.length, recommendation: pTracked.confidence >= predictor._CONFIDENCE_THRESHOLD && pTracked.predictedDuration > 30 ? 'Set TTL to ' + Math.ceil(pTracked.predictedDuration * 1.5) + 's' : 'No special action needed', tracked: !!pTracked.id }); } 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 wmOut = { question: a.question, facts: wm._answerQuery(a.question || ''), version: wm._model.version, synthesized: wm._model.synthesized, // v4.6.83: this was JSON.parse(wm._modelDigest()) and the digest was a // string truncated mid-token, so the default path always threw // "Unexpected end of JSON input". The model now hands back an object. snapshot: (a.detail === 'full') ? wm._model : wm._modelSnapshot() }; return JSON.stringify(wmOut); } 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) }); } } // v4.6.62: query tokeniser for load_skills β€” lowercase, split on non-word chars, // drop short words and stopwords. A sentence query keeps its content tokens instead // of collapsing to a phrase that can never match. _skillTokens(query) { const STOP = new Set(['the','and','for','with','how','what','when','which','who','why', 'does','did','can','could','should','would','you','your','our','their','this','that', 'these','those','from','into','about','over','under','use','using','used','get','got', 'make','made','need','want','please','help','me','my','we','it','is','are','was','were', 'be','been','to','of','in','on','at','by','or','not','no','do','i','a','an','as','if']); const seen = new Set(); const out = []; String(query || '').toLowerCase().split(/[^a-z0-9_\-]+/).forEach(w => { if (!w || w.length < 3 || STOP.has(w) || seen.has(w)) return; seen.add(w); out.push(w); }); // A query made entirely of stopwords still deserves a literal attempt. if (!out.length) { const literal = String(query || '').trim(); if (literal) out.push(literal); } return out; } async runModuleTool(name, argsStr) { let a = {}; try { a = typeof argsStr === 'object' ? argsStr : (argsStr ? JSON.parse(argsStr) : {}); switch (name) { case 'view_module': { const modName = a.name || ''; const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName); const url = entry ? entry.url : null; // v4.6.57: deployed source FIRST. The unversioned `_mods/.js` edit cache // used to shadow it forever, so view_module returned pre-fix code while the // running bundle was new (packet Β§10). The cache is only a labelled fallback. const deployedVer = (typeof localStorage !== 'undefined' && localStorage.getItem('webx11_cache_ver')) || 'dev'; // Try registry URL first if (url) { try { const resp = await fetch(url); if (resp.ok) { const src = await resp.text(); this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, src); return '// deployed: ' + url + ' @ ' + deployedVer + '\n' + src; } } catch (e) { /* fall through */ } } // Fallback: use modules-index.json to map class name to filename try { const idxResp = await fetch('/spaces/newos4/modules-index.json?v=' + (localStorage.getItem('webx11_cache_ver') || '')); if (idxResp.ok) { const idx = await idxResp.json(); // modName is the class name (e.g. "WebX11AIAssistantModule") // or already a filename (e.g. "module-aiassist") // Try matching by class name first let match = Object.values(idx.modules).find(m => m.class === modName); // Then by name if (!match) match = idx.modules[modName]; if (match) { const filePath = match.file; // e.g. "modules/core/module-aiassist.js" const fetchUrl = `/spaces/newos4/${filePath}`; const resp = await fetch(fetchUrl); if (resp.ok) { const src = await resp.text(); this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, src); return '// deployed: ' + fetchUrl + ' @ ' + deployedVer + '\n' + src; } } } } catch (e) { /* fall through */ } const cachedSrc = this.terminal.fs.readFile(`_mods/${modName}.js`); if (cachedSrc) { return '// VFS COPY of ' + modName + '.js β€” may be stale (not the deployed file)\n' + cachedSrc; } return JSON.stringify({ error: `Cannot find source for ${modName} β€” not in registry, not in modules-index.json` }); } case 'edit_module': { const modName = a.name || ''; const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName); const url = entry ? entry.url : null; let code = this.terminal.fs.readFile(`_mods/${modName}.js`); if (!code && url) { try { const resp = await fetch(url); if (resp.ok) { code = await resp.text(); this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, code); this.terminal.fs.writeFile(`_mods/${modName}.js`, code); this.terminal.fs.writeFile(`_mods/${modName}.meta.json`, JSON.stringify({ originalUrl: url, modifiedAt: new Date().toISOString(), editCount: 0 })); } } catch (e) {} } // Fallback: use modules-index.json for core modules (not in registry) if (!code) { try { const idxResp = await fetch('/spaces/newos4/modules-index.json?v=' + (localStorage.getItem('webx11_cache_ver') || '')); if (idxResp.ok) { const idx = await idxResp.json(); let match = Object.values(idx.modules).find(m => m.class === modName); if (!match) match = idx.modules[modName]; if (match) { const filePath = match.file; const fetchUrl = `/spaces/newos4/${filePath}`; const resp = await fetch(fetchUrl); if (resp.ok) { code = await resp.text(); this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, code); this.terminal.fs.writeFile(`_mods/${modName}.js`, code); this.terminal.fs.writeFile(`_mods/${modName}.meta.json`, JSON.stringify({ originalUrl: fetchUrl, modifiedAt: new Date().toISOString(), editCount: 0 })); } } } } catch (e) {} } if (code) this.terminal.openCodeEditor(modName, url || '', code); return code ? `Editor opened for ${modName} (${code.length} chars)` : JSON.stringify({ error: `Could not load source for ${modName}` }); } 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}` }); // 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 || ''; const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName); this.terminal.fs.deleteFile(`_mods/${modName}.js`); this.terminal.fs.deleteFile(`_mods/${modName}.orig.js`); this.terminal.fs.deleteFile(`_mods/${modName}.meta.json`); if (entry && entry.url) { this.terminal.moduleManager.unloadModule(modName); await this.terminal.moduleManager.loadModuleFromURL(entry.url); return `Module "${modName}" reloaded from ${entry.url}`; } return JSON.stringify({ error: `No original URL for ${modName}` }); } case 'list_modified_modules': { const modsDir = this.terminal.fs.getCurrentDir(); const mc = modsDir.content['_mods']; if (!mc || Object.keys(mc.content).length === 0) return JSON.stringify({ modified: [], message: 'No local edits' }); const files = Object.keys(mc.content).filter(k => k.endsWith('.js') && !k.endsWith('.orig.js')); return JSON.stringify({ modified: files.map(f => f.replace('.js', '')), count: files.length }); } case 'get_module_status': { const mm = this.terminal.moduleManager; const modsDir = this.terminal.fs.getCurrentDir(); const hasMods = modsDir.content['_mods'] ? !!Object.keys(modsDir.content['_mods'].content).length : false; // Load module index for file path lookup let idxMap = {}; try { const resp = await fetch('/spaces/newos4/modules-index.json?v=' + (localStorage.getItem('webx11_cache_ver') || '')); if (resp.ok) { const idx = await resp.json(); for (const [name, meta] of Object.entries(idx.modules)) { idxMap[meta.class] = '/spaces/newos4/' + meta.file; } } } catch (e) {} const modules = []; const seen = new Set(); for (const [className, instance] of Object.entries(mm.modules)) { const name = instance.constructor ? instance.constructor.name : className; if (seen.has(name)) continue; seen.add(name); modules.push({ name: name, url: idxMap[name] || null, enabled: true, loaded: true, modified: !!this.terminal.fs.readFile(`_mods/${name}.js`), source: 'loaded' }); } for (const m of mm.moduleRegistry) { if (seen.has(m.name)) continue; seen.add(m.name); modules.push({ name: m.name, url: m.url || idxMap[m.name] || null, enabled: m.enabled, loaded: !!mm.modules[m.name], modified: !!this.terminal.fs.readFile(`_mods/${m.name}.js`), source: m.url ? 'ext' : 'core' }); } 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) }); } } clearContext() { this.context = []; localStorage.removeItem('webx11_ai_context'); this.terminal.print('AI conversation context cleared', 'success'); } _saveContext() { try { const data = JSON.stringify({ context: this.context.slice(-20), messageCount: this._messageCount, savedAt: Date.now() }); localStorage.setItem('webx11_ai_context', data); // Also persist to VFS for crash survival try { const fs = this.terminal.fs; if (fs) { fs.mkdirRecursive('_sessions'); fs.writeFile('/_sessions/_auto-context.json', data); } } catch (e) {} } catch (e) { // localStorage quota exceeded β€” silently ignore } } _loadContext() { try { const saved = localStorage.getItem('webx11_ai_context'); if (saved) { const data = JSON.parse(saved); if (data && data.context) { this.context = data.context.slice(-20); this._messageCount = data.messageCount || 0; } } } catch (e) {} } setSystemPrompt(args) { if (!args[0]) { this.terminal.print('=== System Prompt ===', '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(' '); if (arg === 'reset') { localStorage.removeItem('webx11_ai_system_prompt'); this.systemPrompt = this.buildSystemPrompt(); this.terminal.print('System prompt reset to default', 'success'); return; } if (arg.charAt(0) === '+') { var directive = arg.slice(1).trim(); var lp = this._activeLoopProfile; if (lp) { if (!lp.systemPrompt) lp.systemPrompt = { base: null, prepend: '', append: '', useDefault: true }; lp.systemPrompt.prepend = (lp.systemPrompt.prepend ? lp.systemPrompt.prepend + '\n' : '') + directive; this._saveLoopProfile(lp); } this.systemPrompt = this.buildSystemPrompt(); this.terminal.print('Directive prepended to system prompt (profile: ' + (lp ? lp.name : 'none') + ')', 'success'); return; } if (arg.charAt(0) === '-') { var directive = arg.slice(1).trim(); var lp = this._activeLoopProfile; if (lp) { if (!lp.systemPrompt) lp.systemPrompt = { base: null, prepend: '', append: '', useDefault: true }; lp.systemPrompt.append = (lp.systemPrompt.append ? lp.systemPrompt.append + '\n' : '') + directive; this._saveLoopProfile(lp); } this.systemPrompt = this.buildSystemPrompt(); this.terminal.print('Directive appended to system prompt (profile: ' + (lp ? lp.name : 'none') + ')', 'success'); return; } this.systemPrompt = arg; try { localStorage.setItem('webx11_ai_system_prompt', this.systemPrompt); } catch (e) {} this.terminal.print('System prompt updated (persisted, full replace)', 'success'); } memDebug() { const toolLogSize = this._toolCallLog.length; const errLogSize = this._errorLog.length; const freqKeys = Object.keys(this._commandFreq).length; const contextSize = this.context.length; const factKeys = this._userProfile?.knownFacts ? Object.keys(this._userProfile.knownFacts).length : 0; const shadowSize = this._shadowSnapshot ? JSON.stringify(this._shadowSnapshot).length : 0; const guardrailSize = this._guardrailPolicy ? JSON.stringify(this._guardrailPolicy).length : 0; const vfsSize = (localStorage.getItem('webx11_filesystem') || '').length + (localStorage.getItem('webx11_filesystem_v2') || '').length; const aiCtxSize = (localStorage.getItem('webx11_ai_context') || '').length; const aiPatternSize = (localStorage.getItem('webx11_ai_patterns') || '').length; const userProfileSize = (localStorage.getItem('webx11_user_profile') || '').length; const systemPromptSize = (localStorage.getItem('webx11_ai_system_prompt') || '').length; const guardrailPolicySize = (localStorage.getItem('webx11_guardrail_policy') || '').length; this.terminal.print('=== Memory Debug ===', 'info'); this.terminal.print(` _toolCallLog: ${toolLogSize} entries (cap: 50)`, toolLogSize > 30 ? 'warning' : 'text-muted'); this.terminal.print(` _errorLog: ${errLogSize} entries (cap: 20)`, errLogSize > 10 ? 'warning' : 'text-muted'); this.terminal.print(` _commandFreq: ${freqKeys} tracked patterns (cap: 30)`, freqKeys > 20 ? 'warning' : 'text-muted'); this.terminal.print(` AI context: ${contextSize} messages (max: ${this.maxContext * 2})`, 'text-muted'); this.terminal.print(` User facts: ${factKeys} facts (cap: 50)`, 'text-muted'); this.terminal.print(` VFS localStorage: ${(vfsSize / 1024).toFixed(1)} KB`, 'text-muted'); this.terminal.print(` AI context localStorage: ${(aiCtxSize / 1024).toFixed(1)} KB`, 'text-muted'); this.terminal.print(` AI patterns localStorage: ${(aiPatternSize / 1024).toFixed(1)} KB`, 'text-muted'); this.terminal.print(` User profile localStorage: ${(userProfileSize / 1024).toFixed(1)} KB`, userProfileSize > 10240 ? 'warning' : 'text-muted'); this.terminal.print(` System prompt localStorage: ${(systemPromptSize / 1024).toFixed(1)} KB`, systemPromptSize > 10240 ? 'warning' : 'text-muted'); this.terminal.print(` Guardrail policy localStorage: ${(guardrailPolicySize / 1024).toFixed(1)} KB`, 'text-muted'); this.terminal.print(` Runtime shadow snapshot: ${(shadowSize / 1024).toFixed(1)} KB`, shadowSize > 10240 ? 'warning' : 'text-muted'); this.terminal.print(` Runtime guardrail policy: ${(guardrailSize / 1024).toFixed(1)} KB`, 'text-muted'); const heap = performance.memory ? performance.memory.usedJSHeapSize : null; if (heap) { this.terminal.print(` JS heap: ${(heap / 1048576).toFixed(1)} MB / ${(performance.memory.jsHeapSizeLimit / 1048576).toFixed(1)} MB`, 'text-muted'); } this.terminal.print('Use reset-mem to clear AI internal logs, context, profile, and guardrail policy', 'info'); } resetMem() { // Capture size before clearing for the success message const aiCtxSize = (localStorage.getItem('webx11_ai_context') || '').length; const userProfileSize = (localStorage.getItem('webx11_user_profile') || '').length; const patternSize = (localStorage.getItem('webx11_ai_patterns') || '').length; const guardrailSize = (localStorage.getItem('webx11_guardrail_policy') || '').length; // Runtime in-memory resets this._toolCallLog = []; this._errorLog = []; this._commandFreq = {}; this.context = []; this._messageCount = 0; this._userProfile = {}; this._guardrailViolations = {}; this._shadowSnapshot = null; this._guardrailPolicy = null; this._toolDefs = null; // will be rebuilt on next use // Persisted localStorage resets localStorage.removeItem('webx11_ai_context'); localStorage.removeItem('webx11_ai_patterns'); localStorage.removeItem('webx11_user_profile'); localStorage.removeItem('webx11_guardrail_policy'); localStorage.removeItem('webx11_ai_system_prompt'); // VFS backups that _autoRestoreSession can resurrect try { const fs = this.terminal.fs; if (fs) { fs.deleteFile('/_sessions/_auto-context.json'); fs.deleteFile('/_sessions/_auto.json'); } } catch (e) {} // Mark reset timestamp so auto-restore skips stale backups localStorage.setItem('webx11_last_reset', Date.now().toString()); const totalCleared = aiCtxSize + userProfileSize + patternSize + guardrailSize; this.terminal.print(`βœ… AI memory reset: ${(totalCleared / 1024).toFixed(1)} KB cleared`, 'success'); this.terminal.print(' β€’ tool logs, error logs, command patterns, conversation context', 'text-muted'); this.terminal.print(' β€’ user profile / known facts, guardrail policy, shadow snapshot', 'text-muted'); this.terminal.print(' β€’ VFS session backups and cached system prompt', 'text-muted'); this.terminal.print('VFS files, terminal history, AI config, and module registry preserved', 'text-muted'); } showModel(args) { const om = this.terminal.ollamaManager; if (args.length > 0) { const name = args.join(' '); if (om.models && om.models.some(m => m.name === name)) { om.currentModel = name; localStorage.setItem('webx11_ollama_model', name); this.terminal.print(`AI model set to: ${name}`, 'success'); } else { this.terminal.print(`Model "${name}" not found in available models. Use ai-model to list.`, 'warning'); } return; } this.terminal.print(`Current AI model: ${om.currentModel || 'none'}`, 'info'); if (om.models && om.models.length > 0) { const maxShow = 20; const names = om.models.slice(0, maxShow).map(m => m.name).join(', '); this.terminal.print(`Available (${om.models.length}): ${names}${om.models.length > maxShow ? `... +${om.models.length - maxShow} more` : ''}`, 'info'); } else { this.terminal.print('No models available. Use ollamaconnect first.', 'warning'); } } _getAiConfigKeys() { return ['max-turns', 'max-context', 'max-context-tokens', 'confirm-timeout', 'chat-lock-timeout', 'skill-debounce', 'trivial-optimization', 'temperature', 'top-p', 'max-tokens', 'thinking']; } aiConfig(args) { if (args.length === 0) { this.terminal.print('=== AI Config ===', 'info'); this.terminal.print(`max-turns: ${this.MAX_TOOL_TURNS} (default: 8)`, ''); this.terminal.print(`max-context: ${this.maxContext} (default: 10)`, ''); this.terminal.print(`max-context-tokens: ${this.maxContextTokens} (code default: 12000)`, ''); this.terminal.print(`trivial-optimization: ${(this.terminal?.ollamaManager?.optimizationEnabled) ? 'on' : 'off (default)'}`, ''); 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)`, ''); const om = this.terminal?.ollamaManager; if (om && om.modelParams) { this.terminal.print(`temperature: ${om.modelParams.temperature} (default: 0.7)`, ''); this.terminal.print(`top-p: ${om.modelParams.top_p} (default: 0.9)`, ''); this.terminal.print(`max-tokens: ${om.modelParams.max_tokens} (default: 4096)`, ''); this.terminal.print(`thinking: ${om.modelParams.thinking} (default: off)`, ''); } this.terminal.print('Usage: ai-config ', '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]; const val = args[1]; if (key === 'max-turns') { const n = parseInt(val); if (isNaN(n) || n < 1 || n > 50) return this.terminal.print('max-turns must be 1-50', 'warning'); this.MAX_TOOL_TURNS = n; } else if (key === 'max-context') { const n = parseInt(val); if (isNaN(n) || n < 1 || n > 100) return this.terminal.print('max-context must be 1-100', 'warning'); this.maxContext = n; } else if (key === 'max-context-tokens') { const n = parseInt(val); if (isNaN(n) || n < 100 || n > 10000) return this.terminal.print('max-context-tokens must be 100-10000', 'warning'); this.maxContextTokens = n; } else if (key === 'confirm-timeout') { const n = parseInt(val); if (isNaN(n) || n < 0) return this.terminal.print('confirm-timeout must be 0+ seconds (0 = no confirm / auto-approve all)', 'warning'); this._confirmTimeoutMs = n * 1000; } else if (key === 'chat-lock-timeout') { const n = parseInt(val); if (isNaN(n) || n < 1 || n > 60) return this.terminal.print('chat-lock-timeout must be 1-60 seconds', 'warning'); this._chatLockTimeoutMs = n * 1000; } else if (key === 'skill-debounce') { const n = parseInt(val); if (isNaN(n) || n < 1 || n > 240) return this.terminal.print('skill-debounce must be 1-240 minutes', 'warning'); this._skillDebounceMs = n * 60000; } else if (key === 'trivial-optimization') { if (this.terminal?.ollamaManager) { this.terminal.ollamaManager.optimizationEnabled = val === 'on'; } } else if (key === 'temperature') { const n = parseFloat(val); if (isNaN(n) || n < 0 || n > 2) return this.terminal.print('temperature must be 0-2', 'warning'); const om = this.terminal?.ollamaManager; if (om && om.modelParams) { om.modelParams.temperature = n; om._saveModelParams(); } } else if (key === 'top-p') { const n = parseFloat(val); if (isNaN(n) || n < 0 || n > 1) return this.terminal.print('top-p must be 0-1', 'warning'); const om = this.terminal?.ollamaManager; if (om && om.modelParams) { om.modelParams.top_p = n; om._saveModelParams(); } } else if (key === 'max-tokens') { const n = parseInt(val); if (isNaN(n) || n < 1 || n > 131072) return this.terminal.print('max-tokens must be 1-131072', 'warning'); const om = this.terminal?.ollamaManager; if (om && om.modelParams) { om.modelParams.max_tokens = n; om._saveModelParams(); } } else if (key === 'thinking') { const validModes = ['off', 'deepseek_reasoner', 'deepseek']; 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 { 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({ maxTurns: this.MAX_TOOL_TURNS, maxContext: this.maxContext, maxContextTokens: this.maxContextTokens, confirmTimeoutMs: this._confirmTimeoutMs, chatLockTimeoutMs: this._chatLockTimeoutMs, skillDebounceMs: this._skillDebounceMs })); this.terminal.print(`ai-config ${key} set to ${val}`, 'success'); } showContext() { if (this.context.length === 0) { this.terminal.print('No conversation context', 'info'); return; } this.terminal.print(`=== Context (${this.context.length} messages) ===`, 'info'); this.context.forEach(msg => { const icon = msg.role === 'user' ? 'πŸ‘€' : msg.role === 'assistant' ? 'πŸ€–' : 'πŸ”§'; const content = typeof msg.content === 'string' ? msg.content.substring(0, 60) : JSON.stringify(msg).substring(0, 60); this.terminal.print(`${icon} ${content}...`); }); } // Extract facts about user from conversation _extractFacts(userMsg, aiResp) { const patterns = [ { re: /my name is (\w+)/i, key: 'name' }, { re: /call me (\w+)/i, key: 'name' }, { re: /i(?:'m| am) working on (.+)/i, key: 'project' }, { re: /(?:my|the) project is (.+)/i, key: 'project' }, { re: /i have a server at (.+)/i, key: 'server' }, { re: /i use (\w+).*?(?:editor|ide|text)/i, key: 'preferredEditor' }, { re: /i (?:like|prefer|love) (.+)/i, key: 'preference' }, ]; let changed = false; if (!this._userProfile.knownFacts) this._userProfile.knownFacts = {}; for (const { re, key } of patterns) { const m = (userMsg + ' ' + (aiResp || '')).match(re); if (m && m[1]) { const val = m[1].trim(); if (this._userProfile.knownFacts[key] !== val) { this._userProfile.knownFacts[key] = val; changed = true; } } } // v4.5.47: cap knownFacts so the profile doesn't bloat indefinitely const MAX_FACTS = 50; const factKeys = Object.keys(this._userProfile.knownFacts); if (factKeys.length > MAX_FACTS) { this._userProfile.knownFacts = Object.fromEntries( factKeys.slice(-MAX_FACTS).map(k => [k, this._userProfile.knownFacts[k]]) ); changed = true; } if (changed) { try { localStorage.setItem('webx11_user_profile', JSON.stringify(this._userProfile)); } catch (e) {} } } _trackPattern(userMessage) { const lower = userMessage.toLowerCase(); const patterns = ['font size','bigger text','smaller text','export','backup','save','install','load module','search','find','color','theme','skill','learn','remember','mindmap','brainstorm']; for (const p of patterns) { if (lower.includes(p)) this._commandFreq[p] = (this._commandFreq[p] || 0) + 1; } // v4.5.47: cap command frequency map to top 30 patterns to prevent unbounded growth const MAX_PATTERNS = 30; const keys = Object.keys(this._commandFreq); if (keys.length > MAX_PATTERNS) { const sorted = keys.sort((a, b) => this._commandFreq[b] - this._commandFreq[a]); this._commandFreq = Object.fromEntries(sorted.slice(0, MAX_PATTERNS).map(k => [k, this._commandFreq[k]])); } 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) { if (!grouped[e.tool]) grouped[e.tool] = []; grouped[e.tool].push(e); } for (const [tool, errors] of Object.entries(grouped)) { if (errors.length >= 3) { const skillName = 'tool-' + tool + '-workaround'; const existing = this.terminal.fs.readFile('/skills/' + skillName + '.md'); if (existing) continue; const steps = errors.map((e, i) => (i + 1) + '. When calling ' + tool + ', avoid: "' + JSON.stringify(e.args) + '" which caused: ' + e.error).join('\n'); const content = '---\nname: ' + skillName + '\ndescription: Workarounds for ' + tool + ' failures\ntags: [' + tool + ', error, workaround]\ncategory: system\n---\n## Steps\n' + steps; await this.runTool('create_skill', { name: skillName, description: 'Workarounds for ' + tool + ' failures', content: content, tags: tool + ',error,workaround', category: 'system' }); this.terminal.print('πŸ€– Auto-created skill from repeated errors: ' + skillName, 'info'); } } this._errorLog = []; } async _auditSkills() { const fs = this.terminal.fs; const index = fs.fsReadJSON('/skills/index.json') || {}; const now = Date.now(); const issues = []; for (const [name, meta] of Object.entries(index)) { const content = fs.readFile('/skills/' + name + '.md'); if (!content || content.length < 50) { issues.push({ name, issue: 'empty or too short' }); continue; } const updated = new Date(meta.updated || 0).getTime(); const ageDays = (now - updated) / 86400000; if (ageDays > 30) issues.push({ name, issue: 'not updated in ' + Math.round(ageDays) + ' days' }); } if (issues.length > 0) { this.terminal.print('πŸ“‹ Skill audit: ' + issues.length + ' issue(s) found', 'info'); issues.slice(0, 3).forEach(i => this.terminal.print(' - ' + i.name + ': ' + i.issue, 'warning')); } } _estimateTokens(text) { if (!text || typeof text !== 'string') return 0; return Math.ceil(text.length / 4); } _estimateContextTokens() { let total = 0; for (const msg of this.context) { total += this._estimateTokens(msg.content || ''); } return total; } _showToolCallLog() { if (!this._toolCallLog || this._toolCallLog.length === 0) { this.terminal.print('No tool calls recorded this session.', 'warning'); return; } let out = '── Tool Call Log (' + this._toolCallLog.length + ' calls) ──\n'; for (var i = 0; i < this._toolCallLog.length; i++) { var e = this._toolCallLog[i]; var err = e.error ? ' ⚠️' : ''; var argsStr = typeof e.args === 'string' ? e.args.substring(0, 80) : JSON.stringify(e.args).substring(0, 80); out += ' #' + e.turn + ' ' + e.name + '(' + argsStr + ') ' + e.duration + 's' + err + '\n'; } out += '── End ──'; this.terminal.print(out, 'info'); } _archiveList() { try { const fs = this.terminal.fs; const files = fs.ls('/memory/archive') || []; const jsonFiles = files.filter(function(f) { return f.name && f.name.endsWith('.json'); }); if (jsonFiles.length === 0) { this.terminal.print('No archives found. Run ai-compress to create one.', 'warning'); return; } jsonFiles.sort(function(a, b) { return (a.name || '') < (b.name || '') ? 1 : -1; }); let out = '── Archives (' + jsonFiles.length + ') ──\n'; for (var i = 0; i < Math.min(jsonFiles.length, 30); i++) { var f = jsonFiles[i]; var size = f.size ? '(' + f.size + 'b)' : ''; out += ' ' + f.name + ' ' + size + '\n'; } out += '── End ──\nUse: archive-read '; this.terminal.print(out, 'info'); } catch (e) { this.terminal.print('Error listing archives: ' + e.message, 'error'); } } _archiveRead(args) { var name = (args && args[0]) || ''; if (!name) { this.terminal.print('Usage: archive-read (e.g. 2026-06-25-14-30.json)', 'warning'); return; } try { const fs = this.terminal.fs; var data = fs.fsReadJSON('/memory/archive/' + name); if (!data) { this.terminal.print('Archive not found: ' + name, 'error'); return; } var out = '── Archive: ' + name + ' ──\n'; out += 'Archived: ' + (data.archivedAt || '?') + '\n'; out += 'Summary: ' + (data.summary || '(none)') + '\n'; out += 'User messages: ' + (data.user?.messageCount || 0) + '\n'; out += 'System: ' + (data.system?.modules || 0) + ' modules, ' + (data.system?.model || '?') + '\n'; out += 'Context: ' + (data.context?.messageCount || 0) + ' msgs, ~' + (data.context?.estimatedTokens || 0) + ' tokens\n'; out += '── End ──'; this.terminal.print(out, 'info'); } catch (e) { this.terminal.print('Error reading archive: ' + e.message, 'error'); } } _showLastRawResponse() { if (!this._lastRawResponse) { this.terminal.print('No AI response recorded yet. Send a message first.', 'warning'); return; } const r = this._lastRawResponse; var content = (r.message && r.message.content) || ''; var hasTools = r.tool_calls && r.tool_calls.length > 0; var toolStr = hasTools ? r.tool_calls.map(function(tc) { return tc.function.name + '(' + (tc.function.arguments || '{}') + ')'; }).join(', ') : ''; // Render as HTML card for better readability var html = '
' + '
' + 'πŸ€– Last AI Response' + '' + '' + '
' + '
'; if (r.error) { html += '
⚠️ Error: ' + this._escapeHtml(r.error) + '
'; } if (content) { var safe = this._escapeHtml(content.length > 1000 ? content.substring(0, 1000) + '...(' + content.length + ' chars)' : content); html += '
Content:
' + safe + '
'; } else if (!hasTools) { html += '
⚠️ Empty response (no content, no tool calls)
'; } if (hasTools) { html += '
Tool calls: ' + this._escapeHtml(toolStr) + '
'; } html += '
'; this.terminal.printHTML(html); } _showLastRawRequest() { if (!this._lastRawMessages) { this.terminal.print('No AI request recorded yet. Send a message first.', 'warning'); return; } const msgs = this._lastRawMessages; let out = '── Last AI Request (' + msgs.length + ' messages) ──\n'; for (let i = 0; i < msgs.length; i++) { const m = msgs[i]; const role = m.role || '?'; const content = (m.content || '').substring(0, 200); const suffix = (m.content || '').length > 200 ? '...' : ''; out += ' [' + i + '] ' + role + ': ' + content + suffix + '\n'; if (i > 8 && i < msgs.length - 2) { out += ' ... (' + (msgs.length - i - 2) + ' more messages) ...\n'; i = msgs.length - 3; } } out += '── End ──'; this.terminal.print(out, 'info'); } async _compressContext() { if (this.context.length <= 6) return; const totalTokens = this._estimateContextTokens(); if (totalTokens < this.maxContextTokens) { // Warn before the cliff rather than only reporting the fall. Once per crossing, // so a long session is not nagged every turn. const pct = Math.round((totalTokens / this.maxContextTokens) * 100); if (pct >= 80 && !this._ctxWarned) { this._ctxWarned = true; this.terminal.print('⚠️ Context ' + pct + '% full (' + totalTokens + '/' + this.maxContextTokens + ' tokens) β€” the next few turns will compress earlier history into a summary', 'warning'); } else if (pct < 60) { this._ctxWarned = false; } return; } this._ctxWarned = false; const keepCount = 8; const oldMsgs = this.context.slice(0, -keepCount * 2); if (oldMsgs.length < 2) return; const recentMsgs = this.context.slice(-keepCount * 2); // Tool traces are the only durable record of what was read/run β€” keep them // verbatim instead of folding them into the 400-char digest (2026-10-21). const summaryText = oldMsgs.map(m => /^βš™οΈ tool trace:/.test(m.content || '') ? (m.content || '').slice(0, 600) : (m.role === 'user' ? 'πŸ‘€' : m.role === 'assistant' ? 'πŸ€–' : 'βš™οΈ') + ': ' + (m.content || '').substring(0, 400) ).join(' | '); let compressed = `[Earlier context (${oldMsgs.length} messages): ${summaryText.slice(0, 4000)}]`; // Tool traces must survive compression, so the AI one-sentence summary is only // used when there are none to lose (2026-10-21). const _traces = oldMsgs.filter(m => /^βš™οΈ tool trace:/.test(m.content || '')); // Try AI summarization, fallback to truncation try { if (!_traces.length && this.terminal.ollamaManager && this.terminal.ollamaManager.connected) { const sumPrompt = 'Summarize these conversation exchanges into a single compact sentence. Focus on what the user asked and what actions were taken. Keep it under 200 characters.\n\n' + JSON.stringify(oldMsgs.map(m => ({ role: m.role, content: (m.content || '').substring(0, 200) }))); const sumRes = await this.terminal.ollamaManager.chat(sumPrompt); if (sumRes && sumRes.length < 300) { compressed = '[Earlier context: ' + sumRes.trim() + ']'; } } } catch (e) { /* fallback to truncation */ } this.context = [{ role: 'system', content: compressed }, ...recentMsgs]; this.terminal.print('πŸ“¦ Context compressed (' + oldMsgs.length + ' messages β†’ 1 summary, ~' + totalTokens + ' tokens)', 'info'); // Also archive session snapshot this._archiveSession(compressed); } _archiveSession(summary) { try { const fs = this.terminal.fs; fs.mkdirRecursive('/memory/archive'); const now = new Date(); const pad = function(n) { return String(n).padStart(2, '0'); }; const ts = now.getFullYear() + '-' + pad(now.getMonth()+1) + '-' + pad(now.getDate()) + '-' + pad(now.getHours()) + '-' + pad(now.getMinutes()); const archive = { archivedAt: now.toISOString(), summary: summary || '', user: { profile: this._userProfile || {}, topics: [], messageCount: this._messageCount }, system: { modules: this.terminal?.moduleManager?.modules ? Object.keys(this.terminal.moduleManager.modules).length : 0, model: this.terminal?.ollamaManager?.currentModel || 'unknown', provider: this.terminal?.ollamaManager?.currentProvider?.name || 'unknown' }, context: { messageCount: this.context.length, estimatedTokens: this._estimateContextTokens() } }; fs.fsWriteJSON('/memory/archive/' + ts + '.json', archive); // Rotation: keep last 30 try { const files = fs.ls('/memory/archive') || []; const jsonFiles = files.filter(function(f) { return f.name && f.name.endsWith('.json'); }); if (jsonFiles.length > 30) { jsonFiles.sort(function(a, b) { return (a.name || '') < (b.name || '') ? -1 : 1; }); const toDelete = jsonFiles.slice(0, jsonFiles.length - 30); for (var i = 0; i < toDelete.length; i++) { fs.deleteFile('/memory/archive/' + toDelete[i].name); } } } catch (e) { /* rotation best-effort */ } } catch (e) { /* archive best-effort */ } } _manualCompress() { this._compressContext().then(() => { const fs = this.terminal.fs; let archiveCount = 0; try { const files = fs.ls('/memory/archive') || []; archiveCount = files.filter(function(f) { return f.name && f.name.endsWith('.json'); }).length; } catch (e) {} this.terminal.print('Context: ' + this.context.length + ' message(s), ~' + this._estimateContextTokens() + ' estimated tokens, ' + archiveCount + ' archive(s)', 'info'); }).catch(e => { this.terminal.print('Compression error: ' + e.message, 'error'); }); } async _vaultmindConnectCmd(args) { const network = args[0] || 'testnet'; try { const vault = window.webx11_secretVault; if (!vault) { this.terminal.print('SecretVault not available', 'error'); return; } const privateKey = await vault.decrypt('hyperliquid_key'); if (!privateKey) { this.terminal.print('No hyperliquid_key in SecretVault. Use: secret-set hyperliquid_key 0x...', 'error'); return; } this.terminal.print('Connecting to VaultMind...', 'info'); const resp = await fetch('/vmai-api/auth/wallet', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ private_key: privateKey, account_address: null, network }) }); if (!resp.ok) { this.terminal.print('Auth failed: ' + resp.status, 'error'); return; } const data = await resp.json(); if (data.token) { localStorage.setItem('vaultmind_token', data.token); this.terminal.print('βœ… Connected to VaultMind (' + (data.network || network) + ')', 'success'); } else { this.terminal.print('No token returned', 'error'); } } catch (e) { 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 [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
', '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: true, warnAt: 3, blockAt: 6, hardStopAt: 9, 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) {} 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._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']; } } _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); 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(); } } return true; } agentLoopsCmd(args) { if (!args[0]) { 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 | new | edit | delete | clone | export | reset ', '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 ', '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 + " ", '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 ', '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 ', '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 ', '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 ', '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 }, ]; } async _runBenchmark(profileName, taskIds) { 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'); 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 = []; } 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 ', '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) }); } } 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'); // 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; // MAINT-020: lessons are kept in /lessons with their own index; /skills is // reserved for real skills the user or the agent creates deliberately. try { fs.mkdirRecursive('/lessons'); } catch (ex) {} var index = fs.fsReadJSON('/lessons/index.json') || {}; var autoSkills = Object.entries(index); // One-time migration: earlier builds wrote loop-lesson-* into /skills. if (!this._loopLessonCleaned) { this._loopLessonCleaned = true; try { var sIdx = fs.fsReadJSON('/skills/index.json') || {}; Object.keys(sIdx).filter(function(k) { return k.indexOf('loop-lesson-') === 0; }) .forEach(function(k) { try { fs.unlink('/skills/' + k + '.md'); } catch (ex) {} delete sIdx[k]; }); fs.fsWriteJSON('/skills/index.json', sIdx); } catch (ex) {} } // 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('/lessons/' + 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('/lessons/' + 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('/lessons'); fs.writeFile('/lessons/' + 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('/lessons/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'); } 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 errCounts = { total: 0, debug: 0, latency: 0, log: 0 }; try { if (this.terminal && typeof this.terminal.countErrors === 'function') { errCounts = this.terminal.countErrors(); } } catch (e) {} var totalErrors = errCounts.total; var predictor = mm ? mm.modules['PredictorModule'] : null; var predictorAcc = 'n/a (no paired runs)'; if (predictor && predictor._accuracy && predictor._accuracy.total > 0) { predictorAcc = Math.round(predictor._accuracy.correct / predictor._accuracy.total * 100) + '% (' + predictor._accuracy.correct + '/' + predictor._accuracy.total + ' paired)'; } data.health = { modules: data.modules.loaded + '/' + data.modules.total, vfsSize: data.vfs ? data.vfs.size : '?', online: typeof navigator !== 'undefined' ? navigator.onLine : true, errors: totalErrors, errorBreakdown: errCounts, predictorAccuracy: predictorAcc }; // 2026-10-22: the panel had a renderer and no collector, so it always said // "Not synthesized yet" while WorldModelModule._model was right there. // Map the model shape (modules.loaded, skills.count, files.total, memory.nodes…) // onto the keys the page reads (modules, skills, files, memoryNodes…). try { var wm = mm ? mm.modules['WorldModelModule'] : null; var wmModel = wm && wm._model; if (wmModel) { data.worldModel = { synthesized: wmModel.synthesized || null, version: wmModel.version || '?', modules: wmModel.modules ? wmModel.modules.loaded : null, skills: wmModel.skills ? wmModel.skills.count : null, files: wmModel.files ? wmModel.files.total : null, agents: wmModel.agents ? wmModel.agents.agents : null, memoryNodes: wmModel.memory ? wmModel.memory.nodes : null, memoryEdges: wmModel.memory ? wmModel.memory.edges : null, health: wmModel.system ? wmModel.system.health : null, activeProjects: ((wmModel.projects && wmModel.projects.activeProjects) || []) .map(function (p) { return p.name + ' (' + p.accesses + 'x)'; }), recent: (wmModel.recent || []).slice(0, 5), changes: (typeof wm._diffSincePrev === 'function') ? wm._diffSincePrev() : [], errors: (typeof wm._errorSample === 'function') ? wm._errorSample() : [] }; } else { data.worldModel = { missing: true }; } } catch (e) { data.worldModel = { missing: true, error: e.message }; } 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() }; } } }