aboutsummaryrefslogtreecommitdiff
path: root/modules/core
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-06-05 18:01:01 +0200
committerAgando <roduccomm@gmail.com>2026-06-05 18:01:01 +0200
commitb538577cd0a4292e4cd4ca1c9f9829fc7e133caa (patch)
tree04d76bf4f0d680cdb26b1a205481bc8628cd77b4 /modules/core
parent5092f9a0ce02691dc476d7b64fa731a1518dd834 (diff)
auto: 2026-06-05_18:01 changes
Diffstat (limited to 'modules/core')
-rw-r--r--modules/core/module-aiassist.js48
-rw-r--r--modules/core/module-loader.js6
-rw-r--r--modules/core/module-memory-graph.js400
-rw-r--r--modules/core/module-orchestrator.js474
-rw-r--r--modules/core/module-predictor.js279
-rw-r--r--modules/core/module-sandbox.js332
-rw-r--r--modules/core/module-world-model.js381
-rw-r--r--modules/core/sandbox-worker.js205
-rw-r--r--modules/core/terminal.js4
9 files changed, 2092 insertions, 37 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index cb1482f..b93b12d 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -572,6 +572,28 @@ RULES:
} catch (e) {}
}
+ // Inject memory graph context for cross-session awareness
+ try {
+ const memgraph = this.terminal.moduleManager.modules['MemoryGraphModule'];
+ if (memgraph) {
+ const mctx = memgraph.getContextForPrompt();
+ if (mctx) {
+ messages.push({ role: 'system', content: mctx });
+ }
+ }
+ } catch (e) { /* memory graph not available */ }
+
+ // Inject world model context for system self-awareness
+ try {
+ const wm = this.terminal.moduleManager.modules['WorldModelModule'];
+ if (wm) {
+ const wctx = wm.getContextForPrompt();
+ if (wctx) {
+ messages.push({ role: 'system', content: wctx });
+ }
+ }
+ } catch (e) { /* world model not available */ }
+
loopResult = await this.agenticLoop(messages);
this.context.push({ role: 'user', content: userMessage });
if (loopResult.finalAnswer) {
@@ -1497,7 +1519,7 @@ RULES:
if (resp.ok) {
const src = await resp.text();
this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, src);
- return src.substring(0, 8000);
+ return src;
}
} catch (e) { /* fall through */ }
}
@@ -1519,7 +1541,7 @@ RULES:
if (resp.ok) {
const src = await resp.text();
this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, src);
- return src.substring(0, 8000);
+ return src;
}
}
}
@@ -1542,6 +1564,28 @@ RULES:
}
} catch (e) {}
}
+ // Fallback: use modules-index.json for core modules (not in registry)
+ if (!code) {
+ try {
+ const idxResp = await fetch('/spaces/newos4/modules-index.json?v=' + (localStorage.getItem('webx11_cache_ver') || ''));
+ if (idxResp.ok) {
+ const idx = await idxResp.json();
+ let match = Object.values(idx.modules).find(m => m.class === modName);
+ if (!match) match = idx.modules[modName];
+ if (match) {
+ const filePath = match.file;
+ const fetchUrl = `/spaces/newos4/${filePath}`;
+ const resp = await fetch(fetchUrl);
+ if (resp.ok) {
+ code = await resp.text();
+ this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, code);
+ this.terminal.fs.writeFile(`_mods/${modName}.js`, code);
+ this.terminal.fs.writeFile(`_mods/${modName}.meta.json`, JSON.stringify({ originalUrl: fetchUrl, modifiedAt: new Date().toISOString(), editCount: 0 }));
+ }
+ }
+ }
+ } catch (e) {}
+ }
if (code) this.terminal.openCodeEditor(modName, url || '', code);
return code ? `Editor opened for ${modName} (${code.length} chars)` : JSON.stringify({ error: `Could not load source for ${modName}` });
}
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index e7fc64a..b32577d 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -33,6 +33,10 @@
'module-agent-registry.js',
'module-leases.js',
'module-orchestrator.js',
+ 'module-sandbox.js',
+ 'module-memory-graph.js',
+ 'module-predictor.js',
+ 'module-world-model.js',
'module-neural-vfs.js'
];
@@ -102,7 +106,7 @@
window.__moduleLoaderReload = reloadWithSelection;
-const LATEST_VER = '2026-06-03-neuralvfs';
+const LATEST_VER = '2026-06-05-v4.4.0';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
diff --git a/modules/core/module-memory-graph.js b/modules/core/module-memory-graph.js
new file mode 100644
index 0000000..2382a4a
--- /dev/null
+++ b/modules/core/module-memory-graph.js
@@ -0,0 +1,400 @@
+// module-memory-graph.js — Cross-session persistent memory graph
+// Nodes: files, concepts, tasks, patterns, users
+// Edges: relationships with weight, last_used
+// Commands: memgraph-status, memgraph-search, memgraph-stats, memgraph-export, memgraph-prune
+class MemoryGraphModule {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ this._nodes = {};
+ this._edges = [];
+ this._indexPath = '/.memory_graph/';
+ this._debounceTimer = null;
+ this._initialized = false;
+ this._totalNodeAccesses = 0;
+ }
+
+ getHelpInfo() {
+ return {
+ 'memgraph-status': 'memgraph-status — Show node/edge counts, last access',
+ 'memgraph-search': 'memgraph-search <query> — Find nodes by label or type',
+ 'memgraph-stats': 'memgraph-stats — Most connected nodes, strongest edges, cluster info',
+ 'memgraph-export': 'memgraph-export — Export graph as JSON',
+ 'memgraph-prune': 'memgraph-prune — Remove stale low-weight edges (>30d, <3 accesses)',
+ };
+ }
+
+ registerCommands() {
+ this.commands['memgraph-status'] = () => this.showStatus();
+ this.commands['memgraph-search'] = (args) => this.search(args.join(' '));
+ this.commands['memgraph-stats'] = () => this.showStats();
+ this.commands['memgraph-export'] = () => this.exportGraph();
+ this.commands['memgraph-prune'] = () => this.prune();
+ }
+
+ init(terminal) {
+ this.terminal = terminal;
+ this._loadFromVFS();
+ this._initialized = true;
+ this.terminal.print('🧠 Memory Graph loaded — cross-session context active', 'info');
+ // Log startup for graph
+ this.addNode('session', `session-${Date.now()}`, `Session ${new Date().toLocaleDateString()}`);
+ }
+
+ // ─── Node API ───
+
+ addNode(type, id, label, metadata = {}) {
+ const key = `${type}:${id}`;
+ if (!this._nodes[key]) {
+ this._nodes[key] = {
+ id: key,
+ type,
+ label: label || id,
+ metadata: metadata || {},
+ created: new Date().toISOString(),
+ updated: new Date().toISOString(),
+ access_count: 1
+ };
+ } else {
+ this._nodes[key].updated = new Date().toISOString();
+ this._nodes[key].access_count++;
+ if (metadata && Object.keys(metadata).length > 0) {
+ Object.assign(this._nodes[key].metadata, metadata);
+ }
+ }
+ this._totalNodeAccesses++;
+ this._debounceSave();
+ return key;
+ }
+
+ getNode(type, id) {
+ return this._nodes[`${type}:${id}`] || null;
+ }
+
+ findNodes(query) {
+ const q = query.toLowerCase();
+ return Object.values(this._nodes).filter(n =>
+ n.label.toLowerCase().includes(q) ||
+ n.type.toLowerCase().includes(q) ||
+ (n.id && n.id.toLowerCase().includes(q))
+ );
+ }
+
+ // ─── Edge API ───
+
+ addEdge(sourceKey, targetKey, type, weight = 1) {
+ // Check if edge already exists
+ let edge = this._edges.find(e =>
+ e.source === sourceKey && e.target === targetKey && e.type === type
+ );
+ if (edge) {
+ edge.weight = (edge.weight + weight) / 2;
+ edge.last_used = new Date().toISOString();
+ edge.use_count++;
+ } else {
+ this._edges.push({
+ source: sourceKey,
+ target: targetKey,
+ type,
+ weight,
+ created: new Date().toISOString(),
+ last_used: new Date().toISOString(),
+ use_count: 1
+ });
+ }
+ this._debounceSave();
+ }
+
+ // ─── Event-based Learning ───
+
+ recordFileAccess(path) {
+ const name = path.split('/').pop() || path;
+ const nodeKey = this.addNode('file', path, name);
+ const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
+ this.addEdge(sessionKey, nodeKey, 'accessed', 0.5);
+
+ // Extract file extension edge
+ const ext = path.split('.').pop();
+ if (ext && ext.length < 10) {
+ this.addNode('type', ext, `.${ext} files`);
+ this.addEdge(nodeKey, `type:${ext}`, 'has_type', 0.3);
+ }
+ }
+
+ recordToolCall(toolName, success) {
+ const nodeKey = this.addNode('tool', toolName, toolName);
+ const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
+ this.addEdge(sessionKey, nodeKey, success ? 'used' : 'failed', success ? 0.5 : 0.8);
+ }
+
+ recordConcept(concept, filePath) {
+ const conceptKey = this.addNode('concept', concept, concept);
+ const fileKey = this.addNode('file', filePath, filePath.split('/').pop());
+ this.addEdge(conceptKey, fileKey, 'maps_to', 0.7);
+ this.addEdge(fileKey, conceptKey, 'tagged_as', 0.7);
+ }
+
+ recordTask(task, result, agentId) {
+ const taskId = `task-${Date.now()}`;
+ const taskKey = this.addNode('task', taskId, task.substring(0, 60), {
+ full: task,
+ result: result?.substring(0, 100),
+ agent: agentId
+ });
+ const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
+ this.addEdge(sessionKey, taskKey, 'executed', 0.6);
+
+ // Extract keywords as concepts
+ const words = task.toLowerCase().split(/\s+/).filter(w => w.length > 3);
+ for (const word of [...new Set(words)].slice(0, 5)) {
+ this.addNode('concept', word, word);
+ this.addEdge(taskKey, `concept:${word}`, 'involves', 0.4);
+ }
+
+ // Link to tools used (if any found in task)
+ const knownTools = ['list_files', 'read_file', 'write_file', 'web_search', 'web_fetch',
+ 'edit_module', 'view_module', 'run_command', 'orchestrate'];
+ for (const tool of knownTools) {
+ if (task.toLowerCase().includes(tool.replace('_', ' '))) {
+ this.recordToolCall(tool, true);
+ }
+ }
+ }
+
+ // ─── Context Injection ───
+
+ getContextSummary() {
+ // Get top active nodes from recent session
+ const now = Date.now();
+ const today = `session:session-${Math.floor(now / 86400000)}`;
+ const yesterday = `session:session-${Math.floor(now / 86400000) - 1}`;
+
+ const lines = [];
+ const totalNodes = Object.keys(this._nodes).length;
+ const totalEdges = this._edges.length;
+
+ lines.push(`Memory Graph: ${totalNodes} nodes, ${totalEdges} edges`);
+
+ // Recent task context
+ const recentTasks = Object.values(this._nodes)
+ .filter(n => n.type === 'task')
+ .sort((a, b) => new Date(b.updated) - new Date(a.updated))
+ .slice(0, 3);
+
+ if (recentTasks.length > 0) {
+ lines.push('Recent tasks:');
+ for (const t of recentTasks) {
+ lines.push(` - ${t.label.substring(0, 50)}`);
+ }
+ }
+
+ // Most accessed files
+ const topFiles = Object.values(this._nodes)
+ .filter(n => n.type === 'file')
+ .sort((a, b) => b.access_count - a.access_count)
+ .slice(0, 3);
+
+ if (topFiles.length > 0) {
+ lines.push('Most accessed files:');
+ for (const f of topFiles) {
+ lines.push(` - ${f.label} (${f.access_count}x)`);
+ }
+ }
+
+ return lines.join('\\n');
+ }
+
+ getContextForPrompt() {
+ const summary = this.getContextSummary();
+ if (!summary) return '';
+ return `[Memory Graph Context]\\n${summary}\\n`;
+ }
+
+ // ─── Commands ───
+
+ showStatus() {
+ const types = {};
+ for (const n of Object.values(this._nodes)) {
+ types[n.type] = (types[n.type] || 0) + 1;
+ }
+ const typeStr = Object.entries(types)
+ .sort((a, b) => b[1] - a[1])
+ .map(([t, c]) => `${t}: ${c}`)
+ .join(', ');
+
+ this.terminal.print('🧠 Memory Graph', 'header');
+ this.terminal.print(` Nodes: ${Object.keys(this._nodes).length} (${typeStr})`, 'info');
+ this.terminal.print(` Edges: ${this._edges.length}`, 'info');
+ this.terminal.print(` Total accesses: ${this._totalNodeAccesses}`, 'info');
+ this.terminal.print(` Path: ${this._indexPath}`, 'info');
+ }
+
+ search(query) {
+ if (!query) return this.terminal.print('Usage: memgraph-search <query>', 'error');
+ const results = this.findNodes(query);
+ if (results.length === 0) return this.terminal.print(`No nodes matching "${query}"`, 'info');
+
+ this.terminal.print(`🔍 Found ${results.length} nodes:`, 'header');
+ for (const n of results.slice(0, 20)) {
+ const edges = this._edges.filter(e => e.source === n.id || e.target === n.id);
+ this.terminal.print(` ${n.type}:${n.label} (${edges.length} edges, ${n.access_count}x)`, 'info');
+ for (const e of edges.slice(0, 3)) {
+ const other = e.source === n.id ? e.target : e.source;
+ const otherLabel = (this._nodes[other] || {}).label || other;
+ this.terminal.print(` └─ ${e.type} → ${otherLabel} (w:${e.weight.toFixed(1)})`, 'text-muted');
+ }
+ }
+ if (results.length > 20) this.terminal.print(` ... and ${results.length - 20} more`, 'info');
+ }
+
+ showStats() {
+ if (Object.keys(this._nodes).length === 0) {
+ return this.terminal.print('No graph data yet. Use commands normally to build it.', 'info');
+ }
+
+ // Most connected nodes
+ const connected = {};
+ for (const e of this._edges) {
+ connected[e.source] = (connected[e.source] || 0) + 1;
+ connected[e.target] = (connected[e.target] || 0) + 1;
+ }
+ const top = Object.entries(connected)
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 5);
+
+ this.terminal.print('📊 Memory Graph Stats', 'header');
+ this.terminal.print(' Most connected:', 'info');
+ for (const [key, count] of top) {
+ const label = (this._nodes[key] || {}).label || key;
+ this.terminal.print(` ${label} — ${count} connections`, 'info');
+ }
+
+ // Strongest edges
+ const strong = [...this._edges].sort((a, b) => b.weight - a.weight).slice(0, 3);
+ if (strong.length > 0) {
+ this.terminal.print(' Strongest relationships:', 'info');
+ for (const e of strong) {
+ const sLabel = (this._nodes[e.source] || {}).label || e.source;
+ const tLabel = (this._nodes[e.target] || {}).label || e.target;
+ this.terminal.print(` ${sLabel} → ${tLabel} (${e.type}, w:${e.weight.toFixed(2)}, used ${e.use_count}x)`, 'info');
+ }
+ }
+
+ // Cluster density
+ const density = this._edges.length / Math.max(Object.keys(this._nodes).length, 1);
+ this.terminal.print(` Density: ${density.toFixed(2)} edges/node`, 'info');
+ }
+
+ exportGraph() {
+ const exportData = {
+ version: 1,
+ exported: new Date().toISOString(),
+ nodes: Object.values(this._nodes),
+ edges: this._edges,
+ stats: {
+ totalNodes: Object.keys(this._nodes).length,
+ totalEdges: this._edges.length,
+ totalAccesses: this._totalNodeAccesses
+ }
+ };
+ const json = JSON.stringify(exportData, null, 2);
+ // Save to VFS
+ try {
+ if (!this.terminal.fs.readDir(this._indexPath)) {
+ this.terminal.fs.mkdirRecursive(this._indexPath);
+ }
+ this.terminal.fs.writeFile(`${this._indexPath}export.json`, json);
+ this.terminal.print(`✅ Graph exported (${json.length} bytes) to ${this._indexPath}export.json`, 'success');
+ } catch (e) {
+ this.terminal.print(`❌ Export failed: ${e.message}`, 'error');
+ }
+ }
+
+ prune() {
+ const now = Date.now();
+ const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000;
+
+ const before = this._edges.length;
+ this._edges = this._edges.filter(e => {
+ const age = now - new Date(e.last_used).getTime();
+ // Keep if: used recently (<30d) OR used 3+ times regardless of age
+ if (age < THIRTY_DAYS) return true;
+ if (e.use_count >= 3) return true;
+ // Or if weight is high
+ if (e.weight > 0.8) return true;
+ return false;
+ });
+ const pruned = before - this._edges.length;
+
+ // Also prune orphan nodes (no edges)
+ const connectedNodes = new Set();
+ for (const e of this._edges) {
+ connectedNodes.add(e.source);
+ connectedNodes.add(e.target);
+ }
+ const nodeBefore = Object.keys(this._nodes).length;
+ for (const key of Object.keys(this._nodes)) {
+ if (!connectedNodes.has(key)) {
+ // Keep session and high-access nodes
+ if (this._nodes[key].access_count >= 5) continue;
+ delete this._nodes[key];
+ }
+ }
+ const nodesPruned = nodeBefore - Object.keys(this._nodes).length;
+
+ this._saveNow();
+ this.terminal.print(`🧹 Pruned ${pruned} edges and ${nodesPruned} orphan nodes`, nodesPruned > 0 ? 'warning' : 'info');
+ }
+
+ // ─── Persistence ───
+
+ _loadFromVFS() {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+
+ // Try loading from export first
+ const exportData = fs.fsReadJSON(`${this._indexPath}export.json`);
+ if (exportData && exportData.nodes) {
+ for (const n of exportData.nodes) {
+ this._nodes[n.id] = n;
+ }
+ this._edges = exportData.edges || [];
+ this._totalNodeAccesses = (exportData.stats?.totalAccesses) || 0;
+ return;
+ }
+
+ // Fallback: load nodes and edges separately
+ const nodes = fs.fsReadJSON(`${this._indexPath}nodes.json`);
+ if (nodes) this._nodes = nodes;
+ const edges = fs.fsReadJSON(`${this._indexPath}edges.json`);
+ if (edges) this._edges = edges;
+ } catch (e) {
+ console.warn('[MemoryGraph] Load error (fresh start):', e.message);
+ }
+ }
+
+ _saveNow() {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+ if (!fs.readDir(this._indexPath)) {
+ fs.mkdirRecursive(this._indexPath);
+ }
+ fs.fsWriteJSON(`${this._indexPath}nodes.json`, this._nodes);
+ fs.fsWriteJSON(`${this._indexPath}edges.json`, this._edges);
+ } catch (e) {
+ console.error('[MemoryGraph] Save error:', e.message);
+ }
+ }
+
+ _debounceSave() {
+ if (this._debounceTimer) clearTimeout(this._debounceTimer);
+ this._debounceTimer = setTimeout(() => this._saveNow(), 2000);
+ }
+}
+
+// Required for module system
+if (typeof window !== 'undefined') {
+ window.MemoryGraphModule = MemoryGraphModule;
+} \ No newline at end of file
diff --git a/modules/core/module-orchestrator.js b/modules/core/module-orchestrator.js
index de88424..ec385bb 100644
--- a/modules/core/module-orchestrator.js
+++ b/modules/core/module-orchestrator.js
@@ -1,12 +1,22 @@
-// module-orchestrator.js — Multi-agent orchestration system
-// Commands: orchestrate, orchestrator-status, orchestrator-spawn, orchestrator-kill, orchestrator-purge
+// 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 = 3;
+ this._SPAWN_LIMIT = 5;
+ this._SPAWN_STRATEGY = 'eager';
+ this._HEARTBEAT_TIMEOUT = 15000;
+ this._IDLE_TIMEOUT = 120000;
+ this._RESTART_POLICY = 'never';
+ this._A2A_POLLING = true;
+ this._a2aPollTimer = null;
this._heartbeatInterval = null;
+ this._a2aMessages = {};
+ this._restartBackoffs = {};
}
registerCommands() {
this.commands['orchestrate'] = (args) => this.delegateTask(args.join(' '));
@@ -14,6 +24,29 @@ class OrchestratorModule {
this.commands['orchestrator-spawn'] = (args) => this.spawnAgent(args[0], args.slice(1));
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();
+ }
+ getHelpInfo() {
+ return {
+ 'orchestrate': 'orchestrate <task> — Delegate a task to an available agent (auto-spawns if needed)',
+ 'orchestrator-status': 'orchestrator-status — Show agent pool status and config',
+ 'orchestrator-spawn': 'orchestrator-spawn [role] [capabilities...] — Manually spawn a new agent',
+ 'orchestrator-kill': 'orchestrator-kill <agent-id> — Terminate an agent and revoke leases',
+ '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-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',
+ 'orchestrator-restart-policy': 'orchestrator-restart-policy <agent-id> <on-failure|always|never> — Set per-agent restart policy',
+ 'a2a-send': 'a2a-send <agent-id> <json-message> — 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;
@@ -22,10 +55,129 @@ class OrchestratorModule {
if (registry) {
registry.registerAgent(['register', 'orchestrator-0', 'delegate', 'spawn', 'monitor', 'purge']);
}
+ // Reanimate sleeping agents from VFS
+ this._reanimateFromVFS();
+ // A2A mailbox polling loop (500ms)
+ if (this._A2A_POLLING) {
+ this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), 500);
+ }
// Heartbeat watchdog every 3 seconds
this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), 3000);
- this.terminal.print('🧠 Orchestrator loaded — ready to delegate', 'success');
+ // Idle timeout sweep every 30s
+ setInterval(() => this._sweepIdle(), 30000);
+ this.terminal.print('🧠 Orchestrator loaded — A2A mailbox transport active', 'success');
+ }
+
+ // ─── A2A Mailbox Transport ───
+
+ sendA2AMessage(args) {
+ if (args.length < 2) return this.terminal.print('Usage: a2a-send <agent-id> <json-message>', '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');
+ }
+
+ 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 ───
+
spawnAgent(role, capabilities) {
const id = `agent-${Object.keys(this._agents).length + 1}-${role || 'worker'}`;
if (Object.keys(this._agents).length >= this._SPAWN_LIMIT) {
@@ -39,15 +191,24 @@ class OrchestratorModule {
createdAt: new Date().toISOString(),
lastHeartbeat: Date.now(),
status: 'idle',
- currentTask: null
+ currentTask: null,
+ leaseId: null,
+ restartPolicy: this._RESTART_POLICY,
+ failureCount: 0,
+ restartCount: 0,
+ nextRestartAt: 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(['register', id, ...agent.capabilities]);
this.terminal.print(`🤖 Agent '${id}' spawned (role: ${role || 'worker'}, ${agent.capabilities.length} caps)`, 'success');
return agent;
}
+
killAgent(id) {
if (!this._agents[id]) return this.terminal.print(`❌ Agent '${id}' not found`, 'error');
// Revoke any active lease
@@ -60,31 +221,212 @@ class OrchestratorModule {
// Unregister from registry
const registry = this.terminal.moduleManager.modules['AgentRegistryModule'];
if (registry) registry.unregisterAgent(id);
+ // Clean up VFS
+ this.terminal.fs.deleteFile('agents/' + id);
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');
+ }
+
+ _reanimateFromVFS() {
+ const fs = this.terminal.fs;
+ const agentsDir = fs.readDir('/agents/');
+ if (!agentsDir) return;
+ let reanimated = 0;
+ for (const entry of agentsDir) {
+ if (entry.type !== 'dir') continue;
+ const sleepData = fs.fsReadJSON(`/agents/${entry.name}/sleep.json`);
+ if (sleepData) {
+ const agent = {
+ ...sleepData,
+ status: 'idle',
+ currentTask: null,
+ leaseId: null,
+ lastHeartbeat: Date.now()
+ };
+ delete agent._sleepTime;
+ this._agents[entry.name] = agent;
+ fs.writeFile(`agents/${entry.name}/status.json`, JSON.stringify({ ...agent, status: 'idle' }));
+ reanimated++;
+ }
+ }
+ if (reanimated > 0) {
+ this.terminal.print(`🔄 Reanimated ${reanimated} agent(s) from VFS sleep`, 'info');
+ }
+ }
+
+ // ─── Restart Policies ───
+
+ setRestartPolicy(args) {
+ if (args.length < 2) return this.terminal.print('Usage: orchestrator-restart-policy <agent-id> <on-failure|always|never>', '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();
+ 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');
+ 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 <key> <value>', '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 = setInterval(() => this._pollAgentMailboxes(), 500);
+ } else if (!this._A2A_POLLING && this._a2aPollTimer) {
+ clearInterval(this._a2aPollTimer);
+ this._a2aPollTimer = null;
+ }
+ return `a2a-polling ${this._A2A_POLLING ? 'on' : 'off'}`;
+ }
+ };
+
+ 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');
+ }
+
+ // ─── Task Delegation ───
+
async delegateTask(task) {
if (!task) return this.terminal.print('Usage: orchestrate <task description>', 'error');
- // Find an idle agent or auto-spawn
+
+ // Find an idle agent
let agent = Object.values(this._agents).find(a => a.status === 'idle');
+
+ // Auto-spawn with role-derived capabilities
if (!agent) {
if (Object.keys(this._agents).length >= this._SPAWN_LIMIT) {
- return this.terminal.print('❌ All agents busy and spawn limit reached', 'error');
+ // Try finding any non-failed agent to reuse
+ agent = Object.values(this._agents).find(a => a.status !== 'failed' && a.status !== 'sleeping');
+ }
+ if (!agent) {
+ const role = this._deriveRole(task);
+ const caps = this._deriveCapabilities(task);
+ this.spawnAgent(role, caps);
+ agent = Object.values(this._agents).find(a => a.status === 'idle');
}
- this.spawnAgent('worker', ['list_files', 'read_file', 'write_file', 'web_search']);
- agent = Object.values(this._agents).find(a => a.status === 'idle');
}
if (!agent) return this.terminal.print('❌ No available agents', 'error');
- // A2A Contract handshake — validate task against agent's contract
+ // 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');
- this.terminal.print(` Contract for '${agent.role}' requires: ${(registry.getContract(agent.role) || {}).input_schema || 'free-form'}`, 'warning');
- // Offer to proceed anyway (permissive) — user can create a proper contract
contractValid = false;
} else if (result.reason === 'matched') {
this.terminal.print(`✅ A2A contract matched for role '${agent.role}'`, 'success');
@@ -94,52 +436,88 @@ class OrchestratorModule {
// Create a lease
const leaseModule = this.terminal.moduleManager.modules['LeaseModule'];
const lease = leaseModule ? leaseModule.createLease(['lease-create', agent.id, '60', task]) : null;
+
// Mark agent as busy
agent.status = 'busy';
agent.currentTask = task;
agent.leaseId = lease?.id;
- // Write task to agent's inbox
- if (!this.terminal.fs.readFile(`/agents/${agent.id}/`)) this.terminal.fs.mkdir(`agents/${agent.id}`);
- this.terminal.fs.writeFile(`agents/${agent.id}/inbox.json`, JSON.stringify({
- task,
- from: 'orchestrator-0',
- leaseId: lease?.id,
- timestamp: new Date().toISOString(),
- ttl_seconds: 60
- }));
- this.terminal.print(`🎯 Delegated "${task.substring(0, 60)}" to ${agent.id} (lease: ${lease?.id || 'none'})`, 'info');
+ agent.lastHeartbeat = Date.now();
+
+ // 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 result = await aiModule.chat([`[DELEGATED] ${task}`]);
agent.status = 'completed';
+
+ // Write result to outbox
this.terminal.fs.writeFile(`agents/${agent.id}/outbox.json`, JSON.stringify({
- result: result || 'completed',
+ result: typeof result === 'string' ? result : (result?.content || 'completed'),
status: 'completed',
timestamp: new Date().toISOString()
}));
+
+ // Record in memory graph
+ const memgraph = this.terminal.moduleManager.modules['MemoryGraphModule'];
+ if (memgraph) {
+ memgraph.recordTask(task, typeof result === 'string' ? result : (result?.content || ''), agent.id);
+ memgraph.recordFileAccess(`/agents/${agent.id}/outbox.json`);
+ }
+
this.terminal.print(`✅ ${agent.id} completed task`, 'success');
+
+ // Check restart policy — if always, set back to idle for next task
+ if (agent.restartPolicy === 'always') {
+ agent.status = 'idle';
+ agent.currentTask = null;
+ agent.lastHeartbeat = Date.now();
+ }
} else {
this.terminal.print(`ℹ️ AI module not available — task queued for ${agent.id}`, 'info');
}
} catch (e) {
- agent.status = 'failed';
- this.terminal.print(`❌ ${agent.id} failed: ${e.message}`, 'error');
+ this._handleAgentFailure(agent.id, e.message);
+ }
+
+ // Return to idle pool (unless failed/restarting)
+ if (agent.status === 'failed') {
+ // handled by _handleAgentFailure
+ } else if (agent.status !== 'busy') {
+ agent.status = 'idle';
+ agent.currentTask = null;
+ agent.lastHeartbeat = Date.now();
}
- // Return to idle pool
- agent.status = 'idle';
- agent.currentTask = null;
- agent.lastHeartbeat = Date.now();
}
+
+ // ─── Monitoring ───
+
_checkHeartbeats() {
const now = Date.now();
Object.entries(this._agents).forEach(([id, agent]) => {
- if (agent.status === 'busy' && now - agent.lastHeartbeat > 15000) {
+ 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;
@@ -151,17 +529,45 @@ class OrchestratorModule {
});
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 failed = Object.values(this._agents).filter(a => a.status === 'failed').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, ${failed} failed)`, 'info');
- this.terminal.print(` Spawn limit: ${this._SPAWN_LIMIT}`, 'info');
+ 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(` A2A Messages: ${a2aTotal} (${a2aPending} pending)`, 'info');
Object.values(this._agents).forEach(a => {
const task = a.currentTask ? ` (task: ${a.currentTask.substring(0, 40)})` : '';
- this.terminal.print(` ${a.id} — ${a.status}${task}`, 'info');
+ 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');
+ });
+ }
+
+ 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;
} \ No newline at end of file
diff --git a/modules/core/module-predictor.js b/modules/core/module-predictor.js
new file mode 100644
index 0000000..312325b
--- /dev/null
+++ b/modules/core/module-predictor.js
@@ -0,0 +1,279 @@
+// module-predictor.js — Predictive resource allocation engine
+// Analyzes historical telemetry + memory graph data to predict task duration/turns
+// Commands: predict, predictor-status, predictor-config
+class PredictorModule {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ this._dataPoints = [];
+ this._predictions = 0;
+ this._accuracy = { correct: 0, total: 0 };
+ this._CONFIDENCE_THRESHOLD = 0.5;
+ this._MIN_DATA_POINTS = 5;
+ this._loaded = false;
+ }
+
+ getHelpInfo() {
+ return {
+ 'predict': 'predict <task description> — Predict task duration, tool turns, and confidence',
+ 'predictor-status': 'predictor-status — Model accuracy, data points, prediction count',
+ 'predictor-config': 'predictor-config <key> <value> — Tune: confidence-threshold, min-data-points',
+ };
+ }
+
+ registerCommands() {
+ this.commands['predict'] = (args) => this.predict(args.join(' '));
+ this.commands['predictor-status'] = () => this.showStatus();
+ this.commands['predictor-config'] = (args) => this.setConfig(args);
+ }
+
+ init(terminal) {
+ this.terminal = terminal;
+ this._loadFromVFS();
+ this._loaded = true;
+ this.terminal.print('📊 Predictor loaded — analyzing task patterns', 'info');
+ }
+
+ // ─── Prediction Engine ───
+
+ async predict(task) {
+ if (!task) return this.terminal.print('Usage: predict <task description>', 'error');
+
+ const startTime = performance.now();
+
+ // Extract features
+ const features = this._extractFeatures(task);
+
+ // Find similar historical tasks
+ const similar = this._findSimilar(features);
+
+ // Generate prediction
+ const prediction = this._computePrediction(similar, features);
+
+ // Record this prediction for accuracy tracking
+ const duration = ((performance.now() - startTime) / 1000).toFixed(2);
+
+ this._predictions++;
+ this._dataPoints.push({
+ features,
+ predictedDuration: prediction.duration,
+ predictedTurns: prediction.toolTurns,
+ confidence: prediction.confidence,
+ similarCount: similar.length,
+ timestamp: new Date().toISOString()
+ });
+
+ this._debounceSave();
+
+ // Display result
+ this.terminal.print('📈 Task Prediction', 'header');
+ this.terminal.print(` Task: ${task.substring(0, 50)}${task.length > 50 ? '...' : ''}`, 'info');
+
+ if (prediction.confidence >= this._CONFIDENCE_THRESHOLD) {
+ this.terminal.print(` ⏱ Estimated duration: ${prediction.duration}s`, 'info');
+ this.terminal.print(` 🔧 Estimated tool turns: ${prediction.toolTurns}`, 'info');
+ this.terminal.print(` 📊 Confidence: ${(prediction.confidence * 100).toFixed(0)}% (based on ${similar.length} similar tasks)`, 'success');
+ } else if (similar.length > 0) {
+ this.terminal.print(` ⏱ Estimated duration: ${prediction.duration}s (low confidence)`, 'warning');
+ this.terminal.print(` 🔧 Tool turns: ~${prediction.toolTurns}`, 'warning');
+ this.terminal.print(` 📊 Confidence: ${(prediction.confidence * 100).toFixed(0)}% — only ${similar.length} similar data point(s)`, 'warning');
+ this.terminal.print(' Tip: More task data will improve predictions.', 'info');
+ } else {
+ this.terminal.print(' ❓ No historical data to base prediction on.', 'warning');
+ this.terminal.print(' Tip: Run `orchestrate` with various tasks to build a baseline.', 'info');
+ // Provide hardcoded defaults
+ this.terminal.print(' Default estimate: ~30s duration, ~5 tool turns', 'info');
+ }
+
+ // If got memory graph, offer recommendations
+ if (prediction.confidence >= this._CONFIDENCE_THRESHOLD && prediction.duration > 30) {
+ this.terminal.print(` 💡 Recommendation: Set TTL to ${Math.ceil(prediction.duration * 1.5)}s for predictable completion`, 'info');
+ }
+
+ return prediction;
+ }
+
+ _extractFeatures(task) {
+ const lower = task.toLowerCase();
+ return {
+ length: task.length,
+ wordCount: task.split(/\s+/).length,
+ hasSearch: /\b(search|find|look)\b/.test(lower),
+ hasWrite: /\b(write|create|save|edit)\b/.test(lower),
+ hasDelete: /\b(delete|remove|rm)\b/.test(lower),
+ hasInstall: /\b(install|wpm|package)\b/.test(lower),
+ hasRead: /\b(read|cat|view|list)\b/.test(lower),
+ hasWeb: /\b(fetch|url|http|web)\b/.test(lower),
+ hasOrchestrate: /\b(orchestrat|delegate|agent)\b/.test(lower),
+ hasModule: /\b(module|edit_module|view_module)\b/.test(lower),
+ complexity: Math.min(task.length / 50, 10), // 0-10 scale
+ keywords: [...new Set(lower.split(/\s+/).filter(w => w.length > 3))].slice(0, 10)
+ };
+ }
+
+ _findSimilar(features) {
+ return this._dataPoints.filter(dp => {
+ // Skip predictions (only use actual executions with observed results)
+ if (!dp.observedDuration) return false;
+ const f = dp.features;
+ if (!f) return false;
+ // Feature similarity scoring
+ let score = 0;
+ const matches = [
+ features.hasSearch === f.hasSearch,
+ features.hasWrite === f.hasWrite,
+ features.hasDelete === f.hasDelete,
+ features.hasInstall === f.hasInstall,
+ features.hasRead === f.hasRead,
+ features.hasWeb === f.hasWeb,
+ features.hasOrchestrate === f.hasOrchestrate,
+ features.hasModule === f.hasModule,
+ ];
+ score = matches.filter(Boolean).length / matches.length;
+ // Keyword overlap bonus
+ const kOverlap = features.keywords.filter(k => f.keywords?.includes(k)).length;
+ const kScore = kOverlap / Math.max(features.keywords.length, 1);
+ score = score * 0.6 + kScore * 0.4;
+ return score > 0.3;
+ });
+ }
+
+ _computePrediction(similar, features) {
+ if (similar.length === 0) {
+ // Hardcoded defaults
+ let baseDuration = 15;
+ let baseTurns = 3;
+ if (features.hasSearch || features.hasWeb) { baseDuration = 25; baseTurns = 5; }
+ if (features.hasWrite) { baseDuration = 20; baseTurns = 4; }
+ if (features.hasOrchestrate) { baseDuration = 40; baseTurns = 8; }
+ if (features.complexity > 5) baseDuration *= 1.5;
+ return {
+ duration: baseDuration,
+ toolTurns: baseTurns,
+ confidence: 0.1,
+ similarCount: 0
+ };
+ }
+
+ const avgDuration = similar.reduce((s, dp) => s + (dp.observedDuration || 30), 0) / similar.length;
+ const avgTurns = similar.reduce((s, dp) => s + (dp.observedTurns || 3), 0) / similar.length;
+ const confidence = Math.min(similar.length / 20, 1) * 0.8 + 0.1;
+
+ // Apply task complexity multiplier
+ const complexityMultiplier = 1 + (features.complexity - 3) * 0.1;
+ const adjustedDuration = Math.round(avgDuration * Math.max(complexityMultiplier, 0.5));
+
+ return {
+ duration: adjustedDuration,
+ toolTurns: Math.round(avgTurns),
+ confidence: Math.min(confidence, 0.95),
+ similarCount: similar.length
+ };
+ }
+
+ // ─── Learning from Results ───
+
+ recordResult(features, duration, toolTurns, success) {
+ this._dataPoints.push({
+ features,
+ observedDuration: duration,
+ observedTurns: toolTurns,
+ success,
+ timestamp: new Date().toISOString()
+ });
+ this._accuracy.total++;
+ // Compare with last prediction for this feature set
+ const lastPrediction = [...this._dataPoints]
+ .reverse()
+ .find(dp => dp.predictedDuration && dp.features?.hasSearch === features.hasSearch);
+ if (lastPrediction) {
+ const error = Math.abs(lastPrediction.predictedDuration - duration) / duration;
+ if (error < 0.3) this._accuracy.correct++;
+ }
+ this._debounceSave();
+ }
+
+ // ─── Commands ───
+
+ showStatus() {
+ const actualExecutions = this._dataPoints.filter(dp => dp.observedDuration).length;
+ const accPct = this._accuracy.total > 0
+ ? ((this._accuracy.correct / this._accuracy.total) * 100).toFixed(0)
+ : 'N/A';
+
+ this.terminal.print('📊 Predictor Status', 'header');
+ this.terminal.print(` Total data points: ${this._dataPoints.length}`, 'info');
+ this.terminal.print(` Actual executions: ${actualExecutions}`, 'info');
+ this.terminal.print(` Predictions made: ${this._predictions}`, 'info');
+ this.terminal.print(` Accuracy: ${accPct}% (${this._accuracy.correct}/${this._accuracy.total})`, 'info');
+ this.terminal.print(` Confidence threshold: ${this._CONFIDENCE_THRESHOLD}`, 'info');
+ this.terminal.print(` Min data points: ${this._MIN_DATA_POINTS}`, 'info');
+ }
+
+ setConfig(args) {
+ if (args.length < 2) return this.terminal.print('Usage: predictor-config <key> <value>', 'error');
+ const key = args[0];
+ const val = args[1];
+
+ const configMap = {
+ 'confidence-threshold': () => {
+ const n = parseFloat(val);
+ if (isNaN(n) || n < 0 || n > 1) return 'Must be 0.0-1.0';
+ this._CONFIDENCE_THRESHOLD = n;
+ return `confidence-threshold set to ${n}`;
+ },
+ 'min-data-points': () => {
+ const n = parseInt(val);
+ if (isNaN(n) || n < 1) return 'Must be >= 1';
+ this._MIN_DATA_POINTS = n;
+ return `min-data-points set to ${n}`;
+ },
+ };
+
+ if (!configMap[key]) return this.terminal.print(`Unknown: ${key}. Valid: ${Object.keys(configMap).join(', ')}`, 'error');
+ const msg = configMap[key]();
+ this.terminal.print(`✅ ${msg}`, 'success');
+ }
+
+ // ─── Persistence ───
+
+ _loadFromVFS() {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+ const data = fs.fsReadJSON('/system/metrics/predictor.json');
+ if (data) {
+ this._dataPoints = data.dataPoints || [];
+ this._predictions = data.predictions || 0;
+ this._accuracy = data.accuracy || { correct: 0, total: 0 };
+ }
+ } catch (e) {
+ console.warn('[Predictor] Load error (fresh start):', e.message);
+ }
+ }
+
+ _saveNow() {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+ if (!fs.readDir('/system/metrics')) fs.mkdirRecursive('/system/metrics');
+ fs.fsWriteJSON('/system/metrics/predictor.json', {
+ dataPoints: this._dataPoints.slice(-500), // keep last 500
+ predictions: this._predictions,
+ accuracy: this._accuracy
+ });
+ } catch (e) {
+ console.error('[Predictor] Save error:', e.message);
+ }
+ }
+
+ _debounceSave() {
+ clearTimeout(this._saveTimer);
+ this._saveTimer = setTimeout(() => this._saveNow(), 3000);
+ }
+}
+
+// Required for module system
+if (typeof window !== 'undefined') {
+ window.PredictorModule = PredictorModule;
+} \ No newline at end of file
diff --git a/modules/core/module-sandbox.js b/modules/core/module-sandbox.js
new file mode 100644
index 0000000..dc8dc76
--- /dev/null
+++ b/modules/core/module-sandbox.js
@@ -0,0 +1,332 @@
+// module-sandbox.js — Sandboxed execution pool using WebWorkers
+// Commands: sandbox-status, sandbox-config, sandbox-run
+class SandboxModule {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ this.workers = [];
+ this.MAX_WORKERS = 4;
+ this.MIN_WORKERS = 1;
+ this.queue = [];
+ this.activeTasks = {};
+ this._nextTaskId = 0;
+ this._totalRuns = 0;
+ this._totalErrors = 0;
+ this._totalDuration = 0;
+ this._fallbackMode = false; // true if WebWorkers unavailable
+ this._workerReady = {};
+ }
+
+ getHelpInfo() {
+ return {
+ 'sandbox-status': 'sandbox-status — Show sandbox pool health and task metrics',
+ 'sandbox-config': 'sandbox-config <key> <value> — Tune: max-workers, min-workers, pool-timeout',
+ 'sandbox-run': 'sandbox-run <task> — Run a task in the sandbox (automatic routing)',
+ };
+ }
+
+ registerCommands() {
+ this.commands['sandbox-status'] = () => this.showStatus();
+ this.commands['sandbox-config'] = (args) => this.setConfig(args);
+ this.commands['sandbox-run'] = (args) => this.runInSandbox(args.join(' '));
+ }
+
+ init(terminal) {
+ this.terminal = terminal;
+ this.workerUrl = '/spaces/newos4/modules/core/sandbox-worker.js';
+
+ // Detect Worker availability
+ if (typeof Worker === 'undefined') {
+ this._fallbackMode = true;
+ this.terminal.print('⚠️ WebWorkers not available — sandbox using inline fallback', 'warning');
+ } else {
+ this._initPool();
+ }
+
+ this.terminal.print('🧪 Sandbox module loaded', 'info');
+ }
+
+ _initPool() {
+ // Start with MIN_WORKERS, grow on demand up to MAX_WORKERS
+ for (let i = 0; i < this.MIN_WORKERS; i++) {
+ this._spawnWorker();
+ }
+ }
+
+ _spawnWorker() {
+ if (this.workers.length >= this.MAX_WORKERS) return null;
+ try {
+ const worker = new Worker(this.workerUrl);
+ const id = `w${this.workers.length + 1}`;
+ worker._sandboxId = id;
+ worker._busy = false;
+
+ worker.onmessage = (e) => this._handleResult(e.data, id);
+ worker.onerror = (e) => {
+ console.error(`[Sandbox] Worker ${id} error:`, e.message);
+ this._totalErrors++;
+ this._replaceWorker(id);
+ };
+
+ this.workers.push(worker);
+ this._workerReady[id] = true;
+ return worker;
+ } catch (e) {
+ console.error('[Sandbox] Failed to spawn worker:', e);
+ this._fallbackMode = true;
+ return null;
+ }
+ }
+
+ _replaceWorker(id) {
+ const idx = this.workers.findIndex(w => w._sandboxId === id);
+ if (idx !== -1) {
+ this.workers[idx].terminate();
+ this.workers.splice(idx, 1);
+ delete this._workerReady[id];
+ }
+ // Re-spawn after a delay
+ setTimeout(() => this._spawnWorker(), 1000);
+ }
+
+ _getIdleWorker() {
+ for (const w of this.workers) {
+ if (!w._busy) return w;
+ }
+ // Grow pool if under max
+ if (this.workers.length < this.MAX_WORKERS) {
+ const w = this._spawnWorker();
+ if (w) return w;
+ }
+ return null;
+ }
+
+ async runInSandbox(task, capabilities) {
+ if (!task) return this.terminal.print('Usage: sandbox-run <task>', 'error');
+
+ if (this._fallbackMode) {
+ return this._runInline(task, capabilities);
+ }
+
+ const worker = this._getIdleWorker();
+ if (!worker) {
+ // Queue it
+ return new Promise((resolve) => {
+ this.queue.push({ task, capabilities, resolve });
+ this.terminal.print(`⏳ Task queued (${this.queue.length} waiting)`, 'info');
+ this._drainQueue();
+ });
+ }
+
+ return this._executeOnWorker(worker, task, capabilities);
+ }
+
+ async _executeOnWorker(worker, task, capabilities) {
+ const taskId = `task-${++this._nextTaskId}`;
+ worker._busy = true;
+
+ const startTime = performance.now();
+
+ return new Promise((resolve) => {
+ const handler = (e) => {
+ if (e.data && e.data.taskId === taskId) {
+ worker.removeEventListener('message', handler);
+ worker._busy = false;
+
+ const duration = ((performance.now() - startTime) / 1000).toFixed(2);
+ this._totalRuns++;
+ this._totalDuration += parseFloat(duration);
+
+ if (e.data.error) {
+ this._totalErrors++;
+ this.terminal.print(`❌ Sandbox task failed: ${e.data.error}`, 'error');
+ resolve({ error: e.data.error, duration });
+ } else {
+ this.terminal.print(`✅ Sandbox task complete (${duration}s, ${e.data.toolCalls || 0} tools)`, 'success');
+ resolve({ result: e.data.result, duration, toolCalls: e.data.toolCalls || 0 });
+ }
+
+ this._drainQueue();
+ }
+ };
+
+ worker.addEventListener('message', handler);
+
+ // Get VFS snapshot from terminal
+ let vfsSnapshot = null;
+ try {
+ if (this.terminal && this.terminal.fs && this.terminal.fs.root) {
+ vfsSnapshot = JSON.stringify({
+ root: this.terminal.fs.root,
+ currentPath: this.terminal.fs.currentPath || []
+ });
+ }
+ } catch (e) {
+ console.warn('[Sandbox] Could not serialize VFS:', e.message);
+ }
+
+ const caps = capabilities || ['list_files', 'read_file'];
+ const allowedCommands = ['ls', 'cat', 'echo', 'find', 'grep', 'help', 'status',
+ 'touch', 'mkdir', 'rm', 'cp', 'mv'];
+
+ worker.postMessage({
+ taskId,
+ task,
+ capabilities: caps,
+ vfsSnapshot,
+ allowedCommands,
+ apiProxy: window.location.origin + '/api'
+ });
+
+ // Timeout after 30 seconds
+ setTimeout(() => {
+ if (worker._busy) {
+ worker._busy = false;
+ this._totalErrors++;
+ this.terminal.print(`⏰ Sandbox task ${taskId} timed out after 30s`, 'error');
+ resolve({ error: 'Timeout (30s)', duration: 30 });
+ this._drainQueue();
+ }
+ }, 30000);
+ });
+ }
+
+ _handleResult(data, workerId) {
+ // Handled via promise in _executeOnWorker
+ // This is the fallback handler for tasks not tracked by promise
+ if (data.error) {
+ this.terminal.print(`[Sandbox ${workerId}] Error: ${data.error}`, 'error');
+ }
+ }
+
+ _drainQueue() {
+ if (this.queue.length === 0) return;
+ const next = this.queue.shift();
+ this.runInSandbox(next.task, next.capabilities).then(result => {
+ next.resolve(result);
+ });
+ }
+
+ _runInline(task, capabilities) {
+ // Fallback when WebWorkers unavailable — runs in main thread
+ const startTime = performance.now();
+ const parts = task.trim().split(/\s+/);
+ const cmd = parts[0].toLowerCase();
+ let result = '';
+ let toolCalls = 0;
+
+ const fs = this.terminal ? this.terminal.fs : null;
+
+ if (cmd === 'ls' && fs) {
+ const path = parts[1] || '/';
+ const entries = fs.ls(path);
+ if (!entries) return this.terminal.print(`ls: ${path}: No such directory`, 'error');
+ result = entries.map(e => `${e.type === 'dir' ? '[dir]' : '[file]'} ${e.name}`).join('\\n');
+ toolCalls++;
+ } else if ((cmd === 'cat' || cmd === 'read') && fs) {
+ const path = parts[1] || '';
+ const content = fs.readFile(path);
+ if (content === null) return this.terminal.print(`cat: ${path}: No such file`, 'error');
+ result = typeof content === 'string' ? content.substring(0, 8000) : String(content);
+ toolCalls++;
+ } else if (cmd === 'find' && fs) {
+ const q = parts.slice(1).join(' ') || '';
+ // Walk VFS
+ const results = [];
+ const walk = (p) => {
+ const entries = fs.ls(p);
+ if (!entries) return;
+ for (const e of entries) {
+ const full = p === '/' ? '/' + e.name : p + '/' + e.name;
+ if (e.type === 'file' && (!q || full.toLowerCase().includes(q.toLowerCase()))) {
+ results.push(full);
+ }
+ if (e.type === 'dir') walk(full);
+ }
+ };
+ walk('/');
+ result = results.join('\\n') || '(empty)';
+ toolCalls++;
+ } else if (cmd === 'status') {
+ let f = 0, d = 0;
+ const walk = (p) => {
+ const entries = fs ? fs.ls(p) : [];
+ if (!entries) return;
+ for (const e of entries) {
+ if (e.type === 'file') f++;
+ else { d++; walk(p === '/' ? '/' + e.name : p + '/' + e.name); }
+ }
+ };
+ walk('/');
+ result = `Files: ${f}, Dirs: ${d}`;
+ toolCalls++;
+ } else {
+ result = `Task: "${task}" (inline fallback)`;
+ }
+
+ const duration = ((performance.now() - startTime) / 1000).toFixed(2);
+ this._totalRuns++;
+ this._totalDuration += parseFloat(duration);
+ this.terminal.print(result, 'info');
+ return { result, duration, toolCalls };
+ }
+
+ // ─── Config ───
+
+ setConfig(args) {
+ if (args.length < 2) return this.terminal.print('Usage: sandbox-config <key> <value>', 'error');
+ const key = args[0];
+ const val = args[1];
+
+ const configMap = {
+ 'max-workers': () => {
+ const n = parseInt(val);
+ if (isNaN(n) || n < 1 || n > 8) return 'Must be 1-8';
+ this.MAX_WORKERS = n;
+ // Scale down if needed
+ while (this.workers.length > n) {
+ const w = this.workers.pop();
+ w.terminate();
+ }
+ // Scale up
+ for (let i = this.workers.length; i < Math.min(n, this.MIN_WORKERS); i++) {
+ this._spawnWorker();
+ }
+ return `max-workers set to ${n}`;
+ },
+ 'min-workers': () => {
+ const n = parseInt(val);
+ if (isNaN(n) || n < 0 || n > this.MAX_WORKERS) return `Must be 0-${this.MAX_WORKERS}`;
+ this.MIN_WORKERS = n;
+ while (this.workers.length < n) this._spawnWorker();
+ return `min-workers set to ${n}`;
+ },
+ };
+
+ if (!configMap[key]) {
+ return this.terminal.print(`Unknown: ${key}. Valid: ${Object.keys(configMap).join(', ')}`, 'error');
+ }
+ const msg = configMap[key]();
+ this.terminal.print(`✅ ${msg}`, 'success');
+ }
+
+ // ─── Status ───
+
+ showStatus() {
+ const busy = this.workers.filter(w => w._busy).length;
+ const idle = this.workers.length - busy;
+ const avgDuration = this._totalRuns > 0 ? (this._totalDuration / this._totalRuns).toFixed(2) : 'N/A';
+
+ this.terminal.print('🧪 Sandbox Pool', 'header');
+ this.terminal.print(` Workers: ${this.workers.length}/${this.MAX_WORKERS} (${busy} busy, ${idle} idle)`, 'info');
+ this.terminal.print(` Mode: ${this._fallbackMode ? '⚠️ Inline fallback' : '✅ WebWorker'}`, 'info');
+ this.terminal.print(` Queue: ${this.queue.length} pending`, 'info');
+ this.terminal.print(` Total runs: ${this._totalRuns} (errors: ${this._totalErrors})`, 'info');
+ this.terminal.print(` Avg duration: ${avgDuration}s`, 'info');
+ }
+}
+
+// Required for module system
+if (typeof window !== 'undefined') {
+ window.SandboxModule = SandboxModule;
+} \ No newline at end of file
diff --git a/modules/core/module-world-model.js b/modules/core/module-world-model.js
new file mode 100644
index 0000000..b7bb28f
--- /dev/null
+++ b/modules/core/module-world-model.js
@@ -0,0 +1,381 @@
+// module-world-model.js — Global World Model: system self-awareness synthesis engine
+// Aggregates from memory graph, neural VFS, skills index, telemetry
+// Commands: world-model, world-model-summary, world-model-ask, world-model-refresh, world-model-config
+class WorldModelModule {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ this._model = null;
+ this._lastRefresh = null;
+ this._REFRESH_INTERVAL = 300000; // 5 minutes
+ this._MAX_PROJECTS = 10;
+ this._refreshTimer = null;
+ }
+
+ getHelpInfo() {
+ return {
+ 'world-model': 'world-model — Show full world model as structured output',
+ 'world-model-summary': 'world-model-summary — One-line system self-awareness statement',
+ 'world-model-ask': 'world-model-ask <question> — Query the model (e.g. "what changed since yesterday")',
+ 'world-model-refresh': 'world-model-refresh — Force re-synthesis from all subsystems',
+ 'world-model-config': 'world-model-config <key> <value> — Tune: refresh-interval, max-projects',
+ };
+ }
+
+ registerCommands() {
+ this.commands['world-model'] = () => this.showModel();
+ this.commands['world-model-summary'] = () => this.showSummary();
+ this.commands['world-model-ask'] = (args) => this.ask(args.join(' '));
+ this.commands['world-model-refresh'] = () => this.refresh();
+ this.commands['world-model-config'] = (args) => this.setConfig(args);
+ }
+
+ init(terminal) {
+ this.terminal = terminal;
+ this._loadFromVFS();
+ // Auto-refresh on a timer
+ this._refreshTimer = setInterval(() => {
+ const age = Date.now() - (this._lastRefresh ? new Date(this._lastRefresh).getTime() : 0);
+ if (age > this._REFRESH_INTERVAL) this._synthesize();
+ }, this._REFRESH_INTERVAL);
+ // Initial synthesis
+ this._synthesize();
+ this.terminal.print('🌐 World Model loaded — system self-awareness active', 'info');
+ }
+
+ // ─── Synthesis Engine ───
+
+ _synthesize() {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+
+ const model = {
+ version: 'v4.4',
+ synthesized: new Date().toISOString(),
+ system: this._gatherSystemInfo(fs),
+ modules: this._gatherModuleInfo(),
+ files: this._gatherFileInfo(fs),
+ skills: this._gatherSkillInfo(fs),
+ memory: this._gatherMemoryInfo(),
+ agents: this._gatherAgentInfo(),
+ telemetry: this._gatherTelemetry(fs),
+ projects: this._gatherProjects(fs),
+ recent: this._gatherRecent(fs)
+ };
+
+ this._model = model;
+ this._lastRefresh = model.synthesized;
+ this._saveToVFS(fs);
+
+ // Log synthesis summary
+ const mCount = model.modules?.loaded || '?';
+ const sCount = model.skills?.count || '?';
+ const fCount = model.files?.total || '?';
+ console.log(`[WorldModel] Synthesis complete: ${mCount} modules, ${sCount} skills, ${fCount} files`);
+ } catch (e) {
+ console.error('[WorldModel] Synthesis error:', e.message);
+ }
+ }
+
+ _gatherSystemInfo(fs) {
+ const healthData = fs.fsReadJSON('/system/metrics/health.json');
+ return {
+ version: 'newOS4 v4.4',
+ modules: Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0,
+ vfsSize: JSON.stringify(fs?.root || {}).length,
+ online: typeof navigator !== 'undefined' ? navigator.onLine : true,
+ memory: typeof performance !== 'undefined' && performance.memory
+ ? `${(performance.memory.usedJSHeapSize / 1048576).toFixed(1)}MB`
+ : 'N/A',
+ health: healthData?.status || 'OK',
+ lastHealthCheck: healthData?.timestamp || null
+ };
+ }
+
+ _gatherModuleInfo() {
+ const mm = this.terminal?.moduleManager;
+ if (!mm) return { loaded: 0, modified: [] };
+ const modules = Object.keys(mm.modules || {}).map(name => {
+ const mod = mm.modules[name];
+ const clsName = mod?.constructor?.name || name;
+ return { name: clsName, hasCommands: !!mod?.commands };
+ });
+ // Check for local modifications
+ const modsDir = this.terminal.fs?.readDir('/_mods/');
+ const modified = modsDir
+ ? modsDir.filter(e => e.type === 'file' && e.name.endsWith('.js') && !e.name.endsWith('.orig.js'))
+ .map(e => e.name.replace('.js', ''))
+ : [];
+ return {
+ loaded: modules.length,
+ total: 49,
+ modules,
+ modified,
+ modifiedCount: modified.length
+ };
+ }
+
+ _gatherFileInfo(fs) {
+ if (!fs) return { total: 0, size: '0KB' };
+ let fileCount = 0, dirCount = 0;
+ const walk = (path) => {
+ const entries = fs.ls(path);
+ if (!entries) return;
+ for (const e of entries) {
+ if (e.type === 'file') fileCount++;
+ else { dirCount++; walk(path === '/' ? '/' + e.name : path + '/' + e.name); }
+ }
+ };
+ walk('/');
+ return {
+ total: fileCount,
+ directories: dirCount,
+ size: `${(JSON.stringify(fs.root).length / 1024).toFixed(1)}KB`
+ };
+ }
+
+ _gatherSkillInfo(fs) {
+ try {
+ const idx = fs.fsReadJSON('/skills/index.json');
+ const count = idx ? Object.keys(idx).length : 0;
+ return { count, skills: idx ? Object.keys(idx) : [] };
+ } catch (e) {
+ return { count: 0, skills: [] };
+ }
+ }
+
+ _gatherMemoryInfo() {
+ const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule'];
+ if (mg && mg._nodes) {
+ const nodes = Object.keys(mg._nodes).length;
+ const edges = (mg._edges || []).length;
+ return { nodes, edges, active: true };
+ }
+ // 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 };
+ } catch (e) {
+ return { nodes: 0, edges: 0, active: false };
+ }
+ }
+
+ _gatherAgentInfo() {
+ const orch = this.terminal?.moduleManager?.modules?.['OrchestratorModule'];
+ if (!orch || !orch._agents) return { agents: 0, active: 0 };
+ 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 };
+ }
+
+ _gatherTelemetry(fs) {
+ try {
+ const latency = fs.fsReadJSON('/system/metrics/tool-latency.json');
+ const errors = fs.fsReadJSON('/system/metrics/error-log.json');
+ return {
+ toolLatencyDataPoints: latency ? Object.keys(latency).length : 0,
+ recentErrors: errors ? (Array.isArray(errors) ? errors.length : Object.keys(errors).length) : 0,
+ };
+ } catch (e) {
+ return { toolLatencyDataPoints: 0, recentErrors: 0 };
+ }
+ }
+
+ _gatherProjects(fs) {
+ try {
+ const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule'];
+ if (mg && mg._nodes) {
+ // Find task clusters as "projects"
+ 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)
+ .slice(0, this._MAX_PROJECTS);
+ return {
+ totalTasks: tasks.length,
+ activeProjects: recentFiles.slice(0, 5).map(f => ({
+ name: f.label,
+ accesses: f.access_count,
+ lastModified: f.updated
+ }))
+ };
+ }
+ return { totalTasks: 0, activeProjects: [] };
+ } catch (e) {
+ return { totalTasks: 0, activeProjects: [] };
+ }
+ }
+
+ _gatherRecent(fs) {
+ const recent = [];
+ try {
+ // Check modified modules
+ const modsDir = fs.readDir('/_mods/');
+ if (modsDir) {
+ for (const e of modsDir) {
+ if (e.type === 'file' && e.name.endsWith('.js') && !e.name.endsWith('.orig.js') && !e.name.endsWith('.meta.json')) {
+ recent.push(`Modified module: ${e.name.replace('.js', '')}`);
+ }
+ }
+ }
+ // Check skills activity
+ const skills = fs.ls('/skills/');
+ if (skills && skills.length > 0) {
+ recent.push(`Skills directory: ${skills.filter(s => s.type === 'file').length} files`);
+ }
+ } catch (e) {}
+ return recent;
+ }
+
+ // ─── Commands ───
+
+ showModel() {
+ if (!this._model) return this.terminal.print('No model yet — run world-model-refresh', 'warning');
+ const m = this._model;
+
+ this.terminal.print('🌐 World Model', 'header');
+ this.terminal.print(` Version: ${m.version} (last synthsized: ${new Date(m.synthesized).toLocaleTimeString()})`, 'info');
+
+ this.terminal.print(` ⚙️ System — ${m.system.modules} modules loaded, ${m.system.vfsSize} VFS, ${m.system.memory}`, 'info');
+ this.terminal.print(` 🧩 Modules — ${m.modules.loaded} loaded (${m.modules.modifiedCount} modified)`, 'info');
+ this.terminal.print(` 📁 Files — ${m.files.total} files, ${m.files.directories} dirs, ${m.files.size}`, 'info');
+ this.terminal.print(` 📚 Skills — ${m.skills.count} installed`, 'info');
+ this.terminal.print(` 🧠 Memory Graph — ${m.memory.nodes} nodes, ${m.memory.edges} edges${m.memory.active ? ' ✅' : ''}`, 'info');
+ this.terminal.print(` 🤖 Agents — ${m.agents.agents} registered (${m.agents.active} active)`, 'info');
+ this.terminal.print(` 📊 Telemetry — ${m.telemetry.toolLatencyDataPoints} tool latency points, ${m.telemetry.recentErrors} recent errors`, 'info');
+
+ if (m.projects.activeProjects.length > 0) {
+ this.terminal.print(' Active context:', 'header');
+ for (const p of m.projects.activeProjects.slice(0, 3)) {
+ this.terminal.print(` 📌 ${p.name} (${p.accesses}x accesses)`, 'info');
+ }
+ }
+
+ if (m.recent.length > 0) {
+ this.terminal.print(' Recent changes:', 'header');
+ for (const r of m.recent.slice(0, 3)) {
+ this.terminal.print(` ${r}`, 'info');
+ }
+ }
+ }
+
+ showSummary() {
+ if (!this._model) return this.terminal.print('No model yet', 'warning');
+ const m = this._model;
+ const summary = `newOS4 ${m.version} · ${m.modules.loaded}/${m.modules.total} modules · ${m.skills.count} skills · ${m.files.total} files · ${m.agents.agents} agents · ${m.memory.nodes} graph nodes · ${m.telemetry.recentErrors > 0 ? `⚠️ ${m.telemetry.recentErrors} errors` : '✅ OK'}`;
+ this.terminal.print(summary, 'info');
+ return summary;
+ }
+
+ ask(question) {
+ if (!question) return this.terminal.print('Usage: world-model-ask <question>', 'error');
+ if (!this._model) return this.terminal.print('No model — run world-model-refresh first', 'warning');
+
+ const q = question.toLowerCase();
+ const m = this._model;
+ let answer = '';
+
+ if (q.includes('module') || q.includes('loaded')) {
+ answer = `Loaded ${m.modules.loaded} modules (${m.modules.modifiedCount} modified: ${m.modules.modified.join(', ') || 'none'})`;
+ } else if (q.includes('skill')) {
+ answer = `Installed ${m.skills.count} skills: ${m.skills.skills.join(', ') || 'none'}`;
+ } else if (q.includes('file') || q.includes('vfs')) {
+ answer = `${m.files.total} files in ${m.files.directories} directories, total ${m.files.size}`;
+ } else if (q.includes('agent') || q.includes('orchestrat')) {
+ answer = `${m.agents.agents} agents (${m.agents.active} active)`;
+ } else if (q.includes('health') || q.includes('status')) {
+ answer = `Health: ${m.system.health} · Memory: ${m.system.memory} · ${m.telemetry.recentErrors > 0 ? `${m.telemetry.recentErrors} recent errors` : 'No errors'}`;
+ } else if (q.includes('memory') || q.includes('graph')) {
+ answer = `Memory Graph: ${m.memory.nodes} nodes, ${m.memory.edges} edges (${m.memory.active ? 'active' : 'inactive'})`;
+ } else if (q.includes('change') || q.includes('yesterday') || q.includes('recent')) {
+ const recent = m.recent;
+ answer = recent.length > 0 ? `Recent changes: ${recent.join('; ')}` : 'No recent changes detected';
+ } else if (q.includes('project') || q.includes('work')) {
+ const projects = m.projects.activeProjects;
+ answer = projects.length > 0
+ ? `Active projects: ${projects.map(p => `${p.name} (${p.accesses}x)`).join(', ')}`
+ : 'No active projects detected';
+ } else if (q.includes('telemetry') || q.includes('latency')) {
+ answer = `${m.telemetry.toolLatencyDataPoints} tool latency data points tracked`;
+ } else if (q.includes('summary') || q.includes('overview') || q.includes('all')) {
+ answer = this.showSummary();
+ return;
+ } else {
+ answer = `I'm not sure how to answer "${question}". Try: modules, skills, files, agents, health, memory, changes, projects, telemetry, or summary.`;
+ }
+
+ this.terminal.print(`❓ ${question}`, 'info');
+ this.terminal.print(`💡 ${answer}`, 'success');
+ }
+
+ refresh() {
+ this._synthesize();
+ this.terminal.print(`✅ World Model refreshed at ${new Date().toLocaleTimeString()}`, 'success');
+ }
+
+ setConfig(args) {
+ if (args.length < 2) return this.terminal.print('Usage: world-model-config <key> <value>', 'error');
+ const key = args[0];
+ const val = args[1];
+
+ const configMap = {
+ 'refresh-interval': () => {
+ const n = parseInt(val) * 1000;
+ if (isNaN(n) || n < 10000) return 'Must be >= 10 seconds';
+ this._REFRESH_INTERVAL = n;
+ if (this._refreshTimer) clearInterval(this._refreshTimer);
+ this._refreshTimer = setInterval(() => this._synthesize(), n);
+ return `refresh-interval set to ${val}s`;
+ },
+ 'max-projects': () => {
+ const n = parseInt(val);
+ if (isNaN(n) || n < 1) return 'Must be >= 1';
+ this._MAX_PROJECTS = n;
+ return `max-projects set to ${n}`;
+ },
+ };
+
+ if (!configMap[key]) return this.terminal.print(`Unknown: ${key}. Valid: ${Object.keys(configMap).join(', ')}`, 'error');
+ const msg = configMap[key]();
+ this.terminal.print(`✅ ${msg}`, 'success');
+ }
+
+ // ─── Context Injection ───
+
+ 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)` : ''}` : ''}`;
+ }
+
+ // ─── Persistence ───
+
+ _loadFromVFS() {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+ const data = fs.fsReadJSON('/system/world-model.json');
+ if (data) {
+ this._model = data;
+ this._lastRefresh = data.synthesized;
+ }
+ } catch (e) {
+ console.warn('[WorldModel] Load error (fresh start):', e.message);
+ }
+ }
+
+ _saveToVFS(fs) {
+ try {
+ if (!fs.readDir('/system')) fs.mkdir('system');
+ fs.fsWriteJSON('/system/world-model.json', this._model);
+ } catch (e) {
+ console.error('[WorldModel] Save error:', e.message);
+ }
+ }
+}
+
+// Required for module system
+if (typeof window !== 'undefined') {
+ window.WorldModelModule = WorldModelModule;
+} \ No newline at end of file
diff --git a/modules/core/sandbox-worker.js b/modules/core/sandbox-worker.js
new file mode 100644
index 0000000..5841582
--- /dev/null
+++ b/modules/core/sandbox-worker.js
@@ -0,0 +1,205 @@
+// sandbox-worker.js — WebWorker entry point for isolated agent execution
+// Receives: { taskId, task, capabilities, vfsSnapshot, allowedCommands, apiProxy }
+// Returns: { taskId, result, error, toolCalls, duration }
+
+let _vfs = null;
+let _apiProxyOrigin = null;
+
+self.onmessage = async (e) => {
+ const { taskId, task, capabilities, vfsSnapshot, allowedCommands, apiProxy } = e.data;
+ _apiProxyOrigin = apiProxy ? new URL(apiProxy).origin : null;
+
+ const startTime = performance.now();
+ let toolCalls = 0;
+ let result = '';
+ let error = null;
+
+ try {
+ // Rebuild VFS from snapshot
+ _vfs = vfsSnapshot ? JSON.parse(vfsSnapshot) : { root: { type: 'dir', content: {} }, currentPath: [] };
+
+ // Log start
+ console.log(`[Sandbox ${taskId}] Starting task: "${task.substring(0, 60)}"`);
+
+ // Parse the task for tool use
+ const toolResults = {};
+ const commandWhitelist = allowedCommands || ['ls', 'cat', 'echo', 'find', 'grep', 'help', 'status'];
+
+ // Execute basic commands
+ const parts = task.trim().split(/\s+/);
+ const cmd = parts[0].toLowerCase();
+
+ if (cmd === 'ls' || cmd === 'list') {
+ result = sandbox_ls(parts[1] || '/');
+ toolCalls++;
+ } else if (cmd === 'cat' || cmd === 'read') {
+ result = sandbox_cat(parts[1] || '');
+ toolCalls++;
+ } else if (cmd === 'find' || cmd === 'search') {
+ result = sandbox_find(parts.slice(1).join(' ') || '');
+ toolCalls++;
+ } else if (cmd === 'grep') {
+ result = sandbox_grep(parts.slice(1).join(' '));
+ toolCalls++;
+ } else if (cmd === 'echo') {
+ result = parts.slice(1).join(' ');
+ toolCalls++;
+ } else if (cmd === 'help') {
+ result = 'Available commands: ' + commandWhitelist.join(', ');
+ toolCalls++;
+ } else if (cmd === 'status') {
+ result = sandbox_status();
+ toolCalls++;
+ } else if (capabilities.includes('run_command') && commandWhitelist.includes(cmd)) {
+ result = sandbox_run(cmd, parts.slice(1));
+ toolCalls++;
+ } else {
+ // If the task is descriptive (not a command), run smart handling
+ if (capabilities.includes('list_files') || capabilities.includes('read_file')) {
+ // Smart mode: interpret task
+ const lower = task.toLowerCase();
+ if (lower.includes('list') || lower.includes('file')) {
+ const dir = parts.find(p => p.startsWith('/') || p.startsWith('.')) || '/';
+ result = sandbox_ls(dir);
+ } else if (lower.includes('find') || lower.includes('search')) {
+ const q = task.replace(/find|search/i, '').trim();
+ result = sandbox_find(q || '');
+ } else if (lower.includes('count') || lower.includes('how many')) {
+ result = sandbox_ls('/');
+ const count = (result.match(/\[file\]|\[dir\]/g) || []).length;
+ result = `Found ${count} items in total.\\n${result}`;
+ } else if (lower.includes('status') || lower.includes('health')) {
+ result = sandbox_status();
+ } else {
+ result = `Task interpreted: "${task}"\\nCapabilities: ${capabilities.join(', ')}\\nTip: Be more specific (e.g. "list files in /")`;
+ }
+ toolCalls++;
+ } else {
+ result = `Task: "${task}"\\nNo matching capability for command "${cmd}"`;
+ }
+ }
+ } catch (e) {
+ error = e.message;
+ console.error(`[Sandbox ${taskId}] Error:`, e);
+ }
+
+ const duration = ((performance.now() - startTime) / 1000).toFixed(2);
+
+ self.postMessage({
+ taskId,
+ result,
+ error,
+ toolCalls,
+ duration: parseFloat(duration),
+ vfsSnapshot: JSON.stringify(_vfs)
+ });
+};
+
+// ─── Sandboxed VFS Operations ───
+
+function sandbox_ls(path) {
+ path = path || '/';
+ const entries = _vfs_get_entries(path);
+ if (!entries) return `ls: ${path}: No such directory`;
+ const lines = entries.map(e => {
+ const icon = e.type === 'dir' ? '[dir]' : '[file]';
+ const size = e.size !== undefined ? ` (${e.size}B)` : '';
+ return `${icon} ${e.name}${size}`;
+ });
+ return lines.join('\\n') || '(empty)';
+}
+
+function sandbox_cat(path) {
+ const content = _vfs_get_content(path);
+ if (content === null) return `cat: ${path}: No such file`;
+ if (typeof content === 'object') return `cat: ${path}: Is a directory`;
+ if (content.length > 8000) return content.substring(0, 8000) + '\\n... (truncated at 8KB)';
+ return content;
+}
+
+function sandbox_find(query) {
+ const results = [];
+ const q = query ? query.toLowerCase() : '';
+ _vfs_walk('/', (path, node) => {
+ if (node.type === 'file') {
+ if (!q || path.toLowerCase().includes(q)) {
+ results.push(path);
+ }
+ }
+ });
+ if (results.length === 0) return `No files found${query ? ` matching "${query}"` : ''}`;
+ return results.join('\\n');
+}
+
+function sandbox_grep(query) {
+ if (!query) return 'grep: missing pattern';
+ const q = query.toLowerCase();
+ const results = [];
+ _vfs_walk('/', (path, node) => {
+ if (node.type === 'file' && typeof node.content === 'string') {
+ const lines = node.content.split('\\n');
+ for (let i = 0; i < lines.length; i++) {
+ if (lines[i].toLowerCase().includes(q)) {
+ results.push(`${path}:${i + 1}: ${lines[i].trim().substring(0, 100)}`);
+ }
+ }
+ }
+ });
+ if (results.length === 0) return `grep: no matches for "${query}"`;
+ return results.slice(0, 50).join('\\n') + (results.length > 50 ? `\\n... (+${results.length - 50} more)` : '');
+}
+
+function sandbox_status() {
+ let fileCount = 0, dirCount = 0;
+ _vfs_walk('/', (path, node) => {
+ if (node.type === 'file') fileCount++;
+ else dirCount++;
+ });
+ const vfsSize = JSON.stringify(_vfs).length;
+ return `Sandbox VFS Status:\\n Files: ${fileCount}\\n Directories: ${dirCount}\\n VFS Size: ${(vfsSize / 1024).toFixed(1)}KB`;
+}
+
+function sandbox_run(cmd, args) {
+ return `[sandbox] ${cmd} ${args.join(' ')}\\n(Command execution limited in sandbox mode)`;
+}
+
+// ─── VFS Helpers ───
+
+function _vfs_get_entries(path) {
+ const dir = _vfs_resolve(path);
+ if (!dir || dir.type !== 'dir') return null;
+ return Object.entries(dir.content || {}).map(([name, node]) => ({
+ name,
+ type: node.type || 'file',
+ size: typeof node.content === 'string' ? node.content.length : undefined
+ })).sort((a, b) => a.name.localeCompare(b.name));
+}
+
+function _vfs_get_content(path) {
+ const node = _vfs_resolve(path);
+ if (!node || node.type === 'dir') return null;
+ return node.content;
+}
+
+function _vfs_resolve(path) {
+ if (!_vfs || !_vfs.root) return null;
+ if (path === '/' || path === '') return _vfs.root;
+ const parts = path.split('/').filter(p => p);
+ let current = _vfs.root;
+ for (const part of parts) {
+ if (!current.content || !current.content[part]) return null;
+ current = current.content[part];
+ }
+ return current;
+}
+
+function _vfs_walk(path, cb) {
+ const node = _vfs_resolve(path);
+ if (!node) return;
+ cb(path, node);
+ if (node.type === 'dir' && node.content) {
+ for (const [name, child] of Object.entries(node.content)) {
+ _vfs_walk(path === '/' ? '/' + name : path + '/' + name, cb);
+ }
+ }
+} \ No newline at end of file
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index e3f222c..d916ae6 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -1028,6 +1028,10 @@
this._safeRegister('LeaseModule', 'lease system');
this._safeRegister('OrchestratorModule', 'orchestrator');
this._safeRegister('NeuralVFSModule', 'neural VFS');
+ this._safeRegister('SandboxModule', 'sandbox');
+ this._safeRegister('MemoryGraphModule', 'memory graph');
+ this._safeRegister('PredictorModule', 'predictor');
+ this._safeRegister('WorldModelModule', 'world model');
this._safeRegister('ServerMonitorModule', 'server monitor');
this._safeRegister('SimpleGUIModule', 'simple GUI builder');
}