aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-orchestrator.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-06-06 06:00:27 +0200
committerAgando <roduccomm@gmail.com>2026-06-06 06:00:27 +0200
commitccc7d9956559d2195e20345e7a794662b52cd403 (patch)
tree757354fbfec5c75b4577b2cb620d5ab1582e19b1 /modules/core/module-orchestrator.js
parentb538577cd0a4292e4cd4ca1c9f9829fc7e133caa (diff)
auto: 2026-06-06_06:00 changes
Diffstat (limited to 'modules/core/module-orchestrator.js')
-rw-r--r--modules/core/module-orchestrator.js108
1 files changed, 91 insertions, 17 deletions
diff --git a/modules/core/module-orchestrator.js b/modules/core/module-orchestrator.js
index ec385bb..05e1598 100644
--- a/modules/core/module-orchestrator.js
+++ b/modules/core/module-orchestrator.js
@@ -13,10 +13,32 @@ class OrchestratorModule {
this._IDLE_TIMEOUT = 120000;
this._RESTART_POLICY = 'never';
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 = {};
this._restartBackoffs = {};
+ this._eventBus = {};
+ this._idleSweepInterval = 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.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; } } 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,
+ a2aPollInterval: this._A2A_POLL_INTERVAL,
+ heartbeatCheck: this._HEARTBEAT_CHECK,
+ idleSweepInterval: this._IDLE_SWEEP_INTERVAL,
+ deadPurgeTimeout: this._DEAD_PURGE_TIMEOUT
+ }));
}
registerCommands() {
this.commands['orchestrate'] = (args) => this.delegateTask(args.join(' '));
@@ -39,7 +61,7 @@ class OrchestratorModule {
'orchestrator-spawn': 'orchestrator-spawn [role] [capabilities...] — Manually spawn a new agent',
'orchestrator-kill': 'orchestrator-kill <agent-id> — Terminate an agent and revoke leases',
'orchestrator-purge': 'orchestrator-purge — Remove dead agents (>60s since heartbeat)',
- 'orchestrator-config': 'orchestrator-config <key> <value> — Tune: spawn-limit, spawn-strategy, heartbeat-timeout, idle-timeout, restart-policy, a2a-polling',
+ 'orchestrator-config': 'orchestrator-config <key> <value> — 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 <agent-id> — Suspend agent to VFS',
'orchestrator-wake': 'orchestrator-wake <agent-id> — Restore agent from VFS sleep',
@@ -59,13 +81,39 @@ class OrchestratorModule {
this._reanimateFromVFS();
// A2A mailbox polling loop (500ms)
if (this._A2A_POLLING) {
- this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), 500);
+ this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
}
- // Heartbeat watchdog every 3 seconds
- this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), 3000);
- // Idle timeout sweep every 30s
- setInterval(() => this._sweepIdle(), 30000);
+ // Heartbeat watchdog
+ this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), this._HEARTBEAT_CHECK);
+ // Idle timeout sweep
+ this._idleSweepInterval = setInterval(() => this._sweepIdle(), this._IDLE_SWEEP_INTERVAL);
this.terminal.print('🧠 Orchestrator loaded — A2A mailbox transport active', 'success');
+ // 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 ───
@@ -104,6 +152,10 @@ class OrchestratorModule {
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() {
@@ -178,8 +230,12 @@ class OrchestratorModule {
// ─── Agent Lifecycle ───
+ _sanitizeId(name) {
+ return (name || 'worker').replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').substring(0, 32) || 'worker';
+ }
spawnAgent(role, capabilities) {
- const id = `agent-${Object.keys(this._agents).length + 1}-${role || 'worker'}`;
+ const safeRole = this._sanitizeId(role);
+ const id = `agent-${Object.keys(this._agents).length + 1}-${safeRole}`;
if (Object.keys(this._agents).length >= this._SPAWN_LIMIT) {
this.terminal.print(`❌ Agent spawn limit (${this._SPAWN_LIMIT}) reached`, 'error');
return null;
@@ -343,13 +399,26 @@ class OrchestratorModule {
_deriveCapabilities(task) {
const caps = ['list_files', 'read_file'];
const lower = task.toLowerCase();
- if (lower.includes('search') || lower.includes('find') || lower.includes('look up') || lower.includes('web')) caps.push('web_search');
- if (lower.includes('write') || lower.includes('create') || lower.includes('save') || lower.includes('edit')) caps.push('write_file');
- if (lower.includes('delete') || lower.includes('remove') || lower.includes('rm ')) caps.push('delete_file');
- if (lower.includes('install') || lower.includes('package') || lower.includes('wpm')) caps.push('wpm_install');
- if (lower.includes('module') || lower.includes('view') || lower.includes('edit')) caps.push('view_module', 'edit_module');
- if (lower.includes('list') || lower.includes('status') || lower.includes('health')) caps.push('get_status');
- if (lower.includes('fetch') || lower.includes('url') || lower.includes('http')) caps.push('web_fetch');
+ 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)];
}
@@ -381,13 +450,17 @@ class OrchestratorModule {
'a2a-polling': () => {
this._A2A_POLLING = val === 'on' || val === 'true';
if (this._A2A_POLLING && !this._a2aPollTimer) {
- this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), 500);
+ this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL);
} else if (!this._A2A_POLLING && this._a2aPollTimer) {
clearInterval(this._a2aPollTimer);
this._a2aPollTimer = null;
}
return `a2a-polling ${this._A2A_POLLING ? 'on' : 'off'}`;
- }
+ },
+ 'a2a-poll-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 50 || n > 30000) return 'Must be 50-30000ms'; clearInterval(this._a2aPollTimer); this._a2aPollTimer = null; this._A2A_POLL_INTERVAL = n; if (this._A2A_POLLING) { this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), n); } return `a2a-poll-interval set to ${n}ms`; },
+ 'heartbeat-check': () => { const n = parseInt(val); if (isNaN(n) || n < 500 || n > 60000) return 'Must be 500-60000ms'; clearInterval(this._heartbeatInterval); this._HEARTBEAT_CHECK = n; this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), n); return `heartbeat-check set to ${n}ms`; },
+ 'idle-sweep-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; clearInterval(this._idleSweepInterval); this._IDLE_SWEEP_INTERVAL = n; this._idleSweepInterval = setInterval(() => this._sweepIdle(), n); return `idle-sweep-interval set to ${n}ms`; },
+ '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`; }
};
if (!configMap[key]) {
@@ -395,6 +468,7 @@ class OrchestratorModule {
}
const result = configMap[key]();
this.terminal.print(`✅ ${result}`, 'success');
+ this._saveConfig();
}
// ─── Task Delegation ───
@@ -522,7 +596,7 @@ class OrchestratorModule {
const now = Date.now();
let count = 0;
Object.entries(this._agents).forEach(([id, agent]) => {
- if (now - agent.lastHeartbeat > 60000) {
+ if (now - agent.lastHeartbeat > this._DEAD_PURGE_TIMEOUT) {
this.killAgent(id);
count++;
}