diff options
| author | Agando <roduccomm@gmail.com> | 2026-06-17 06:01:20 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-06-17 06:01:20 +0200 |
| commit | cebacc27a48168e666679018f8e27156d12d4527 (patch) | |
| tree | b306d33b67b9d8c84bd9b9c16b1e91ee217628a6 /modules/core | |
| parent | 6aa24b976952a698c673d350977721b27193183d (diff) | |
auto: 2026-06-17_06:01 changes
Diffstat (limited to 'modules/core')
| -rw-r--r-- | modules/core/core-bundle.js | 1124 | ||||
| -rw-r--r-- | modules/core/module-agent-registry.js | 320 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 196 | ||||
| -rw-r--r-- | modules/core/module-fs.js | 74 | ||||
| -rw-r--r-- | modules/core/module-leases.js | 25 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/core/module-neural-vfs.js | 2 | ||||
| -rw-r--r-- | modules/core/module-orchestrator.js | 41 | ||||
| -rw-r--r-- | modules/core/module-smartctx.js | 59 | ||||
| -rw-r--r-- | modules/core/module-world-model.js | 83 | ||||
| -rw-r--r-- | modules/core/ollama-manager.js | 41 | ||||
| -rw-r--r-- | modules/core/terminal.js | 97 | ||||
| -rw-r--r-- | modules/core/virtual-fs.js | 39 |
13 files changed, 1762 insertions, 341 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index 24ec5af..29d6fc8 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -1,5 +1,4 @@ -// Core module bundle — generated for v4.5.54 - +// Core module bundle// Generated from /var/www/webx11.duckdns.org/spaces/newos4/modules/core /* === virtual-fs.js === */ class VirtualFileSystem { constructor() { @@ -9,6 +8,29 @@ class VirtualFileSystem { this._virtualMounts = null; // lazy-init on first /sys/ access // Register /sys/ in emit-change exclusion (also added below) } + // ── Auto-purge trash older than 24 hours ── + _purgeTrash() { + try { + const trash = this.root.content && this.root.content['.trash']; + if (!trash || !trash.content) return; + const now = Date.now(); + const maxAge = 86400000; // 24 hours + let purged = 0; + for (const name of Object.keys(trash.content)) { + const tsMatch = name.match(/^(\d+)/); + if (tsMatch) { + const ts = parseInt(tsMatch[1], 10); + if (now - ts > maxAge) { + delete trash.content[name]; + purged++; + } + } + } + if (purged > 0) { + this.saveToStorage(); + } + } catch (e) { /* silent */ } + } // ── Virtual /sys/ filesystem (live runtime state, never persisted) ── _initVirtualMounts() { if (this._virtualMounts) return; @@ -345,6 +367,7 @@ class VirtualFileSystem { const data = JSON.parse(text); this.root = data.root || this.root; this.currentPath = data.currentPath || []; + this._purgeTrash(); return true; } catch (e) { console.error('FS v2 decompress error:', e); @@ -829,10 +852,17 @@ class VirtualFileSystem { checkpoint(name) { if (!name) name = new Date().toISOString(); const createdAt = new Date().toISOString(); + // Strip system dirs to prevent fractal bloat + const rootClone = JSON.parse(JSON.stringify(this.root)); + if (rootClone && rootClone.content) { + delete rootClone.content['_sessions']; + delete rootClone.content['_snapshots']; + delete rootClone.content['.trash']; + } const data = { version: 1, createdAt, - root: JSON.parse(JSON.stringify(this.root)), + root: rootClone, currentPath: [...this.currentPath] }; if (!this.readDir('/_snapshots')) { @@ -840,11 +870,11 @@ class VirtualFileSystem { } this.fsWriteJSON('/_snapshots/checkpoint-' + name + '.json', data); this.saveToStorage(); - // Auto-manage: keep max 10 checkpoints, delete oldest + // Auto-manage: keep max 3 checkpoints, delete oldest const all = this.listCheckpoints(); - if (all.length > 10) { + if (all.length > 3) { const sorted = all.slice().sort(); - const toDelete = sorted.slice(0, all.length - 10); + const toDelete = sorted.slice(0, all.length - 3); for (const old of toDelete) this.deleteCheckpoint(old); } return true; @@ -918,13 +948,6 @@ window.VirtualFileSystem = VirtualFileSystem; localStorage.setItem('webx11_modules', JSON.stringify(this.moduleRegistry)); } catch (e) { console.error('Module save error:', e); } } - async restorePersistedModules() { - for (const entry of this.moduleRegistry) { - if (entry.enabled && entry.url) { - await this.loadModuleFromURL(entry.url); - } - } - } async loadModuleFromURL(url) { try { this.terminal.print(`Loading module from ${url}...`, 'info'); @@ -934,17 +957,10 @@ window.VirtualFileSystem = VirtualFileSystem; const ModuleClass = this.createModuleClass(code, url); if (ModuleClass) { const instance = new ModuleClass(); + this.registerModule(instance, url); if (instance.init) { - try { - await instance.init(this.terminal); - instance._initComplete = true; - } catch (e) { - console.error(`[ModuleManager] init() failed for ${instance.constructor.name}:`, e); - this.terminal.print(`Module '${instance.constructor.name}' init() failed: ${e.message}`, 'error'); - return false; - } + await instance.init(this.terminal); } - this.registerModule(instance, url); this.terminal.print(`Module loaded successfully from ${url}`, 'success'); return true; } @@ -957,13 +973,6 @@ window.VirtualFileSystem = VirtualFileSystem; try { // Strip leading comments to avoid ASI: `return\n/**` → `return; /**` const cleanCode = code.replace(/^\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*\s*/, ''); - // Try wrapper convention first: `return class MyModule { ... };` - const wrapperMatch = cleanCode.match(/return\s+class\s+(\w+)/); - if (wrapperMatch) { - const mod = new Function(cleanCode)(); - if (mod && typeof mod === 'function') return mod; - } - // Fall back to regex class extraction const classMatch = cleanCode.match(/class\s+(\w+)\s+(extends\s+\S+)?\s*\{/); if (classMatch) { const className = classMatch[1]; @@ -980,15 +989,7 @@ window.VirtualFileSystem = VirtualFileSystem; const name = instance.constructor.name; this.modules[name] = instance; instance.terminal = this.terminal; - instance.fs = this.terminal.fs; - try { - if (instance.registerCommands) { instance.registerCommands(); } - } catch (e) { - console.error(`[ModuleManager] registerCommands() failed for ${name}:`, e); - delete this.modules[name]; - this.terminal.print(`Module '${name}' registration failed: ${e.message}`, 'error'); - return; - } + if (instance.registerCommands) { instance.registerCommands(); } // Only persist modules loaded from absolute URLs (not boot-time ext modules) if (url && url.startsWith('http') && !this.moduleRegistry.find(m => m.name === name)) { this.moduleRegistry.push({ @@ -1008,17 +1009,16 @@ window.VirtualFileSystem = VirtualFileSystem; } return false; } - async loadModuleFromCode(code, url) { + loadModuleFromCode(code, url) { try { this.terminal.print('Loading module from editor...', 'info'); const ModuleClass = this.createModuleClass(code, url); if (ModuleClass) { const instance = new ModuleClass(); + this.registerModule(instance, url); if (instance.init) { - await instance.init(this.terminal); - instance._initComplete = true; + instance.init(this.terminal); } - this.registerModule(instance, url); this.terminal.print(`Module loaded from editor: ${instance.constructor.name}`, 'success'); return true; } @@ -1084,31 +1084,6 @@ window.VirtualFileSystem = VirtualFileSystem; } return help; } - diagnose() { - const report = { - totalInModules: Object.keys(this.modules).length, - totalInRegistry: this.moduleRegistry.length, - orphaned: [], - missingFs: [], - missingTerminal: [], - uninitialized: [], - healthy: 0 - }; - const registryNames = new Set(this.moduleRegistry.map(m => m.name)); - for (const name in this.modules) { - const mod = this.modules[name]; - if (!registryNames.has(name)) { - report.orphaned.push(name); - } - if (!mod.fs) report.missingFs.push(name); - if (!mod.terminal) report.missingTerminal.push(name); - if (typeof mod.init === 'function' && !mod._initComplete) { - report.uninitialized.push(name); - } - if (mod.fs && mod.terminal) report.healthy++; - } - return report; - } } /* === ollama-manager.js === */ @@ -1133,6 +1108,8 @@ class OllamaManager { this.validatorEnabled = localStorage.getItem('webx11_validator_enabled') === 'true'; this.validatorModel = localStorage.getItem('webx11_validator_model') || 'ministral-3:3b-cloud'; this.validatorEndpoint = localStorage.getItem('webx11_validator_endpoint') || ''; + // --- Model Parameters (temperature, top_p, max_tokens, thinking) --- + this.modelParams = this._loadModelParams(); // Build fallback endpoints this.fallbackEndpoints = []; const origin = typeof window !== 'undefined' ? window.location.origin : ''; @@ -1187,6 +1164,28 @@ class OllamaManager { this._setSecret('webx11_llm_providers', JSON.stringify(this.providers)); } + _loadModelParams() { + try { + const saved = localStorage.getItem('webx11_model_params'); + if (saved) { + const parsed = JSON.parse(saved); + return { + temperature: parsed.temperature !== undefined ? parsed.temperature : 0.7, + top_p: parsed.top_p !== undefined ? parsed.top_p : 0.9, + max_tokens: parsed.max_tokens !== undefined ? parsed.max_tokens : 4096, + thinking: parsed.thinking || 'off' + }; + } + } catch (e) {} + return { temperature: 0.7, top_p: 0.9, max_tokens: 4096, thinking: 'off' }; + } + + _saveModelParams() { + try { + localStorage.setItem('webx11_model_params', JSON.stringify(this.modelParams)); + } catch (e) {} + } + _getSecret(key) { if (typeof window !== 'undefined' && window.webx11_secretVault && typeof window.webx11_secretVault.decrypt === 'function') { return window.webx11_secretVault.decrypt(key); @@ -1825,10 +1824,21 @@ class OllamaManager { messages: messages, stream: false }; + // Apply model parameters (user-configured temperature, top_p, max_tokens, thinking) + if (this.modelParams) { + body.temperature = this.modelParams.temperature; + body.top_p = this.modelParams.top_p; + body.max_tokens = this.modelParams.max_tokens; + } if (isDeepSeek) { - body.temperature = 0.3; - body.max_tokens = 4096; + // DeepSeek defaults: user params override hardcoded defaults + if (body.temperature === undefined) body.temperature = 0.3; + if (body.max_tokens === undefined) body.max_tokens = 4096; body.stop = ['<|end|>']; + // DeepSeek thinking mode + if (this.modelParams && this.modelParams.thinking !== 'off') { + body.thinking = { type: this.modelParams.thinking }; + } } if (tools && tools.length > 0) { body.tools = tools; @@ -1914,7 +1924,7 @@ class OllamaManager { return { message: { role: 'assistant', content: '' }, tool_calls: null, - error: null + error: 'DeepSeek returned empty response after retry — model may be overloaded or context too long' }; } @@ -1970,11 +1980,6 @@ class OllamaManager { if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); const text = data.choices?.[0]?.message?.content || ''; - // Sync provider state after successful non-Ollama fallback - if (provider !== this.currentProvider) { - this.currentProvider = provider; - this._syncCompatProperties(provider); - } return { message: { role: 'assistant', content: text }, tool_calls: null, error: null }; } catch (e) { if (e.name === 'AbortError') { @@ -2001,11 +2006,10 @@ ${toolDesc} </tools> Only output XML function calls for tool tasks. If no tool is needed, answer normally.` }; -const msgsWithTools = [toolContext, ...messages]; + const msgsWithTools = [toolContext, ...messages]; const body = { model: useModel, messages: msgsWithTools, stream: false, options: { temperature: 0.1 } }; - const ep = this.currentProvider ? this.currentProvider.endpoint : this.endpoint; - const response = await fetch(`${ep}/api/chat`, { + const response = await fetch(`${this.endpoint}/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal }); @@ -2203,7 +2207,7 @@ const msgsWithTools = [toolContext, ...messages]; this.destructiveCmds = new Set(['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']); this.systemCmds = new Set(['reboot','shutdown','format','kill','halt','poweroff']); this.suspiciousFlags = new Set(['--dump','--execute','--eval','--exec','--shell','--script']); - this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem']); + this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register']); // Load persisted overrides from localStorage try { const d = localStorage.getItem('webx11_security_destructive'); if(d){JSON.parse(d).forEach(c=>this.destructiveCmds.add(c));} }catch(e){} try { const s = localStorage.getItem('webx11_security_system'); if(s){JSON.parse(s).forEach(c=>this.systemCmds.add(c));} }catch(e){} @@ -2212,7 +2216,7 @@ const msgsWithTools = [toolContext, ...messages]; this._registerBuiltIn(); } _registerBuiltIn() { - const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg']; + const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register']; internals.forEach(c => this.registerCommand(c, { category: 'builtin' })); } setLevel(n) { @@ -3047,35 +3051,14 @@ const msgsWithTools = [toolContext, ...messages]; _autoSaveSession() { try { this.fs.mkdirRecursive('_sessions'); - var rootClone; - try { - const json = JSON.stringify(this.fs.root); - if (json.length > 52428800) { // 50MB cap - rootClone = { type: 'directory', name: '/', content: {}, _truncated: true }; - this.print('⚠ Auto-save skipped (VFS too large)', 'warning'); - return; - } - rootClone = JSON.parse(json); - } catch (e) { - rootClone = { type: 'directory', name: '/', content: {}, _error: true }; - return; - } - if (rootClone && rootClone.content && rootClone.content['_sessions']) { - delete rootClone.content['_sessions']; - } const snapshot = { type: 'session', + version: 2, savedAt: new Date().toISOString(), - fsRoot: rootClone, currentPath: this.fs.currentPath, - version: 1, terminalLines: this._historyBuffer.slice(-50) }; const snapshotJson = JSON.stringify(snapshot); - if (snapshotJson.length > 52428800) { - this.print('⚠ Auto-save snapshot too large, skipping', 'warning'); - return; - } this.fs.writeFile('_sessions/_auto.json', snapshotJson); try { const ctx = localStorage.getItem('webx11_ai_context'); @@ -3100,19 +3083,40 @@ const msgsWithTools = [toolContext, ...messages]; this.print('↻ Skipped stale auto-session backup (older than last reset)', 'info'); return; } - if (data && data.fsRoot) { + if (data.version === 2) { + // v2: metadata-only restore (VFS is persisted independently in localStorage) + this.fs.currentPath = data.currentPath || []; + this.fs.saveToStorage(); + this.updatePrompt(); + this.print('↻ Auto-restored session from ' + (data.savedAt ? new Date(data.savedAt).toLocaleTimeString() : 'recent'), 'success'); + // Optionally re-render last few terminal lines + if (data.terminalLines && Array.isArray(data.terminalLines)) { + data.terminalLines.slice(-10).forEach(line => { + const text = typeof line === 'string' ? line : (line.l || ''); + this.print(text, typeof line === 'object' ? (line.t || '') : ''); + }); + } + try { + const aiCtx = this.fs.readFile('_sessions/_auto-context.json'); + if (aiCtx) { + localStorage.setItem('webx11_ai_context', aiCtx); + } + } catch (e) {} + this.fs.deleteFile('_sessions/_auto.json'); + this.fs.deleteFile('_sessions/_auto-context.json'); + } else if (data && data.fsRoot) { + // v1: full VFS restore (legacy) this.fs.root = data.fsRoot; this.fs.currentPath = data.currentPath || []; this.fs.saveToStorage(); + this.updatePrompt(); this.print('↻ Auto-restored session from ' + (data.savedAt ? new Date(data.savedAt).toLocaleTimeString() : 'recent'), 'success'); - // Defer AI context restoration (AI module loads after terminal init) try { const aiCtx = this.fs.readFile('_sessions/_auto-context.json'); if (aiCtx) { localStorage.setItem('webx11_ai_context', aiCtx); } } catch (e) {} - // v4.5.47: delete backup after successful restore so reset-mem doesn't get resurrected this.fs.deleteFile('_sessions/_auto.json'); this.fs.deleteFile('_sessions/_auto-context.json'); } @@ -3759,22 +3763,41 @@ const msgsWithTools = [toolContext, ...messages]; return; } if (cmd === 'history') { - const search = args.join(' ').toLowerCase(); const entries = this.history; if (!entries.length) { this.print('No command history.', 'info'); return; } - if (search) { - const filtered = entries.map((c, i) => ({ i, c })).filter(e => e.c.toLowerCase().includes(search)); - if (!filtered.length) { this.print(`No history entries matching: ${search}`, 'info'); return; } - this.print(`=== History (matching: ${search}) ===`, 'info'); - filtered.forEach(e => this.print(` ${e.i + 1} ${e.c}`)); - } else { - this.print(`=== Command History (${entries.length} entries) ===`, 'info'); - const start = Math.max(0, entries.length - 50); - for (let i = start; i < entries.length; i++) { - this.print(` ${i + 1} ${entries[i]}`); + // Flags + if (args[0] === '--clear' || args[0] === '-c') { + this.history = []; + this.historyIndex = 0; + localStorage.removeItem('webx11_command_history'); + this.print('History cleared.', 'success'); + return; + } + if (args[0] === '--export' || args[0] === '-e') { + try { + this.fs.mkdirRecursive('system'); + this.fs.writeFile('system/history.json', JSON.stringify(entries)); + this.print('History exported to /system/history.json (' + entries.length + ' entries).', 'success'); + } catch (e) { + this.print('Export failed: ' + e.message, 'error'); } - if (entries.length > 50) this.print(`... (${entries.length - 50} older entries hidden, use history <search> to find)`); + return; } + if (args[0] === '--search' || args[0] === '-s') { + const term = args.slice(1).join(' ').toLowerCase(); + if (!term) { this.print('Usage: history --search <term>', 'warning'); return; } + const filtered = entries.filter(e => e.toLowerCase().includes(term)); + if (!filtered.length) { this.print('No matches for: ' + term, 'info'); return; } + this.print('=== History (matching: ' + term + ') ===', 'info'); + filtered.forEach(e => this.print(' ' + e)); + return; + } + let count = 50; + if (args[0] === '-n' && args[1]) count = parseInt(args[1]) || 50; + const slice = entries.slice(-count); + this.print('=== Command History (' + entries.length + ' entries) ===', 'info'); + slice.forEach(c => this.print(' ' + c)); + if (entries.length > count) this.print('... (' + (entries.length - count) + ' older, use history --search <term> to find)'); return; } for (const modName in this.moduleManager.modules) { @@ -5771,24 +5794,18 @@ class FileSystemModule { this.commands['session-save'] = (args) => { if (!args[0]) return this.terminal.print('Usage: session-save <name>', 'warning'); const name = args[0].replace(/[^a-zA-Z0-9_-]/g, '_'); - // Strip system dirs to avoid recursive bloat in snapshots - const fsRoot = JSON.parse(JSON.stringify(this.terminal.fs.root)); - if (fsRoot && fsRoot.content) { - delete fsRoot.content['_sessions']; - delete fsRoot.content['_snapshots']; - delete fsRoot.content['.trash']; - } - const snapshot = { + // v2: metadata-only — VFS is persisted independently in localStorage + const manifest = { type: 'session', + version: 2, savedAt: new Date().toISOString(), - fsRoot: fsRoot, currentPath: this.terminal.fs.currentPath, - version: 1 + terminalLines: this.terminal._historyBuffer ? this.terminal._historyBuffer.slice(-50) : [] }; const filePath = '_sessions/' + name + '.json'; this.terminal.fs.mkdirRecursive('_sessions'); - this.terminal.fs.writeFile(filePath, JSON.stringify(snapshot, null, 2)); - this.terminal.print('Session saved: ' + name, 'success'); + this.terminal.fs.writeFile(filePath, JSON.stringify(manifest)); + this.terminal.print('Session saved: ' + name + ' (' + (manifest.terminalLines.length || 0) + ' history lines)', 'success'); }; this.commands['session-load'] = (args) => { if (!args[0]) return this.terminal.print('Usage: session-load <name>', 'warning'); @@ -5797,11 +5814,28 @@ class FileSystemModule { if (content === null) return this.terminal.print('Session not found: ' + args[0], 'error'); try { const session = JSON.parse(content); - if (!session || session.type !== 'session' || !session.fsRoot) { + if (!session || session.type !== 'session') { return this.terminal.print('Invalid session file: ' + args[0], 'error'); } - this.terminal.fs.root = session.fsRoot; - this.terminal.fs.currentPath = session.currentPath || []; + if (session.version === 2) { + // v2: metadata-only restore — keep existing VFS, just restore path + terminal history + this.terminal.fs.currentPath = session.currentPath || []; + if (session.terminalLines && Array.isArray(session.terminalLines) && this.terminal._historyBuffer) { + this.terminal._historyBuffer = session.terminalLines.slice(-50); + // Re-render last few lines for context + this.terminal.outputContainer.innerHTML = ''; + session.terminalLines.forEach(line => { + const text = typeof line === 'string' ? line : (line.l || ''); + const type = typeof line === 'object' ? (line.t || '') : ''; + this.terminal.print(text, type); + }); + } + } else { + // v1: full VFS restore (legacy) + if (!session.fsRoot) return this.terminal.print('Invalid session file', 'error'); + this.terminal.fs.root = session.fsRoot; + this.terminal.fs.currentPath = session.currentPath || []; + } this.terminal.fs.saveToStorage(); this.terminal.updatePrompt(); this.terminal.print('Session loaded: ' + args[0] + ' (' + new Date(session.savedAt).toLocaleString() + ')', 'success'); @@ -5821,8 +5855,13 @@ class FileSystemModule { const content = JSON.parse(this.terminal.fs.readFile('_sessions/' + f.name)); const label = f.name.replace('.json', ''); const date = content.savedAt ? new Date(content.savedAt).toLocaleString() : 'unknown'; - const fileCount = content.fsRoot ? Object.keys(content.fsRoot.content || {}).length : 0; - this.terminal.print(' ' + label + ' (' + fileCount + ' items, ' + date + ')'); + if (content.version === 2) { + const lines = content.terminalLines ? content.terminalLines.length : 0; + this.terminal.print(' ' + label + ' (' + lines + ' history lines, ' + date + ')'); + } else { + const fileCount = content.fsRoot ? Object.keys(content.fsRoot.content || {}).length : 0; + this.terminal.print(' ' + label + ' (' + fileCount + ' items, ' + date + ')'); + } } catch (e) { this.terminal.print(' ' + f.name.replace('.json', '') + ' (corrupt)'); } @@ -5865,12 +5904,30 @@ class FileSystemModule { reader.onload = (evt) => { try { const session = JSON.parse(evt.target.result); - if (!session || session.type !== 'session' || !session.fsRoot) { + if (!session || session.type !== 'session') { this.terminal.print('Invalid session file (not a valid session export)', 'error'); return; } - this.terminal.fs.root = session.fsRoot; - this.terminal.fs.currentPath = session.currentPath || []; + if (session.version === 2) { + // v2: metadata-only — keep existing VFS, restore path + this.terminal.fs.currentPath = session.currentPath || []; + if (session.terminalLines && Array.isArray(session.terminalLines) && this.terminal._historyBuffer) { + this.terminal._historyBuffer = session.terminalLines.slice(-50); + this.terminal.outputContainer.innerHTML = ''; + session.terminalLines.forEach(line => { + const text = typeof line === 'string' ? line : (line.l || ''); + const type = typeof line === 'object' ? (line.t || '') : ''; + this.terminal.print(text, type); + }); + } + } else { + if (!session.fsRoot) { + this.terminal.print('Invalid session file (not a valid session export)', 'error'); + return; + } + this.terminal.fs.root = session.fsRoot; + this.terminal.fs.currentPath = session.currentPath || []; + } this.terminal.fs.saveToStorage(); this.terminal.updatePrompt(); this.terminal.print('Session imported: ' + file.name + ' (' + (session.savedAt ? new Date(session.savedAt).toLocaleString() : 'unknown date') + ')', 'success'); @@ -5979,17 +6036,11 @@ class OllamaModule { }; this.commands['ai'] = async (args) => { if(!args[0]) return this.terminal.print("Usage: ai <prompt>", 'warning'); - const result = await this.terminal.ollamaManager.chat(args.join(' ')); - if (result === null) { - this.terminal.print('AI returned no response — check provider connection', 'warning'); - } + await this.terminal.ollamaManager.chat(args.join(' ')); }; this.commands['ask'] = async (args) => { if(!args[0]) return this.terminal.print("Usage: ask <prompt>", 'warning'); - const result = await this.terminal.ollamaManager.chat(args.join(' ')); - if (result === null) { - this.terminal.print('AI returned no response — check provider connection', 'warning'); - } + await this.terminal.ollamaManager.chat(args.join(' ')); }; // Merged from OllamaModelsModule this.commands['models'] = () => this.listModels(); @@ -6215,29 +6266,7 @@ class MyModule { return mm.modules[name]; } registerCommands() { - this.commands['modules'] = (args) => { - if (args[0] === '--diagnose' || args[0] === '-d') { - const report = this.terminal.moduleManager.diagnose(); - this.terminal.print(`=== Module Diagnostics ===`, 'info'); - this.terminal.print(`Total in modules: ${report.totalInModules}`); - this.terminal.print(`Total in registry: ${report.totalInRegistry}`); - this.terminal.print(`Healthy: ${report.healthy}`); - if (report.orphaned.length) { - this.terminal.print(`Orphaned (in modules but not registry): ${report.orphaned.join(', ')}`, 'warning'); - } - if (report.missingFs.length) { - this.terminal.print(`Missing fs: ${report.missingFs.join(', ')}`, 'error'); - } - if (report.missingTerminal.length) { - this.terminal.print(`Missing terminal: ${report.missingTerminal.join(', ')}`, 'error'); - } - if (report.uninitialized.length) { - this.terminal.print(`Uninitialized (init() never completed): ${report.uninitialized.join(', ')}`, 'warning'); - } - return; - } - this.terminal.openModuleManager(); - }; + this.commands['modules'] = () => { this.terminal.openModuleManager(); }; this.commands['loadmod'] = async (args) => { if(!args[0]) return this.terminal.print("Usage: loadmod <url>", 'warning'); await this.terminal.moduleManager.loadModuleFromURL(args[0]); @@ -7341,6 +7370,14 @@ buildSystemPrompt() { return `Current date: ${dateStr}\nCurrent time: ${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 nvfs = this.terminal?.moduleManager?.modules?.NeuralVFSModule; @@ -7351,11 +7388,15 @@ buildSystemPrompt() { if (ageMs > 14400000) nvfsNote = ' |nvfs ' + Math.floor(ageMs/3600000) + 'h stale'; } } catch (e) {} - return 'Current date: ' + dateStr + nvfsNote + '\n' + let base = 'Current date: ' + dateStr + nvfsNote + '\n' + 'VFS→Modules→AI. nvfs-index for commands. Tool→Observe→Answer. nvfs-resolve. Destructive cmds need confirm.\n' - + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind: vaultmind_query(ep,msg,agent) — Hyperliquid prices/portfolio/chat\n' + + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind: vaultmind_connect(network) — connect wallet, vaultmind_status — check session, vaultmind_query(ep,msg,agent) — Hyperliquid prices/portfolio/chat | wallet_lookup(address) — read-only portfolio for any wallet\n' + 'web_fetch tool: GET for normal pages (returns title+content+word_count), POST for JSON APIs like Hyperliquid /info (pass method="POST" and body as JSON string).\n' + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, guardrail, wget, tar, secret-* — discoverable via help'; + if (profilePrompt) { + base = profilePrompt + '\n\n' + base; + } + return base; } getToolDefinitions() { if (!this._toolDefs) this._toolDefs = this._buildToolDefs(); @@ -7724,6 +7765,43 @@ buildSystemPrompt() { 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'] + } + } }]; } getHelpInfo() { @@ -7740,7 +7818,9 @@ buildSystemPrompt() { '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))', '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' + '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', }; } registerCommands() { @@ -7765,6 +7845,8 @@ buildSystemPrompt() { 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(); } async chat(args) { const userMessage = args.join(' '); @@ -7988,16 +8070,6 @@ buildSystemPrompt() { } async agenticLoop(messages) { let toolTurns = 0; - // Auto-connect if not connected - const om = this.terminal.ollamaManager; - if (om && !om.connected) { - this.terminal.print('⏳ AI provider not connected — attempting auto-connect...', 'info'); - const connected = await om.connect(); - if (!connected) { - this.terminal.print('❌ Could not connect to any AI provider.', 'error'); - return { finalAnswer: null, toolTurns: 0 }; - } - } while (toolTurns < this.MAX_TOOL_TURNS) { if (this._cancelled) { this.terminal.print('AI loop cancelled', 'warning'); return { finalAnswer: null, toolTurns }; } toolTurns++; @@ -8046,9 +8118,9 @@ buildSystemPrompt() { 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(); - return { fn, toolResult, t1, toolCallId: tc.id }; + return { fn, toolResult, t1 }; })); - for (const { fn, toolResult, t1, toolCallId } of safeResults) { + for (const { 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 { @@ -8075,7 +8147,7 @@ buildSystemPrompt() { this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`); this.terminal.print(`📊 Result (${t1}s): ${obs}`, 'info'); } - messages.push({ role: 'tool', tool_call_id: toolCallId, content: toolResult }); + messages.push({ role: 'tool', content: toolResult }); } // Only block on destructive tools if (destructiveTools.length > 0) { @@ -8137,7 +8209,7 @@ buildSystemPrompt() { this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`); this.terminal.print(`📊 Result (${t1}s): ${dObs}`, 'info'); } - messages.push({ role: 'tool', tool_call_id: tc.id, content: toolResult }); + messages.push({ role: 'tool', content: toolResult }); } } else { messages.push({ @@ -9140,6 +9212,8 @@ buildSystemPrompt() { } scm.activeProfile = name; const profile = scm.contextProfiles[name]; + // Rebuild system prompt to reflect new context profile + this.systemPrompt = this.buildSystemPrompt(); return JSON.stringify({ success: true, name, displayName: profile.name, systemPrompt: profile.systemPrompt, temperature: profile.temperature }); } case 'create_mindmap': { @@ -9170,29 +9244,74 @@ buildSystemPrompt() { 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 <pin>' }); + 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 epMap = { - 'prices': '/vmai-api/market/prices', - 'portfolio': '/vmai-api/portfolio', - 'transactions': '/vmai-api/transactions/summary', - 'copy-trade': '/vmai-api/copy-trading/start', - 'chat': '/vmai-api/chat', - }; - const url = epMap[ep]; - if (!url) { - return JSON.stringify({ error: 'Invalid endpoint. Valid: ' + Object.keys(epMap).join(', ') }); + const validEps = ['prices', 'portfolio', 'transactions', 'copy-trade', 'chat']; + if (!validEps.includes(ep)) { + return JSON.stringify({ error: 'Invalid endpoint. Valid: ' + validEps.join(', ') }); } try { - // Read VaultMind session token from localStorage (set by VaultMind dashboard) - const vmToken = typeof localStorage !== 'undefined' ? localStorage.getItem('vaultmind_token') : null; + const epMap = { prices: '/vmai/market/prices', portfolio: '/vmai/portfolio', transactions: '/vmai/transactions/summary', 'copy-trade': '/vmai/copy-trading/traders', chat: '/vmai/chat' }; + let url = epMap[ep]; let opts = { method: 'GET', headers: { 'Accept': 'application/json' } }; - if (vmToken) { - opts.headers['Authorization'] = 'Bearer ' + vmToken; + // 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' @@ -9205,6 +9324,17 @@ buildSystemPrompt() { 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 }); + } + } default: return JSON.stringify({ error: `Unknown tool: ${name}` }); } @@ -9531,7 +9661,7 @@ buildSystemPrompt() { } } _getAiConfigKeys() { - return ['max-turns', 'max-context', 'max-context-tokens', 'confirm-timeout', 'chat-lock-timeout', 'skill-debounce', 'trivial-optimization']; + 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) { @@ -9543,8 +9673,15 @@ buildSystemPrompt() { this.terminal.print(`confirm-timeout: ${this._confirmTimeoutMs > 0 ? this._confirmTimeoutMs / 1000 + 's' : 'disabled (no auto-deny)'} (default: 120s)`, ''); this.terminal.print(`chat-lock-timeout: ${this._chatLockTimeoutMs / 1000}s (default: 10s)`, ''); this.terminal.print(`skill-debounce: ${this._skillDebounceMs / 60000}min (default: 30min)`, ''); + 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 <key> <value>', 'info'); - this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout(0=disabled), chat-lock-timeout, skill-debounce, trivial-optimization', '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]; @@ -9577,8 +9714,28 @@ buildSystemPrompt() { 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`, 'warning'); + return this.terminal.print(`Unknown key: ${key}. Valid: max-turns, max-context, max-context-tokens, confirm-timeout, chat-lock-timeout, skill-debounce, trivial-optimization, temperature, top-p, max-tokens, thinking`, 'warning'); } // Persist localStorage.setItem('webx11_ai_config', JSON.stringify({ @@ -9736,6 +9893,39 @@ buildSystemPrompt() { 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'); + } + } } /* === module-aiasm.js === */ @@ -9771,7 +9961,7 @@ buildSystemPrompt() { class SmartContextModule { constructor() { this.commands = {}; this.terminal = null; - this.contextProfiles = this.loadProfiles(); this.activeProfile = null; + this.contextProfiles = this.loadProfiles(); this.activeProfile = this.loadActiveProfile(); } getHelpInfo() { return { @@ -9791,6 +9981,22 @@ buildSystemPrompt() { saveProfiles() { localStorage.setItem('webx11_context_profiles', JSON.stringify(this.contextProfiles)); } + loadActiveProfile() { + try { + const saved = localStorage.getItem('webx11_active_context_profile'); + if (saved && this.contextProfiles[saved]) return saved; + } catch (e) {} + return null; + } + saveActiveProfile() { + try { + if (this.activeProfile) { + localStorage.setItem('webx11_active_context_profile', this.activeProfile); + } else { + localStorage.removeItem('webx11_active_context_profile'); + } + } catch (e) {} + } getDefaultProfiles() { return { 'coding': { name: 'Coding Assistant', systemPrompt: "You are an expert programmer.", temperature: 0.7 }, @@ -9813,6 +10019,7 @@ buildSystemPrompt() { } if (this.contextProfiles[args[0]]) { this.activeProfile = args[0]; + this.saveActiveProfile(); this.terminal.print(`Context set to: ${this.contextProfiles[args[0]].name}`, "success"); } else { this.terminal.print(`Profile '${args[0]}' not found`, "error"); @@ -9852,12 +10059,44 @@ buildSystemPrompt() { this.terminal.print("Usage: smart-ask <your-question>", "warning"); return; } let systemPrompt = "You are a helpful assistant."; + let temperature = 0.7; if (this.activeProfile && this.contextProfiles[this.activeProfile]) { - systemPrompt = this.contextProfiles[this.activeProfile].systemPrompt; - this.terminal.print(`Using context: ${this.contextProfiles[this.activeProfile].name}`, "info"); + const profile = this.contextProfiles[this.activeProfile]; + systemPrompt = profile.systemPrompt; + temperature = profile.temperature || 0.7; + this.terminal.print(`Using context: ${profile.name} (temp: ${temperature})`, "info"); + } + // Use agentic loop via the AI assistant module for tool-enabled responses + const aiModule = this.terminal?.moduleManager?.modules?.WebX11AIAssistantModule; + if (aiModule) { + // Set temperature on ollamaManager modelParams temporarily + const om = this.terminal.ollamaManager; + const savedTemp = om.modelParams ? om.modelParams.temperature : undefined; + if (om.modelParams) { + om.modelParams.temperature = temperature; + } + try { + const messages = [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: args.join(' ') } + ]; + const result = await om.chatWithTools(messages, aiModule.getToolDefinitions()); + if (result && result.message) { + this.terminal.printAIResponse(result.message.content || '(no response)'); + } else if (result && result.error) { + this.terminal.print(`⚠️ AI error: ${result.error}`, 'error'); + } + } finally { + if (om.modelParams && savedTemp !== undefined) { + om.modelParams.temperature = savedTemp; + } + } + } else { + // Fallback: simple chat with temperature + const om = this.terminal.ollamaManager; + const fullPrompt = `${systemPrompt}\n\nUser: ${args.join(' ')}`; + await om.chat(fullPrompt); } - const fullPrompt = `${systemPrompt}\n\nUser: ${args.join(' ')}`; - await this.terminal.ollamaManager.chat(fullPrompt); } } @@ -11548,11 +11787,13 @@ class ChatStudioModule { /* === module-agent-registry.js === */ // module-agent-registry.js — VFS-backed agent directory with A2A contracts // Commands: agent-list, agent-register, agent-unregister, agent-status, contract-register, contract-list +// A2A Scratchpad commands: a2a-post, a2a-poll, a2a-ack, a2a-reject, a2a-status, a2a-prune class AgentRegistryModule { constructor() { this.commands = {}; this.terminal = null; this._contracts = {}; + this._a2aPruneTimer = null; } registerCommands() { this.commands['agent-list'] = () => this.listAgents(); @@ -11561,14 +11802,321 @@ class AgentRegistryModule { this.commands['agent-unregister'] = (args) => this.unregisterAgent(args[0]); this.commands['contract-register'] = (args) => this.registerContract(args); this.commands['contract-list'] = () => this.listContracts(); + // A2A Scratchpad commands + this.commands['a2a-post'] = (args) => this.a2aPostCommand(args); + this.commands['a2a-poll'] = (args) => this.a2aPollCommand(args); + this.commands['a2a-ack'] = (args) => this.a2aAckCommand(args); + this.commands['a2a-reject'] = (args) => this.a2aRejectCommand(args); + this.commands['a2a-status'] = () => this.a2aStatus(); + this.commands['a2a-prune'] = () => this.a2aPrune(); + } + getHelpInfo() { + return { + 'a2a-post': 'a2a-post <to> <type> <capability> <payload...> — Post an A2A message', + 'a2a-poll': 'a2a-poll [agentId] — Poll for messages addressed to an agent (default: current or "user")', + 'a2a-ack': 'a2a-ack <msgId> <result...> — Acknowledge a message with result', + 'a2a-reject': 'a2a-reject <msgId> <error...> — Reject a message with error', + 'a2a-status': 'a2a-status — Show scratchpad stats per agent', + 'a2a-prune': 'a2a-prune — Clean up expired and old completed messages' + }; } init(terminal) { this.terminal = terminal; if (!this.terminal.fs.readDir('/agents/')) this.terminal.fs.mkdir('agents'); if (!this.terminal.fs.readDir('/.contracts/')) this.terminal.fs.mkdir('.contracts'); this._loadContracts(); + // Initialize scratchpad + this._ensureScratchpad(); + // Auto-prune every 60 seconds + this._a2aPruneTimer = setInterval(() => this.a2aPrune(), 60000); + // Save the spec-vs-reality delta table + this._saveDeltaTable(); this.terminal.print('📋 Agent Registry loaded', 'info'); } + // ─── Task 1: Save spec-vs-reality delta table ─── + _saveDeltaTable() { + try { + const fs = this.terminal.fs; + if (!fs.readDir('/_docs/')) fs.mkdir('_docs'); + const delta = `# Spec-vs-Reality Delta — 2026-06-17\n\n` + +`| # | Whitepaper Concept | Spec'd Behavior | Current Reality | Δ | Priority |\n` + +`|---|---|---|---|---|---|\n` + +`| 1 | Hyper-Modular Kernel | Real-time self-rewriting drivers for task-specific optimization via WPM | WPM exists but is manual; no hot-patch daemon or runtime driver swap | Partial | Medium |\n` + +`| 2 | A2A Protocol | Standardized neural communication layer for agent negotiation; typed messages, no central broker | VFS mailbox transport works (a2a-send/a2a-list/500ms polling) but orchestrator acts as central broker | Partial | High |\n` + +`| 3 | Neural VFS | Semantic-based file storage (concept:// URIs) as primary access method; hierarchical paths are secondary | concept:// URIs work (nvfs-index/nvfs-resolve) but hierarchical paths remain primary access; concept resolution requires explicit index | Partial | High |\n` + +`| 4 | Recursive Self-Optimization | Monitor → Profile → Diagnose → Patch → Test → Promote feedback loop | system-health telemetry captures latency/VFS stats but no auto-patch generation or hot-promotion pipeline | Partial | Medium |\n` + +`| 5 | Agent Discovery | No central broker — agents discover each other via VFS registrations spontaneously | Orchestrator is central broker; all task delegation goes through it; agents don't autonomously discover each other | Critical Gap | Critical |\n` + +`| 6 | Contract Schema | Every A2A message carries a contract schema defining capabilities and constraints | Contracts exist (contract-register/contract-list) but schema validation is basic substring matching; not enforced on every message | Partial | Medium |\n` + +`| 7 | Semantic Addressing | Neural VFS as primary storage paradigm — users navigate by concept, not path | Hierarchical path access still primary; concept:// requires explicit nvfs-index; no transparent concept-to-path fallback | Critical Gap | Critical |\n` + +`| 8 | Warm-start Guarantee | Kernel caches last N stable driver configs; boot in under 200ms | No golden config cache; full localStorage restore on every page load | Spec Violation | High |\n` + +`| 9 | Self-rewrite Daemon | Monitors performance telemetry and generates optimized driver variants at runtime | No self-rewrite daemon exists; all module changes are manual | Missing | Low |\n` + +`| 10 | Agent Lifecycle | spawn, sleep, wake, kill, restart policies with exponential backoff | Fully implemented in orchestrator with heartbeat, idle sweep, restart policies, VFS sleep serialization | Fully Aligned | — |\n\n` + +`**Summary: 2 Critical gaps, 5 Partial, 1 Spec Violation, 1 Missing, 1 Fully Aligned**\n`; + fs.writeFile('_docs/spec-vs-reality-delta.md', delta); + this.terminal.print('📄 Spec-vs-Reality delta saved to /_docs/spec-vs-reality-delta.md', 'info'); + } catch (e) { + console.warn('Failed to save delta table:', e.message); + } + } + // ─── A2A Scratchpad Helpers ─── + _ensureScratchpad() { + try { + const data = this.terminal.fs.fsReadJSON('/_agents/scratchpad.json'); + if (!data) throw new Error('no scratchpad'); + } catch (e) { + this.terminal.fs.mkdir('_agents'); + this.terminal.fs.fsWriteJSON('/_agents/scratchpad.json', { + messages: [], + seq: 0, + agents: {} + }); + } + } + _getScratchpad() { + try { + const data = this.terminal.fs.fsReadJSON('/_agents/scratchpad.json'); + if (data && data.messages) return data; + } catch (e) {} + return { messages: [], seq: 0, agents: {} }; + } + _saveScratchpad(sp) { + try { + this.terminal.fs.mkdir('_agents'); + this.terminal.fs.fsWriteJSON('/_agents/scratchpad.json', sp); + } catch (e) { + console.warn('Scratchpad save error:', e.message); + } + } + // ─── A2A Scratchpad Functions ─── + a2aPost(from, to, type, capability, payload, ttl) { + if (!from || !to || !type) { + this.terminal.print('a2aPost: from, to, and type are required', 'warning'); + return null; + } + const sp = this._getScratchpad(); + sp.seq = (sp.seq || 0) + 1; + const msg = { + id: `msg_${String(sp.seq).padStart(3, '0')}`, + from: from, + to: to, + type: type, + capability: capability || null, + payload: payload || {}, + ttl: (typeof ttl === 'number' ? ttl : 300000), + status: 'pending', + ts: Date.now(), + reply_to: null + }; + sp.messages.push(msg); + this._saveScratchpad(sp); + return msg; + } + a2aPoll(agentId) { + if (!agentId) return []; + const sp = this._getScratchpad(); + const pending = []; + for (const msg of sp.messages) { + if (msg.to === agentId && msg.status === 'pending') { + msg.status = 'delivered'; + pending.push(msg); + } + } + if (pending.length > 0) this._saveScratchpad(sp); + return pending; + } + a2aAck(messageId, resultPayload) { + const sp = this._getScratchpad(); + const msg = sp.messages.find(m => m.id === messageId); + if (!msg) { + this.terminal.print(`a2aAck: message ${messageId} not found`, 'warning'); + return false; + } + if (msg.status === 'completed' || msg.status === 'failed') { + this.terminal.print(`a2aAck: message ${messageId} already ${msg.status}`, 'warning'); + return false; + } + msg.status = 'completed'; + msg.result = resultPayload || {}; + msg.completedAt = Date.now(); + this._saveScratchpad(sp); + return true; + } + a2aReject(messageId, error) { + const sp = this._getScratchpad(); + const msg = sp.messages.find(m => m.id === messageId); + if (!msg) { + this.terminal.print(`a2aReject: message ${messageId} not found`, 'warning'); + return false; + } + if (msg.status === 'completed' || msg.status === 'failed') { + this.terminal.print(`a2aReject: message ${messageId} already ${msg.status}`, 'warning'); + return false; + } + msg.status = 'failed'; + msg.error = error || 'unspecified error'; + msg.completedAt = Date.now(); + this._saveScratchpad(sp); + return true; + } + a2aRegister(agentId, capabilities) { + if (!agentId) return; + const sp = this._getScratchpad(); + sp.agents[agentId] = { + last_poll: Date.now(), + capabilities: capabilities || [] + }; + this._saveScratchpad(sp); + } + a2aUnregister(agentId) { + if (!agentId) return; + const sp = this._getScratchpad(); + delete sp.agents[agentId]; + this._saveScratchpad(sp); + } + a2aPrune() { + try { + const sp = this._getScratchpad(); + const now = Date.now(); + const before = sp.messages.length; + sp.messages = sp.messages.filter(m => { + // Remove expired messages (past TTL) + if (m.status === 'pending' && (now - m.ts > m.ttl)) return false; + // Remove completed/failed messages older than 1 hour + if ((m.status === 'completed' || m.status === 'failed') && m.completedAt && (now - m.completedAt > 3600000)) return false; + return true; + }); + const removed = before - sp.messages.length; + if (removed > 0) this._saveScratchpad(sp); + return removed; + } catch (e) { + console.warn('a2aPrune error:', e.message); + return 0; + } + } + a2aStatus() { + const sp = this._getScratchpad(); + const counts = {}; + for (const msg of sp.messages) { + if (!counts[msg.to]) counts[msg.to] = { pending: 0, delivered: 0, completed: 0, failed: 0 }; + if (msg.status === 'pending') counts[msg.to].pending++; + else if (msg.status === 'delivered') counts[msg.to].delivered++; + else if (msg.status === 'completed') counts[msg.to].completed++; + else if (msg.status === 'failed') counts[msg.to].failed++; + } + this.terminal.print('📊 A2A Scratchpad Status:', 'header'); + this.terminal.print(` Total messages: ${sp.messages.length}`, 'info'); + this.terminal.print(` Registered agents: ${Object.keys(sp.agents).length}`, 'info'); + this.terminal.print(` Sequence: ${sp.seq}`, 'info'); + const agentNames = Object.keys(counts); + if (agentNames.length === 0) { + this.terminal.print(' No messages in scratchpad', 'info'); + return; + } + for (const agentId of agentNames) { + const c = counts[agentId]; + const total = c.pending + c.delivered + c.completed + c.failed; + const caps = sp.agents[agentId] ? ` [${sp.agents[agentId].capabilities.join(', ')}]` : ''; + this.terminal.print(` ${agentId}${caps}: ${total} msgs (⏳${c.pending} 📬${c.delivered} ✅${c.completed} ❌${c.failed})`, 'info'); + } + } + _a2aCleanup() { + // Remove agent entries from scratchpad when agent directory is deleted from /agents/ + try { + const sp = this._getScratchpad(); + const agentDirs = {}; + const dir = this.terminal.fs.readDir('/agents/'); + if (dir) { + for (const entry of dir) { + if (entry.type === 'dir') agentDirs[entry.name] = true; + } + } + let changed = false; + for (const agentId of Object.keys(sp.agents)) { + if (!agentDirs[agentId]) { + delete sp.agents[agentId]; + changed = true; + } + } + if (changed) this._saveScratchpad(sp); + } catch (e) { + console.warn('a2aCleanup error:', e.message); + } + } + // ─── A2A Command Handlers ─── + a2aPostCommand(args) { + // a2a-post <to> <type> <capability> <payload...> + if (args.length < 3) { + return this.terminal.print('Usage: a2a-post <to> <type> <capability> <payload...>', 'warning'); + } + const to = args[0]; + const type = args[1]; + const capability = args[2]; + const payloadRaw = args.slice(3).join(' '); + let payload = {}; + try { + // Try parsing as JSON + payload = JSON.parse(payloadRaw || '{}'); + } catch (e) { + // Treat as plain text + payload = { text: payloadRaw }; + } + const from = 'user'; + const ttl = 300000; + const msg = this.a2aPost(from, to, type, capability, payload, ttl); + if (msg) { + this.terminal.print(`📨 A2A message ${msg.id} posted to ${to} (type: ${type})`, 'success'); + } + } + a2aPollCommand(args) { + // Use explicit agent ID if provided, otherwise try to determine current agent + let agentId = (args && args[0]) || 'user'; + if (!args || !args[0]) { + try { + const orchestrator = this.terminal.moduleManager.modules['OrchestratorModule']; + if (orchestrator && orchestrator._agents) { + const myAgents = Object.entries(orchestrator._agents).filter(([, a]) => a.status === 'busy'); + if (myAgents.length > 0) agentId = myAgents[0][0]; + } + } catch (e) {} + } + const msgs = this.a2aPoll(agentId); + if (msgs.length === 0) { + this.terminal.print(`📭 No pending messages for ${agentId}`, 'info'); + return; + } + this.terminal.print(`📬 ${msgs.length} message(s) for ${agentId}:`, 'header'); + for (const msg of msgs) { + const capInfo = msg.capability ? ` [${msg.capability}]` : ''; + this.terminal.print(` ${msg.id} from ${msg.from} (${msg.type}${capInfo})`, 'info'); + this.terminal.print(` Payload: ${JSON.stringify(msg.payload).substring(0, 120)}`, 'text-muted'); + } + } + a2aAckCommand(args) { + if (args.length < 1) { + return this.terminal.print('Usage: a2a-ack <msgId> [result...]', 'warning'); + } + const msgId = args[0]; + const resultRaw = args.slice(1).join(' '); + let result = {}; + try { + result = JSON.parse(resultRaw || '{}'); + } catch (e) { + result = { text: resultRaw }; + } + if (this.a2aAck(msgId, result)) { + this.terminal.print(`✅ A2A message ${msgId} completed`, 'success'); + } + } + a2aRejectCommand(args) { + if (args.length < 1) { + return this.terminal.print('Usage: a2a-reject <msgId> [error...]', 'warning'); + } + const msgId = args[0]; + const error = args.slice(1).join(' ') || 'rejected by user'; + if (this.a2aReject(msgId, error)) { + this.terminal.print(`❌ A2A message ${msgId} rejected: ${error}`, 'warning'); + } + } _loadContracts() { try { const data = this.terminal.fs.readFile('/.contracts/index.json'); @@ -11593,18 +12141,21 @@ class AgentRegistryModule { } catch (e) { console.error('Contract save error:', e); } } registerAgent(args) { - const id = args[1] || `agent-${Date.now()}`; - const capabilities = args.slice(2).length ? args.slice(2) : ['list_files', 'read_file']; + const id = args[0] || `agent-${Date.now()}`; + const capabilities = args[1] ? args[1].split(',').map(s => s.trim()).filter(Boolean) : ['list_files', 'read_file']; const agentDir = `agents/${id}`; if (!this.terminal.fs.readDir('/' + agentDir + '/')) this.terminal.fs.mkdir(agentDir); this.terminal.fs.writeFile(`${agentDir}/status.json`, JSON.stringify({ id, status: 'idle', + model: null, capabilities, registered: new Date().toISOString(), lastHeartbeat: new Date().toISOString(), contract: null })); + // Auto-register in A2A scratchpad + this.a2aRegister(id, capabilities); this.terminal.print(`✅ Agent '${id}' registered with ${capabilities.length} capabilities`, 'success'); } registerContract(args) { @@ -11677,6 +12228,8 @@ class AgentRegistryModule { }); } unregisterAgent(id) { + // Unregister from A2A scratchpad + this.a2aUnregister(id); const ok = this.terminal.fs.deleteFile('agents/' + id); if (ok) this.terminal.print(`✅ Agent '${id}' unregistered`, 'success'); else this.terminal.print(`❌ Agent '${id}' not found`, 'error'); @@ -11764,6 +12317,19 @@ class LeaseModule { }; this._leases[lease.id] = lease; this.terminal.fs.writeFile(`leases/${lease.id}.json`, JSON.stringify(lease)); + // Post A2A message to lease system agent + try { + const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; + if (registry && typeof registry.a2aPost === 'function') { + registry.a2aPost('orchestrator-0', 'lease-system', 'lease.create', 'leases.create', { + leaseId: lease.id, + workerId, + task, + duration, + expiresAt: lease.expiresAt + }); + } + } catch (e) {} this.terminal.print(`📜 Lease ${lease.id} created for ${workerId} (${duration}s, task: ${task.substring(0, 40)})`, 'success'); return lease; } @@ -11783,6 +12349,18 @@ class LeaseModule { this._leases[id].status = 'revoked'; this._leases[id].revokedAt = new Date().toISOString(); this.terminal.fs.writeFile(`leases/${id}.json`, JSON.stringify(this._leases[id])); + // Post A2A completion message + try { + const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; + if (registry && typeof registry.a2aPost === 'function') { + registry.a2aPost('orchestrator-0', 'lease-system', 'lease.terminate', 'leases.terminate', { + leaseId: id, + status: 'revoked', + revokedAt: this._leases[id].revokedAt, + workerId: this._leases[id].workerId + }); + } + } catch (e) {} this.terminal.print(`✅ Lease ${id} revoked`, 'warning'); } renewLease(id, additionalSeconds) { @@ -11825,6 +12403,7 @@ class OrchestratorModule { this._HEARTBEAT_TIMEOUT = 15000; this._IDLE_TIMEOUT = 120000; this._RESTART_POLICY = 'never'; + this._AGENT_DEFAULT_MODEL = null; this._A2A_POLLING = true; this._A2A_POLL_INTERVAL = 500; this._HEARTBEAT_CHECK = 3000; @@ -11847,7 +12426,7 @@ class OrchestratorModule { this._loadConfig(); } _loadConfig() { - try { const c = localStorage.getItem('webx11_orchestrator_config'); if (c) { const p = JSON.parse(c); if (p.spawnLimit) this._SPAWN_LIMIT = p.spawnLimit; if (p.heartbeatTimeout) this._HEARTBEAT_TIMEOUT = p.heartbeatTimeout; if (p.idleTimeout) this._IDLE_TIMEOUT = p.idleTimeout; if (p.restartPolicy) this._RESTART_POLICY = p.restartPolicy; if (p.a2aPollInterval) this._A2A_POLL_INTERVAL = p.a2aPollInterval; if (p.heartbeatCheck) this._HEARTBEAT_CHECK = p.heartbeatCheck; if (p.idleSweepInterval) this._IDLE_SWEEP_INTERVAL = p.idleSweepInterval; if (p.deadPurgeTimeout) this._DEAD_PURGE_TIMEOUT = p.deadPurgeTimeout; if (p.consensusSize) this._CONSENSUS_SIZE = p.consensusSize; if (p.consensusStrategy) this._CONSENSUS_STRATEGY = p.consensusStrategy; if (p.consensusTimeout) this._CONSENSUS_TIMEOUT = p.consensusTimeout; if (p.pipelineAutoFix !== undefined) this._PIPELINE_CONFIG.autoFix = p.pipelineAutoFix; if (p.pipelineStageTimeout) this._PIPELINE_CONFIG.stageTimeout = p.pipelineStageTimeout; } } catch (e) {} + try { const c = localStorage.getItem('webx11_orchestrator_config'); if (c) { const p = JSON.parse(c); if (p.spawnLimit) this._SPAWN_LIMIT = p.spawnLimit; if (p.heartbeatTimeout) this._HEARTBEAT_TIMEOUT = p.heartbeatTimeout; if (p.idleTimeout) this._IDLE_TIMEOUT = p.idleTimeout; if (p.restartPolicy) this._RESTART_POLICY = p.restartPolicy; if (p.agentDefaultModel !== undefined) this._AGENT_DEFAULT_MODEL = p.agentDefaultModel; if (p.a2aPollInterval) this._A2A_POLL_INTERVAL = p.a2aPollInterval; if (p.heartbeatCheck) this._HEARTBEAT_CHECK = p.heartbeatCheck; if (p.idleSweepInterval) this._IDLE_SWEEP_INTERVAL = p.idleSweepInterval; if (p.deadPurgeTimeout) this._DEAD_PURGE_TIMEOUT = p.deadPurgeTimeout; if (p.consensusSize) this._CONSENSUS_SIZE = p.consensusSize; if (p.consensusStrategy) this._CONSENSUS_STRATEGY = p.consensusStrategy; if (p.consensusTimeout) this._CONSENSUS_TIMEOUT = p.consensusTimeout; if (p.pipelineAutoFix !== undefined) this._PIPELINE_CONFIG.autoFix = p.pipelineAutoFix; if (p.pipelineStageTimeout) this._PIPELINE_CONFIG.stageTimeout = p.pipelineStageTimeout; } } catch (e) {} } _saveConfig() { localStorage.setItem('webx11_orchestrator_config', JSON.stringify({ @@ -11855,6 +12434,7 @@ class OrchestratorModule { heartbeatTimeout: this._HEARTBEAT_TIMEOUT, idleTimeout: this._IDLE_TIMEOUT, restartPolicy: this._RESTART_POLICY, + agentDefaultModel: this._AGENT_DEFAULT_MODEL, a2aPollInterval: this._A2A_POLL_INTERVAL, heartbeatCheck: this._HEARTBEAT_CHECK, idleSweepInterval: this._IDLE_SWEEP_INTERVAL, @@ -11869,7 +12449,16 @@ class OrchestratorModule { registerCommands() { this.commands['orchestrate'] = (args) => this.delegateTask(args.join(' ')); this.commands['orchestrator-status'] = () => this.showStatus(); - this.commands['orchestrator-spawn'] = (args) => this.spawnAgent(args[0], args.slice(1)); + this.commands['orchestrator-spawn'] = (args) => { + // Parse --model flag before passing remaining as capabilities + const modelIdx = args.indexOf('--model'); + let modelName = null; + if (modelIdx !== -1 && args[modelIdx + 1]) { + modelName = args[modelIdx + 1]; + args.splice(modelIdx, 2); + } + this.spawnAgent(args[0], args.slice(1), modelName); + }; this.commands['orchestrator-kill'] = (args) => this.killAgent(args[0]); this.commands['orchestrator-purge'] = () => this.purgeDeadAgents(); this.commands['orchestrator-config'] = (args) => this.setConfig(args); @@ -12124,7 +12713,7 @@ class OrchestratorModule { _sanitizeId(name) { return (name || 'worker').replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').substring(0, 32) || 'worker'; } - spawnAgent(role, capabilities) { + spawnAgent(role, capabilities, modelName = null) { if (this._spawnLock) { this.terminal.print('⚠️ Spawn in progress — queued', 'warning'); return null; @@ -12140,6 +12729,7 @@ class OrchestratorModule { const agent = { id, role: role || 'worker', + model: modelName || this._AGENT_DEFAULT_MODEL, capabilities: capabilities.length ? capabilities : ['list_files', 'read_file', 'web_search'], createdAt: new Date().toISOString(), lastHeartbeat: Date.now(), @@ -12157,7 +12747,13 @@ class OrchestratorModule { this.terminal.fs.writeFile(`agents/${id}/status.json`, JSON.stringify(agent)); // Register in agent registry const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; - if (registry) registry.registerAgent(['register', id, ...agent.capabilities]); + if (registry) { + registry.registerAgent(['register', id, ...agent.capabilities]); + // A2A Scratchpad registration + if (typeof registry.a2aRegister === 'function') { + registry.a2aRegister(id, agent.capabilities); + } + } this.terminal.print(`🤖 Agent '${id}' spawned (role: ${role || 'worker'}, ${agent.capabilities.length} caps)`, 'success'); return agent; } finally { @@ -12176,7 +12772,13 @@ class OrchestratorModule { } // Unregister from registry const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; - if (registry) registry.unregisterAgent(id); + if (registry) { + registry.unregisterAgent(id); + // A2A Scratchpad unregister + if (typeof registry.a2aUnregister === 'function') { + registry.a2aUnregister(id); + } + } // Clean up VFS this.terminal.fs.deleteFile('agents/' + id); delete this._agents[id]; @@ -12365,7 +12967,8 @@ class OrchestratorModule { 'consensus-strategy': () => { if (!['majority', 'unanimous', 'weighted'].includes(val)) return 'Must be majority, unanimous, or weighted'; this._CONSENSUS_STRATEGY = val; return `consensus-strategy set to ${val}`; }, 'consensus-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 300000) return 'Must be 5000-300000ms'; this._CONSENSUS_TIMEOUT = n; return `consensus-timeout set to ${n}ms`; }, 'pipeline-auto-fix': () => { this._PIPELINE_CONFIG.autoFix = val === 'on' || val === 'true'; return `auto-fix ${this._PIPELINE_CONFIG.autoFix ? 'on' : 'off'}`; }, - 'pipeline-stage-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 120000) return 'Must be 5000-120000ms'; this._PIPELINE_CONFIG.stageTimeout = n; return `stage-timeout set to ${n}ms`; } + 'pipeline-stage-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 120000) return 'Must be 5000-120000ms'; this._PIPELINE_CONFIG.stageTimeout = n; return `stage-timeout set to ${n}ms`; }, + 'agent-default-model': () => { this._AGENT_DEFAULT_MODEL = val; this._saveConfig(); return `agent-default-model set to ${val}`; } }; if (!configMap[key]) { @@ -13020,11 +13623,13 @@ class OrchestratorModule { this.terminal.print(` Agents: ${total} (${busy} busy, ${idle} idle, ${sleeping} sleeping, ${failed} failed)`, 'info'); this.terminal.print(` Spawn limit: ${this._SPAWN_LIMIT} (strategy: ${this._SPAWN_STRATEGY})`, 'info'); this.terminal.print(` Default restart: ${this._RESTART_POLICY}`, 'info'); + this.terminal.print(` Default agent model: ${this._AGENT_DEFAULT_MODEL || 'none (uses main AI model)'}`, 'info'); this.terminal.print(` A2A Messages: ${a2aTotal} (${a2aPending} pending)`, 'info'); Object.values(this._agents).forEach(a => { const task = a.currentTask ? ` (task: ${a.currentTask.substring(0, 40)})` : ''; + const modelInfo = a.model ? ` [model: ${a.model}]` : ''; const backoff = a.nextRestartAt && a.nextRestartAt > Date.now() ? ` [restart in ${Math.round((a.nextRestartAt - Date.now()) / 1000)}s]` : ''; - this.terminal.print(` ${a.id} [${a.role}] — ${a.status}${task}${backoff}`, 'info'); + this.terminal.print(` ${a.id} [${a.role}] — ${a.status}${modelInfo}${task}${backoff}`, 'info'); }); } @@ -14206,7 +14811,22 @@ class WorldModelModule { _deriveVersion(mInfo) { // Single source of truth: the constant set at bootstrap if (window.NEWOS4_VERSION) return window.NEWOS4_VERSION; - // No fallback version string — if the constant is missing, report unknown + // Fallback: try to extract from script tag ?v= query param or LATEST_VER constant + try { + const scripts = document.querySelectorAll('script[src*="module-loader"]'); + for (const s of scripts) { + const match = s.src.match(/[?&]v=([^&]+)/); + if (match) return match[1]; + } + // Try to find LATEST_VER in any inline script + const allScripts = document.querySelectorAll('script:not([src])'); + for (const s of allScripts) { + const match = s.textContent.match(/LATEST_VER\s*=\s*['"]([^'"]+)['"]/); + if (match) return match[1]; + } + } catch (e) { + console.warn('[WorldModel] Version extraction error:', e.message); + } return 'v?'; } @@ -14216,14 +14836,20 @@ class WorldModelModule { if (!mm) return 0; // Count registry entries (all known modules) vs loaded const registry = mm.moduleRegistry || []; - return registry.length || Object.keys(mm.modules || {}).length + 5; // +5 for expected unloaded + return registry.length || Object.keys(mm.modules || {}).length; } catch (e) { + console.warn('[WorldModel] Total modules gather error:', e.message); return Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0; } } _gatherSystemInfo(fs, moduleCount, version) { - const healthData = fs.fsReadJSON('/system/metrics/health.json'); + // Get real health data from ServerMonitorModule's _latencyLog and AI assistant's _errorLog + const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule']; + const ai = this.terminal?.moduleManager?.modules?.['WebX11AIAssistantModule']; + const errorCount = ai?._errorLog?.length || 0; + const latencyErrors = sm?._latencyLog ? Object.values(sm._latencyLog).filter(e => e.status === 'error').length : 0; + const totalErrors = errorCount + latencyErrors; return { version: `newOS4 ${version}`, modules: moduleCount || Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0, @@ -14232,8 +14858,8 @@ class WorldModelModule { memory: typeof performance !== 'undefined' && performance.memory ? `${(performance.memory.usedJSHeapSize / 1048576).toFixed(1)}MB` : 'N/A', - health: healthData?.status || 'OK', - lastHealthCheck: healthData?.timestamp || null + health: totalErrors > 0 ? `⚠️ ${totalErrors} errors` : 'OK', + lastHealthCheck: new Date().toISOString() }; } @@ -14285,6 +14911,7 @@ class WorldModelModule { const count = idx ? Object.keys(idx).length : 0; return { count, skills: idx ? Object.keys(idx) : [] }; } catch (e) { + console.warn('[WorldModel] Skill info gather error:', e.message); return { count: 0, skills: [] }; } } @@ -14294,20 +14921,21 @@ class WorldModelModule { if (mg && mg._nodes) { const nodes = Object.keys(mg._nodes).length; const edges = (mg._edges || []).length; - return { nodes, edges, active: true }; + return { nodes, edges, active: true, fallback: false }; } // Fallback: count memory files try { const memIndex = this.terminal.fs?.fsReadJSON('/memory/index.json'); - return { nodes: memIndex ? Object.keys(memIndex).length : 0, edges: 0, active: false }; + return { nodes: memIndex ? Object.keys(memIndex).length : 0, edges: 0, active: false, fallback: true }; } catch (e) { - return { nodes: 0, edges: 0, active: false }; + console.warn('[WorldModel] Memory info gather error:', e.message); + return { nodes: 0, edges: 0, active: false, fallback: true }; } } _gatherAgentInfo() { const orch = this.terminal?.moduleManager?.modules?.['OrchestratorModule']; - if (!orch || !orch._agents) return { agents: 0, active: 0 }; + if (!orch || !orch._agents) return { agents: -1, active: -1, error: 'Orchestrator not loaded' }; const agents = Object.keys(orch._agents).length; const active = Object.values(orch._agents).filter(a => a.status === 'busy').length; return { agents, active, totalLeases: Object.keys(orch._a2aMessages || {}).length }; @@ -14315,13 +14943,17 @@ class WorldModelModule { _gatherTelemetry(fs) { try { - const latency = fs.fsReadJSON('/system/metrics/tool-latency.json'); - const errors = fs.fsReadJSON('/system/metrics/error-log.json'); + // Get real latency data from ServerMonitorModule and errors from AI assistant + const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule']; + const ai = this.terminal?.moduleManager?.modules?.['WebX11AIAssistantModule']; + const latencyDataPoints = sm?._latencyLog ? Object.keys(sm._latencyLog).length : 0; + const recentErrors = ai?._errorLog?.length || 0; return { - toolLatencyDataPoints: latency ? Object.keys(latency).length : 0, - recentErrors: errors ? (Array.isArray(errors) ? errors.length : Object.keys(errors).length) : 0, + toolLatencyDataPoints: latencyDataPoints, + recentErrors: recentErrors, }; } catch (e) { + console.warn('[WorldModel] Telemetry gather error:', e.message); return { toolLatencyDataPoints: 0, recentErrors: 0 }; } } @@ -14334,19 +14966,20 @@ class WorldModelModule { const tasks = Object.values(mg._nodes).filter(n => n.type === 'task'); const recentFiles = Object.values(mg._nodes) .filter(n => n.type === 'file') - .sort((a, b) => b.access_count - a.access_count) + .sort((a, b) => (b.access_count || 0) - (a.access_count || 0)) .slice(0, this._MAX_PROJECTS); return { totalTasks: tasks.length, activeProjects: recentFiles.slice(0, 5).map(f => ({ name: f.label, - accesses: f.access_count, + accesses: f.access_count || 0, lastModified: f.updated })) }; } return { totalTasks: 0, activeProjects: [] }; } catch (e) { + console.warn('[WorldModel] Projects gather error:', e.message); return { totalTasks: 0, activeProjects: [] }; } } @@ -14368,7 +15001,25 @@ class WorldModelModule { if (skills && skills.length > 0) { recent.push(`Skills directory: ${skills.filter(s => s.type === 'file').length} files`); } - } catch (e) {} + // Check memory directory for recent changes + const memDir = fs.readDir('/memory/'); + if (memDir && memDir.length > 0) { + const memFiles = memDir.filter(s => s.type === 'file'); + if (memFiles.length > 0) { + recent.push(`Memory: ${memFiles.length} files in /memory/`); + } + } + // Check system config changes + const sysDir = fs.readDir('/system/'); + if (sysDir && sysDir.length > 0) { + const configFiles = sysDir.filter(s => s.type === 'file' && s.name.endsWith('.json')); + if (configFiles.length > 0) { + recent.push(`System config: ${configFiles.length} config files`); + } + } + } catch (e) { + console.warn('[WorldModel] Recent gather error:', e.message); + } return recent; } @@ -14490,7 +15141,10 @@ class WorldModelModule { getContextForPrompt() { if (!this._model) return ''; const m = this._model; - return `[World Model Context]\\nSystem: newOS4 ${m.version} · ${m.modules.loaded} modules · ${m.skills.count} skills · ${m.files.total} files · ${m.agents.agents} agents${m.telemetry.recentErrors > 0 ? ` · ⚠️ ${m.telemetry.recentErrors} errors` : ' · ✅ OK'}${m.modules.modifiedCount > 0 ? `\\nModified modules: ${m.modules.modified.slice(0, 3).join(', ')}${m.modules.modifiedCount > 3 ? ` (+${m.modules.modifiedCount - 3} more)` : ''}` : ''}`; + const agentStr = m.agents.error + ? 'agents: N/A' + : `${m.agents.agents} agents`; + return `[World Model Context]\\nSystem: newOS4 ${m.version} · ${m.modules.loaded} modules · ${m.skills.count} skills · ${m.files.total} files · ${agentStr}${m.telemetry.recentErrors > 0 ? ` · ⚠️ ${m.telemetry.recentErrors} errors` : ' · ✅ OK'}${m.modules.modifiedCount > 0 ? `\\nModified modules: ${m.modules.modified.slice(0, 3).join(', ')}${m.modules.modifiedCount > 3 ? ` (+${m.modules.modifiedCount - 3} more)` : ''}` : ''}`; } // ─── Persistence ─── @@ -15052,7 +15706,7 @@ class NeuralVFSModule { _isSystemFile(path) { const normalized = path.startsWith('/') ? path : '/' + path; - const sysPrefixes = ['/.trash', '/.semantic_index', '/.memory_graph', '/_snapshots', '/_sessions', '/logs', '/memory', '/system', '/sys', '/_mods', '/backups']; + const sysPrefixes = ['/.trash', '/.semantic_index', '/.memory_graph', '/_snapshots', '/_sessions', '/logs', '/memory', '/system', '/sys', '/_mods', '/backups', '/agents', '/_agents', '/leases']; if (sysPrefixes.some(p => normalized.startsWith(p))) return true; if (normalized.includes('/node_modules/')) return true; return false; @@ -15577,3 +16231,5 @@ Concepts:`; if (typeof window !== 'undefined') { window.NeuralVFSModule = NeuralVFSModule; } + +/* === end core module bundle === */ diff --git a/modules/core/module-agent-registry.js b/modules/core/module-agent-registry.js index 2439ab8..68dca66 100644 --- a/modules/core/module-agent-registry.js +++ b/modules/core/module-agent-registry.js @@ -1,10 +1,12 @@ // module-agent-registry.js — VFS-backed agent directory with A2A contracts // Commands: agent-list, agent-register, agent-unregister, agent-status, contract-register, contract-list +// A2A Scratchpad commands: a2a-post, a2a-poll, a2a-ack, a2a-reject, a2a-status, a2a-prune class AgentRegistryModule { constructor() { this.commands = {}; this.terminal = null; this._contracts = {}; + this._a2aPruneTimer = null; } registerCommands() { this.commands['agent-list'] = () => this.listAgents(); @@ -13,14 +15,321 @@ class AgentRegistryModule { this.commands['agent-unregister'] = (args) => this.unregisterAgent(args[0]); this.commands['contract-register'] = (args) => this.registerContract(args); this.commands['contract-list'] = () => this.listContracts(); + // A2A Scratchpad commands + this.commands['a2a-post'] = (args) => this.a2aPostCommand(args); + this.commands['a2a-poll'] = (args) => this.a2aPollCommand(args); + this.commands['a2a-ack'] = (args) => this.a2aAckCommand(args); + this.commands['a2a-reject'] = (args) => this.a2aRejectCommand(args); + this.commands['a2a-status'] = () => this.a2aStatus(); + this.commands['a2a-prune'] = () => this.a2aPrune(); + } + getHelpInfo() { + return { + 'a2a-post': 'a2a-post <to> <type> <capability> <payload...> — Post an A2A message', + 'a2a-poll': 'a2a-poll [agentId] — Poll for messages addressed to an agent (default: current or "user")', + 'a2a-ack': 'a2a-ack <msgId> <result...> — Acknowledge a message with result', + 'a2a-reject': 'a2a-reject <msgId> <error...> — Reject a message with error', + 'a2a-status': 'a2a-status — Show scratchpad stats per agent', + 'a2a-prune': 'a2a-prune — Clean up expired and old completed messages' + }; } init(terminal) { this.terminal = terminal; if (!this.terminal.fs.readDir('/agents/')) this.terminal.fs.mkdir('agents'); if (!this.terminal.fs.readDir('/.contracts/')) this.terminal.fs.mkdir('.contracts'); this._loadContracts(); + // Initialize scratchpad + this._ensureScratchpad(); + // Auto-prune every 60 seconds + this._a2aPruneTimer = setInterval(() => this.a2aPrune(), 60000); + // Save the spec-vs-reality delta table + this._saveDeltaTable(); this.terminal.print('📋 Agent Registry loaded', 'info'); } + // ─── Task 1: Save spec-vs-reality delta table ─── + _saveDeltaTable() { + try { + const fs = this.terminal.fs; + if (!fs.readDir('/_docs/')) fs.mkdir('_docs'); + const delta = `# Spec-vs-Reality Delta — 2026-06-17\n\n` + +`| # | Whitepaper Concept | Spec'd Behavior | Current Reality | Δ | Priority |\n` + +`|---|---|---|---|---|---|\n` + +`| 1 | Hyper-Modular Kernel | Real-time self-rewriting drivers for task-specific optimization via WPM | WPM exists but is manual; no hot-patch daemon or runtime driver swap | Partial | Medium |\n` + +`| 2 | A2A Protocol | Standardized neural communication layer for agent negotiation; typed messages, no central broker | VFS mailbox transport works (a2a-send/a2a-list/500ms polling) but orchestrator acts as central broker | Partial | High |\n` + +`| 3 | Neural VFS | Semantic-based file storage (concept:// URIs) as primary access method; hierarchical paths are secondary | concept:// URIs work (nvfs-index/nvfs-resolve) but hierarchical paths remain primary access; concept resolution requires explicit index | Partial | High |\n` + +`| 4 | Recursive Self-Optimization | Monitor → Profile → Diagnose → Patch → Test → Promote feedback loop | system-health telemetry captures latency/VFS stats but no auto-patch generation or hot-promotion pipeline | Partial | Medium |\n` + +`| 5 | Agent Discovery | No central broker — agents discover each other via VFS registrations spontaneously | Orchestrator is central broker; all task delegation goes through it; agents don't autonomously discover each other | Critical Gap | Critical |\n` + +`| 6 | Contract Schema | Every A2A message carries a contract schema defining capabilities and constraints | Contracts exist (contract-register/contract-list) but schema validation is basic substring matching; not enforced on every message | Partial | Medium |\n` + +`| 7 | Semantic Addressing | Neural VFS as primary storage paradigm — users navigate by concept, not path | Hierarchical path access still primary; concept:// requires explicit nvfs-index; no transparent concept-to-path fallback | Critical Gap | Critical |\n` + +`| 8 | Warm-start Guarantee | Kernel caches last N stable driver configs; boot in under 200ms | No golden config cache; full localStorage restore on every page load | Spec Violation | High |\n` + +`| 9 | Self-rewrite Daemon | Monitors performance telemetry and generates optimized driver variants at runtime | No self-rewrite daemon exists; all module changes are manual | Missing | Low |\n` + +`| 10 | Agent Lifecycle | spawn, sleep, wake, kill, restart policies with exponential backoff | Fully implemented in orchestrator with heartbeat, idle sweep, restart policies, VFS sleep serialization | Fully Aligned | — |\n\n` + +`**Summary: 2 Critical gaps, 5 Partial, 1 Spec Violation, 1 Missing, 1 Fully Aligned**\n`; + fs.writeFile('_docs/spec-vs-reality-delta.md', delta); + this.terminal.print('📄 Spec-vs-Reality delta saved to /_docs/spec-vs-reality-delta.md', 'info'); + } catch (e) { + console.warn('Failed to save delta table:', e.message); + } + } + // ─── A2A Scratchpad Helpers ─── + _ensureScratchpad() { + try { + const data = this.terminal.fs.fsReadJSON('/_agents/scratchpad.json'); + if (!data) throw new Error('no scratchpad'); + } catch (e) { + this.terminal.fs.mkdir('_agents'); + this.terminal.fs.fsWriteJSON('/_agents/scratchpad.json', { + messages: [], + seq: 0, + agents: {} + }); + } + } + _getScratchpad() { + try { + const data = this.terminal.fs.fsReadJSON('/_agents/scratchpad.json'); + if (data && data.messages) return data; + } catch (e) {} + return { messages: [], seq: 0, agents: {} }; + } + _saveScratchpad(sp) { + try { + this.terminal.fs.mkdir('_agents'); + this.terminal.fs.fsWriteJSON('/_agents/scratchpad.json', sp); + } catch (e) { + console.warn('Scratchpad save error:', e.message); + } + } + // ─── A2A Scratchpad Functions ─── + a2aPost(from, to, type, capability, payload, ttl) { + if (!from || !to || !type) { + this.terminal.print('a2aPost: from, to, and type are required', 'warning'); + return null; + } + const sp = this._getScratchpad(); + sp.seq = (sp.seq || 0) + 1; + const msg = { + id: `msg_${String(sp.seq).padStart(3, '0')}`, + from: from, + to: to, + type: type, + capability: capability || null, + payload: payload || {}, + ttl: (typeof ttl === 'number' ? ttl : 300000), + status: 'pending', + ts: Date.now(), + reply_to: null + }; + sp.messages.push(msg); + this._saveScratchpad(sp); + return msg; + } + a2aPoll(agentId) { + if (!agentId) return []; + const sp = this._getScratchpad(); + const pending = []; + for (const msg of sp.messages) { + if (msg.to === agentId && msg.status === 'pending') { + msg.status = 'delivered'; + pending.push(msg); + } + } + if (pending.length > 0) this._saveScratchpad(sp); + return pending; + } + a2aAck(messageId, resultPayload) { + const sp = this._getScratchpad(); + const msg = sp.messages.find(m => m.id === messageId); + if (!msg) { + this.terminal.print(`a2aAck: message ${messageId} not found`, 'warning'); + return false; + } + if (msg.status === 'completed' || msg.status === 'failed') { + this.terminal.print(`a2aAck: message ${messageId} already ${msg.status}`, 'warning'); + return false; + } + msg.status = 'completed'; + msg.result = resultPayload || {}; + msg.completedAt = Date.now(); + this._saveScratchpad(sp); + return true; + } + a2aReject(messageId, error) { + const sp = this._getScratchpad(); + const msg = sp.messages.find(m => m.id === messageId); + if (!msg) { + this.terminal.print(`a2aReject: message ${messageId} not found`, 'warning'); + return false; + } + if (msg.status === 'completed' || msg.status === 'failed') { + this.terminal.print(`a2aReject: message ${messageId} already ${msg.status}`, 'warning'); + return false; + } + msg.status = 'failed'; + msg.error = error || 'unspecified error'; + msg.completedAt = Date.now(); + this._saveScratchpad(sp); + return true; + } + a2aRegister(agentId, capabilities) { + if (!agentId) return; + const sp = this._getScratchpad(); + sp.agents[agentId] = { + last_poll: Date.now(), + capabilities: capabilities || [] + }; + this._saveScratchpad(sp); + } + a2aUnregister(agentId) { + if (!agentId) return; + const sp = this._getScratchpad(); + delete sp.agents[agentId]; + this._saveScratchpad(sp); + } + a2aPrune() { + try { + const sp = this._getScratchpad(); + const now = Date.now(); + const before = sp.messages.length; + sp.messages = sp.messages.filter(m => { + // Remove expired messages (past TTL) + if (m.status === 'pending' && (now - m.ts > m.ttl)) return false; + // Remove completed/failed messages older than 1 hour + if ((m.status === 'completed' || m.status === 'failed') && m.completedAt && (now - m.completedAt > 3600000)) return false; + return true; + }); + const removed = before - sp.messages.length; + if (removed > 0) this._saveScratchpad(sp); + return removed; + } catch (e) { + console.warn('a2aPrune error:', e.message); + return 0; + } + } + a2aStatus() { + const sp = this._getScratchpad(); + const counts = {}; + for (const msg of sp.messages) { + if (!counts[msg.to]) counts[msg.to] = { pending: 0, delivered: 0, completed: 0, failed: 0 }; + if (msg.status === 'pending') counts[msg.to].pending++; + else if (msg.status === 'delivered') counts[msg.to].delivered++; + else if (msg.status === 'completed') counts[msg.to].completed++; + else if (msg.status === 'failed') counts[msg.to].failed++; + } + this.terminal.print('📊 A2A Scratchpad Status:', 'header'); + this.terminal.print(` Total messages: ${sp.messages.length}`, 'info'); + this.terminal.print(` Registered agents: ${Object.keys(sp.agents).length}`, 'info'); + this.terminal.print(` Sequence: ${sp.seq}`, 'info'); + const agentNames = Object.keys(counts); + if (agentNames.length === 0) { + this.terminal.print(' No messages in scratchpad', 'info'); + return; + } + for (const agentId of agentNames) { + const c = counts[agentId]; + const total = c.pending + c.delivered + c.completed + c.failed; + const caps = sp.agents[agentId] ? ` [${sp.agents[agentId].capabilities.join(', ')}]` : ''; + this.terminal.print(` ${agentId}${caps}: ${total} msgs (⏳${c.pending} 📬${c.delivered} ✅${c.completed} ❌${c.failed})`, 'info'); + } + } + _a2aCleanup() { + // Remove agent entries from scratchpad when agent directory is deleted from /agents/ + try { + const sp = this._getScratchpad(); + const agentDirs = {}; + const dir = this.terminal.fs.readDir('/agents/'); + if (dir) { + for (const entry of dir) { + if (entry.type === 'dir') agentDirs[entry.name] = true; + } + } + let changed = false; + for (const agentId of Object.keys(sp.agents)) { + if (!agentDirs[agentId]) { + delete sp.agents[agentId]; + changed = true; + } + } + if (changed) this._saveScratchpad(sp); + } catch (e) { + console.warn('a2aCleanup error:', e.message); + } + } + // ─── A2A Command Handlers ─── + a2aPostCommand(args) { + // a2a-post <to> <type> <capability> <payload...> + if (args.length < 3) { + return this.terminal.print('Usage: a2a-post <to> <type> <capability> <payload...>', 'warning'); + } + const to = args[0]; + const type = args[1]; + const capability = args[2]; + const payloadRaw = args.slice(3).join(' '); + let payload = {}; + try { + // Try parsing as JSON + payload = JSON.parse(payloadRaw || '{}'); + } catch (e) { + // Treat as plain text + payload = { text: payloadRaw }; + } + const from = 'user'; + const ttl = 300000; + const msg = this.a2aPost(from, to, type, capability, payload, ttl); + if (msg) { + this.terminal.print(`📨 A2A message ${msg.id} posted to ${to} (type: ${type})`, 'success'); + } + } + a2aPollCommand(args) { + // Use explicit agent ID if provided, otherwise try to determine current agent + let agentId = (args && args[0]) || 'user'; + if (!args || !args[0]) { + try { + const orchestrator = this.terminal.moduleManager.modules['OrchestratorModule']; + if (orchestrator && orchestrator._agents) { + const myAgents = Object.entries(orchestrator._agents).filter(([, a]) => a.status === 'busy'); + if (myAgents.length > 0) agentId = myAgents[0][0]; + } + } catch (e) {} + } + const msgs = this.a2aPoll(agentId); + if (msgs.length === 0) { + this.terminal.print(`📭 No pending messages for ${agentId}`, 'info'); + return; + } + this.terminal.print(`📬 ${msgs.length} message(s) for ${agentId}:`, 'header'); + for (const msg of msgs) { + const capInfo = msg.capability ? ` [${msg.capability}]` : ''; + this.terminal.print(` ${msg.id} from ${msg.from} (${msg.type}${capInfo})`, 'info'); + this.terminal.print(` Payload: ${JSON.stringify(msg.payload).substring(0, 120)}`, 'text-muted'); + } + } + a2aAckCommand(args) { + if (args.length < 1) { + return this.terminal.print('Usage: a2a-ack <msgId> [result...]', 'warning'); + } + const msgId = args[0]; + const resultRaw = args.slice(1).join(' '); + let result = {}; + try { + result = JSON.parse(resultRaw || '{}'); + } catch (e) { + result = { text: resultRaw }; + } + if (this.a2aAck(msgId, result)) { + this.terminal.print(`✅ A2A message ${msgId} completed`, 'success'); + } + } + a2aRejectCommand(args) { + if (args.length < 1) { + return this.terminal.print('Usage: a2a-reject <msgId> [error...]', 'warning'); + } + const msgId = args[0]; + const error = args.slice(1).join(' ') || 'rejected by user'; + if (this.a2aReject(msgId, error)) { + this.terminal.print(`❌ A2A message ${msgId} rejected: ${error}`, 'warning'); + } + } _loadContracts() { try { const data = this.terminal.fs.readFile('/.contracts/index.json'); @@ -45,18 +354,21 @@ class AgentRegistryModule { } catch (e) { console.error('Contract save error:', e); } } registerAgent(args) { - const id = args[1] || `agent-${Date.now()}`; - const capabilities = args.slice(2).length ? args.slice(2) : ['list_files', 'read_file']; + const id = args[0] || `agent-${Date.now()}`; + const capabilities = args[1] ? args[1].split(',').map(s => s.trim()).filter(Boolean) : ['list_files', 'read_file']; const agentDir = `agents/${id}`; if (!this.terminal.fs.readDir('/' + agentDir + '/')) this.terminal.fs.mkdir(agentDir); this.terminal.fs.writeFile(`${agentDir}/status.json`, JSON.stringify({ id, status: 'idle', + model: null, capabilities, registered: new Date().toISOString(), lastHeartbeat: new Date().toISOString(), contract: null })); + // Auto-register in A2A scratchpad + this.a2aRegister(id, capabilities); this.terminal.print(`✅ Agent '${id}' registered with ${capabilities.length} capabilities`, 'success'); } registerContract(args) { @@ -129,6 +441,8 @@ class AgentRegistryModule { }); } unregisterAgent(id) { + // Unregister from A2A scratchpad + this.a2aUnregister(id); const ok = this.terminal.fs.deleteFile('agents/' + id); if (ok) this.terminal.print(`✅ Agent '${id}' unregistered`, 'success'); else this.terminal.print(`❌ Agent '${id}' not found`, 'error'); @@ -156,4 +470,4 @@ class AgentRegistryModule { this.terminal.fs.writeFile(`agents/${id}/status.json`, JSON.stringify(agent)); return true; } -}
\ No newline at end of file +} diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index c5a91c9..3cf8b65 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -57,6 +57,14 @@ buildSystemPrompt() { return `Current date: ${dateStr}\nCurrent time: ${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 nvfs = this.terminal?.moduleManager?.modules?.NeuralVFSModule; @@ -67,11 +75,15 @@ buildSystemPrompt() { if (ageMs > 14400000) nvfsNote = ' |nvfs ' + Math.floor(ageMs/3600000) + 'h stale'; } } catch (e) {} - return 'Current date: ' + dateStr + nvfsNote + '\n' + let base = 'Current date: ' + dateStr + nvfsNote + '\n' + 'VFS→Modules→AI. nvfs-index for commands. Tool→Observe→Answer. nvfs-resolve. Destructive cmds need confirm.\n' - + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind: vaultmind_query(ep,msg,agent) — Hyperliquid prices/portfolio/chat\n' + + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind: vaultmind_connect(network) — connect wallet, vaultmind_status — check session, vaultmind_query(ep,msg,agent) — Hyperliquid prices/portfolio/chat | wallet_lookup(address) — read-only portfolio for any wallet\n' + 'web_fetch tool: GET for normal pages (returns title+content+word_count), POST for JSON APIs like Hyperliquid /info (pass method="POST" and body as JSON string).\n' + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, guardrail, wget, tar, secret-* — discoverable via help'; + if (profilePrompt) { + base = profilePrompt + '\n\n' + base; + } + return base; } getToolDefinitions() { if (!this._toolDefs) this._toolDefs = this._buildToolDefs(); @@ -440,6 +452,43 @@ buildSystemPrompt() { 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'] + } + } }]; } getHelpInfo() { @@ -456,7 +505,9 @@ buildSystemPrompt() { '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))', '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' + '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', }; } registerCommands() { @@ -481,6 +532,8 @@ buildSystemPrompt() { 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(); } async chat(args) { const userMessage = args.join(' '); @@ -1846,6 +1899,8 @@ buildSystemPrompt() { } scm.activeProfile = name; const profile = scm.contextProfiles[name]; + // Rebuild system prompt to reflect new context profile + this.systemPrompt = this.buildSystemPrompt(); return JSON.stringify({ success: true, name, displayName: profile.name, systemPrompt: profile.systemPrompt, temperature: profile.temperature }); } case 'create_mindmap': { @@ -1876,6 +1931,53 @@ buildSystemPrompt() { 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 <pin>' }); + 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']; @@ -1883,11 +1985,20 @@ buildSystemPrompt() { return JSON.stringify({ error: 'Invalid endpoint. Valid: ' + validEps.join(', ') }); } try { - let url = '/vmai/' + ep; + const epMap = { prices: '/vmai/market/prices', portfolio: '/vmai/portfolio', transactions: '/vmai/transactions/summary', 'copy-trade': '/vmai/copy-trading/traders', chat: '/vmai/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' @@ -1900,6 +2011,17 @@ buildSystemPrompt() { 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 }); + } + } default: return JSON.stringify({ error: `Unknown tool: ${name}` }); } @@ -2226,7 +2348,7 @@ buildSystemPrompt() { } } _getAiConfigKeys() { - return ['max-turns', 'max-context', 'max-context-tokens', 'confirm-timeout', 'chat-lock-timeout', 'skill-debounce', 'trivial-optimization']; + 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) { @@ -2238,8 +2360,15 @@ buildSystemPrompt() { this.terminal.print(`confirm-timeout: ${this._confirmTimeoutMs > 0 ? this._confirmTimeoutMs / 1000 + 's' : 'disabled (no auto-deny)'} (default: 120s)`, ''); this.terminal.print(`chat-lock-timeout: ${this._chatLockTimeoutMs / 1000}s (default: 10s)`, ''); this.terminal.print(`skill-debounce: ${this._skillDebounceMs / 60000}min (default: 30min)`, ''); + 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 <key> <value>', 'info'); - this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout(0=disabled), chat-lock-timeout, skill-debounce, trivial-optimization', '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]; @@ -2272,8 +2401,28 @@ buildSystemPrompt() { 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`, 'warning'); + return this.terminal.print(`Unknown key: ${key}. Valid: max-turns, max-context, max-context-tokens, confirm-timeout, chat-lock-timeout, skill-debounce, trivial-optimization, temperature, top-p, max-tokens, thinking`, 'warning'); } // Persist localStorage.setItem('webx11_ai_config', JSON.stringify({ @@ -2431,4 +2580,37 @@ buildSystemPrompt() { 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'); + } + } } diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js index 309010d..ab8fd99 100644 --- a/modules/core/module-fs.js +++ b/modules/core/module-fs.js @@ -403,24 +403,18 @@ class FileSystemModule { this.commands['session-save'] = (args) => { if (!args[0]) return this.terminal.print('Usage: session-save <name>', 'warning'); const name = args[0].replace(/[^a-zA-Z0-9_-]/g, '_'); - // Strip system dirs to avoid recursive bloat in snapshots - const fsRoot = JSON.parse(JSON.stringify(this.terminal.fs.root)); - if (fsRoot && fsRoot.content) { - delete fsRoot.content['_sessions']; - delete fsRoot.content['_snapshots']; - delete fsRoot.content['.trash']; - } - const snapshot = { + // v2: metadata-only — VFS is persisted independently in localStorage + const manifest = { type: 'session', + version: 2, savedAt: new Date().toISOString(), - fsRoot: fsRoot, currentPath: this.terminal.fs.currentPath, - version: 1 + terminalLines: this.terminal._historyBuffer ? this.terminal._historyBuffer.slice(-50) : [] }; const filePath = '_sessions/' + name + '.json'; this.terminal.fs.mkdirRecursive('_sessions'); - this.terminal.fs.writeFile(filePath, JSON.stringify(snapshot, null, 2)); - this.terminal.print('Session saved: ' + name, 'success'); + this.terminal.fs.writeFile(filePath, JSON.stringify(manifest)); + this.terminal.print('Session saved: ' + name + ' (' + (manifest.terminalLines.length || 0) + ' history lines)', 'success'); }; this.commands['session-load'] = (args) => { if (!args[0]) return this.terminal.print('Usage: session-load <name>', 'warning'); @@ -429,11 +423,28 @@ class FileSystemModule { if (content === null) return this.terminal.print('Session not found: ' + args[0], 'error'); try { const session = JSON.parse(content); - if (!session || session.type !== 'session' || !session.fsRoot) { + if (!session || session.type !== 'session') { return this.terminal.print('Invalid session file: ' + args[0], 'error'); } - this.terminal.fs.root = session.fsRoot; - this.terminal.fs.currentPath = session.currentPath || []; + if (session.version === 2) { + // v2: metadata-only restore — keep existing VFS, just restore path + terminal history + this.terminal.fs.currentPath = session.currentPath || []; + if (session.terminalLines && Array.isArray(session.terminalLines) && this.terminal._historyBuffer) { + this.terminal._historyBuffer = session.terminalLines.slice(-50); + // Re-render last few lines for context + this.terminal.outputContainer.innerHTML = ''; + session.terminalLines.forEach(line => { + const text = typeof line === 'string' ? line : (line.l || ''); + const type = typeof line === 'object' ? (line.t || '') : ''; + this.terminal.print(text, type); + }); + } + } else { + // v1: full VFS restore (legacy) + if (!session.fsRoot) return this.terminal.print('Invalid session file', 'error'); + this.terminal.fs.root = session.fsRoot; + this.terminal.fs.currentPath = session.currentPath || []; + } this.terminal.fs.saveToStorage(); this.terminal.updatePrompt(); this.terminal.print('Session loaded: ' + args[0] + ' (' + new Date(session.savedAt).toLocaleString() + ')', 'success'); @@ -453,8 +464,13 @@ class FileSystemModule { const content = JSON.parse(this.terminal.fs.readFile('_sessions/' + f.name)); const label = f.name.replace('.json', ''); const date = content.savedAt ? new Date(content.savedAt).toLocaleString() : 'unknown'; - const fileCount = content.fsRoot ? Object.keys(content.fsRoot.content || {}).length : 0; - this.terminal.print(' ' + label + ' (' + fileCount + ' items, ' + date + ')'); + if (content.version === 2) { + const lines = content.terminalLines ? content.terminalLines.length : 0; + this.terminal.print(' ' + label + ' (' + lines + ' history lines, ' + date + ')'); + } else { + const fileCount = content.fsRoot ? Object.keys(content.fsRoot.content || {}).length : 0; + this.terminal.print(' ' + label + ' (' + fileCount + ' items, ' + date + ')'); + } } catch (e) { this.terminal.print(' ' + f.name.replace('.json', '') + ' (corrupt)'); } @@ -497,12 +513,30 @@ class FileSystemModule { reader.onload = (evt) => { try { const session = JSON.parse(evt.target.result); - if (!session || session.type !== 'session' || !session.fsRoot) { + if (!session || session.type !== 'session') { this.terminal.print('Invalid session file (not a valid session export)', 'error'); return; } - this.terminal.fs.root = session.fsRoot; - this.terminal.fs.currentPath = session.currentPath || []; + if (session.version === 2) { + // v2: metadata-only — keep existing VFS, restore path + this.terminal.fs.currentPath = session.currentPath || []; + if (session.terminalLines && Array.isArray(session.terminalLines) && this.terminal._historyBuffer) { + this.terminal._historyBuffer = session.terminalLines.slice(-50); + this.terminal.outputContainer.innerHTML = ''; + session.terminalLines.forEach(line => { + const text = typeof line === 'string' ? line : (line.l || ''); + const type = typeof line === 'object' ? (line.t || '') : ''; + this.terminal.print(text, type); + }); + } + } else { + if (!session.fsRoot) { + this.terminal.print('Invalid session file (not a valid session export)', 'error'); + return; + } + this.terminal.fs.root = session.fsRoot; + this.terminal.fs.currentPath = session.currentPath || []; + } this.terminal.fs.saveToStorage(); this.terminal.updatePrompt(); this.terminal.print('Session imported: ' + file.name + ' (' + (session.savedAt ? new Date(session.savedAt).toLocaleString() : 'unknown date') + ')', 'success'); diff --git a/modules/core/module-leases.js b/modules/core/module-leases.js index b79870c..7489c92 100644 --- a/modules/core/module-leases.js +++ b/modules/core/module-leases.js @@ -55,6 +55,19 @@ class LeaseModule { }; this._leases[lease.id] = lease; this.terminal.fs.writeFile(`leases/${lease.id}.json`, JSON.stringify(lease)); + // Post A2A message to lease system agent + try { + const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; + if (registry && typeof registry.a2aPost === 'function') { + registry.a2aPost('orchestrator-0', 'lease-system', 'lease.create', 'leases.create', { + leaseId: lease.id, + workerId, + task, + duration, + expiresAt: lease.expiresAt + }); + } + } catch (e) {} this.terminal.print(`📜 Lease ${lease.id} created for ${workerId} (${duration}s, task: ${task.substring(0, 40)})`, 'success'); return lease; } @@ -74,6 +87,18 @@ class LeaseModule { this._leases[id].status = 'revoked'; this._leases[id].revokedAt = new Date().toISOString(); this.terminal.fs.writeFile(`leases/${id}.json`, JSON.stringify(this._leases[id])); + // Post A2A completion message + try { + const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; + if (registry && typeof registry.a2aPost === 'function') { + registry.a2aPost('orchestrator-0', 'lease-system', 'lease.terminate', 'leases.terminate', { + leaseId: id, + status: 'revoked', + revokedAt: this._leases[id].revokedAt, + workerId: this._leases[id].workerId + }); + } + } catch (e) {} this.terminal.print(`✅ Lease ${id} revoked`, 'warning'); } renewLease(id, additionalSeconds) { diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index bd02873..cfd8a68 100644 --- a/modules/core/module-loader.js +++ b/modules/core/module-loader.js @@ -4,7 +4,7 @@ // Core module bundle loaded synchronously in main.html. // This loader only handles version cache-busting, splash, terminal init, and optional ext modules. - const LATEST_VER = '2026-06-16-v4.5.61'; + const LATEST_VER = '2026-06-17-v4.5.63'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { diff --git a/modules/core/module-neural-vfs.js b/modules/core/module-neural-vfs.js index f9bc7f4..32fb0eb 100644 --- a/modules/core/module-neural-vfs.js +++ b/modules/core/module-neural-vfs.js @@ -525,7 +525,7 @@ class NeuralVFSModule { _isSystemFile(path) { const normalized = path.startsWith('/') ? path : '/' + path; - const sysPrefixes = ['/.trash', '/.semantic_index', '/.memory_graph', '/_snapshots', '/_sessions', '/logs', '/memory', '/system', '/sys', '/_mods', '/backups']; + const sysPrefixes = ['/.trash', '/.semantic_index', '/.memory_graph', '/_snapshots', '/_sessions', '/logs', '/memory', '/system', '/sys', '/_mods', '/backups', '/agents', '/_agents', '/leases']; if (sysPrefixes.some(p => normalized.startsWith(p))) return true; if (normalized.includes('/node_modules/')) return true; return false; diff --git a/modules/core/module-orchestrator.js b/modules/core/module-orchestrator.js index bcbd00d..ba43dfd 100644 --- a/modules/core/module-orchestrator.js +++ b/modules/core/module-orchestrator.js @@ -12,6 +12,7 @@ class OrchestratorModule { this._HEARTBEAT_TIMEOUT = 15000; this._IDLE_TIMEOUT = 120000; this._RESTART_POLICY = 'never'; + this._AGENT_DEFAULT_MODEL = null; this._A2A_POLLING = true; this._A2A_POLL_INTERVAL = 500; this._HEARTBEAT_CHECK = 3000; @@ -34,7 +35,7 @@ class OrchestratorModule { this._loadConfig(); } _loadConfig() { - try { const c = localStorage.getItem('webx11_orchestrator_config'); if (c) { const p = JSON.parse(c); if (p.spawnLimit) this._SPAWN_LIMIT = p.spawnLimit; if (p.heartbeatTimeout) this._HEARTBEAT_TIMEOUT = p.heartbeatTimeout; if (p.idleTimeout) this._IDLE_TIMEOUT = p.idleTimeout; if (p.restartPolicy) this._RESTART_POLICY = p.restartPolicy; if (p.a2aPollInterval) this._A2A_POLL_INTERVAL = p.a2aPollInterval; if (p.heartbeatCheck) this._HEARTBEAT_CHECK = p.heartbeatCheck; if (p.idleSweepInterval) this._IDLE_SWEEP_INTERVAL = p.idleSweepInterval; if (p.deadPurgeTimeout) this._DEAD_PURGE_TIMEOUT = p.deadPurgeTimeout; if (p.consensusSize) this._CONSENSUS_SIZE = p.consensusSize; if (p.consensusStrategy) this._CONSENSUS_STRATEGY = p.consensusStrategy; if (p.consensusTimeout) this._CONSENSUS_TIMEOUT = p.consensusTimeout; if (p.pipelineAutoFix !== undefined) this._PIPELINE_CONFIG.autoFix = p.pipelineAutoFix; if (p.pipelineStageTimeout) this._PIPELINE_CONFIG.stageTimeout = p.pipelineStageTimeout; } } catch (e) {} + try { const c = localStorage.getItem('webx11_orchestrator_config'); if (c) { const p = JSON.parse(c); if (p.spawnLimit) this._SPAWN_LIMIT = p.spawnLimit; if (p.heartbeatTimeout) this._HEARTBEAT_TIMEOUT = p.heartbeatTimeout; if (p.idleTimeout) this._IDLE_TIMEOUT = p.idleTimeout; if (p.restartPolicy) this._RESTART_POLICY = p.restartPolicy; if (p.agentDefaultModel !== undefined) this._AGENT_DEFAULT_MODEL = p.agentDefaultModel; if (p.a2aPollInterval) this._A2A_POLL_INTERVAL = p.a2aPollInterval; if (p.heartbeatCheck) this._HEARTBEAT_CHECK = p.heartbeatCheck; if (p.idleSweepInterval) this._IDLE_SWEEP_INTERVAL = p.idleSweepInterval; if (p.deadPurgeTimeout) this._DEAD_PURGE_TIMEOUT = p.deadPurgeTimeout; if (p.consensusSize) this._CONSENSUS_SIZE = p.consensusSize; if (p.consensusStrategy) this._CONSENSUS_STRATEGY = p.consensusStrategy; if (p.consensusTimeout) this._CONSENSUS_TIMEOUT = p.consensusTimeout; if (p.pipelineAutoFix !== undefined) this._PIPELINE_CONFIG.autoFix = p.pipelineAutoFix; if (p.pipelineStageTimeout) this._PIPELINE_CONFIG.stageTimeout = p.pipelineStageTimeout; } } catch (e) {} } _saveConfig() { localStorage.setItem('webx11_orchestrator_config', JSON.stringify({ @@ -42,6 +43,7 @@ class OrchestratorModule { heartbeatTimeout: this._HEARTBEAT_TIMEOUT, idleTimeout: this._IDLE_TIMEOUT, restartPolicy: this._RESTART_POLICY, + agentDefaultModel: this._AGENT_DEFAULT_MODEL, a2aPollInterval: this._A2A_POLL_INTERVAL, heartbeatCheck: this._HEARTBEAT_CHECK, idleSweepInterval: this._IDLE_SWEEP_INTERVAL, @@ -56,7 +58,16 @@ class OrchestratorModule { registerCommands() { this.commands['orchestrate'] = (args) => this.delegateTask(args.join(' ')); this.commands['orchestrator-status'] = () => this.showStatus(); - this.commands['orchestrator-spawn'] = (args) => this.spawnAgent(args[0], args.slice(1)); + this.commands['orchestrator-spawn'] = (args) => { + // Parse --model flag before passing remaining as capabilities + const modelIdx = args.indexOf('--model'); + let modelName = null; + if (modelIdx !== -1 && args[modelIdx + 1]) { + modelName = args[modelIdx + 1]; + args.splice(modelIdx, 2); + } + this.spawnAgent(args[0], args.slice(1), modelName); + }; this.commands['orchestrator-kill'] = (args) => this.killAgent(args[0]); this.commands['orchestrator-purge'] = () => this.purgeDeadAgents(); this.commands['orchestrator-config'] = (args) => this.setConfig(args); @@ -311,7 +322,7 @@ class OrchestratorModule { _sanitizeId(name) { return (name || 'worker').replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').substring(0, 32) || 'worker'; } - spawnAgent(role, capabilities) { + spawnAgent(role, capabilities, modelName = null) { if (this._spawnLock) { this.terminal.print('⚠️ Spawn in progress — queued', 'warning'); return null; @@ -327,6 +338,7 @@ class OrchestratorModule { const agent = { id, role: role || 'worker', + model: modelName || this._AGENT_DEFAULT_MODEL, capabilities: capabilities.length ? capabilities : ['list_files', 'read_file', 'web_search'], createdAt: new Date().toISOString(), lastHeartbeat: Date.now(), @@ -344,7 +356,13 @@ class OrchestratorModule { this.terminal.fs.writeFile(`agents/${id}/status.json`, JSON.stringify(agent)); // Register in agent registry const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; - if (registry) registry.registerAgent(['register', id, ...agent.capabilities]); + if (registry) { + registry.registerAgent(['register', id, ...agent.capabilities]); + // A2A Scratchpad registration + if (typeof registry.a2aRegister === 'function') { + registry.a2aRegister(id, agent.capabilities); + } + } this.terminal.print(`🤖 Agent '${id}' spawned (role: ${role || 'worker'}, ${agent.capabilities.length} caps)`, 'success'); return agent; } finally { @@ -363,7 +381,13 @@ class OrchestratorModule { } // Unregister from registry const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; - if (registry) registry.unregisterAgent(id); + if (registry) { + registry.unregisterAgent(id); + // A2A Scratchpad unregister + if (typeof registry.a2aUnregister === 'function') { + registry.a2aUnregister(id); + } + } // Clean up VFS this.terminal.fs.deleteFile('agents/' + id); delete this._agents[id]; @@ -552,7 +576,8 @@ class OrchestratorModule { 'consensus-strategy': () => { if (!['majority', 'unanimous', 'weighted'].includes(val)) return 'Must be majority, unanimous, or weighted'; this._CONSENSUS_STRATEGY = val; return `consensus-strategy set to ${val}`; }, 'consensus-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 300000) return 'Must be 5000-300000ms'; this._CONSENSUS_TIMEOUT = n; return `consensus-timeout set to ${n}ms`; }, 'pipeline-auto-fix': () => { this._PIPELINE_CONFIG.autoFix = val === 'on' || val === 'true'; return `auto-fix ${this._PIPELINE_CONFIG.autoFix ? 'on' : 'off'}`; }, - 'pipeline-stage-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 120000) return 'Must be 5000-120000ms'; this._PIPELINE_CONFIG.stageTimeout = n; return `stage-timeout set to ${n}ms`; } + 'pipeline-stage-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 120000) return 'Must be 5000-120000ms'; this._PIPELINE_CONFIG.stageTimeout = n; return `stage-timeout set to ${n}ms`; }, + 'agent-default-model': () => { this._AGENT_DEFAULT_MODEL = val; this._saveConfig(); return `agent-default-model set to ${val}`; } }; if (!configMap[key]) { @@ -1207,11 +1232,13 @@ class OrchestratorModule { this.terminal.print(` Agents: ${total} (${busy} busy, ${idle} idle, ${sleeping} sleeping, ${failed} failed)`, 'info'); this.terminal.print(` Spawn limit: ${this._SPAWN_LIMIT} (strategy: ${this._SPAWN_STRATEGY})`, 'info'); this.terminal.print(` Default restart: ${this._RESTART_POLICY}`, 'info'); + this.terminal.print(` Default agent model: ${this._AGENT_DEFAULT_MODEL || 'none (uses main AI model)'}`, 'info'); this.terminal.print(` A2A Messages: ${a2aTotal} (${a2aPending} pending)`, 'info'); Object.values(this._agents).forEach(a => { const task = a.currentTask ? ` (task: ${a.currentTask.substring(0, 40)})` : ''; + const modelInfo = a.model ? ` [model: ${a.model}]` : ''; const backoff = a.nextRestartAt && a.nextRestartAt > Date.now() ? ` [restart in ${Math.round((a.nextRestartAt - Date.now()) / 1000)}s]` : ''; - this.terminal.print(` ${a.id} [${a.role}] — ${a.status}${task}${backoff}`, 'info'); + this.terminal.print(` ${a.id} [${a.role}] — ${a.status}${modelInfo}${task}${backoff}`, 'info'); }); } diff --git a/modules/core/module-smartctx.js b/modules/core/module-smartctx.js index 2b4712d..81929f8 100644 --- a/modules/core/module-smartctx.js +++ b/modules/core/module-smartctx.js @@ -2,7 +2,7 @@ class SmartContextModule { constructor() { this.commands = {}; this.terminal = null; - this.contextProfiles = this.loadProfiles(); this.activeProfile = null; + this.contextProfiles = this.loadProfiles(); this.activeProfile = this.loadActiveProfile(); } getHelpInfo() { return { @@ -22,6 +22,22 @@ saveProfiles() { localStorage.setItem('webx11_context_profiles', JSON.stringify(this.contextProfiles)); } + loadActiveProfile() { + try { + const saved = localStorage.getItem('webx11_active_context_profile'); + if (saved && this.contextProfiles[saved]) return saved; + } catch (e) {} + return null; + } + saveActiveProfile() { + try { + if (this.activeProfile) { + localStorage.setItem('webx11_active_context_profile', this.activeProfile); + } else { + localStorage.removeItem('webx11_active_context_profile'); + } + } catch (e) {} + } getDefaultProfiles() { return { 'coding': { name: 'Coding Assistant', systemPrompt: "You are an expert programmer.", temperature: 0.7 }, @@ -44,6 +60,7 @@ } if (this.contextProfiles[args[0]]) { this.activeProfile = args[0]; + this.saveActiveProfile(); this.terminal.print(`Context set to: ${this.contextProfiles[args[0]].name}`, "success"); } else { this.terminal.print(`Profile '${args[0]}' not found`, "error"); @@ -83,11 +100,43 @@ this.terminal.print("Usage: smart-ask <your-question>", "warning"); return; } let systemPrompt = "You are a helpful assistant."; + let temperature = 0.7; if (this.activeProfile && this.contextProfiles[this.activeProfile]) { - systemPrompt = this.contextProfiles[this.activeProfile].systemPrompt; - this.terminal.print(`Using context: ${this.contextProfiles[this.activeProfile].name}`, "info"); + const profile = this.contextProfiles[this.activeProfile]; + systemPrompt = profile.systemPrompt; + temperature = profile.temperature || 0.7; + this.terminal.print(`Using context: ${profile.name} (temp: ${temperature})`, "info"); + } + // Use agentic loop via the AI assistant module for tool-enabled responses + const aiModule = this.terminal?.moduleManager?.modules?.WebX11AIAssistantModule; + if (aiModule) { + // Set temperature on ollamaManager modelParams temporarily + const om = this.terminal.ollamaManager; + const savedTemp = om.modelParams ? om.modelParams.temperature : undefined; + if (om.modelParams) { + om.modelParams.temperature = temperature; + } + try { + const messages = [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: args.join(' ') } + ]; + const result = await om.chatWithTools(messages, aiModule.getToolDefinitions()); + if (result && result.message) { + this.terminal.printAIResponse(result.message.content || '(no response)'); + } else if (result && result.error) { + this.terminal.print(`⚠️ AI error: ${result.error}`, 'error'); + } + } finally { + if (om.modelParams && savedTemp !== undefined) { + om.modelParams.temperature = savedTemp; + } + } + } else { + // Fallback: simple chat with temperature + const om = this.terminal.ollamaManager; + const fullPrompt = `${systemPrompt}\n\nUser: ${args.join(' ')}`; + await om.chat(fullPrompt); } - const fullPrompt = `${systemPrompt}\n\nUser: ${args.join(' ')}`; - await this.terminal.ollamaManager.chat(fullPrompt); } } diff --git a/modules/core/module-world-model.js b/modules/core/module-world-model.js index e8e3405..6f0c4e5 100644 --- a/modules/core/module-world-model.js +++ b/modules/core/module-world-model.js @@ -96,7 +96,22 @@ class WorldModelModule { _deriveVersion(mInfo) { // Single source of truth: the constant set at bootstrap if (window.NEWOS4_VERSION) return window.NEWOS4_VERSION; - // No fallback version string — if the constant is missing, report unknown + // Fallback: try to extract from script tag ?v= query param or LATEST_VER constant + try { + const scripts = document.querySelectorAll('script[src*="module-loader"]'); + for (const s of scripts) { + const match = s.src.match(/[?&]v=([^&]+)/); + if (match) return match[1]; + } + // Try to find LATEST_VER in any inline script + const allScripts = document.querySelectorAll('script:not([src])'); + for (const s of allScripts) { + const match = s.textContent.match(/LATEST_VER\s*=\s*['"]([^'"]+)['"]/); + if (match) return match[1]; + } + } catch (e) { + console.warn('[WorldModel] Version extraction error:', e.message); + } return 'v?'; } @@ -106,14 +121,20 @@ class WorldModelModule { if (!mm) return 0; // Count registry entries (all known modules) vs loaded const registry = mm.moduleRegistry || []; - return registry.length || Object.keys(mm.modules || {}).length + 5; // +5 for expected unloaded + return registry.length || Object.keys(mm.modules || {}).length; } catch (e) { + console.warn('[WorldModel] Total modules gather error:', e.message); return Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0; } } _gatherSystemInfo(fs, moduleCount, version) { - const healthData = fs.fsReadJSON('/system/metrics/health.json'); + // Get real health data from ServerMonitorModule's _latencyLog and AI assistant's _errorLog + const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule']; + const ai = this.terminal?.moduleManager?.modules?.['WebX11AIAssistantModule']; + const errorCount = ai?._errorLog?.length || 0; + const latencyErrors = sm?._latencyLog ? Object.values(sm._latencyLog).filter(e => e.status === 'error').length : 0; + const totalErrors = errorCount + latencyErrors; return { version: `newOS4 ${version}`, modules: moduleCount || Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0, @@ -122,8 +143,8 @@ class WorldModelModule { memory: typeof performance !== 'undefined' && performance.memory ? `${(performance.memory.usedJSHeapSize / 1048576).toFixed(1)}MB` : 'N/A', - health: healthData?.status || 'OK', - lastHealthCheck: healthData?.timestamp || null + health: totalErrors > 0 ? `⚠️ ${totalErrors} errors` : 'OK', + lastHealthCheck: new Date().toISOString() }; } @@ -175,6 +196,7 @@ class WorldModelModule { const count = idx ? Object.keys(idx).length : 0; return { count, skills: idx ? Object.keys(idx) : [] }; } catch (e) { + console.warn('[WorldModel] Skill info gather error:', e.message); return { count: 0, skills: [] }; } } @@ -184,20 +206,21 @@ class WorldModelModule { if (mg && mg._nodes) { const nodes = Object.keys(mg._nodes).length; const edges = (mg._edges || []).length; - return { nodes, edges, active: true }; + return { nodes, edges, active: true, fallback: false }; } // Fallback: count memory files try { const memIndex = this.terminal.fs?.fsReadJSON('/memory/index.json'); - return { nodes: memIndex ? Object.keys(memIndex).length : 0, edges: 0, active: false }; + return { nodes: memIndex ? Object.keys(memIndex).length : 0, edges: 0, active: false, fallback: true }; } catch (e) { - return { nodes: 0, edges: 0, active: false }; + console.warn('[WorldModel] Memory info gather error:', e.message); + return { nodes: 0, edges: 0, active: false, fallback: true }; } } _gatherAgentInfo() { const orch = this.terminal?.moduleManager?.modules?.['OrchestratorModule']; - if (!orch || !orch._agents) return { agents: 0, active: 0 }; + if (!orch || !orch._agents) return { agents: -1, active: -1, error: 'Orchestrator not loaded' }; const agents = Object.keys(orch._agents).length; const active = Object.values(orch._agents).filter(a => a.status === 'busy').length; return { agents, active, totalLeases: Object.keys(orch._a2aMessages || {}).length }; @@ -205,13 +228,17 @@ class WorldModelModule { _gatherTelemetry(fs) { try { - const latency = fs.fsReadJSON('/system/metrics/tool-latency.json'); - const errors = fs.fsReadJSON('/system/metrics/error-log.json'); + // Get real latency data from ServerMonitorModule and errors from AI assistant + const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule']; + const ai = this.terminal?.moduleManager?.modules?.['WebX11AIAssistantModule']; + const latencyDataPoints = sm?._latencyLog ? Object.keys(sm._latencyLog).length : 0; + const recentErrors = ai?._errorLog?.length || 0; return { - toolLatencyDataPoints: latency ? Object.keys(latency).length : 0, - recentErrors: errors ? (Array.isArray(errors) ? errors.length : Object.keys(errors).length) : 0, + toolLatencyDataPoints: latencyDataPoints, + recentErrors: recentErrors, }; } catch (e) { + console.warn('[WorldModel] Telemetry gather error:', e.message); return { toolLatencyDataPoints: 0, recentErrors: 0 }; } } @@ -224,19 +251,20 @@ class WorldModelModule { const tasks = Object.values(mg._nodes).filter(n => n.type === 'task'); const recentFiles = Object.values(mg._nodes) .filter(n => n.type === 'file') - .sort((a, b) => b.access_count - a.access_count) + .sort((a, b) => (b.access_count || 0) - (a.access_count || 0)) .slice(0, this._MAX_PROJECTS); return { totalTasks: tasks.length, activeProjects: recentFiles.slice(0, 5).map(f => ({ name: f.label, - accesses: f.access_count, + accesses: f.access_count || 0, lastModified: f.updated })) }; } return { totalTasks: 0, activeProjects: [] }; } catch (e) { + console.warn('[WorldModel] Projects gather error:', e.message); return { totalTasks: 0, activeProjects: [] }; } } @@ -258,7 +286,25 @@ class WorldModelModule { if (skills && skills.length > 0) { recent.push(`Skills directory: ${skills.filter(s => s.type === 'file').length} files`); } - } catch (e) {} + // Check memory directory for recent changes + const memDir = fs.readDir('/memory/'); + if (memDir && memDir.length > 0) { + const memFiles = memDir.filter(s => s.type === 'file'); + if (memFiles.length > 0) { + recent.push(`Memory: ${memFiles.length} files in /memory/`); + } + } + // Check system config changes + const sysDir = fs.readDir('/system/'); + if (sysDir && sysDir.length > 0) { + const configFiles = sysDir.filter(s => s.type === 'file' && s.name.endsWith('.json')); + if (configFiles.length > 0) { + recent.push(`System config: ${configFiles.length} config files`); + } + } + } catch (e) { + console.warn('[WorldModel] Recent gather error:', e.message); + } return recent; } @@ -380,7 +426,10 @@ class WorldModelModule { getContextForPrompt() { if (!this._model) return ''; const m = this._model; - return `[World Model Context]\\nSystem: newOS4 ${m.version} · ${m.modules.loaded} modules · ${m.skills.count} skills · ${m.files.total} files · ${m.agents.agents} agents${m.telemetry.recentErrors > 0 ? ` · ⚠️ ${m.telemetry.recentErrors} errors` : ' · ✅ OK'}${m.modules.modifiedCount > 0 ? `\\nModified modules: ${m.modules.modified.slice(0, 3).join(', ')}${m.modules.modifiedCount > 3 ? ` (+${m.modules.modifiedCount - 3} more)` : ''}` : ''}`; + const agentStr = m.agents.error + ? 'agents: N/A' + : `${m.agents.agents} agents`; + return `[World Model Context]\\nSystem: newOS4 ${m.version} · ${m.modules.loaded} modules · ${m.skills.count} skills · ${m.files.total} files · ${agentStr}${m.telemetry.recentErrors > 0 ? ` · ⚠️ ${m.telemetry.recentErrors} errors` : ' · ✅ OK'}${m.modules.modifiedCount > 0 ? `\\nModified modules: ${m.modules.modified.slice(0, 3).join(', ')}${m.modules.modifiedCount > 3 ? ` (+${m.modules.modifiedCount - 3} more)` : ''}` : ''}`; } // ─── Persistence ─── diff --git a/modules/core/ollama-manager.js b/modules/core/ollama-manager.js index 253e319..2f14aa8 100644 --- a/modules/core/ollama-manager.js +++ b/modules/core/ollama-manager.js @@ -19,6 +19,8 @@ class OllamaManager { this.validatorEnabled = localStorage.getItem('webx11_validator_enabled') === 'true'; this.validatorModel = localStorage.getItem('webx11_validator_model') || 'ministral-3:3b-cloud'; this.validatorEndpoint = localStorage.getItem('webx11_validator_endpoint') || ''; + // --- Model Parameters (temperature, top_p, max_tokens, thinking) --- + this.modelParams = this._loadModelParams(); // Build fallback endpoints this.fallbackEndpoints = []; const origin = typeof window !== 'undefined' ? window.location.origin : ''; @@ -73,6 +75,28 @@ class OllamaManager { this._setSecret('webx11_llm_providers', JSON.stringify(this.providers)); } + _loadModelParams() { + try { + const saved = localStorage.getItem('webx11_model_params'); + if (saved) { + const parsed = JSON.parse(saved); + return { + temperature: parsed.temperature !== undefined ? parsed.temperature : 0.7, + top_p: parsed.top_p !== undefined ? parsed.top_p : 0.9, + max_tokens: parsed.max_tokens !== undefined ? parsed.max_tokens : 4096, + thinking: parsed.thinking || 'off' + }; + } + } catch (e) {} + return { temperature: 0.7, top_p: 0.9, max_tokens: 4096, thinking: 'off' }; + } + + _saveModelParams() { + try { + localStorage.setItem('webx11_model_params', JSON.stringify(this.modelParams)); + } catch (e) {} + } + _getSecret(key) { if (typeof window !== 'undefined' && window.webx11_secretVault && typeof window.webx11_secretVault.decrypt === 'function') { return window.webx11_secretVault.decrypt(key); @@ -711,10 +735,21 @@ class OllamaManager { messages: messages, stream: false }; + // Apply model parameters (user-configured temperature, top_p, max_tokens, thinking) + if (this.modelParams) { + body.temperature = this.modelParams.temperature; + body.top_p = this.modelParams.top_p; + body.max_tokens = this.modelParams.max_tokens; + } if (isDeepSeek) { - body.temperature = 0.3; - body.max_tokens = 4096; + // DeepSeek defaults: user params override hardcoded defaults + if (body.temperature === undefined) body.temperature = 0.3; + if (body.max_tokens === undefined) body.max_tokens = 4096; body.stop = ['<|end|>']; + // DeepSeek thinking mode + if (this.modelParams && this.modelParams.thinking !== 'off') { + body.thinking = { type: this.modelParams.thinking }; + } } if (tools && tools.length > 0) { body.tools = tools; @@ -800,7 +835,7 @@ class OllamaManager { return { message: { role: 'assistant', content: '' }, tool_calls: null, - error: null + error: 'DeepSeek returned empty response after retry — model may be overloaded or context too long' }; } diff --git a/modules/core/terminal.js b/modules/core/terminal.js index 504e08d..05aefef 100644 --- a/modules/core/terminal.js +++ b/modules/core/terminal.js @@ -10,7 +10,7 @@ this.destructiveCmds = new Set(['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']); this.systemCmds = new Set(['reboot','shutdown','format','kill','halt','poweroff']); this.suspiciousFlags = new Set(['--dump','--execute','--eval','--exec','--shell','--script']); - this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem']); + this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register']); // Load persisted overrides from localStorage try { const d = localStorage.getItem('webx11_security_destructive'); if(d){JSON.parse(d).forEach(c=>this.destructiveCmds.add(c));} }catch(e){} try { const s = localStorage.getItem('webx11_security_system'); if(s){JSON.parse(s).forEach(c=>this.systemCmds.add(c));} }catch(e){} @@ -19,7 +19,7 @@ this._registerBuiltIn(); } _registerBuiltIn() { - const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg']; + const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register']; internals.forEach(c => this.registerCommand(c, { category: 'builtin' })); } setLevel(n) { @@ -854,35 +854,14 @@ _autoSaveSession() { try { this.fs.mkdirRecursive('_sessions'); - var rootClone; - try { - const json = JSON.stringify(this.fs.root); - if (json.length > 52428800) { // 50MB cap - rootClone = { type: 'directory', name: '/', content: {}, _truncated: true }; - this.print('⚠ Auto-save skipped (VFS too large)', 'warning'); - return; - } - rootClone = JSON.parse(json); - } catch (e) { - rootClone = { type: 'directory', name: '/', content: {}, _error: true }; - return; - } - if (rootClone && rootClone.content && rootClone.content['_sessions']) { - delete rootClone.content['_sessions']; - } const snapshot = { type: 'session', + version: 2, savedAt: new Date().toISOString(), - fsRoot: rootClone, currentPath: this.fs.currentPath, - version: 1, terminalLines: this._historyBuffer.slice(-50) }; const snapshotJson = JSON.stringify(snapshot); - if (snapshotJson.length > 52428800) { - this.print('⚠ Auto-save snapshot too large, skipping', 'warning'); - return; - } this.fs.writeFile('_sessions/_auto.json', snapshotJson); try { const ctx = localStorage.getItem('webx11_ai_context'); @@ -907,19 +886,40 @@ this.print('↻ Skipped stale auto-session backup (older than last reset)', 'info'); return; } - if (data && data.fsRoot) { + if (data.version === 2) { + // v2: metadata-only restore (VFS is persisted independently in localStorage) + this.fs.currentPath = data.currentPath || []; + this.fs.saveToStorage(); + this.updatePrompt(); + this.print('↻ Auto-restored session from ' + (data.savedAt ? new Date(data.savedAt).toLocaleTimeString() : 'recent'), 'success'); + // Optionally re-render last few terminal lines + if (data.terminalLines && Array.isArray(data.terminalLines)) { + data.terminalLines.slice(-10).forEach(line => { + const text = typeof line === 'string' ? line : (line.l || ''); + this.print(text, typeof line === 'object' ? (line.t || '') : ''); + }); + } + try { + const aiCtx = this.fs.readFile('_sessions/_auto-context.json'); + if (aiCtx) { + localStorage.setItem('webx11_ai_context', aiCtx); + } + } catch (e) {} + this.fs.deleteFile('_sessions/_auto.json'); + this.fs.deleteFile('_sessions/_auto-context.json'); + } else if (data && data.fsRoot) { + // v1: full VFS restore (legacy) this.fs.root = data.fsRoot; this.fs.currentPath = data.currentPath || []; this.fs.saveToStorage(); + this.updatePrompt(); this.print('↻ Auto-restored session from ' + (data.savedAt ? new Date(data.savedAt).toLocaleTimeString() : 'recent'), 'success'); - // Defer AI context restoration (AI module loads after terminal init) try { const aiCtx = this.fs.readFile('_sessions/_auto-context.json'); if (aiCtx) { localStorage.setItem('webx11_ai_context', aiCtx); } } catch (e) {} - // v4.5.47: delete backup after successful restore so reset-mem doesn't get resurrected this.fs.deleteFile('_sessions/_auto.json'); this.fs.deleteFile('_sessions/_auto-context.json'); } @@ -1566,22 +1566,41 @@ return; } if (cmd === 'history') { - const search = args.join(' ').toLowerCase(); const entries = this.history; if (!entries.length) { this.print('No command history.', 'info'); return; } - if (search) { - const filtered = entries.map((c, i) => ({ i, c })).filter(e => e.c.toLowerCase().includes(search)); - if (!filtered.length) { this.print(`No history entries matching: ${search}`, 'info'); return; } - this.print(`=== History (matching: ${search}) ===`, 'info'); - filtered.forEach(e => this.print(` ${e.i + 1} ${e.c}`)); - } else { - this.print(`=== Command History (${entries.length} entries) ===`, 'info'); - const start = Math.max(0, entries.length - 50); - for (let i = start; i < entries.length; i++) { - this.print(` ${i + 1} ${entries[i]}`); + // Flags + if (args[0] === '--clear' || args[0] === '-c') { + this.history = []; + this.historyIndex = 0; + localStorage.removeItem('webx11_command_history'); + this.print('History cleared.', 'success'); + return; + } + if (args[0] === '--export' || args[0] === '-e') { + try { + this.fs.mkdirRecursive('system'); + this.fs.writeFile('system/history.json', JSON.stringify(entries)); + this.print('History exported to /system/history.json (' + entries.length + ' entries).', 'success'); + } catch (e) { + this.print('Export failed: ' + e.message, 'error'); } - if (entries.length > 50) this.print(`... (${entries.length - 50} older entries hidden, use history <search> to find)`); + return; + } + if (args[0] === '--search' || args[0] === '-s') { + const term = args.slice(1).join(' ').toLowerCase(); + if (!term) { this.print('Usage: history --search <term>', 'warning'); return; } + const filtered = entries.filter(e => e.toLowerCase().includes(term)); + if (!filtered.length) { this.print('No matches for: ' + term, 'info'); return; } + this.print('=== History (matching: ' + term + ') ===', 'info'); + filtered.forEach(e => this.print(' ' + e)); + return; } + let count = 50; + if (args[0] === '-n' && args[1]) count = parseInt(args[1]) || 50; + const slice = entries.slice(-count); + this.print('=== Command History (' + entries.length + ' entries) ===', 'info'); + slice.forEach(c => this.print(' ' + c)); + if (entries.length > count) this.print('... (' + (entries.length - count) + ' older, use history --search <term> to find)'); return; } for (const modName in this.moduleManager.modules) { diff --git a/modules/core/virtual-fs.js b/modules/core/virtual-fs.js index 9d3ea79..827c446 100644 --- a/modules/core/virtual-fs.js +++ b/modules/core/virtual-fs.js @@ -6,6 +6,29 @@ class VirtualFileSystem { this._virtualMounts = null; // lazy-init on first /sys/ access // Register /sys/ in emit-change exclusion (also added below) } + // ── Auto-purge trash older than 24 hours ── + _purgeTrash() { + try { + const trash = this.root.content && this.root.content['.trash']; + if (!trash || !trash.content) return; + const now = Date.now(); + const maxAge = 86400000; // 24 hours + let purged = 0; + for (const name of Object.keys(trash.content)) { + const tsMatch = name.match(/^(\d+)/); + if (tsMatch) { + const ts = parseInt(tsMatch[1], 10); + if (now - ts > maxAge) { + delete trash.content[name]; + purged++; + } + } + } + if (purged > 0) { + this.saveToStorage(); + } + } catch (e) { /* silent */ } + } // ── Virtual /sys/ filesystem (live runtime state, never persisted) ── _initVirtualMounts() { if (this._virtualMounts) return; @@ -342,6 +365,7 @@ class VirtualFileSystem { const data = JSON.parse(text); this.root = data.root || this.root; this.currentPath = data.currentPath || []; + this._purgeTrash(); return true; } catch (e) { console.error('FS v2 decompress error:', e); @@ -826,10 +850,17 @@ class VirtualFileSystem { checkpoint(name) { if (!name) name = new Date().toISOString(); const createdAt = new Date().toISOString(); + // Strip system dirs to prevent fractal bloat + const rootClone = JSON.parse(JSON.stringify(this.root)); + if (rootClone && rootClone.content) { + delete rootClone.content['_sessions']; + delete rootClone.content['_snapshots']; + delete rootClone.content['.trash']; + } const data = { version: 1, createdAt, - root: JSON.parse(JSON.stringify(this.root)), + root: rootClone, currentPath: [...this.currentPath] }; if (!this.readDir('/_snapshots')) { @@ -837,11 +868,11 @@ class VirtualFileSystem { } this.fsWriteJSON('/_snapshots/checkpoint-' + name + '.json', data); this.saveToStorage(); - // Auto-manage: keep max 10 checkpoints, delete oldest + // Auto-manage: keep max 3 checkpoints, delete oldest const all = this.listCheckpoints(); - if (all.length > 10) { + if (all.length > 3) { const sorted = all.slice().sort(); - const toDelete = sorted.slice(0, all.length - 10); + const toDelete = sorted.slice(0, all.length - 3); for (const old of toDelete) this.deleteCheckpoint(old); } return true; |