// module-orchestrator.js — Multi-agent orchestration system with A2A mailbox transport // Commands: orchestrate, orchestrator-status, orchestrator-spawn, orchestrator-kill, // orchestrator-purge, orchestrator-config, orchestrator-list, orchestrator-sleep, // orchestrator-wake, orchestrator-restart-policy, a2a-send, a2a-list class OrchestratorModule { constructor() { this.commands = {}; this.terminal = null; this._agents = {}; this._SPAWN_LIMIT = 5; // Boot-time agents — stable per-role ids so a reload re-adopts the existing VFS dir // instead of minting a new timestamp id every boot (that was the /agents/ accumulation bug). this._DEFAULT_ROLES = ['auto-learner', 'kernel-architect', 'searcher']; this._BOOT_DELAY = 2000; // let other modules init before the boot team spawns (tests set 0) this._SPAWN_STRATEGY = 'eager'; 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; this._IDLE_SWEEP_INTERVAL = 30000; this._DEAD_PURGE_TIMEOUT = 60000; this._a2aPollTimer = null; this._heartbeatInterval = null; this._a2aMessages = {}; // v4.6.68: on-demand is the design (decision 2026-10-23). Delegations — not // "agents ever scheduled" — is the number that means something. this._delegations = 0; this._restartBackoffs = {}; this._eventBus = {}; this._idleSweepInterval = null; this._spawnLock = false; this._CONSENSUS_SIZE = 3; this._CONSENSUS_STRATEGY = 'majority'; this._CONSENSUS_TIMEOUT = 60000; this._consensusRounds = []; this._activeConsensus = null; this._pipelineHistory = []; this._PIPELINE_CONFIG = { autoFix: false, stageTimeout: 30000 }; this._schedules = []; this._scheduleTimer = null; 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.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({ spawnLimit: this._SPAWN_LIMIT, 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, deadPurgeTimeout: this._DEAD_PURGE_TIMEOUT, consensusSize: this._CONSENSUS_SIZE, consensusStrategy: this._CONSENSUS_STRATEGY, consensusTimeout: this._CONSENSUS_TIMEOUT, pipelineAutoFix: this._PIPELINE_CONFIG.autoFix, pipelineStageTimeout: this._PIPELINE_CONFIG.stageTimeout })); } registerCommands() { this.commands['orchestrate'] = (args) => this.delegateTask(args.join(' ')); this.commands['orchestrator-status'] = () => this.showStatus(); 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); this.commands['orchestrator-list'] = () => this.listAgents(); this.commands['orchestrator-sleep'] = (args) => this.sleepAgent(args[0]); this.commands['orchestrator-wake'] = (args) => this.wakeAgent(args[0]); this.commands['orchestrator-restart-policy'] = (args) => this.setRestartPolicy(args); this.commands['a2a-send'] = (args) => this.sendA2AMessage(args); this.commands['a2a-list'] = () => this.listA2AMessages(); this.commands['orchestrate-parallel'] = (args) => this.delegateTask(args.join(' '), { parallel: true }); this.commands['orchestrate-decompose'] = (args) => this.showDecomposition(args.join(' ')); this.commands['orchestrate-result'] = () => this.showParallelResult(); this.commands['consensus'] = (args) => this.handleConsensus(args); this.commands['consensus-status'] = () => this.consensusStatus(); this.commands['consensus-history'] = () => this.consensusHistory(); this.commands['pipeline'] = (args) => this.runPipeline(args); this.commands['orchestrator-schedule'] = (args) => this.addSchedule(args); this.commands['orchestrator-schedules'] = () => this.listSchedules(); this.commands['orchestrator-unschedule'] = (args) => this.removeSchedule(args[0]); } // ─── Task Decomposition Engine ─── _decomposeGoal(goal) { if (!goal || typeof goal !== 'string') return [{ goal }]; const lower = goal; // Heuristic: only split at conjunctions when both sides have distinct action verbs // Action verb patterns from _deriveCapabilities const ACTION_VERBS = [ 'search', 'find', 'research', 'lookup', 'query', 'write', 'create', 'generate', 'compose', 'draft', 'save', 'delete', 'remove', 'trash', 'rm', 'read', 'view', 'cat', 'show', 'display', 'inspect', 'preview', 'edit', 'modify', 'update', 'patch', 'change', 'fix', 'install', 'package', 'setup', 'list', 'status', 'health', 'audit', 'report', 'check', 'fetch', 'scrape', 'download', 'build', 'develop', 'compile', 'analyze', 'analyse', 'summarize', 'review', 'investigate', 'deploy', 'publish', 'upload', 'monitor', 'watch', 'poll', 'track', 'observe', 'test', 'verify', 'cmp', 'diff', 'compare' ]; const hasActionVerb = (s) => ACTION_VERBS.some(v => { const re = new RegExp('\\b' + v + '\\b', 'i'); return re.test(s); }); // Try splitting on " and " / " then " / " also " / "; " / ", then" const separators = [ /\s+and\s+/i, /\s+then\s+/i, /\s+also\s+/i, /;\s+/, /,\s+then\s+/i, /\s+followed by\s+/i, ]; let bestSplit = null; for (const sep of separators) { const parts = goal.split(sep); if (parts.length >= 2 && parts.every(p => hasActionVerb(p.trim()))) { bestSplit = parts.map(p => p.trim()).filter(p => p.length > 3); break; // first matching separator wins } } if (!bestSplit || bestSplit.length < 2) { return [{ goal }]; // atomic } return bestSplit.map(sub => ({ goal: sub, role: this._deriveRole(sub), caps: this._deriveCapabilities(sub) })); } getHelpInfo() { return { 'orchestrate': 'orchestrate — Delegate a task to an available agent (auto-spawns if needed)', 'orchestrate-parallel': 'orchestrate-parallel — Decompose and delegate sub-tasks in parallel (auto-splits on "and"/"then")', 'orchestrate-decompose': 'orchestrate-decompose — Preview how a task would be decomposed into sub-tasks', 'orchestrate-result': 'orchestrate-result — Show the last parallel batch results (stored in VFS)', 'consensus': 'consensus — Start a consensus round: spawn 3 agents, debate the question, resolve by majority', 'consensus-status': 'consensus-status — Show current/active consensus round status', 'consensus-history': 'consensus-history — Show past consensus rounds and their resolutions', 'pipeline': 'pipeline — Autonomous CI/CD: test agents/queue/modules, deploy (test→auto-fix→verify), history (show runs), config (show config)', 'orchestrator-schedule': 'orchestrator-schedule — Schedule a recurring task delegation', 'orchestrator-schedules': 'orchestrator-schedules — List all active recurring schedules', 'orchestrator-unschedule': 'orchestrator-unschedule — Remove a recurring schedule', 'orchestrator-status': 'orchestrator-status — Show agent pool status and config', 'orchestrator-spawn': 'orchestrator-spawn [role] [capabilities...] — Manually spawn a new agent', 'orchestrator-kill': 'orchestrator-kill — Terminate an agent and revoke leases', 'orchestrator-purge': 'orchestrator-purge — Remove dead agents (>60s since heartbeat)', 'orchestrator-config': 'orchestrator-config — Tune: spawn-limit, spawn-strategy, heartbeat-timeout, idle-timeout, restart-policy, a2a-polling, a2a-poll-interval, heartbeat-check, idle-sweep-interval, dead-purge-timeout', 'orchestrator-list': 'orchestrator-list — List all spawned agents with full details', 'orchestrator-sleep': 'orchestrator-sleep — Suspend agent to VFS', 'orchestrator-wake': 'orchestrator-wake — Restore agent from VFS sleep', 'orchestrator-restart-policy': 'orchestrator-restart-policy — Set per-agent restart policy', 'a2a-send': 'a2a-send — Send an A2A message to an agent\'s inbox', 'a2a-list': 'a2a-list — Show pending A2A messages across all agents', }; } init(terminal) { this.terminal = terminal; // Register orchestrator in the agent registry const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; if (registry) { // MAINT-008: the agent id comes first; passing the command word as args[0] registered an // agent literally named "register" with one capability. registry.registerAgent(['orchestrator-0', 'delegate', 'spawn', 'monitor', 'purge']); } // Reconcile + pre-spawn the default team. VirtualFileSystem.loadFromStorage() is ASYNC, // so reconciliation MUST wait for it — running it synchronously saw an empty root and // silently did nothing, which let duplicate agents accumulate on every reload. this._bootAgents(); // A2A mailbox polling loop (500ms) if (this._A2A_POLLING) { this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL); } // Heartbeat watchdog this._heartbeatInterval = this.terminal.moduleManager._setInterval(() => this._checkHeartbeats(), this._HEARTBEAT_CHECK); // Idle timeout sweep this._idleSweepInterval = this.terminal.moduleManager._setInterval(() => this._sweepIdle(), this._IDLE_SWEEP_INTERVAL); this.terminal.print('🧠 Orchestrator loaded — A2A mailbox transport active', 'success'); // Load recurring schedules from VFS this._loadSchedules(); // Subscribe to event bus for in-memory A2A delivery (instant, no VFS poll) this._on('a2a:message', (msg) => { if (msg.type === 'heartbeat' && msg.from && this._agents[msg.from]) { this._agents[msg.from].lastHeartbeat = Date.now(); } if (msg.type === 'response' && msg.from && this._agents[msg.from]) { this._agents[msg.from].status = 'idle'; this._agents[msg.from].currentTask = null; } }); } // ─── Event Bus (in-memory pub/sub for agent communication) ─── _on(event, callback) { if (!this._eventBus[event]) this._eventBus[event] = []; this._eventBus[event].push(callback); return () => { // Return unsubscribe function this._eventBus[event] = this._eventBus[event].filter(cb => cb !== callback); }; } _emit(event, data) { for (const cb of (this._eventBus[event] || [])) { try { cb(data); } catch (e) { /* subscriber error */ } } } // ─── A2A Mailbox Transport ─── sendA2AMessage(args) { if (args.length < 2) return this.terminal.print('Usage: a2a-send ', 'error'); const agentId = args[0]; let payload; try { payload = JSON.parse(args.slice(1).join(' ')); } catch (e) { // Treat as plain text message payload = { type: 'request', payload: { text: args.slice(1).join(' ') } }; } const agent = this._agents[agentId]; if (!agent) return this.terminal.print(`❌ Agent '${agentId}' not found`, 'error'); const msg = { a2a_version: '0.1', msg_id: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`, type: payload.type || 'request', from: 'orchestrator-0', to: agentId, timestamp: new Date().toISOString(), ttl_seconds: 30, contract: agent.capabilities ? { capabilities: agent.capabilities } : {}, payload: payload.payload || payload }; // Write to agent's VFS inbox if (!this.terminal.fs.readFile(`/agents/${agentId}/`)) this.terminal.fs.mkdir(`agents/${agentId}`); this.terminal.fs.writeFile(`agents/${agentId}/inbox.json`, JSON.stringify(msg)); // Track in memory if (!this._a2aMessages[agentId]) this._a2aMessages[agentId] = []; this._a2aMessages[agentId].push({ ...msg, _processed: false }); this.terminal.print(`📨 A2A message sent to ${agentId} (${msg.type}, id: ${msg.msg_id})`, 'success'); // Emit via event bus for in-memory delivery (bypasses VFS polling) this._emit('a2a:message', msg); this._emit(`a2a:message:${agentId}`, msg); } listA2AMessages() { const ids = Object.keys(this._a2aMessages); if (ids.length === 0) return this.terminal.print('No A2A messages', 'info'); this.terminal.print('📨 A2A Messages:', 'header'); for (const agentId of ids) { const msgs = this._a2aMessages[agentId]; const pending = msgs.filter(m => !m._processed).length; this.terminal.print(` ${agentId}: ${msgs.length} total (${pending} pending)`, 'info'); for (const m of msgs.slice(-3)) { const status = m._processed ? '✅' : '⏳'; this.terminal.print(` ${status} ${m.msg_id} ${m.type} → ${m.to} (${(m.payload?.text || JSON.stringify(m.payload)).substring(0, 40)})`, 'text-muted'); } } } _pollAgentMailboxes() { const fs = this.terminal.fs; if (!fs) return; for (const [id, agent] of Object.entries(this._agents)) { try { const inbox = fs.fsReadJSON(`/agents/${id}/inbox.json`); if (!inbox) continue; // Check if this message is already processed const existing = this._a2aMessages[id] || []; if (existing.find(m => m.msg_id === inbox.msg_id && m._processed)) continue; // Route based on message type if (inbox.a2a_version && inbox.type) { inbox._processed = true; if (!this._a2aMessages[id]) this._a2aMessages[id] = []; this._a2aMessages[id].push(inbox); if (inbox.type === 'request') { this.terminal.print(`📨 A2A: ${inbox.from} → ${id} (${inbox.msg_id})`, 'info'); // Check outbox for response this._checkAgentOutbox(id); } else if (inbox.type === 'broadcast') { this.terminal.print(`📢 A2A broadcast from ${inbox.from}: ${(inbox.payload?.text || JSON.stringify(inbox.payload)).substring(0, 60)}`, 'info'); } else if (inbox.type === 'heartbeat') { agent.lastHeartbeat = Date.now(); } else if (inbox.type === 'response') { this.terminal.print(`📨 A2A response from ${id}: ${(inbox.payload?.result || JSON.stringify(inbox.payload)).substring(0, 60)}`, 'success'); agent.status = 'idle'; agent.currentTask = null; } } } catch (e) { /* skip agent with bad inbox */ } } } _checkAgentOutbox(agentId) { try { const outbox = this.terminal.fs.fsReadJSON(`/agents/${agentId}/outbox.json`); if (outbox && outbox.status === 'completed') { const msg = { a2a_version: '0.1', msg_id: `resp-${Date.now()}`, type: 'response', from: agentId, to: 'orchestrator-0', timestamp: new Date().toISOString(), payload: outbox }; if (!this._a2aMessages['orchestrator-0']) this._a2aMessages['orchestrator-0'] = []; this._a2aMessages['orchestrator-0'].push(msg); this.terminal.print(`📨 A2A: ${agentId} completed task (outbox)`, 'success'); } } catch (e) {} } // ─── Agent Lifecycle ─── _sanitizeId(name) { return (name || 'worker').replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').substring(0, 32) || 'worker'; } spawnAgent(role, capabilities, modelName = null, idHint = null) { if (this._spawnLock) { this.terminal.print('⚠️ Spawn in progress — queued', 'warning'); return null; } this._spawnLock = true; try { const safeRole = this._sanitizeId(role); const id = idHint || `agent-${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 4)}-${safeRole}`; // Idempotent: a stable-id agent that is already live is reused, never duplicated if (this._agents[id]) { this.terminal.print(`♻️ Agent '${id}' already live — reusing`, 'info'); return this._agents[id]; } if (Object.keys(this._agents).length >= this._SPAWN_LIMIT) { this.terminal.print(`❌ Agent spawn limit (${this._SPAWN_LIMIT}) reached`, 'error'); return null; } 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(), status: 'idle', currentTask: null, leaseId: null, restartPolicy: this._RESTART_POLICY, failureCount: 0, restartCount: 0, nextRestartAt: null, // v4.6.63: "sleeping" alone could not tell an agent WAITING for work from one // that was never handed a task (packet §6: 4 registered, 0 executing). tasksAssigned: 0, lastTaskAt: null }; this._agents[id] = agent; // Create VFS agent directory with status if (!this.terminal.fs.readFile(`/agents/${id}/`)) this.terminal.fs.mkdir(`agents/${id}`); 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([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 { this._spawnLock = false; } } killAgent(id) { if (!this._agents[id]) return this.terminal.print(`❌ Agent '${id}' not found`, 'error'); // Revoke any active lease const leaseModule = this.terminal.moduleManager.modules['LeaseModule']; if (leaseModule) { Object.values(leaseModule._leases).forEach(l => { if (l.workerId === id && l.status === 'active') leaseModule.revokeLease(l.id); }); } // Unregister from registry const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; if (registry) { registry.unregisterAgent(id); // A2A Scratchpad unregister if (typeof registry.a2aUnregister === 'function') { registry.a2aUnregister(id); } } // Clean up VFS — rm() dispatches file|dir → trash (deleteFile() is file-only // and silently returns false for directories, which leaked /agents//) const removed = this.terminal.fs.rm('agents/' + id); if (!removed) this.terminal.print(`⚠️ VFS cleanup failed for '${id}' — /agents/${id}/ still present`, 'warning'); delete this._agents[id]; delete this._restartBackoffs[id]; this.terminal.print(`💀 Agent '${id}' killed`, 'warning'); } sleepAgent(id) { if (!this._agents[id]) return this.terminal.print(`❌ Agent '${id}' not found`, 'error'); const agent = this._agents[id]; // Serialize to VFS const state = { ...agent, _sleepTime: new Date().toISOString() }; delete state.leaseId; // leases don't survive sleep this.terminal.fs.writeFile(`agents/${id}/sleep.json`, JSON.stringify(state)); agent.status = 'sleeping'; this.terminal.print(`💤 Agent '${id}' sleeping (state saved to VFS)`, 'warning'); } wakeAgent(id) { const data = this.terminal.fs.fsReadJSON(`/agents/${id}/sleep.json`); if (!data) return this.terminal.print(`❌ No saved state for '${id}' — use orchestrator-spawn instead`, 'error'); // Restore agent from VFS const agent = { ...data, status: 'idle', currentTask: null, leaseId: null, lastHeartbeat: Date.now() }; delete agent._sleepTime; this._agents[id] = agent; this.terminal.fs.writeFile(`agents/${id}/status.json`, JSON.stringify({ ...agent, status: 'idle' })); this.terminal.fs.deleteFile(`agents/${id}/sleep.json`); this.terminal.print(`🔄 Agent '${id}' reanimated from VFS sleep`, 'success'); } async _bootAgents() { // Boot sequence for /agents/ — order is load-bearing: // 1. await VFS hydration (loadFromStorage is async; a sync reconcile sees an empty root) // 2. reconcile /agents/ (adopt what is already there, purge duplicates/orphans) // 3. only then spawn any missing default agent try { await Promise.resolve(this.terminal.fs._ready); } catch (e) { this.terminal.print('⚠️ VFS hydration failed — agent reconciliation skipped', 'warning'); return this._agents; } this._reanimateFromVFS(); if (this._BOOT_DELAY > 0) await new Promise(r => setTimeout(r, this._BOOT_DELAY)); this._preSpawnDefaultAgents(); return this._agents; } _reanimateFromVFS() { // Boot reconciliation for /agents/: // 1. sleep.json → reanimate the agent (previous behaviour) // 2. status.json + default role → ADOPT it (a reload left it behind, still wanted) // 3. no usable state / duplicate → orphan → fs.rm(), so a reload cannot grow /agents/ // Only `agent-*` dirs are touched; /agents/register/ belongs to AgentRegistry. const fs = this.terminal.fs; const agentsDir = fs.readDir('/agents/'); if (!agentsDir) return; // Stable ids (agent-) beat timestamped ones; among timestamped ids, newest wins. const tsOf = (id) => { const m = /^agent-([0-9a-z]+)-[0-9a-z]{4}-/.exec(id); return m ? (parseInt(m[1], 36) || 0) : Number.MAX_SAFE_INTEGER; }; const keep = new Map(); // role: for default agents (singleton) | id: for workers const orphans = []; for (const entry of agentsDir) { if (entry.type !== 'dir' || !entry.name.startsWith('agent-')) continue; const id = entry.name; if (this._agents[id]) continue; const sleepData = fs.fsReadJSON(`/agents/${id}/sleep.json`); const statusData = fs.fsReadJSON(`/agents/${id}/status.json`); const state = sleepData || null; const role = (state || statusData || {}).role; const wanted = !!state || (statusData && this._DEFAULT_ROLES.includes(role)); if (!wanted) { orphans.push(id); continue; } // Default roles are singletons (one dir per role); worker agents stay unique per id. const key = this._DEFAULT_ROLES.includes(role) ? 'role:' + role : 'id:' + id; const prev = keep.get(key); if (!prev || tsOf(id) > tsOf(prev.id)) { if (prev) orphans.push(prev.id); keep.set(key, { id, state: state || statusData }); } else { orphans.push(id); } } for (const { id, state } of keep.values()) { const agent = { ...state, status: 'idle', currentTask: null, leaseId: null, lastHeartbeat: Date.now() }; delete agent._sleepTime; this._agents[id] = agent; fs.writeFile(`agents/${id}/status.json`, JSON.stringify({ ...agent, status: 'idle' })); if (fs.readFile(`/agents/${id}/sleep.json`)) fs.rm(`agents/${id}/sleep.json`); } for (const id of orphans) fs.rm('agents/' + id); // rm() dispatches dir → trashDir if (keep.size > 0) this.terminal.print(`🔄 Restored ${keep.size} agent(s) from VFS`, 'info'); if (orphans.length > 0) this.terminal.print(`🧹 Purged ${orphans.length} orphaned agent dir(s)`, 'warning'); } // ─── Restart Policies ─── setRestartPolicy(args) { if (args.length < 2) return this.terminal.print('Usage: orchestrator-restart-policy ', 'error'); const id = args[0]; const policy = args[1]; if (!['on-failure', 'always', 'never'].includes(policy)) { return this.terminal.print('Policy must be: on-failure, always, or never', 'error'); } if (id === 'default') { this._RESTART_POLICY = policy; return this.terminal.print(`✅ Default restart policy set to '${policy}'`, 'success'); } const agent = this._agents[id]; if (!agent) return this.terminal.print(`❌ Agent '${id}' not found`, 'error'); agent.restartPolicy = policy; this.terminal.print(`✅ Restart policy for '${id}' set to '${policy}'`, 'success'); } _handleAgentFailure(id, error) { const agent = this._agents[id]; if (!agent) return; agent.failureCount = (agent.failureCount || 0) + 1; agent.lastError = error; agent.lastErrorAt = new Date().toISOString(); let shouldRestart = false; if (agent.restartPolicy === 'always') { shouldRestart = true; } else if (agent.restartPolicy === 'on-failure' && agent.failureCount <= 1) { shouldRestart = true; } if (shouldRestart) { const backoff = Math.min(Math.pow(2, agent.failureCount), 60); // 2^N capped at 60s agent.nextRestartAt = Date.now() + (backoff * 1000); agent.restartCount = (agent.restartCount || 0) + 1; this._restartBackoffs[id] = setTimeout(() => { if (this._agents[id]) { this._agents[id].status = 'idle'; this._agents[id].currentTask = null; this.terminal.print(`🔄 Agent '${id}' auto-restarted (backoff: ${backoff}s, attempt: ${agent.restartCount})`, 'info'); } }, backoff * 1000); this.terminal.print(`⚠️ Agent '${id}' failed — restart in ${backoff}s (policy: ${agent.restartPolicy})`, 'warning'); } else { agent.status = 'failed'; this.terminal.print(`❌ Agent '${id}' failed — no restart (policy: ${agent.restartPolicy})`, 'error'); } } // ─── Autonomous Spawning ─── _deriveCapabilities(task) { const caps = ['list_files', 'read_file']; const lower = task.toLowerCase(); const CAP_MAP = [ { patterns: ['search', 'find', 'research', 'lookup', 'query', 'look up'], caps: ['web_search'] }, { patterns: ['write', 'create', 'generate', 'compose', 'draft', 'save'], caps: ['write_file'] }, { patterns: ['delete', 'remove', 'trash', 'rm '], caps: ['delete_file'] }, { patterns: ['read', 'view', 'cat', 'show', 'display', 'inspect', 'preview'], caps: [] }, // already has read_file { patterns: ['edit', 'modify', 'update', 'patch', 'change', 'fix'], caps: ['write_file'] }, { patterns: ['install', 'package', 'wpm', 'setup'], caps: ['wpm_install'] }, { patterns: ['module', 'plugin', 'ext'], caps: ['view_module', 'edit_module'] }, { patterns: ['list', 'status', 'health', 'audit', 'report', 'check'], caps: ['get_status'] }, { patterns: ['fetch', 'url', 'http', 'scrape'], caps: ['web_fetch'] }, { patterns: ['code', 'program', 'script', 'build', 'develop', 'compile'], caps: ['list_files', 'read_file', 'write_file'] }, { patterns: ['analyze', 'analyse', 'summarize', 'review', 'investigate'], caps: ['web_search', 'list_files', 'read_file'] }, { patterns: ['deploy', 'publish', 'upload'], caps: ['write_file', 'webdav_upload'] }, { patterns: ['monitor', 'watch', 'poll', 'track', 'observe'], caps: ['get_status'] }, ]; for (const entry of CAP_MAP) { if (entry.patterns.some(p => lower.includes(p))) { entry.caps.forEach(c => caps.push(c)); } } return [...new Set(caps)]; } _deriveRole(task) { const lower = task.toLowerCase(); if (lower.includes('search') || lower.includes('find')) return 'searcher'; if (lower.includes('write') || lower.includes('create') || lower.includes('save')) return 'writer'; if (lower.includes('delete') || lower.includes('remove') || lower.includes('clean')) return 'janitor'; if (lower.includes('install') || lower.includes('package')) return 'packager'; if (lower.includes('module') || lower.includes('code') || lower.includes('fix')) return 'coder'; if (lower.includes('health') || lower.includes('check') || lower.includes('audit')) return 'auditor'; if (lower.includes('fetch') || lower.includes('url') || lower.includes('http')) return 'fetcher'; return 'worker'; } // ─── Config ─── setConfig(args) { if (args.length < 2) return this.terminal.print('Usage: orchestrator-config ', 'error'); const key = args[0]; const val = args[1]; const configMap = { 'spawn-limit': () => { this._SPAWN_LIMIT = parseInt(val); return `spawn-limit set to ${val}`; }, 'spawn-strategy': () => { if (!['eager', 'lazy'].includes(val)) return 'Must be eager or lazy'; this._SPAWN_STRATEGY = val; return `spawn-strategy set to ${val}`; }, 'heartbeat-timeout': () => { this._HEARTBEAT_TIMEOUT = parseInt(val); return `heartbeat-timeout set to ${val}ms`; }, 'idle-timeout': () => { this._IDLE_TIMEOUT = parseInt(val); return `idle-timeout set to ${val}ms`; }, 'restart-policy': () => { if (!['on-failure', 'always', 'never'].includes(val)) return 'Must be on-failure, always, or never'; this._RESTART_POLICY = val; return `default restart-policy set to ${val}`; }, 'a2a-polling': () => { this._A2A_POLLING = val === 'on' || val === 'true'; if (this._A2A_POLLING && !this._a2aPollTimer) { this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL); } else if (!this._A2A_POLLING && this._a2aPollTimer) { this.terminal.moduleManager._clearTimer(this._a2aPollTimer); this._a2aPollTimer = null; } return `a2a-polling ${this._A2A_POLLING ? 'on' : 'off'}`; }, 'a2a-poll-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 50 || n > 30000) return 'Must be 50-30000ms'; this.terminal.moduleManager._clearTimer(this._a2aPollTimer); this._a2aPollTimer = null; this._A2A_POLL_INTERVAL = n; if (this._A2A_POLLING) { this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), n); } return `a2a-poll-interval set to ${n}ms`; }, 'heartbeat-check': () => { const n = parseInt(val); if (isNaN(n) || n < 500 || n > 60000) return 'Must be 500-60000ms'; this.terminal.moduleManager._clearTimer(this._heartbeatInterval); this._HEARTBEAT_CHECK = n; this._heartbeatInterval = this.terminal.moduleManager._setInterval(() => this._checkHeartbeats(), n); return `heartbeat-check set to ${n}ms`; }, 'idle-sweep-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; this.terminal.moduleManager._clearTimer(this._idleSweepInterval); this._IDLE_SWEEP_INTERVAL = n; this._idleSweepInterval = this.terminal.moduleManager._setInterval(() => this._sweepIdle(), n); return `idle-sweep-interval set to ${n}ms`; }, 'dead-purge-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; this._DEAD_PURGE_TIMEOUT = n; return `dead-purge-timeout set to ${n}ms`; }, 'consensus-size': () => { const n = parseInt(val); if (isNaN(n) || n < 2 || n > 10) return 'Must be 2-10'; this._CONSENSUS_SIZE = n; return `consensus-size set to ${n}`; }, 'consensus-strategy': () => { if (!['majority', 'unanimous', 'weighted'].includes(val)) return 'Must be majority, unanimous, or weighted'; this._CONSENSUS_STRATEGY = val; return `consensus-strategy set to ${val}`; }, '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`; }, 'agent-default-model': () => { this._AGENT_DEFAULT_MODEL = val; this._saveConfig(); return `agent-default-model set to ${val}`; } }; if (!configMap[key]) { return this.terminal.print(`Unknown config key: ${key}. Valid: ${Object.keys(configMap).join(', ')}`, 'error'); } const result = configMap[key](); this.terminal.print(`✅ ${result}`, 'success'); this._saveConfig(); } // ─── Show Decomposition Preview ─── showDecomposition(goal) { if (!goal) return this.terminal.print('Usage: orchestrate-decompose ', 'error'); const parts = this._decomposeGoal(goal); this.terminal.print('🔍 Decomposition of "' + goal.substring(0, 60) + '":', 'header'); if (parts.length === 1 && parts[0].goal === goal) { this.terminal.print(' → Atomic (no split detected)', 'info'); return; } for (let i = 0; i < parts.length; i++) { const p = parts[i]; this.terminal.print(' [' + (i + 1) + '] ' + p.goal.substring(0, 50), 'success'); this.terminal.print(' Role: ' + p.role + ' | Caps: ' + (p.caps || []).join(', '), 'text-muted'); } this.terminal.print('→ ' + parts.length + ' sub-task(s) — run with: orchestrate-parallel "' + goal + '"', 'info'); } // ─── Show Last Parallel Result ─── showParallelResult() { const data = this.terminal.fs.fsReadJSON('orchestrator/parallel-results.json'); if (!data) return this.terminal.print('No parallel results saved yet', 'info'); this.terminal.print('📊 Last Parallel Batch Results', 'header'); this.terminal.print(' Task: ' + (data.parent_task || '').substring(0, 60), 'info'); this.terminal.print(' ' + data.successes + '/' + data.total + ' succeeded' + (data.failures > 0 ? ', ' + data.failures + ' failed' : ''), 'info'); this.terminal.print(' Timestamp: ' + data.timestamp, 'text-muted'); for (const s of (data.summaries || [])) { const icon = s.status === 'ok' ? '✅' : '❌'; const snippet = s.result ? s.result.replace(/\n/g, ' ').substring(0, 80) : (s.error || ''); this.terminal.print(' ' + icon + ' ' + s.task.substring(0, 40) + ' → ' + snippet, 'info'); } } // ─── Consensus Protocol ─── async handleConsensus(args) { if (!args || args.length === 0) return this.terminal.print('Usage: consensus ', 'error'); const question = args.join(' '); if (this._activeConsensus) return this.terminal.print('❌ A consensus round is already in progress — wait for it to resolve', 'error'); const roundId = `consensus-${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 4)}`; const size = this._CONSENSUS_SIZE; this.terminal.print(`🤝 Starting consensus round ${roundId} (${size} agents, strategy: ${this._CONSENSUS_STRATEGY})`, 'header'); this.terminal.print(` Question: ${question.substring(0, 80)}`, 'info'); this._activeConsensus = { id: roundId, question, strategy: this._CONSENSUS_STRATEGY, size, votes: [], startedAt: new Date().toISOString(), resolved: false, result: null }; // Spawn N agents with same question const tasks = []; for (let i = 0; i < size; i++) { tasks.push(`analyze and answer: ${question}. Provide your reasoning then your final answer as "VOTE: "`); } const timeout = setTimeout(() => { if (this._activeConsensus && !this._activeConsensus.resolved) { this._resolveConsensus('timeout'); } }, this._CONSENSUS_TIMEOUT); try { const results = await Promise.allSettled(tasks.map(t => this.delegateTask(t))); for (let i = 0; i < results.length; i++) { const r = results[i]; const voteText = r.status === 'fulfilled' ? (r.value || '') : 'FAILED'; // Extract VOTE: marker or use full text let answer = 'no vote'; const voteMatch = voteText.match(/VOTE:\s*(.+?)(?:\n|$)/i); if (voteMatch) answer = voteMatch[1].trim(); else if (voteText.length > 0 && voteText !== 'FAILED') answer = voteText.substring(0, 60).trim(); this._activeConsensus.votes.push({ agentIdx: i, answer, raw: voteText.substring(0, 200), status: r.status === 'fulfilled' ? 'voted' : 'failed' }); } } catch (e) { this.terminal.print(`⚠️ Consensus error: ${e.message}`, 'warning'); } finally { clearTimeout(timeout); } this._resolveConsensus('complete'); return this._activeConsensus.result; } _resolveConsensus(reason) { const round = this._activeConsensus; if (!round || round.resolved) return; round.resolved = true; round.resolvedAt = new Date().toISOString(); round.resolvedBy = reason; // Count votes const voteCounts = {}; for (const v of round.votes) { if (v.status === 'failed') continue; voteCounts[v.answer] = (voteCounts[v.answer] || 0) + 1; } let winner = null; let maxVotes = 0; let totalVoted = 0; for (const [ans, count] of Object.entries(voteCounts)) { totalVoted += count; if (count > maxVotes) { maxVotes = count; winner = ans; } } if (this._CONSENSUS_STRATEGY === 'majority') { const needed = Math.ceil(round.size / 2); if (maxVotes >= needed) { round.result = { consensus: 'reached', answer: winner, votes: maxVotes, total: round.size }; } else { round.result = { consensus: 'hung', votes: voteCounts, total: round.size }; } } else if (this._CONSENSUS_STRATEGY === 'unanimous') { if (totalVoted === round.size && Object.keys(voteCounts).length === 1) { round.result = { consensus: 'reached', answer: winner, votes: maxVotes, total: round.size }; } else { round.result = { consensus: 'hung', votes: voteCounts, total: round.size }; } } else { // weighted — fallback to majority const needed = Math.ceil(round.size / 2); round.result = maxVotes >= needed ? { consensus: 'reached', answer: winner, votes: maxVotes, total: round.size } : { consensus: 'hung', votes: voteCounts, total: round.size }; } // Store the round this._consensusRounds.push(round); if (!this.terminal.fs.readDir('orchestrator')) this.terminal.fs.mkdir('orchestrator'); this.terminal.fs.writeFile(`orchestrator/consensus-round-${round.id}.json`, JSON.stringify(round, null, 2)); // Print result const r = round.result; if (r.consensus === 'reached') { this.terminal.print(`✅ Consensus reached: "${r.answer}" (${r.votes}/${r.total} votes, ${round.strategy})`, 'success'); } else { this.terminal.print(`⚖️ Consensus hung — no clear winner. Votes: ${JSON.stringify(r.votes)}`, 'warning'); } this._activeConsensus = null; } consensusStatus() { if (this._activeConsensus) { const r = this._activeConsensus; this.terminal.print('🤝 Active Consensus Round', 'header'); this.terminal.print(` ID: ${r.id}`, 'info'); this.terminal.print(` Question: ${r.question.substring(0, 60)}`, 'info'); this.terminal.print(` Strategy: ${r.strategy} | Size: ${r.size}`, 'info'); this.terminal.print(` Votes collected: ${r.votes.length}/${r.size}`, 'info'); for (const v of r.votes) { this.terminal.print(` ${v.status === 'voted' ? '✅' : '❌'} Agent ${v.agentIdx}: ${v.answer}`, 'info'); } } else { this.terminal.print('No active consensus round', 'info'); } } consensusHistory() { const rounds = this._consensusRounds; if (rounds.length === 0) { this.terminal.print('No consensus rounds in memory — checking VFS...', 'info'); // Try VFS const dir = this.terminal.fs.readDir('orchestrator/'); if (dir) { const files = dir.filter(e => e.name && e.name.startsWith('consensus-round-')); if (files.length > 0) { this.terminal.print(`📜 ${files.length} round(s) in VFS`, 'header'); for (const f of files.slice(-5)) { const data = this.terminal.fs.fsReadJSON(`orchestrator/${f.name}`); if (data) { const r = data.result; const status = r?.consensus === 'reached' ? '✅' : '⚖️'; this.terminal.print(` ${status} ${data.id}: "${data.question.substring(0, 40)}" → ${r?.answer || 'no result'}`, 'info'); } } return; } } this.terminal.print('No past consensus rounds found', 'info'); return; } this.terminal.print('📜 Consensus History', 'header'); for (const round of rounds.slice(-10)) { const r = round.result; const status = r?.consensus === 'reached' ? '✅' : '⚖️'; this.terminal.print(` ${status} ${round.id}: "${round.question.substring(0, 40)}"`, 'info'); this.terminal.print(` Strategy: ${round.strategy} | Votes: ${round.votes.length}/${round.size} | Result: ${r?.answer || 'hung'}`, 'text-muted'); } } // ─── Autonomous CI/CD Pipeline ─── async runPipeline(args) { if (!args || args.length === 0) return this.terminal.print('Usage: pipeline [options]', 'error'); const cmd = args[0].toLowerCase(); const opts = args.slice(1); const run = { id: `pipeline-${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 4)}`, command: cmd, startedAt: new Date().toISOString(), stages: [], status: 'running' }; try { if (cmd === 'test') { await this._pipelineStage(run, 'agent-health', () => this._testAgentHealth()); await this._pipelineStage(run, 'a2a-mailbox', () => this._testA2AMailbox()); await this._pipelineStage(run, 'module-check', () => this._testModules()); run.status = 'passed'; this.terminal.print('✅ All pipeline tests passed', 'success'); } else if (cmd === 'deploy') { const deployTask = opts.join(' ') || 'run all checks'; await this._pipelineStage(run, 'test', () => this.runPipeline(['test'])); if (run.stages.find(s => s.name === 'test' && s.status === 'failed')) { if (this._PIPELINE_CONFIG.autoFix) { await this._pipelineStage(run, 'auto-fix', () => this._autoFix()); } else { run.status = 'blocked'; this.terminal.print('❌ Pipeline blocked — tests failed. Set pipeline-auto-fix on to auto-repair', 'error'); return; } } await this._pipelineStage(run, 'verify', () => this._pipelineVerify()); run.status = 'passed'; this.terminal.print('✅ Pipeline deploy passed all stages', 'success'); } else if (cmd === 'verify') { await this._pipelineStage(run, 'verify', () => this._pipelineVerify()); run.status = 'passed'; } else if (cmd === 'history') { this._pipelineHistory.slice(-10).forEach(p => { const icon = p.status === 'passed' ? '✅' : (p.status === 'blocked' ? '🚫' : '❌'); this.terminal.print(` ${icon} ${p.id}: ${p.command} (${p.startedAt.slice(11, 19)})`, 'info'); p.stages.forEach(s => { this.terminal.print(` ${s.status === 'passed' ? '✅' : '❌'} ${s.name} (${(s.duration || 0).toFixed(0)}ms)`, 'text-muted'); }); }); if (this._pipelineHistory.length === 0) this.terminal.print('No pipeline runs yet', 'info'); return; } else if (cmd === 'config') { this.terminal.print('Pipeline Config:', 'header'); this.terminal.print(` auto-fix: ${this._PIPELINE_CONFIG.autoFix ? 'on' : 'off'}`, 'info'); this.terminal.print(` stage-timeout: ${this._PIPELINE_CONFIG.stageTimeout}ms`, 'info'); return; } else { return this.terminal.print('Unknown pipeline command. Usage: test, deploy, verify, history, config', 'error'); } } catch (e) { run.status = 'failed'; run.error = e.message; this.terminal.print(`❌ Pipeline failed: ${e.message}`, 'error'); } run.completedAt = new Date().toISOString(); this._pipelineHistory.push(run); if (!this.terminal.fs.readDir('orchestrator')) this.terminal.fs.mkdir('orchestrator'); this.terminal.fs.writeFile(`orchestrator/pipeline-${run.id}.json`, JSON.stringify(run, null, 2)); return run; } async _pipelineStage(run, name, fn) { const stage = { name, status: 'running', startedAt: Date.now() }; run.stages.push(stage); this.terminal.print(`⏳ Pipeline stage: ${name}...`, 'info'); try { const timeout = this._PIPELINE_CONFIG.stageTimeout; const result = await Promise.race([ fn(), new Promise((_, reject) => setTimeout(() => reject(new Error(`${name} timed out (${timeout}ms)`)), timeout)) ]); stage.status = 'passed'; stage.duration = Date.now() - stage.startedAt; stage.result = typeof result === 'string' ? result.substring(0, 200) : 'ok'; this.terminal.print(`✅ Stage ${name} passed (${stage.duration}ms)`, 'success'); return result; } catch (e) { stage.status = 'failed'; stage.duration = Date.now() - stage.startedAt; stage.error = e.message; this.terminal.print(`❌ Stage ${name} failed: ${e.message}`, 'error'); throw e; } } _testAgentHealth() { const total = Object.keys(this._agents).length; const busy = Object.values(this._agents).filter(a => a.status === 'busy').length; const failed = Object.values(this._agents).filter(a => a.status === 'failed').length; const issues = []; if (failed > 0) issues.push(`${failed} agent(s) in failed state`); if (total > 10) issues.push(`high agent count: ${total}`); if (issues.length > 0) throw new Error(issues.join('; ')); this.terminal.print(` Agents: ${total} (${busy} busy, ${failed} failed) — healthy`, 'info'); return `agents_ok:${total}:${busy}:${failed}`; } _testA2AMailbox() { // Verify A2A polling is running if (!this._A2A_POLLING) throw new Error('A2A polling is disabled'); if (!this._a2aPollTimer) throw new Error('A2A polling timer not running'); // Verify each agent has a VFS directory for (const [id, agent] of Object.entries(this._agents)) { const dir = this.terminal.fs.readDir(`/agents/${id}/`); if (!dir) throw new Error(`Agent ${id} missing VFS directory`); } this.terminal.print(` A2A: ${Object.keys(this._agents).length} agent mailboxes OK`, 'info'); return `a2a_ok:${Object.keys(this._agents).length}`; } _testModules() { const mm = this.terminal.moduleManager; if (!mm || !mm.modules) throw new Error('ModuleManager not available'); const required = ['OrchestratorModule', 'AgentRegistryModule', 'LeaseModule', 'MemoryGraphModule']; const missing = required.filter(n => !mm.modules[n]); if (missing.length > 0) throw new Error(`Missing modules: ${missing.join(', ')}`); // Check orchestrator is in registry const registry = mm.modules['AgentRegistryModule']; if (registry && !registry.agents?.find(a => a.id === 'orchestrator-0')) { this.terminal.print(' ⚠️ orchestrator-0 not in registry (will register on next init)', 'warning'); } this.terminal.print(` Modules: ${required.length}/${required.length} required loaded`, 'info'); return `modules_ok:${required.length}`; } _pipelineVerify() { // Verify running state — internal checks const issues = []; // Check orchestrator status const total = Object.keys(this._agents).length; const failed = Object.values(this._agents).filter(a => a.status === 'failed').length; if (failed > 0) issues.push(`${failed} failed agents`); if (total + failed > 0 && failed === total) issues.push('all agents failed'); // Verify last parallel result exists const lastResult = this.terminal.fs.fsReadJSON('orchestrator/parallel-results.json'); if (!lastResult) this.terminal.print(' ℹ️ No parallel results yet (first deploy)', 'info'); // Verify consensus history const consensusDir = this.terminal.fs.readDir('orchestrator/'); if (consensusDir) { const rounds = consensusDir.filter(e => e.name && e.name.startsWith('consensus-round-')); this.terminal.print(` VFS: ${rounds.length} consensus rounds archived`, 'info'); } // Check memory graph const memgraph = this.terminal.moduleManager?.modules['MemoryGraphModule']; if (memgraph && typeof memgraph.getStats === 'function') { const stats = memgraph.getStats(); this.terminal.print(` Memory graph: ${stats?.edges || 0} edges, ${stats?.nodes || 0} nodes`, 'info'); } if (issues.length > 0) throw new Error(issues.join('; ')); return `verify_ok:${total} agents, ${issues.length} issues`; } async _autoFix() { this.terminal.print('🔄 Auto-fix running...', 'warning'); let fixed = 0; // Fix 1: Purge failed agents const failedIds = Object.entries(this._agents) .filter(([, a]) => a.status === 'failed') .map(([id]) => id); for (const id of failedIds) { this.killAgent(id); fixed++; } if (fixed > 0) this.terminal.print(` Purged ${fixed} failed agent(s)`, 'warning'); // Fix 2: Restart A2A poller if stopped if (!this._A2A_POLLING) { this._A2A_POLLING = true; this._a2aPollTimer = this.terminal.moduleManager._setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL); this.terminal.print(' Restarted A2A polling', 'warning'); fixed++; } // Fix 3: Ensure VFS orchestrator dir exists if (!this.terminal.fs.readDir('orchestrator')) { this.terminal.fs.mkdir('orchestrator'); fixed++; } if (fixed === 0) this.terminal.print(' No auto-fix actions needed', 'info'); return `fixed ${fixed} issues`; } // ─── Task Delegation ─── async delegateTask(task, opts = {}) { if (!task) return this.terminal.print('Usage: orchestrate ', 'error'); this._delegations = (this._delegations || 0) + 1; // Parallel fan-out: decompose and run sub-tasks concurrently if (opts.parallel) { const parts = this._decomposeGoal(task); if (parts.length > 1) { this.terminal.print('🧩 Decomposed into ' + parts.length + ' sub-tasks — running in parallel', 'info'); // Auto-scale: ensure enough idle agents for N sub-tasks const idleCount = Object.values(this._agents).filter(a => a.status === 'idle').length; const totalSlots = this._SPAWN_LIMIT; const usedSlots = Object.keys(this._agents).length; const availableSlots = totalSlots - usedSlots; const needed = parts.length - idleCount; if (needed > 0 && availableSlots > 0) { const toSpawn = Math.min(needed, availableSlots); this.terminal.print('📈 Auto-scaling: spawning ' + toSpawn + ' pre-warm agent(s)', 'info'); for (let i = 0; i < toSpawn; i++) { this.spawnAgent('worker', ['list_files', 'read_file', 'write_file', 'web_search']); } } const results = await Promise.allSettled(parts.map(sub => this.delegateTask(sub.goal))); const successes = results.filter(r => r.status === 'fulfilled').length; const failures = results.filter(r => r.status === 'rejected').length; // Result synthesis: collect outbox results from each sub-task's agent const summaries = []; for (let i = 0; i < parts.length; i++) { const sub = parts[i]; const r = results[i]; if (r.status === 'fulfilled') { // Find the agent that handled this sub-task const agent = Object.values(this._agents).find(a => a.currentTask === sub.goal || (a.lastTask === sub.goal)); if (!agent) { // "ok / completed (agent recycled)" told the caller a task succeeded // when we could not even find its agent. summaries.push({ task: sub.goal, status: 'failed', error: 'agent not found — result unverifiable' }); continue; } const outbox = this.terminal.fs.fsReadJSON(`/agents/${agent.id}/outbox.json`); const body = outbox && typeof outbox.result === 'string' ? outbox.result : ''; const snippet = body.substring(0, 200); summaries.push({ task: sub.goal, agent: agent.id, status: body ? 'ok' : 'failed', result: body || null, error: body ? undefined : (outbox && outbox.error) || 'no result in outbox' }); } else { summaries.push({ task: sub.goal, status: 'failed', error: r.reason?.message || 'unknown error' }); } } // Store merged result to VFS for persistence const merged = { type: 'parallel_result', parent_task: task, timestamp: new Date().toISOString(), total: parts.length, successes, failures, summaries }; if (!this.terminal.fs.readDir('orchestrator')) this.terminal.fs.mkdir('orchestrator'); this.terminal.fs.writeFile('orchestrator/parallel-results.json', JSON.stringify(merged, null, 2)); // Print synthesis to terminal this.terminal.print('📊 Parallel Batch Results:', 'header'); for (const s of summaries) { const icon = s.status === 'ok' ? '✅' : '❌'; const agentTag = s.agent ? ' [' + s.agent.substring(0, 20) + ']' : ''; const snippet = s.result ? s.result.replace(/\n/g, ' ').substring(0, 60) : ''; this.terminal.print(' ' + icon + ' ' + s.task.substring(0, 35) + agentTag + ' → ' + snippet, 'info'); } this.terminal.print('✅ Parallel batch: ' + successes + '/' + parts.length + ' succeeded' + (failures > 0 ? ', ' + failures + ' failed' : ''), 'header'); return merged; } // Single sub-task — fall through to normal path } // Declared in function scope: the AI branch assigns it, but the return at the end of // this method runs even when that branch never did. let outboxResult = null; // Find an idle agent and immediately stamp it busy (atomic claim) let agent = Object.values(this._agents).find(a => a.status === 'idle'); if (agent) { agent.status = 'busy'; } // Auto-spawn with role-derived capabilities if (!agent) { if (Object.keys(this._agents).length >= this._SPAWN_LIMIT) { // Try finding any non-failed agent to reuse const reuse = Object.values(this._agents).find(a => a.status !== 'failed' && a.status !== 'sleeping'); if (reuse) { reuse.status = 'busy'; agent = reuse; } } if (!agent) { const role = this._deriveRole(task); const caps = this._deriveCapabilities(task); agent = this.spawnAgent(role, caps); if (!agent) return this.terminal.print('❌ No available agents — spawn limit reached', 'error'); agent.status = 'busy'; // claim immediately after spawn } } if (!agent) return this.terminal.print('❌ No available agents', 'error'); // A2A Contract handshake const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; let contractValid = true; if (registry) { const result = registry.validateTask(agent.role, task); if (!result.valid) { this.terminal.print(`⚠️ A2A validation: ${result.reason}`, 'warning'); contractValid = false; } else if (result.reason === 'matched') { this.terminal.print(`✅ A2A contract matched for role '${agent.role}'`, 'success'); } } // Create a lease — with --write if agent has write capabilities const leaseModule = this.terminal.moduleManager.modules['LeaseModule']; const hasWriteCap = (agent.capabilities || []).some(function(c) { return c === 'write_file' || c === 'edit_module' || c === 'delete_file'; }); const leaseArgs = ['lease-create', agent.id, '60', task]; if (hasWriteCap) leaseArgs.push('--write'); const lease = leaseModule ? leaseModule.createLease(leaseArgs) : null; // Mark agent as busy agent.status = 'busy'; agent.currentTask = task; agent.leaseId = lease?.id; agent.lastHeartbeat = Date.now(); agent.tasksAssigned = (agent.tasksAssigned || 0) + 1; agent.lastTaskAt = new Date().toISOString(); // Send A2A request via inbox this.sendA2AMessage([agent.id, JSON.stringify({ type: 'request', payload: { task, from: 'orchestrator-0', leaseId: lease?.id } })]); this.terminal.print(`🎯 Delegated \"${task.substring(0, 60)}\" to ${agent.id} (lease: ${lease?.id || 'none'})`, 'info'); // Execute via AI assistant try { const aiModule = this.terminal.moduleManager.modules['WebX11AIAssistantModule']; if (aiModule && typeof aiModule.chat === 'function') { const _agentTag = agent.id + '/' + (agent.role || 'worker'); const result = await aiModule.chat([`[DELEGATED] ${task}`], { agentTag: _agentTag }); try { aiModule._sessions.delete(_agentTag); } catch (_e) {} // chat() resolves {content, toolTurns}. Anything that is not a non-empty answer // is a failure — the old code substituted the literal "completed", so a worker // that returned nothing was indistinguishable from one that finished. const _answer = typeof result === 'string' ? result : ((result && result.content) || ''); agent.status = _answer ? 'completed' : 'failed'; // Write result to outbox this.terminal.fs.writeFile(`agents/${agent.id}/outbox.json`, JSON.stringify({ result: _answer || null, status: _answer ? 'completed' : 'failed', error: _answer ? null : 'agent produced no answer', timestamp: new Date().toISOString() })); // Record in memory graph const memgraph = this.terminal.moduleManager.modules['MemoryGraphModule']; if (memgraph) { memgraph.recordTask(task, _answer, agent.id); memgraph.recordFileAccess(`/agents/${agent.id}/outbox.json`); } this.terminal.print(`✅ ${agent.id} completed task`, 'success'); // Return result for callers that await delegateTask outboxResult = _answer; // '' when the worker returned nothing; callers see that // Check restart policy — if always, set back to idle for next task if (agent.restartPolicy === 'always') { agent.lastTask = agent.currentTask; agent.status = 'idle'; agent.currentTask = null; agent.lastHeartbeat = Date.now(); } } else { this.terminal.print(`ℹ️ AI module not available — task queued for ${agent.id}`, 'info'); outboxResult = 'queued — AI module not loaded, nothing executed'; } } catch (e) { this._handleAgentFailure(agent.id, e.message); } // Return to idle pool (unless failed/restarting) if (agent.status === 'failed') { // handled by _handleAgentFailure throw new Error(agent.lastError || 'agent failed'); } else if (agent.status !== 'busy') { agent.lastTask = agent.currentTask; agent.currentTask = null; agent.status = 'idle'; agent.lastHeartbeat = Date.now(); } return outboxResult || 'no result recorded'; } // ─── Recurring Scheduling ─── addSchedule(args) { if (args.length < 2) return this.terminal.print('Usage: orchestrator-schedule ', 'error'); const role = args[0]; const interval = parseInt(args[1]); if (isNaN(interval) || interval < 10) return this.terminal.print('Interval must be >= 10 seconds', 'error'); const task = args.slice(2).join(' '); if (!task) return this.terminal.print('Task description required', 'error'); const schedule = { id: 'sched-' + Date.now(), role: role, interval: interval * 1000, task: task, createdAt: new Date().toISOString(), lastRun: null, nextRun: Date.now() + interval * 1000, runs: 0 }; this._schedules.push(schedule); this._saveSchedules(); this._startScheduleTimer(); this.terminal.print('📅 Schedule created: "' + task.substring(0, 40) + '" every ' + interval + 's (role: ' + role + ')', 'success'); return schedule; } listSchedules() { if (this._schedules.length === 0) return this.terminal.print('No active schedules', 'info'); this.terminal.print('📅 Recurring Schedules (' + this._schedules.length + '):', 'header'); for (const s of this._schedules) { const nextIn = Math.max(0, Math.round((s.nextRun - Date.now()) / 1000)); this.terminal.print(' ' + s.id + ' — "' + s.task.substring(0, 40) + '" every ' + (s.interval / 1000) + 's [' + s.role + '] (next: ' + nextIn + 's, runs: ' + s.runs + ')', 'info'); } } removeSchedule(id) { const idx = this._schedules.findIndex(function(s) { return s.id === id; }); if (idx === -1) return this.terminal.print('Schedule ' + id + ' not found', 'error'); this._schedules.splice(idx, 1); this._saveSchedules(); if (this._schedules.length === 0 && this._scheduleTimer) { clearInterval(this._scheduleTimer); this._scheduleTimer = null; } this.terminal.print('Schedule ' + id + ' removed', 'success'); } _startScheduleTimer() { if (this._scheduleTimer) return; this._scheduleTimer = setInterval(function() { var now = Date.now(); for (var i = 0; i < this._schedules.length; i++) { var s = this._schedules[i]; if (now >= s.nextRun) { s.lastRun = new Date().toISOString(); s.nextRun = now + s.interval; s.runs++; this.terminal.print('📅 Schedule trigger: "' + s.task.substring(0, 40) + '"', 'info'); this.delegateTask(s.task); } } this._saveSchedules(); }.bind(this), 5000); } _saveSchedules() { try { this.terminal.fs.fsWriteJSON('/orchestrator/schedules.json', this._schedules); } catch (e) { console.warn('[Orchestrator] Schedule save error:', e.message); } } _loadSchedules() { try { const data = this.terminal.fs.fsReadJSON('/orchestrator/schedules.json'); if (data && Array.isArray(data)) { this._schedules = data; if (this._schedules.length > 0) this._startScheduleTimer(); } } catch (e) { this._schedules = []; } } _preSpawnDefaultAgents() { // Spawn the default team ONCE per browser profile. Stable ids (`agent-`) mean a // reload re-adopts the existing dir in _reanimateFromVFS() instead of writing a new one. var roles = [ { role: 'auto-learner', caps: ['list_files', 'read_file', 'write_file', 'view_module'] }, { role: 'kernel-architect', caps: ['list_files', 'read_file', 'write_file', 'run_command'] }, { role: 'searcher', caps: ['list_files', 'read_file', 'web_search'] } ]; for (var i = 0; i < roles.length; i++) { var r = roles[i]; // Already live (fresh spawn this boot, or adopted from VFS) → just keep it alive var existing = Object.values(this._agents).find(function(a) { return a.role === r.role; }); if (existing) { existing.restartPolicy = 'always'; continue; } var id = 'agent-' + this._sanitizeId(r.role); var agent = this.spawnAgent(r.role, r.caps, null, id); if (agent) { agent.restartPolicy = 'always'; this.terminal.print('🤖 Pre-spawned agent: ' + r.role + ' (' + r.caps.length + ' caps)', 'info'); } } } // ─── Monitoring ─── _checkHeartbeats() { const now = Date.now(); Object.entries(this._agents).forEach(([id, agent]) => { if (agent.status === 'busy' && now - agent.lastHeartbeat > this._HEARTBEAT_TIMEOUT) { this.terminal.print(`⚠️ Agent '${id}' unresponsive (${Math.round((now - agent.lastHeartbeat) / 1000)}s since heartbeat)`, 'warning'); } }); } _sweepIdle() { const now = Date.now(); Object.entries(this._agents).forEach(([id, agent]) => { if (agent.status === 'idle' && now - agent.lastHeartbeat > this._IDLE_TIMEOUT) { this.terminal.print(`💤 Agent '${id}' idle for >${this._IDLE_TIMEOUT/1000}s — auto-sleeping`, 'info'); this.sleepAgent(id); } }); } purgeDeadAgents() { const now = Date.now(); let count = 0; Object.entries(this._agents).forEach(([id, agent]) => { if (now - agent.lastHeartbeat > this._DEAD_PURGE_TIMEOUT) { this.killAgent(id); count++; } }); this.terminal.print(`🧹 Purged ${count} dead agents`, count > 0 ? 'warning' : 'info'); } // ─── Status & List ─── showStatus() { const total = Object.keys(this._agents).length; const busy = Object.values(this._agents).filter(a => a.status === 'busy').length; const idle = Object.values(this._agents).filter(a => a.status === 'idle').length; const sleeping = Object.values(this._agents).filter(a => a.status === 'sleeping').length; const failed = Object.values(this._agents).filter(a => a.status === 'failed' || a.status === 'restarting').length; const a2aTotal = Object.values(this._a2aMessages).reduce((s, msgs) => s + msgs.length, 0); const a2aPending = Object.values(this._a2aMessages).reduce((s, msgs) => s + msgs.filter(m => !m._processed).length, 0); this.terminal.print('🧠 Orchestrator Status', 'header'); 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'); // v4.6.63: say what the state MEANS. A parked agent with 0 assignments and a parked // agent between tasks looked identical from outside (packet §6). const agentsArr = Object.values(this._agents); const available = agentsArr.filter(a => a.status === 'idle' || a.status === 'sleeping').length; const describe = (a) => { const n = a.tasksAssigned || 0; const since = a.createdAt ? String(a.createdAt).slice(0, 16).replace('T', ' ') : '?'; const last = a.lastTaskAt ? String(a.lastTaskAt).slice(0, 16).replace('T', ' ') : null; if (a.status === 'busy') { return 'busy' + (a.currentTask ? ' — ' + String(a.currentTask).substring(0, 40) : ''); } if (a.status === 'failed' || a.status === 'restarting') { return a.status + ` — ${a.failureCount || 0} failure(s), ${n} task(s) ever`; } if (n === 0) { return (a.status === 'sleeping' ? 'parked' : a.status) + ` — available, no delegations yet (spawned ${since})`; } return (a.status === 'sleeping' ? 'parked' : a.status) + ` — ${n} delegation(s), last ${last || '?'}`; }; agentsArr.forEach(a => { 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}] — ${describe(a)}${modelInfo}${backoff}`, 'info'); }); // v4.6.68: on-demand by design — a worker with nothing to do is healthy, not broken. // The number to watch is how many delegations the AI actually made. this.terminal.print(` Mode: on-demand — ${available}/${total} worker(s) available, ${this._delegations || 0} delegation(s) this session`, 'info'); if (total > 0 && (this._delegations || 0) === 0) { this.terminal.print(' Workers wait for delegate_task — spawn-on-demand, no background dispatch queue by design.', 'text-muted'); } } listAgents() { const all = Object.values(this._agents); if (all.length === 0) return this.terminal.print('No agents spawned', 'info'); this.terminal.print(`📋 Agents (${all.length}):`, 'header'); all.forEach(a => { const caps = (a.capabilities || []).join(', '); this.terminal.print(` ${a.id} [${a.role}] — ${a.status}`, 'info'); this.terminal.print(` Caps: ${caps}`, 'text-muted'); this.terminal.print(` Policy: ${a.restartPolicy || this._RESTART_POLICY} | Failures: ${a.failureCount || 0} | Restarts: ${a.restartCount || 0}`, 'text-muted'); if (a.currentTask) this.terminal.print(` Task: ${a.currentTask.substring(0, 60)}`, 'text-muted'); }); } } // Required for module system if (typeof window !== 'undefined') { window.OrchestratorModule = OrchestratorModule; }