aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-06-17 06:01:20 +0200
committerAgando <roduccomm@gmail.com>2026-06-17 06:01:20 +0200
commitcebacc27a48168e666679018f8e27156d12d4527 (patch)
treeb306d33b67b9d8c84bd9b9c16b1e91ee217628a6
parent6aa24b976952a698c673d350977721b27193183d (diff)
auto: 2026-06-17_06:01 changes
-rw-r--r--A2A_protocol_spec.md115
-rw-r--r--cheatsheet.html7
-rw-r--r--docs/session-notes/2026-06-17_readonly-wallet-lookup.md45
-rw-r--r--help.html34
-rw-r--r--main.html8
-rw-r--r--modules/core/core-bundle.js1124
-rw-r--r--modules/core/module-agent-registry.js320
-rw-r--r--modules/core/module-aiassist.js196
-rw-r--r--modules/core/module-fs.js74
-rw-r--r--modules/core/module-leases.js25
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/module-neural-vfs.js2
-rw-r--r--modules/core/module-orchestrator.js41
-rw-r--r--modules/core/module-smartctx.js59
-rw-r--r--modules/core/module-world-model.js83
-rw-r--r--modules/core/ollama-manager.js41
-rw-r--r--modules/core/terminal.js97
-rw-r--r--modules/core/virtual-fs.js39
-rw-r--r--modules/ext/critical-ext-bundle.js4
-rw-r--r--modules/ext/webdav.js14
-rw-r--r--newos4_whitepaper.md2
-rw-r--r--session-resume-2026-06-17.md67
-rw-r--r--skills/agent-orchestration.md14
23 files changed, 2010 insertions, 403 deletions
diff --git a/A2A_protocol_spec.md b/A2A_protocol_spec.md
index bb3d713..3c4d7ac 100644
--- a/A2A_protocol_spec.md
+++ b/A2A_protocol_spec.md
@@ -1,69 +1,78 @@
-# A2A Protocol Specification v0.2
+# A2A Protocol Specification v0.3
## Overview
-Agent-to-Agent (A2A) is a decentralized message protocol for agent negotiation within newOS4. Agents discover each other via VFS registrations and communicate through a VFS mailbox system. No central broker.
+Agent-to-Agent (A2A) is a decentralized message protocol for agent negotiation within newOS4. Agents communicate through a **shared VFS scratchpad** (`/_agents/scratchpad.json`). No central broker — discovery is via VFS registrations.
-## Message Format
-All messages are JSON with this envelope:
+## Scratchpad Data Structure
+
+All messages live in a single JSON file at `/_agents/scratchpad.json`:
```json
{
- "a2a_version": "0.1",
- "msg_id": "<uuid>",
- "type": "request" | "response" | "broadcast" | "lease" | "heartbeat",
- "from": "<agent_id>",
- "to": "<agent_id>" | "*",
- "timestamp": "<ISO 8601>",
- "ttl_seconds": 30,
- "contract": {
- "capabilities": ["list_files", "read_file", "web_search"],
- "constraints": {
- "max_tool_turns": 8,
- "max_duration_seconds": 60
+ "messages": [
+ {
+ "id": "msg_001",
+ "from": "user",
+ "to": "researcher",
+ "type": "request",
+ "capability": "code.search",
+ "payload": { "query": "test" },
+ "ttl": 300000,
+ "status": "pending",
+ "ts": 1234567890,
+ "reply_to": null
+ }
+ ],
+ "seq": 1,
+ "agents": {
+ "researcher": {
+ "last_poll": 1234567890,
+ "capabilities": ["coder", "reader", "writer"]
}
- },
- "payload": { }
+ }
}
```
-## Transport Layer (VFS Mailbox)
-Each agent has a VFS mailbox:
-- **Inbox** — `/agents/<agent_id>/inbox.json` — receives incoming A2A messages
-- **Outbox** — `/agents/<agent_id>/outbox.json` — agent writes results here after execution
-
-The orchestrator polls all agent inboxes every **500ms** via `_pollAgentMailboxes()`. When a new A2A message appears, it:
-1. Reads the message envelope
-2. Routes by type:
- - `request` — task delegation, checks outbox for completion
- - `response` — task result from agent
- - `broadcast` — capability advertisement
- - `heartbeat` — updates agent's lastHeartbeat timestamp
-3. Marks messages as `_processed` to avoid re-routing
-
-## Message Types
-- **request**: Point-to-point task delegation. From=orchestrator, To=worker.
-- **response**: Task result. From=worker, To=orchestrator.
-- **broadcast**: Discovery / capability advertisement. To="*".
-- **lease**: Time-bound contract granting a worker exclusive task rights.
-- **heartbeat**: Alive signal. Every 5 seconds. Missing 3 consecutive = presumed dead.
+### Message Statuses
+| Status | Meaning |
+|---|---|
+| `pending` | Not yet delivered |
+| `delivered` | Read by recipient via poll |
+| `completed` | Acknowledged with result |
+| `failed` | Rejected with error |
## Commands
-- `a2a-send <agent-id> <json-message>` — Send an A2A message to an agent's inbox
-- `a2a-list` — Show all A2A messages across agents with status
+
+| Command | Description |
+|---|---|
+| `a2a-post <to> <type> <capability> <payload...>` | Post a message to an agent |
+| `a2a-poll [agentId]` | Poll for pending messages (default: current agent or "user") |
+| `a2a-ack <msgId> <result...>` | Mark message completed with result |
+| `a2a-reject <msgId> <error...>` | Mark message failed with error |
+| `a2a-status` | Show scratchpad stats per agent |
+| `a2a-prune` | Clean up expired and old completed messages |
## Lifecycle
-1. Orchestrator sends `request` to agent's inbox via `a2a-send`
-2. AI module receives delegated task, executes it
-3. Result written to agent's outbox
-4. Polling loop detects outbox completion, sends `response` message
-5. Orchestrator marks lease as complete, agent returns to idle pool
+
+1. **Register** — `agent-register <name> <capabilities>` creates agent dir + auto-registers in scratchpad
+2. **Post** — `a2a-post <to> <type> <capability> <payload>` writes to scratchpad with `status: pending`
+3. **Poll** — `a2a-poll <agentId>` returns pending messages, marks them `delivered`
+4. **Ack/Reject** — Recipient acknowledges with result or rejects with error
+5. **Unregister** — `agent-unregister <id>` removes agent from scratchpad + deletes VFS dir
+6. **Auto-prune** — Every 60 seconds, expired messages (past TTL) and completed messages older than 1 hour are removed
+
+## Integration Points
+
+- **Orchestrator** — auto-calls `a2aRegister` on agent spawn, `a2aUnregister` on kill
+- **Lease system** — posts A2A messages on lease create and lease revoke
+- **Garbage collection** — `_a2aCleanup()` removes orphaned agent entries when agent dirs are deleted
## Implementation Status
-- ✅ Message format (v0.1 envelope)
-- ✅ VFS mailbox transport (inbox.json / outbox.json)
-- ✅ 500ms polling loop in orchestrator
-- ✅ Type routing (request, response, broadcast, heartbeat)
-- ✅ a2a-send / a2a-list CLI commands
-- ⬜ Heartbeat sending from worker agents
-- ⬜ Full broadcast discovery flow
-- ⬜ Lease revocation via A2A message (currently uses separate LeaseModule) \ No newline at end of file
+- ✅ Scratchpad data structure (/_agents/scratchpad.json)
+- ✅ a2a-post / a2a-poll / a2a-ack / a2a-reject / a2a-status / a2a-prune CLI commands
+- ✅ Auto-register on agent spawn
+- ✅ Auto-unregister on agent kill
+- ✅ A2A messages on lease create/revoke
+- ✅ Auto-prune timer (60s interval)
+- ✅ Garbage collection for orphaned agent entries
+- ✅ Security whitelist (all A2A commands whitelisted, not destructive)
diff --git a/cheatsheet.html b/cheatsheet.html
index 61ead4e..25ce256 100644
--- a/cheatsheet.html
+++ b/cheatsheet.html
@@ -269,6 +269,13 @@ pre{background:var(--bg2);border:1px solid var(--border);border-radius:var(--rad
<div class="item"><span class="cmd">shadow-rollback</span><span class="desc">Undo shadow VFS changes</span></div>
</div>
+<h2>🧠 VaultMind AI</h2>
+<div class="grid">
+<div class="item"><span class="cmd">vaultmind_connect</span><span class="desc">Connect wallet to VaultMind (reads key from SecretVault)</span></div>
+<div class="item"><span class="cmd">vaultmind_status</span><span class="desc">Check VaultMind session status</span></div>
+<div class="item"><span class="cmd">vaultmind_query</span><span class="desc">Query prices, portfolio, chat, etc.</span></div>
+</div>
+
<h2>🛠️ Developer</h2>
<div class="grid">
<div class="item"><span class="cmd">js &lt;expr&gt;</span><span class="desc">Eval JS in page context</span></div>
diff --git a/docs/session-notes/2026-06-17_readonly-wallet-lookup.md b/docs/session-notes/2026-06-17_readonly-wallet-lookup.md
new file mode 100644
index 0000000..c387954
--- /dev/null
+++ b/docs/session-notes/2026-06-17_readonly-wallet-lookup.md
@@ -0,0 +1,45 @@
+# Read-Only Wallet Lookup — Session Notes
+
+**Date:** 2026-06-17
+**Goal:** Add read-only Hyperliquid wallet lookup for `0xA12b193832d401A1B5f738170631fD6547aF3C3F` — no private key needed.
+
+## What was done
+
+### 1. Backend endpoint (`/home/darkbot/ai/vaultmind-ai/backend/main.py`)
+- Added `GET /wallet/{address}` — uses `hl_client.info` (no key required)
+- Returns: `account_value`, `margin_used`, `withdrawable`, `positions[]` (coin, size, entry_px, pnl, market)
+- Hardcoded `mainnet1` for the read-only endpoint (was hitting testnet)
+- Cache: `Cache-Control: public, max-age=5`
+
+### 2. Nginx proxy
+- Added `location /wallet/` → `http://127.0.0.1:8000/wallet/`
+- **Bug:** The nginx config was corrupted — `/vmai/` block missing closing `}`, `/vmai-api/` nested inside it, 7 duplicate `/wallet/` blocks from repeated patch attempts. Rewrote the entire file cleanly.
+- **Important:** `/etc/nginx/sites-enabled/webx11.duckdns.org` is a **regular file** (not a symlink to sites-available). Both files must be kept in sync.
+
+### 3. AI tool (`module-aiassist.js` + `core-bundle.js`)
+- Added `wallet_lookup` tool: schema + handler + system prompt
+- Usage: `ai show wallet 0xA12b...3C3F`
+- Tool calls `GET /wallet/{address}` via `web_fetch`
+
+### 4. Stale `.pyc` fix
+- `find /home/darkbot/ai/vaultmind-ai/ -name '*.pyc' -delete` after adding new route
+- Without this, the running process doesn't see the new endpoint
+
+## Files changed
+- `backend/main.py` — new `/wallet/{address}` route
+- `module-aiassist.js` — `wallet_lookup` tool def + handler + system prompt
+- `core-bundle.js` — same changes in bundled copy
+- `/etc/nginx/sites-enabled/webx11.duckdns.org` — added `/wallet/` proxy
+- `/etc/nginx/sites-available/webx11.duckdns.org` — synced
+
+## Verification
+```bash
+curl -s https://webx11.duckdns.org/wallet/0xA12b193832d401A1B5f738170631fD6547aF3C3F
+```
+Returns: $11.67 equity, 4 positions (BTC, NEAR, FIL, TAO)
+
+## Key lessons
+- Nginx sites-enabled is a regular file, not a symlink — edit both
+- Stale `.pyc` hides new routes — always clear after adding endpoints
+- Read-only Hyperliquid queries use `Info` class, no key needed
+- The stored key (`0x0e80...055c`) controls a different wallet (`0x6cB4...1Aa2`, empty) — not the user's trading wallet
diff --git a/help.html b/help.html
index e50124c..2429121 100644
--- a/help.html
+++ b/help.html
@@ -664,6 +664,9 @@ diff a.txt b.txt</pre>
<tr><td><code>view/edit/test/reload</code></td><td>Module operations</td><td>edit ✅</td></tr>
<tr><td><code>user_get/set/list/forget</code></td><td>User profile</td><td>❌</td></tr>
<tr><td><code>session_save/load/recent/stats</code></td><td>Session memory</td><td>❌</td></tr>
+ <tr><td><code>vaultmind_connect</code></td><td>Connect wallet to VaultMind AI (reads key from SecretVault)</td><td>❌</td></tr>
+ <tr><td><code>vaultmind_status</code></td><td>Check VaultMind session status</td><td>❌</td></tr>
+ <tr><td><code>vaultmind_query</code></td><td>Query VaultMind: prices, portfolio, chat</td><td>❌</td></tr>
</table></div>
<h3>Sound Notification</h3>
@@ -824,6 +827,37 @@ ollamaconnect</pre>
</table></div>
</section>
+ <!-- ── VaultMind AI ── -->
+ <section id="vaultmind">
+ <h2>🧠 VaultMind AI</h2>
+ <div class="callout callout-success">
+ <span class="callout-title">Hyperliquid AI Assistant</span>
+ VaultMind is an AI-powered wallet assistant for Hyperliquid. It provides market data, portfolio analysis, transaction analysis, and AI chat agents.
+ </div>
+
+ <h3>Setup</h3>
+ <ol>
+ <li>Store your Hyperliquid private key: <code>secret-set hyperliquid_key 0x...</code></li>
+ <li>Ask the AI to connect: <code>ai connect to vaultmind</code> or use the <code>vaultmind_connect</code> tool</li>
+ <li>Check status: <code>ai check vaultmind status</code> or <code>vaultmind_status</code></li>
+ </ol>
+
+ <h3>AI Tools</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Tool</th><th>Description</th></tr>
+ <tr><td><code>vaultmind_connect</code></td><td>Reads <code>hyperliquid_key</code> from SecretVault, authenticates with VaultMind backend, stores session token</td></tr>
+ <tr><td><code>vaultmind_status</code></td><td>Checks if a valid VaultMind session token exists in localStorage</td></tr>
+ <tr><td><code>vaultmind_query</code></td><td>Query endpoints: <code>prices</code>, <code>portfolio</code>, <code>transactions</code>, <code>copy-trade</code>, or <code>chat</code> with an AI agent</td></tr>
+ </table></div>
+
+ <h3>API Endpoints (proxied via nginx)</h3>
+ <div class="table-wrap"><table>
+ <tr><th>Path</th><th>Backend</th></tr>
+ <tr><td><code>/vmai/</code></td><td>Proxied to VaultMind backend at <code>127.0.0.1:8000</code></td></tr>
+ <tr><td><code>/vmai-api/</code></td><td>Alias for frontend chat — also proxied to <code>127.0.0.1:8000</code></td></tr>
+ </table></div>
+ </section>
+
<!-- ── Modules ── -->
<section id="modules">
<h2>🧩 Module System</h2>
diff --git a/main.html b/main.html
index fc1b4dd..2e963bc 100644
--- a/main.html
+++ b/main.html
@@ -1280,12 +1280,12 @@
}
</script>
- <!-- v4.5.61: vaultmind_query passes Bearer token from localStorage -->
-<script src="modules/core/core-bundle.js?v=2026-06-16-v4.5.61"></script>
-<script src="modules/ext/critical-ext-bundle.js?v=2026-06-16-v4.5.61"></script>
+ <!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix -->
+<script src="modules/core/core-bundle.js?v=2026-06-17-v4.5.63"></script>
+<script src="modules/ext/critical-ext-bundle.js?v=2026-06-17-v4.5.63"></script>
<!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules -->
-<script src="modules/core/module-loader.js?v=2026-06-16-v4.5.61"></script>
+<script src="modules/core/module-loader.js?v=2026-06-17-v4.5.63"></script>
<script>
// ═══ ONE TRUE VERSION SOURCE ═══
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index 24ec5af..29d6fc8 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -1,5 +1,4 @@
-// Core module bundle — generated for v4.5.54
-
+// Core module bundle// Generated from /var/www/webx11.duckdns.org/spaces/newos4/modules/core
/* === virtual-fs.js === */
class VirtualFileSystem {
constructor() {
@@ -9,6 +8,29 @@ class VirtualFileSystem {
this._virtualMounts = null; // lazy-init on first /sys/ access
// Register /sys/ in emit-change exclusion (also added below)
}
+ // ── Auto-purge trash older than 24 hours ──
+ _purgeTrash() {
+ try {
+ const trash = this.root.content && this.root.content['.trash'];
+ if (!trash || !trash.content) return;
+ const now = Date.now();
+ const maxAge = 86400000; // 24 hours
+ let purged = 0;
+ for (const name of Object.keys(trash.content)) {
+ const tsMatch = name.match(/^(\d+)/);
+ if (tsMatch) {
+ const ts = parseInt(tsMatch[1], 10);
+ if (now - ts > maxAge) {
+ delete trash.content[name];
+ purged++;
+ }
+ }
+ }
+ if (purged > 0) {
+ this.saveToStorage();
+ }
+ } catch (e) { /* silent */ }
+ }
// ── Virtual /sys/ filesystem (live runtime state, never persisted) ──
_initVirtualMounts() {
if (this._virtualMounts) return;
@@ -345,6 +367,7 @@ class VirtualFileSystem {
const data = JSON.parse(text);
this.root = data.root || this.root;
this.currentPath = data.currentPath || [];
+ this._purgeTrash();
return true;
} catch (e) {
console.error('FS v2 decompress error:', e);
@@ -829,10 +852,17 @@ class VirtualFileSystem {
checkpoint(name) {
if (!name) name = new Date().toISOString();
const createdAt = new Date().toISOString();
+ // Strip system dirs to prevent fractal bloat
+ const rootClone = JSON.parse(JSON.stringify(this.root));
+ if (rootClone && rootClone.content) {
+ delete rootClone.content['_sessions'];
+ delete rootClone.content['_snapshots'];
+ delete rootClone.content['.trash'];
+ }
const data = {
version: 1,
createdAt,
- root: JSON.parse(JSON.stringify(this.root)),
+ root: rootClone,
currentPath: [...this.currentPath]
};
if (!this.readDir('/_snapshots')) {
@@ -840,11 +870,11 @@ class VirtualFileSystem {
}
this.fsWriteJSON('/_snapshots/checkpoint-' + name + '.json', data);
this.saveToStorage();
- // Auto-manage: keep max 10 checkpoints, delete oldest
+ // Auto-manage: keep max 3 checkpoints, delete oldest
const all = this.listCheckpoints();
- if (all.length > 10) {
+ if (all.length > 3) {
const sorted = all.slice().sort();
- const toDelete = sorted.slice(0, all.length - 10);
+ const toDelete = sorted.slice(0, all.length - 3);
for (const old of toDelete) this.deleteCheckpoint(old);
}
return true;
@@ -918,13 +948,6 @@ window.VirtualFileSystem = VirtualFileSystem;
localStorage.setItem('webx11_modules', JSON.stringify(this.moduleRegistry));
} catch (e) { console.error('Module save error:', e); }
}
- async restorePersistedModules() {
- for (const entry of this.moduleRegistry) {
- if (entry.enabled && entry.url) {
- await this.loadModuleFromURL(entry.url);
- }
- }
- }
async loadModuleFromURL(url) {
try {
this.terminal.print(`Loading module from ${url}...`, 'info');
@@ -934,17 +957,10 @@ window.VirtualFileSystem = VirtualFileSystem;
const ModuleClass = this.createModuleClass(code, url);
if (ModuleClass) {
const instance = new ModuleClass();
+ this.registerModule(instance, url);
if (instance.init) {
- try {
- await instance.init(this.terminal);
- instance._initComplete = true;
- } catch (e) {
- console.error(`[ModuleManager] init() failed for ${instance.constructor.name}:`, e);
- this.terminal.print(`Module '${instance.constructor.name}' init() failed: ${e.message}`, 'error');
- return false;
- }
+ await instance.init(this.terminal);
}
- this.registerModule(instance, url);
this.terminal.print(`Module loaded successfully from ${url}`, 'success');
return true;
}
@@ -957,13 +973,6 @@ window.VirtualFileSystem = VirtualFileSystem;
try {
// Strip leading comments to avoid ASI: `return\n/**` → `return; /**`
const cleanCode = code.replace(/^\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*\s*/, '');
- // Try wrapper convention first: `return class MyModule { ... };`
- const wrapperMatch = cleanCode.match(/return\s+class\s+(\w+)/);
- if (wrapperMatch) {
- const mod = new Function(cleanCode)();
- if (mod && typeof mod === 'function') return mod;
- }
- // Fall back to regex class extraction
const classMatch = cleanCode.match(/class\s+(\w+)\s+(extends\s+\S+)?\s*\{/);
if (classMatch) {
const className = classMatch[1];
@@ -980,15 +989,7 @@ window.VirtualFileSystem = VirtualFileSystem;
const name = instance.constructor.name;
this.modules[name] = instance;
instance.terminal = this.terminal;
- instance.fs = this.terminal.fs;
- try {
- if (instance.registerCommands) { instance.registerCommands(); }
- } catch (e) {
- console.error(`[ModuleManager] registerCommands() failed for ${name}:`, e);
- delete this.modules[name];
- this.terminal.print(`Module '${name}' registration failed: ${e.message}`, 'error');
- return;
- }
+ if (instance.registerCommands) { instance.registerCommands(); }
// Only persist modules loaded from absolute URLs (not boot-time ext modules)
if (url && url.startsWith('http') && !this.moduleRegistry.find(m => m.name === name)) {
this.moduleRegistry.push({
@@ -1008,17 +1009,16 @@ window.VirtualFileSystem = VirtualFileSystem;
}
return false;
}
- async loadModuleFromCode(code, url) {
+ loadModuleFromCode(code, url) {
try {
this.terminal.print('Loading module from editor...', 'info');
const ModuleClass = this.createModuleClass(code, url);
if (ModuleClass) {
const instance = new ModuleClass();
+ this.registerModule(instance, url);
if (instance.init) {
- await instance.init(this.terminal);
- instance._initComplete = true;
+ instance.init(this.terminal);
}
- this.registerModule(instance, url);
this.terminal.print(`Module loaded from editor: ${instance.constructor.name}`, 'success');
return true;
}
@@ -1084,31 +1084,6 @@ window.VirtualFileSystem = VirtualFileSystem;
}
return help;
}
- diagnose() {
- const report = {
- totalInModules: Object.keys(this.modules).length,
- totalInRegistry: this.moduleRegistry.length,
- orphaned: [],
- missingFs: [],
- missingTerminal: [],
- uninitialized: [],
- healthy: 0
- };
- const registryNames = new Set(this.moduleRegistry.map(m => m.name));
- for (const name in this.modules) {
- const mod = this.modules[name];
- if (!registryNames.has(name)) {
- report.orphaned.push(name);
- }
- if (!mod.fs) report.missingFs.push(name);
- if (!mod.terminal) report.missingTerminal.push(name);
- if (typeof mod.init === 'function' && !mod._initComplete) {
- report.uninitialized.push(name);
- }
- if (mod.fs && mod.terminal) report.healthy++;
- }
- return report;
- }
}
/* === ollama-manager.js === */
@@ -1133,6 +1108,8 @@ class OllamaManager {
this.validatorEnabled = localStorage.getItem('webx11_validator_enabled') === 'true';
this.validatorModel = localStorage.getItem('webx11_validator_model') || 'ministral-3:3b-cloud';
this.validatorEndpoint = localStorage.getItem('webx11_validator_endpoint') || '';
+ // --- Model Parameters (temperature, top_p, max_tokens, thinking) ---
+ this.modelParams = this._loadModelParams();
// Build fallback endpoints
this.fallbackEndpoints = [];
const origin = typeof window !== 'undefined' ? window.location.origin : '';
@@ -1187,6 +1164,28 @@ class OllamaManager {
this._setSecret('webx11_llm_providers', JSON.stringify(this.providers));
}
+ _loadModelParams() {
+ try {
+ const saved = localStorage.getItem('webx11_model_params');
+ if (saved) {
+ const parsed = JSON.parse(saved);
+ return {
+ temperature: parsed.temperature !== undefined ? parsed.temperature : 0.7,
+ top_p: parsed.top_p !== undefined ? parsed.top_p : 0.9,
+ max_tokens: parsed.max_tokens !== undefined ? parsed.max_tokens : 4096,
+ thinking: parsed.thinking || 'off'
+ };
+ }
+ } catch (e) {}
+ return { temperature: 0.7, top_p: 0.9, max_tokens: 4096, thinking: 'off' };
+ }
+
+ _saveModelParams() {
+ try {
+ localStorage.setItem('webx11_model_params', JSON.stringify(this.modelParams));
+ } catch (e) {}
+ }
+
_getSecret(key) {
if (typeof window !== 'undefined' && window.webx11_secretVault && typeof window.webx11_secretVault.decrypt === 'function') {
return window.webx11_secretVault.decrypt(key);
@@ -1825,10 +1824,21 @@ class OllamaManager {
messages: messages,
stream: false
};
+ // Apply model parameters (user-configured temperature, top_p, max_tokens, thinking)
+ if (this.modelParams) {
+ body.temperature = this.modelParams.temperature;
+ body.top_p = this.modelParams.top_p;
+ body.max_tokens = this.modelParams.max_tokens;
+ }
if (isDeepSeek) {
- body.temperature = 0.3;
- body.max_tokens = 4096;
+ // DeepSeek defaults: user params override hardcoded defaults
+ if (body.temperature === undefined) body.temperature = 0.3;
+ if (body.max_tokens === undefined) body.max_tokens = 4096;
body.stop = ['<|end|>'];
+ // DeepSeek thinking mode
+ if (this.modelParams && this.modelParams.thinking !== 'off') {
+ body.thinking = { type: this.modelParams.thinking };
+ }
}
if (tools && tools.length > 0) {
body.tools = tools;
@@ -1914,7 +1924,7 @@ class OllamaManager {
return {
message: { role: 'assistant', content: '' },
tool_calls: null,
- error: null
+ error: 'DeepSeek returned empty response after retry — model may be overloaded or context too long'
};
}
@@ -1970,11 +1980,6 @@ class OllamaManager {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
const text = data.choices?.[0]?.message?.content || '';
- // Sync provider state after successful non-Ollama fallback
- if (provider !== this.currentProvider) {
- this.currentProvider = provider;
- this._syncCompatProperties(provider);
- }
return { message: { role: 'assistant', content: text }, tool_calls: null, error: null };
} catch (e) {
if (e.name === 'AbortError') {
@@ -2001,11 +2006,10 @@ ${toolDesc}
</tools>
Only output XML function calls for tool tasks. If no tool is needed, answer normally.` };
-const msgsWithTools = [toolContext, ...messages];
+ const msgsWithTools = [toolContext, ...messages];
const body = { model: useModel, messages: msgsWithTools, stream: false, options: { temperature: 0.1 } };
- const ep = this.currentProvider ? this.currentProvider.endpoint : this.endpoint;
- const response = await fetch(`${ep}/api/chat`, {
+ const response = await fetch(`${this.endpoint}/api/chat`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body), signal
});
@@ -2203,7 +2207,7 @@ const msgsWithTools = [toolContext, ...messages];
this.destructiveCmds = new Set(['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']);
this.systemCmds = new Set(['reboot','shutdown','format','kill','halt','poweroff']);
this.suspiciousFlags = new Set(['--dump','--execute','--eval','--exec','--shell','--script']);
- this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem']);
+ this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register']);
// Load persisted overrides from localStorage
try { const d = localStorage.getItem('webx11_security_destructive'); if(d){JSON.parse(d).forEach(c=>this.destructiveCmds.add(c));} }catch(e){}
try { const s = localStorage.getItem('webx11_security_system'); if(s){JSON.parse(s).forEach(c=>this.systemCmds.add(c));} }catch(e){}
@@ -2212,7 +2216,7 @@ const msgsWithTools = [toolContext, ...messages];
this._registerBuiltIn();
}
_registerBuiltIn() {
- const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg'];
+ const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register'];
internals.forEach(c => this.registerCommand(c, { category: 'builtin' }));
}
setLevel(n) {
@@ -3047,35 +3051,14 @@ const msgsWithTools = [toolContext, ...messages];
_autoSaveSession() {
try {
this.fs.mkdirRecursive('_sessions');
- var rootClone;
- try {
- const json = JSON.stringify(this.fs.root);
- if (json.length > 52428800) { // 50MB cap
- rootClone = { type: 'directory', name: '/', content: {}, _truncated: true };
- this.print('⚠ Auto-save skipped (VFS too large)', 'warning');
- return;
- }
- rootClone = JSON.parse(json);
- } catch (e) {
- rootClone = { type: 'directory', name: '/', content: {}, _error: true };
- return;
- }
- if (rootClone && rootClone.content && rootClone.content['_sessions']) {
- delete rootClone.content['_sessions'];
- }
const snapshot = {
type: 'session',
+ version: 2,
savedAt: new Date().toISOString(),
- fsRoot: rootClone,
currentPath: this.fs.currentPath,
- version: 1,
terminalLines: this._historyBuffer.slice(-50)
};
const snapshotJson = JSON.stringify(snapshot);
- if (snapshotJson.length > 52428800) {
- this.print('⚠ Auto-save snapshot too large, skipping', 'warning');
- return;
- }
this.fs.writeFile('_sessions/_auto.json', snapshotJson);
try {
const ctx = localStorage.getItem('webx11_ai_context');
@@ -3100,19 +3083,40 @@ const msgsWithTools = [toolContext, ...messages];
this.print('↻ Skipped stale auto-session backup (older than last reset)', 'info');
return;
}
- if (data && data.fsRoot) {
+ if (data.version === 2) {
+ // v2: metadata-only restore (VFS is persisted independently in localStorage)
+ this.fs.currentPath = data.currentPath || [];
+ this.fs.saveToStorage();
+ this.updatePrompt();
+ this.print('↻ Auto-restored session from ' + (data.savedAt ? new Date(data.savedAt).toLocaleTimeString() : 'recent'), 'success');
+ // Optionally re-render last few terminal lines
+ if (data.terminalLines && Array.isArray(data.terminalLines)) {
+ data.terminalLines.slice(-10).forEach(line => {
+ const text = typeof line === 'string' ? line : (line.l || '');
+ this.print(text, typeof line === 'object' ? (line.t || '') : '');
+ });
+ }
+ try {
+ const aiCtx = this.fs.readFile('_sessions/_auto-context.json');
+ if (aiCtx) {
+ localStorage.setItem('webx11_ai_context', aiCtx);
+ }
+ } catch (e) {}
+ this.fs.deleteFile('_sessions/_auto.json');
+ this.fs.deleteFile('_sessions/_auto-context.json');
+ } else if (data && data.fsRoot) {
+ // v1: full VFS restore (legacy)
this.fs.root = data.fsRoot;
this.fs.currentPath = data.currentPath || [];
this.fs.saveToStorage();
+ this.updatePrompt();
this.print('↻ Auto-restored session from ' + (data.savedAt ? new Date(data.savedAt).toLocaleTimeString() : 'recent'), 'success');
- // Defer AI context restoration (AI module loads after terminal init)
try {
const aiCtx = this.fs.readFile('_sessions/_auto-context.json');
if (aiCtx) {
localStorage.setItem('webx11_ai_context', aiCtx);
}
} catch (e) {}
- // v4.5.47: delete backup after successful restore so reset-mem doesn't get resurrected
this.fs.deleteFile('_sessions/_auto.json');
this.fs.deleteFile('_sessions/_auto-context.json');
}
@@ -3759,22 +3763,41 @@ const msgsWithTools = [toolContext, ...messages];
return;
}
if (cmd === 'history') {
- const search = args.join(' ').toLowerCase();
const entries = this.history;
if (!entries.length) { this.print('No command history.', 'info'); return; }
- if (search) {
- const filtered = entries.map((c, i) => ({ i, c })).filter(e => e.c.toLowerCase().includes(search));
- if (!filtered.length) { this.print(`No history entries matching: ${search}`, 'info'); return; }
- this.print(`=== History (matching: ${search}) ===`, 'info');
- filtered.forEach(e => this.print(` ${e.i + 1} ${e.c}`));
- } else {
- this.print(`=== Command History (${entries.length} entries) ===`, 'info');
- const start = Math.max(0, entries.length - 50);
- for (let i = start; i < entries.length; i++) {
- this.print(` ${i + 1} ${entries[i]}`);
+ // Flags
+ if (args[0] === '--clear' || args[0] === '-c') {
+ this.history = [];
+ this.historyIndex = 0;
+ localStorage.removeItem('webx11_command_history');
+ this.print('History cleared.', 'success');
+ return;
+ }
+ if (args[0] === '--export' || args[0] === '-e') {
+ try {
+ this.fs.mkdirRecursive('system');
+ this.fs.writeFile('system/history.json', JSON.stringify(entries));
+ this.print('History exported to /system/history.json (' + entries.length + ' entries).', 'success');
+ } catch (e) {
+ this.print('Export failed: ' + e.message, 'error');
}
- if (entries.length > 50) this.print(`... (${entries.length - 50} older entries hidden, use history <search> to find)`);
+ return;
}
+ if (args[0] === '--search' || args[0] === '-s') {
+ const term = args.slice(1).join(' ').toLowerCase();
+ if (!term) { this.print('Usage: history --search <term>', 'warning'); return; }
+ const filtered = entries.filter(e => e.toLowerCase().includes(term));
+ if (!filtered.length) { this.print('No matches for: ' + term, 'info'); return; }
+ this.print('=== History (matching: ' + term + ') ===', 'info');
+ filtered.forEach(e => this.print(' ' + e));
+ return;
+ }
+ let count = 50;
+ if (args[0] === '-n' && args[1]) count = parseInt(args[1]) || 50;
+ const slice = entries.slice(-count);
+ this.print('=== Command History (' + entries.length + ' entries) ===', 'info');
+ slice.forEach(c => this.print(' ' + c));
+ if (entries.length > count) this.print('... (' + (entries.length - count) + ' older, use history --search <term> to find)');
return;
}
for (const modName in this.moduleManager.modules) {
@@ -5771,24 +5794,18 @@ class FileSystemModule {
this.commands['session-save'] = (args) => {
if (!args[0]) return this.terminal.print('Usage: session-save <name>', 'warning');
const name = args[0].replace(/[^a-zA-Z0-9_-]/g, '_');
- // Strip system dirs to avoid recursive bloat in snapshots
- const fsRoot = JSON.parse(JSON.stringify(this.terminal.fs.root));
- if (fsRoot && fsRoot.content) {
- delete fsRoot.content['_sessions'];
- delete fsRoot.content['_snapshots'];
- delete fsRoot.content['.trash'];
- }
- const snapshot = {
+ // v2: metadata-only — VFS is persisted independently in localStorage
+ const manifest = {
type: 'session',
+ version: 2,
savedAt: new Date().toISOString(),
- fsRoot: fsRoot,
currentPath: this.terminal.fs.currentPath,
- version: 1
+ terminalLines: this.terminal._historyBuffer ? this.terminal._historyBuffer.slice(-50) : []
};
const filePath = '_sessions/' + name + '.json';
this.terminal.fs.mkdirRecursive('_sessions');
- this.terminal.fs.writeFile(filePath, JSON.stringify(snapshot, null, 2));
- this.terminal.print('Session saved: ' + name, 'success');
+ this.terminal.fs.writeFile(filePath, JSON.stringify(manifest));
+ this.terminal.print('Session saved: ' + name + ' (' + (manifest.terminalLines.length || 0) + ' history lines)', 'success');
};
this.commands['session-load'] = (args) => {
if (!args[0]) return this.terminal.print('Usage: session-load <name>', 'warning');
@@ -5797,11 +5814,28 @@ class FileSystemModule {
if (content === null) return this.terminal.print('Session not found: ' + args[0], 'error');
try {
const session = JSON.parse(content);
- if (!session || session.type !== 'session' || !session.fsRoot) {
+ if (!session || session.type !== 'session') {
return this.terminal.print('Invalid session file: ' + args[0], 'error');
}
- this.terminal.fs.root = session.fsRoot;
- this.terminal.fs.currentPath = session.currentPath || [];
+ if (session.version === 2) {
+ // v2: metadata-only restore — keep existing VFS, just restore path + terminal history
+ this.terminal.fs.currentPath = session.currentPath || [];
+ if (session.terminalLines && Array.isArray(session.terminalLines) && this.terminal._historyBuffer) {
+ this.terminal._historyBuffer = session.terminalLines.slice(-50);
+ // Re-render last few lines for context
+ this.terminal.outputContainer.innerHTML = '';
+ session.terminalLines.forEach(line => {
+ const text = typeof line === 'string' ? line : (line.l || '');
+ const type = typeof line === 'object' ? (line.t || '') : '';
+ this.terminal.print(text, type);
+ });
+ }
+ } else {
+ // v1: full VFS restore (legacy)
+ if (!session.fsRoot) return this.terminal.print('Invalid session file', 'error');
+ this.terminal.fs.root = session.fsRoot;
+ this.terminal.fs.currentPath = session.currentPath || [];
+ }
this.terminal.fs.saveToStorage();
this.terminal.updatePrompt();
this.terminal.print('Session loaded: ' + args[0] + ' (' + new Date(session.savedAt).toLocaleString() + ')', 'success');
@@ -5821,8 +5855,13 @@ class FileSystemModule {
const content = JSON.parse(this.terminal.fs.readFile('_sessions/' + f.name));
const label = f.name.replace('.json', '');
const date = content.savedAt ? new Date(content.savedAt).toLocaleString() : 'unknown';
- const fileCount = content.fsRoot ? Object.keys(content.fsRoot.content || {}).length : 0;
- this.terminal.print(' ' + label + ' (' + fileCount + ' items, ' + date + ')');
+ if (content.version === 2) {
+ const lines = content.terminalLines ? content.terminalLines.length : 0;
+ this.terminal.print(' ' + label + ' (' + lines + ' history lines, ' + date + ')');
+ } else {
+ const fileCount = content.fsRoot ? Object.keys(content.fsRoot.content || {}).length : 0;
+ this.terminal.print(' ' + label + ' (' + fileCount + ' items, ' + date + ')');
+ }
} catch (e) {
this.terminal.print(' ' + f.name.replace('.json', '') + ' (corrupt)');
}
@@ -5865,12 +5904,30 @@ class FileSystemModule {
reader.onload = (evt) => {
try {
const session = JSON.parse(evt.target.result);
- if (!session || session.type !== 'session' || !session.fsRoot) {
+ if (!session || session.type !== 'session') {
this.terminal.print('Invalid session file (not a valid session export)', 'error');
return;
}
- this.terminal.fs.root = session.fsRoot;
- this.terminal.fs.currentPath = session.currentPath || [];
+ if (session.version === 2) {
+ // v2: metadata-only — keep existing VFS, restore path
+ this.terminal.fs.currentPath = session.currentPath || [];
+ if (session.terminalLines && Array.isArray(session.terminalLines) && this.terminal._historyBuffer) {
+ this.terminal._historyBuffer = session.terminalLines.slice(-50);
+ this.terminal.outputContainer.innerHTML = '';
+ session.terminalLines.forEach(line => {
+ const text = typeof line === 'string' ? line : (line.l || '');
+ const type = typeof line === 'object' ? (line.t || '') : '';
+ this.terminal.print(text, type);
+ });
+ }
+ } else {
+ if (!session.fsRoot) {
+ this.terminal.print('Invalid session file (not a valid session export)', 'error');
+ return;
+ }
+ this.terminal.fs.root = session.fsRoot;
+ this.terminal.fs.currentPath = session.currentPath || [];
+ }
this.terminal.fs.saveToStorage();
this.terminal.updatePrompt();
this.terminal.print('Session imported: ' + file.name + ' (' + (session.savedAt ? new Date(session.savedAt).toLocaleString() : 'unknown date') + ')', 'success');
@@ -5979,17 +6036,11 @@ class OllamaModule {
};
this.commands['ai'] = async (args) => {
if(!args[0]) return this.terminal.print("Usage: ai <prompt>", 'warning');
- const result = await this.terminal.ollamaManager.chat(args.join(' '));
- if (result === null) {
- this.terminal.print('AI returned no response — check provider connection', 'warning');
- }
+ await this.terminal.ollamaManager.chat(args.join(' '));
};
this.commands['ask'] = async (args) => {
if(!args[0]) return this.terminal.print("Usage: ask <prompt>", 'warning');
- const result = await this.terminal.ollamaManager.chat(args.join(' '));
- if (result === null) {
- this.terminal.print('AI returned no response — check provider connection', 'warning');
- }
+ await this.terminal.ollamaManager.chat(args.join(' '));
};
// Merged from OllamaModelsModule
this.commands['models'] = () => this.listModels();
@@ -6215,29 +6266,7 @@ class MyModule {
return mm.modules[name];
}
registerCommands() {
- this.commands['modules'] = (args) => {
- if (args[0] === '--diagnose' || args[0] === '-d') {
- const report = this.terminal.moduleManager.diagnose();
- this.terminal.print(`=== Module Diagnostics ===`, 'info');
- this.terminal.print(`Total in modules: ${report.totalInModules}`);
- this.terminal.print(`Total in registry: ${report.totalInRegistry}`);
- this.terminal.print(`Healthy: ${report.healthy}`);
- if (report.orphaned.length) {
- this.terminal.print(`Orphaned (in modules but not registry): ${report.orphaned.join(', ')}`, 'warning');
- }
- if (report.missingFs.length) {
- this.terminal.print(`Missing fs: ${report.missingFs.join(', ')}`, 'error');
- }
- if (report.missingTerminal.length) {
- this.terminal.print(`Missing terminal: ${report.missingTerminal.join(', ')}`, 'error');
- }
- if (report.uninitialized.length) {
- this.terminal.print(`Uninitialized (init() never completed): ${report.uninitialized.join(', ')}`, 'warning');
- }
- return;
- }
- this.terminal.openModuleManager();
- };
+ this.commands['modules'] = () => { this.terminal.openModuleManager(); };
this.commands['loadmod'] = async (args) => {
if(!args[0]) return this.terminal.print("Usage: loadmod <url>", 'warning');
await this.terminal.moduleManager.loadModuleFromURL(args[0]);
@@ -7341,6 +7370,14 @@ buildSystemPrompt() {
return `Current date: ${dateStr}\nCurrent time: ${timeStr}\n\n${saved}`;
}
} catch (e) {}
+ // Check SmartContextModule for active profile system prompt
+ let profilePrompt = '';
+ try {
+ const scm = this.terminal?.moduleManager?.modules?.SmartContextModule;
+ if (scm && scm.activeProfile && scm.contextProfiles[scm.activeProfile]) {
+ profilePrompt = scm.contextProfiles[scm.activeProfile].systemPrompt || '';
+ }
+ } catch (e) {}
const now = new Date();
const dateStr = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0');
const nvfs = this.terminal?.moduleManager?.modules?.NeuralVFSModule;
@@ -7351,11 +7388,15 @@ buildSystemPrompt() {
if (ageMs > 14400000) nvfsNote = ' |nvfs ' + Math.floor(ageMs/3600000) + 'h stale';
}
} catch (e) {}
- return 'Current date: ' + dateStr + nvfsNote + '\n'
+ let base = 'Current date: ' + dateStr + nvfsNote + '\n'
+ 'VFS→Modules→AI. nvfs-index for commands. Tool→Observe→Answer. nvfs-resolve. Destructive cmds need confirm.\n'
- + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind: vaultmind_query(ep,msg,agent) — Hyperliquid prices/portfolio/chat\n'
+ + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind: vaultmind_connect(network) — connect wallet, vaultmind_status — check session, vaultmind_query(ep,msg,agent) — Hyperliquid prices/portfolio/chat | wallet_lookup(address) — read-only portfolio for any wallet\n'
+ 'web_fetch tool: GET for normal pages (returns title+content+word_count), POST for JSON APIs like Hyperliquid /info (pass method="POST" and body as JSON string).\n'
+ 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, guardrail, wget, tar, secret-* — discoverable via help';
+ if (profilePrompt) {
+ base = profilePrompt + '\n\n' + base;
+ }
+ return base;
}
getToolDefinitions() {
if (!this._toolDefs) this._toolDefs = this._buildToolDefs();
@@ -7724,6 +7765,43 @@ buildSystemPrompt() {
required: ['endpoint']
}
}
+ }, {
+ type: 'function',
+ function: {
+ name: 'vaultmind_connect',
+ description: 'Connect to VaultMind AI by reading Hyperliquid private key from SecretVault (stored as "hyperliquid_key") and authenticating with the backend. Stores session token in localStorage.',
+ parameters: {
+ type: 'object',
+ properties: {
+ network: { type: 'string', enum: ['testnet', 'mainnet'], description: 'Hyperliquid network (default testnet)' }
+ },
+ required: []
+ }
+ }
+ }, {
+ type: 'function',
+ function: {
+ name: 'vaultmind_status',
+ description: 'Check if VaultMind AI session token exists and is valid in localStorage.',
+ parameters: {
+ type: 'object',
+ properties: {},
+ required: []
+ }
+ }
+ }, {
+ type: 'function',
+ function: {
+ name: 'wallet_lookup',
+ description: 'Read-only Hyperliquid wallet lookup — returns portfolio, positions, and account value for any wallet address. No auth needed.',
+ parameters: {
+ type: 'object',
+ properties: {
+ address: { type: 'string', description: 'Hyperliquid wallet address (0x...)' }
+ },
+ required: ['address']
+ }
+ }
}];
}
getHelpInfo() {
@@ -7740,7 +7818,9 @@ buildSystemPrompt() {
'ai-model': 'ai-model [model] — Show or set AI model',
'ai-config': 'ai-config [key] [value] — View or set AI loop config (max-turns, max-context, confirm-timeout(0=disabled))',
'mem-debug': 'mem-debug — Show memory breakdown by component',
- 'reset-mem': 'reset-mem — Reset AI memory (logs, context, profile, guardrails) without losing VFS or config'
+ 'reset-mem': 'reset-mem — Reset AI memory (logs, context, profile, guardrails) without losing VFS or config',
+ 'vaultmind-connect': 'vaultmind-connect [testnet|mainnet] — Connect VaultMind wallet using hyperliquid_key from SecretVault',
+ 'vaultmind-status': 'vaultmind-status — Check VaultMind session status',
};
}
registerCommands() {
@@ -7765,6 +7845,8 @@ buildSystemPrompt() {
this.commands['shadow-rollback'] = () => this.shadowRollback();
this.commands['mem-debug'] = () => this.memDebug();
this.commands['reset-mem'] = () => this.resetMem();
+ this.commands['vaultmind-connect'] = (args) => this._vaultmindConnectCmd(args);
+ this.commands['vaultmind-status'] = () => this._vaultmindStatusCmd();
}
async chat(args) {
const userMessage = args.join(' ');
@@ -7988,16 +8070,6 @@ buildSystemPrompt() {
}
async agenticLoop(messages) {
let toolTurns = 0;
- // Auto-connect if not connected
- const om = this.terminal.ollamaManager;
- if (om && !om.connected) {
- this.terminal.print('⏳ AI provider not connected — attempting auto-connect...', 'info');
- const connected = await om.connect();
- if (!connected) {
- this.terminal.print('❌ Could not connect to any AI provider.', 'error');
- return { finalAnswer: null, toolTurns: 0 };
- }
- }
while (toolTurns < this.MAX_TOOL_TURNS) {
if (this._cancelled) { this.terminal.print('AI loop cancelled', 'warning'); return { finalAnswer: null, toolTurns }; }
toolTurns++;
@@ -8046,9 +8118,9 @@ buildSystemPrompt() {
const sm = this.terminal.moduleManager.modules['ServerMonitorModule'];
if (sm && sm.logToolCall) sm.logToolCall(fn.name, (t1 * 1000) || 1, this._isToolError(toolResult));
if (this._debugPanelEnabled) this._updateDebugPanel();
- return { fn, toolResult, t1, toolCallId: tc.id };
+ return { fn, toolResult, t1 };
}));
- for (const { fn, toolResult, t1, toolCallId } of safeResults) {
+ for (const { fn, toolResult, t1 } of safeResults) {
this.terminal.print(`🔧 Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info');
// Memory graph: record tool call + file access
try {
@@ -8075,7 +8147,7 @@ buildSystemPrompt() {
this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`);
this.terminal.print(`📊 Result (${t1}s): ${obs}`, 'info');
}
- messages.push({ role: 'tool', tool_call_id: toolCallId, content: toolResult });
+ messages.push({ role: 'tool', content: toolResult });
}
// Only block on destructive tools
if (destructiveTools.length > 0) {
@@ -8137,7 +8209,7 @@ buildSystemPrompt() {
this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`);
this.terminal.print(`📊 Result (${t1}s): ${dObs}`, 'info');
}
- messages.push({ role: 'tool', tool_call_id: tc.id, content: toolResult });
+ messages.push({ role: 'tool', content: toolResult });
}
} else {
messages.push({
@@ -9140,6 +9212,8 @@ buildSystemPrompt() {
}
scm.activeProfile = name;
const profile = scm.contextProfiles[name];
+ // Rebuild system prompt to reflect new context profile
+ this.systemPrompt = this.buildSystemPrompt();
return JSON.stringify({ success: true, name, displayName: profile.name, systemPrompt: profile.systemPrompt, temperature: profile.temperature });
}
case 'create_mindmap': {
@@ -9170,29 +9244,74 @@ buildSystemPrompt() {
case 'list_modified_modules':
case 'get_module_status':
return await this.runModuleTool(name, argsStr);
+ case 'vaultmind_connect': {
+ const network = a.network || 'testnet';
+ try {
+ // Read Hyperliquid key from SecretVault
+ const vault = window.webx11_secretVault;
+ if (!vault) return JSON.stringify({ error: 'SecretVault not available — unlock it first with secret-unlock <pin>' });
+ const privateKey = await vault.decrypt('hyperliquid_key');
+ if (!privateKey) return JSON.stringify({ error: 'No hyperliquid_key found in SecretVault. Store it first: secret-set hyperliquid_key 0x...' });
+ // Authenticate with VaultMind backend
+ const resp = await fetch('/vmai-api/auth/wallet', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ private_key: privateKey, account_address: null, network: network })
+ });
+ if (!resp.ok) {
+ const errText = await resp.text();
+ return JSON.stringify({ error: 'VaultMind auth failed (' + resp.status + '): ' + errText });
+ }
+ const data = await resp.json();
+ if (data.token) {
+ localStorage.setItem('vaultmind_token', data.token);
+ return JSON.stringify({ success: true, status: data.status || 'connected', network: data.network || network, message: 'Connected to VaultMind on ' + (data.network || network) });
+ }
+ return JSON.stringify({ error: 'No token returned from VaultMind', data: data });
+ } catch (e) {
+ return JSON.stringify({ error: 'VaultMind connect error: ' + e.message });
+ }
+ }
+ case 'vaultmind_status': {
+ try {
+ const token = localStorage.getItem('vaultmind_token');
+ if (!token) return JSON.stringify({ connected: false, message: 'No VaultMind session token found. Use vaultmind_connect to authenticate.' });
+ // Try to verify by calling a lightweight endpoint
+ try {
+ const resp = await fetch('/vmai-api/portfolio', {
+ headers: { 'Authorization': 'Bearer ' + token }
+ });
+ if (resp.ok) return JSON.stringify({ connected: true, token_exists: true, token_prefix: token.substring(0, 12) + '...', message: 'VaultMind session is active and valid.' });
+ if (resp.status === 401) return JSON.stringify({ connected: false, token_exists: true, message: 'Token exists but is expired or invalid. Reconnect with vaultmind_connect.' });
+ return JSON.stringify({ connected: true, token_exists: true, message: 'Token exists (status: ' + resp.status + ').' });
+ } catch (e) {
+ return JSON.stringify({ connected: true, token_exists: true, message: 'Token exists but backend unreachable (' + e.message + ').' });
+ }
+ } catch (e) {
+ return JSON.stringify({ error: 'VaultMind status error: ' + e.message });
+ }
+ }
case 'vaultmind_query': {
const ep = a.endpoint;
- const epMap = {
- 'prices': '/vmai-api/market/prices',
- 'portfolio': '/vmai-api/portfolio',
- 'transactions': '/vmai-api/transactions/summary',
- 'copy-trade': '/vmai-api/copy-trading/start',
- 'chat': '/vmai-api/chat',
- };
- const url = epMap[ep];
- if (!url) {
- return JSON.stringify({ error: 'Invalid endpoint. Valid: ' + Object.keys(epMap).join(', ') });
+ const validEps = ['prices', 'portfolio', 'transactions', 'copy-trade', 'chat'];
+ if (!validEps.includes(ep)) {
+ return JSON.stringify({ error: 'Invalid endpoint. Valid: ' + validEps.join(', ') });
}
try {
- // Read VaultMind session token from localStorage (set by VaultMind dashboard)
- const vmToken = typeof localStorage !== 'undefined' ? localStorage.getItem('vaultmind_token') : null;
+ const epMap = { prices: '/vmai/market/prices', portfolio: '/vmai/portfolio', transactions: '/vmai/transactions/summary', 'copy-trade': '/vmai/copy-trading/traders', chat: '/vmai/chat' };
+ let url = epMap[ep];
let opts = { method: 'GET', headers: { 'Accept': 'application/json' } };
- if (vmToken) {
- opts.headers['Authorization'] = 'Bearer ' + vmToken;
+ // Portfolio, transactions, copy-trade need auth token
+ const authEps = ['portfolio', 'transactions', 'copy-trade'];
+ if (authEps.includes(ep)) {
+ const token = localStorage.getItem('vaultmind_token');
+ if (token) opts.headers['Authorization'] = 'Bearer ' + token;
}
if (ep === 'chat') {
opts.method = 'POST';
opts.headers['Content-Type'] = 'application/json';
+ const token = localStorage.getItem('vaultmind_token');
+ if (token) opts.headers['Authorization'] = 'Bearer ' + token;
opts.body = JSON.stringify({
message: a.message || 'What is the current market status?',
agent: a.agent || 'market'
@@ -9205,6 +9324,17 @@ buildSystemPrompt() {
return JSON.stringify({ error: 'VaultMind API error: ' + e.message });
}
}
+ case 'wallet_lookup': {
+ const addr = a.address;
+ if (!addr) return JSON.stringify({ error: 'Wallet address required' });
+ try {
+ const resp = await fetch('/wallet/' + encodeURIComponent(addr));
+ const data = await resp.json();
+ return JSON.stringify({ success: true, data });
+ } catch (e) {
+ return JSON.stringify({ error: 'Wallet lookup error: ' + e.message });
+ }
+ }
default:
return JSON.stringify({ error: `Unknown tool: ${name}` });
}
@@ -9531,7 +9661,7 @@ buildSystemPrompt() {
}
}
_getAiConfigKeys() {
- return ['max-turns', 'max-context', 'max-context-tokens', 'confirm-timeout', 'chat-lock-timeout', 'skill-debounce', 'trivial-optimization'];
+ return ['max-turns', 'max-context', 'max-context-tokens', 'confirm-timeout', 'chat-lock-timeout', 'skill-debounce', 'trivial-optimization', 'temperature', 'top-p', 'max-tokens', 'thinking'];
}
aiConfig(args) {
if (args.length === 0) {
@@ -9543,8 +9673,15 @@ buildSystemPrompt() {
this.terminal.print(`confirm-timeout: ${this._confirmTimeoutMs > 0 ? this._confirmTimeoutMs / 1000 + 's' : 'disabled (no auto-deny)'} (default: 120s)`, '');
this.terminal.print(`chat-lock-timeout: ${this._chatLockTimeoutMs / 1000}s (default: 10s)`, '');
this.terminal.print(`skill-debounce: ${this._skillDebounceMs / 60000}min (default: 30min)`, '');
+ const om = this.terminal?.ollamaManager;
+ if (om && om.modelParams) {
+ this.terminal.print(`temperature: ${om.modelParams.temperature} (default: 0.7)`, '');
+ this.terminal.print(`top-p: ${om.modelParams.top_p} (default: 0.9)`, '');
+ this.terminal.print(`max-tokens: ${om.modelParams.max_tokens} (default: 4096)`, '');
+ this.terminal.print(`thinking: ${om.modelParams.thinking} (default: off)`, '');
+ }
this.terminal.print('Usage: ai-config <key> <value>', 'info');
- this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout(0=disabled), chat-lock-timeout, skill-debounce, trivial-optimization', 'info');
+ this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout(0=disabled), chat-lock-timeout, skill-debounce, trivial-optimization, temperature, top-p, max-tokens, thinking', 'info');
return;
}
const key = args[0];
@@ -9577,8 +9714,28 @@ buildSystemPrompt() {
if (this.terminal?.ollamaManager) {
this.terminal.ollamaManager.optimizationEnabled = val === 'on';
}
+ } else if (key === 'temperature') {
+ const n = parseFloat(val);
+ if (isNaN(n) || n < 0 || n > 2) return this.terminal.print('temperature must be 0-2', 'warning');
+ const om = this.terminal?.ollamaManager;
+ if (om && om.modelParams) { om.modelParams.temperature = n; om._saveModelParams(); }
+ } else if (key === 'top-p') {
+ const n = parseFloat(val);
+ if (isNaN(n) || n < 0 || n > 1) return this.terminal.print('top-p must be 0-1', 'warning');
+ const om = this.terminal?.ollamaManager;
+ if (om && om.modelParams) { om.modelParams.top_p = n; om._saveModelParams(); }
+ } else if (key === 'max-tokens') {
+ const n = parseInt(val);
+ if (isNaN(n) || n < 1 || n > 131072) return this.terminal.print('max-tokens must be 1-131072', 'warning');
+ const om = this.terminal?.ollamaManager;
+ if (om && om.modelParams) { om.modelParams.max_tokens = n; om._saveModelParams(); }
+ } else if (key === 'thinking') {
+ const validModes = ['off', 'deepseek_reasoner', 'deepseek'];
+ if (!validModes.includes(val)) return this.terminal.print(`thinking must be one of: ${validModes.join(', ')}`, 'warning');
+ const om = this.terminal?.ollamaManager;
+ if (om && om.modelParams) { om.modelParams.thinking = val; om._saveModelParams(); }
} else {
- return this.terminal.print(`Unknown key: ${key}. Valid: max-turns, max-context, max-context-tokens, confirm-timeout, chat-lock-timeout, skill-debounce, trivial-optimization`, 'warning');
+ return this.terminal.print(`Unknown key: ${key}. Valid: max-turns, max-context, max-context-tokens, confirm-timeout, chat-lock-timeout, skill-debounce, trivial-optimization, temperature, top-p, max-tokens, thinking`, 'warning');
}
// Persist
localStorage.setItem('webx11_ai_config', JSON.stringify({
@@ -9736,6 +9893,39 @@ buildSystemPrompt() {
this.terminal.print('Compression error: ' + e.message, 'error');
});
}
+ async _vaultmindConnectCmd(args) {
+ const network = args[0] || 'testnet';
+ try {
+ const vault = window.webx11_secretVault;
+ if (!vault) { this.terminal.print('SecretVault not available', 'error'); return; }
+ const privateKey = await vault.decrypt('hyperliquid_key');
+ if (!privateKey) { this.terminal.print('No hyperliquid_key in SecretVault. Use: secret-set hyperliquid_key 0x...', 'error'); return; }
+ this.terminal.print('Connecting to VaultMind...', 'info');
+ const resp = await fetch('/vmai-api/auth/wallet', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ private_key: privateKey, account_address: null, network })
+ });
+ if (!resp.ok) { this.terminal.print('Auth failed: ' + resp.status, 'error'); return; }
+ const data = await resp.json();
+ if (data.token) {
+ localStorage.setItem('vaultmind_token', data.token);
+ this.terminal.print('✅ Connected to VaultMind (' + (data.network || network) + ')', 'success');
+ } else {
+ this.terminal.print('No token returned', 'error');
+ }
+ } catch (e) {
+ this.terminal.print('Error: ' + e.message, 'error');
+ }
+ }
+ _vaultmindStatusCmd() {
+ const token = localStorage.getItem('vaultmind_token');
+ if (token) {
+ this.terminal.print('✅ VaultMind session active (token: ' + token.substring(0, 12) + '...)', 'success');
+ } else {
+ this.terminal.print('❌ No VaultMind session. Use: vaultmind-connect [testnet|mainnet]', 'warning');
+ }
+ }
}
/* === module-aiasm.js === */
@@ -9771,7 +9961,7 @@ buildSystemPrompt() {
class SmartContextModule {
constructor() {
this.commands = {}; this.terminal = null;
- this.contextProfiles = this.loadProfiles(); this.activeProfile = null;
+ this.contextProfiles = this.loadProfiles(); this.activeProfile = this.loadActiveProfile();
}
getHelpInfo() {
return {
@@ -9791,6 +9981,22 @@ buildSystemPrompt() {
saveProfiles() {
localStorage.setItem('webx11_context_profiles', JSON.stringify(this.contextProfiles));
}
+ loadActiveProfile() {
+ try {
+ const saved = localStorage.getItem('webx11_active_context_profile');
+ if (saved && this.contextProfiles[saved]) return saved;
+ } catch (e) {}
+ return null;
+ }
+ saveActiveProfile() {
+ try {
+ if (this.activeProfile) {
+ localStorage.setItem('webx11_active_context_profile', this.activeProfile);
+ } else {
+ localStorage.removeItem('webx11_active_context_profile');
+ }
+ } catch (e) {}
+ }
getDefaultProfiles() {
return {
'coding': { name: 'Coding Assistant', systemPrompt: "You are an expert programmer.", temperature: 0.7 },
@@ -9813,6 +10019,7 @@ buildSystemPrompt() {
}
if (this.contextProfiles[args[0]]) {
this.activeProfile = args[0];
+ this.saveActiveProfile();
this.terminal.print(`Context set to: ${this.contextProfiles[args[0]].name}`, "success");
} else {
this.terminal.print(`Profile '${args[0]}' not found`, "error");
@@ -9852,12 +10059,44 @@ buildSystemPrompt() {
this.terminal.print("Usage: smart-ask <your-question>", "warning"); return;
}
let systemPrompt = "You are a helpful assistant.";
+ let temperature = 0.7;
if (this.activeProfile && this.contextProfiles[this.activeProfile]) {
- systemPrompt = this.contextProfiles[this.activeProfile].systemPrompt;
- this.terminal.print(`Using context: ${this.contextProfiles[this.activeProfile].name}`, "info");
+ const profile = this.contextProfiles[this.activeProfile];
+ systemPrompt = profile.systemPrompt;
+ temperature = profile.temperature || 0.7;
+ this.terminal.print(`Using context: ${profile.name} (temp: ${temperature})`, "info");
+ }
+ // Use agentic loop via the AI assistant module for tool-enabled responses
+ const aiModule = this.terminal?.moduleManager?.modules?.WebX11AIAssistantModule;
+ if (aiModule) {
+ // Set temperature on ollamaManager modelParams temporarily
+ const om = this.terminal.ollamaManager;
+ const savedTemp = om.modelParams ? om.modelParams.temperature : undefined;
+ if (om.modelParams) {
+ om.modelParams.temperature = temperature;
+ }
+ try {
+ const messages = [
+ { role: 'system', content: systemPrompt },
+ { role: 'user', content: args.join(' ') }
+ ];
+ const result = await om.chatWithTools(messages, aiModule.getToolDefinitions());
+ if (result && result.message) {
+ this.terminal.printAIResponse(result.message.content || '(no response)');
+ } else if (result && result.error) {
+ this.terminal.print(`⚠️ AI error: ${result.error}`, 'error');
+ }
+ } finally {
+ if (om.modelParams && savedTemp !== undefined) {
+ om.modelParams.temperature = savedTemp;
+ }
+ }
+ } else {
+ // Fallback: simple chat with temperature
+ const om = this.terminal.ollamaManager;
+ const fullPrompt = `${systemPrompt}\n\nUser: ${args.join(' ')}`;
+ await om.chat(fullPrompt);
}
- const fullPrompt = `${systemPrompt}\n\nUser: ${args.join(' ')}`;
- await this.terminal.ollamaManager.chat(fullPrompt);
}
}
@@ -11548,11 +11787,13 @@ class ChatStudioModule {
/* === module-agent-registry.js === */
// module-agent-registry.js — VFS-backed agent directory with A2A contracts
// Commands: agent-list, agent-register, agent-unregister, agent-status, contract-register, contract-list
+// A2A Scratchpad commands: a2a-post, a2a-poll, a2a-ack, a2a-reject, a2a-status, a2a-prune
class AgentRegistryModule {
constructor() {
this.commands = {};
this.terminal = null;
this._contracts = {};
+ this._a2aPruneTimer = null;
}
registerCommands() {
this.commands['agent-list'] = () => this.listAgents();
@@ -11561,14 +11802,321 @@ class AgentRegistryModule {
this.commands['agent-unregister'] = (args) => this.unregisterAgent(args[0]);
this.commands['contract-register'] = (args) => this.registerContract(args);
this.commands['contract-list'] = () => this.listContracts();
+ // A2A Scratchpad commands
+ this.commands['a2a-post'] = (args) => this.a2aPostCommand(args);
+ this.commands['a2a-poll'] = (args) => this.a2aPollCommand(args);
+ this.commands['a2a-ack'] = (args) => this.a2aAckCommand(args);
+ this.commands['a2a-reject'] = (args) => this.a2aRejectCommand(args);
+ this.commands['a2a-status'] = () => this.a2aStatus();
+ this.commands['a2a-prune'] = () => this.a2aPrune();
+ }
+ getHelpInfo() {
+ return {
+ 'a2a-post': 'a2a-post <to> <type> <capability> <payload...> — Post an A2A message',
+ 'a2a-poll': 'a2a-poll [agentId] — Poll for messages addressed to an agent (default: current or "user")',
+ 'a2a-ack': 'a2a-ack <msgId> <result...> — Acknowledge a message with result',
+ 'a2a-reject': 'a2a-reject <msgId> <error...> — Reject a message with error',
+ 'a2a-status': 'a2a-status — Show scratchpad stats per agent',
+ 'a2a-prune': 'a2a-prune — Clean up expired and old completed messages'
+ };
}
init(terminal) {
this.terminal = terminal;
if (!this.terminal.fs.readDir('/agents/')) this.terminal.fs.mkdir('agents');
if (!this.terminal.fs.readDir('/.contracts/')) this.terminal.fs.mkdir('.contracts');
this._loadContracts();
+ // Initialize scratchpad
+ this._ensureScratchpad();
+ // Auto-prune every 60 seconds
+ this._a2aPruneTimer = setInterval(() => this.a2aPrune(), 60000);
+ // Save the spec-vs-reality delta table
+ this._saveDeltaTable();
this.terminal.print('📋 Agent Registry loaded', 'info');
}
+ // ─── Task 1: Save spec-vs-reality delta table ───
+ _saveDeltaTable() {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs.readDir('/_docs/')) fs.mkdir('_docs');
+ const delta = `# Spec-vs-Reality Delta — 2026-06-17\n\n` +
+`| # | Whitepaper Concept | Spec'd Behavior | Current Reality | Δ | Priority |\n` +
+`|---|---|---|---|---|---|\n` +
+`| 1 | Hyper-Modular Kernel | Real-time self-rewriting drivers for task-specific optimization via WPM | WPM exists but is manual; no hot-patch daemon or runtime driver swap | Partial | Medium |\n` +
+`| 2 | A2A Protocol | Standardized neural communication layer for agent negotiation; typed messages, no central broker | VFS mailbox transport works (a2a-send/a2a-list/500ms polling) but orchestrator acts as central broker | Partial | High |\n` +
+`| 3 | Neural VFS | Semantic-based file storage (concept:// URIs) as primary access method; hierarchical paths are secondary | concept:// URIs work (nvfs-index/nvfs-resolve) but hierarchical paths remain primary access; concept resolution requires explicit index | Partial | High |\n` +
+`| 4 | Recursive Self-Optimization | Monitor → Profile → Diagnose → Patch → Test → Promote feedback loop | system-health telemetry captures latency/VFS stats but no auto-patch generation or hot-promotion pipeline | Partial | Medium |\n` +
+`| 5 | Agent Discovery | No central broker — agents discover each other via VFS registrations spontaneously | Orchestrator is central broker; all task delegation goes through it; agents don't autonomously discover each other | Critical Gap | Critical |\n` +
+`| 6 | Contract Schema | Every A2A message carries a contract schema defining capabilities and constraints | Contracts exist (contract-register/contract-list) but schema validation is basic substring matching; not enforced on every message | Partial | Medium |\n` +
+`| 7 | Semantic Addressing | Neural VFS as primary storage paradigm — users navigate by concept, not path | Hierarchical path access still primary; concept:// requires explicit nvfs-index; no transparent concept-to-path fallback | Critical Gap | Critical |\n` +
+`| 8 | Warm-start Guarantee | Kernel caches last N stable driver configs; boot in under 200ms | No golden config cache; full localStorage restore on every page load | Spec Violation | High |\n` +
+`| 9 | Self-rewrite Daemon | Monitors performance telemetry and generates optimized driver variants at runtime | No self-rewrite daemon exists; all module changes are manual | Missing | Low |\n` +
+`| 10 | Agent Lifecycle | spawn, sleep, wake, kill, restart policies with exponential backoff | Fully implemented in orchestrator with heartbeat, idle sweep, restart policies, VFS sleep serialization | Fully Aligned | — |\n\n` +
+`**Summary: 2 Critical gaps, 5 Partial, 1 Spec Violation, 1 Missing, 1 Fully Aligned**\n`;
+ fs.writeFile('_docs/spec-vs-reality-delta.md', delta);
+ this.terminal.print('📄 Spec-vs-Reality delta saved to /_docs/spec-vs-reality-delta.md', 'info');
+ } catch (e) {
+ console.warn('Failed to save delta table:', e.message);
+ }
+ }
+ // ─── A2A Scratchpad Helpers ───
+ _ensureScratchpad() {
+ try {
+ const data = this.terminal.fs.fsReadJSON('/_agents/scratchpad.json');
+ if (!data) throw new Error('no scratchpad');
+ } catch (e) {
+ this.terminal.fs.mkdir('_agents');
+ this.terminal.fs.fsWriteJSON('/_agents/scratchpad.json', {
+ messages: [],
+ seq: 0,
+ agents: {}
+ });
+ }
+ }
+ _getScratchpad() {
+ try {
+ const data = this.terminal.fs.fsReadJSON('/_agents/scratchpad.json');
+ if (data && data.messages) return data;
+ } catch (e) {}
+ return { messages: [], seq: 0, agents: {} };
+ }
+ _saveScratchpad(sp) {
+ try {
+ this.terminal.fs.mkdir('_agents');
+ this.terminal.fs.fsWriteJSON('/_agents/scratchpad.json', sp);
+ } catch (e) {
+ console.warn('Scratchpad save error:', e.message);
+ }
+ }
+ // ─── A2A Scratchpad Functions ───
+ a2aPost(from, to, type, capability, payload, ttl) {
+ if (!from || !to || !type) {
+ this.terminal.print('a2aPost: from, to, and type are required', 'warning');
+ return null;
+ }
+ const sp = this._getScratchpad();
+ sp.seq = (sp.seq || 0) + 1;
+ const msg = {
+ id: `msg_${String(sp.seq).padStart(3, '0')}`,
+ from: from,
+ to: to,
+ type: type,
+ capability: capability || null,
+ payload: payload || {},
+ ttl: (typeof ttl === 'number' ? ttl : 300000),
+ status: 'pending',
+ ts: Date.now(),
+ reply_to: null
+ };
+ sp.messages.push(msg);
+ this._saveScratchpad(sp);
+ return msg;
+ }
+ a2aPoll(agentId) {
+ if (!agentId) return [];
+ const sp = this._getScratchpad();
+ const pending = [];
+ for (const msg of sp.messages) {
+ if (msg.to === agentId && msg.status === 'pending') {
+ msg.status = 'delivered';
+ pending.push(msg);
+ }
+ }
+ if (pending.length > 0) this._saveScratchpad(sp);
+ return pending;
+ }
+ a2aAck(messageId, resultPayload) {
+ const sp = this._getScratchpad();
+ const msg = sp.messages.find(m => m.id === messageId);
+ if (!msg) {
+ this.terminal.print(`a2aAck: message ${messageId} not found`, 'warning');
+ return false;
+ }
+ if (msg.status === 'completed' || msg.status === 'failed') {
+ this.terminal.print(`a2aAck: message ${messageId} already ${msg.status}`, 'warning');
+ return false;
+ }
+ msg.status = 'completed';
+ msg.result = resultPayload || {};
+ msg.completedAt = Date.now();
+ this._saveScratchpad(sp);
+ return true;
+ }
+ a2aReject(messageId, error) {
+ const sp = this._getScratchpad();
+ const msg = sp.messages.find(m => m.id === messageId);
+ if (!msg) {
+ this.terminal.print(`a2aReject: message ${messageId} not found`, 'warning');
+ return false;
+ }
+ if (msg.status === 'completed' || msg.status === 'failed') {
+ this.terminal.print(`a2aReject: message ${messageId} already ${msg.status}`, 'warning');
+ return false;
+ }
+ msg.status = 'failed';
+ msg.error = error || 'unspecified error';
+ msg.completedAt = Date.now();
+ this._saveScratchpad(sp);
+ return true;
+ }
+ a2aRegister(agentId, capabilities) {
+ if (!agentId) return;
+ const sp = this._getScratchpad();
+ sp.agents[agentId] = {
+ last_poll: Date.now(),
+ capabilities: capabilities || []
+ };
+ this._saveScratchpad(sp);
+ }
+ a2aUnregister(agentId) {
+ if (!agentId) return;
+ const sp = this._getScratchpad();
+ delete sp.agents[agentId];
+ this._saveScratchpad(sp);
+ }
+ a2aPrune() {
+ try {
+ const sp = this._getScratchpad();
+ const now = Date.now();
+ const before = sp.messages.length;
+ sp.messages = sp.messages.filter(m => {
+ // Remove expired messages (past TTL)
+ if (m.status === 'pending' && (now - m.ts > m.ttl)) return false;
+ // Remove completed/failed messages older than 1 hour
+ if ((m.status === 'completed' || m.status === 'failed') && m.completedAt && (now - m.completedAt > 3600000)) return false;
+ return true;
+ });
+ const removed = before - sp.messages.length;
+ if (removed > 0) this._saveScratchpad(sp);
+ return removed;
+ } catch (e) {
+ console.warn('a2aPrune error:', e.message);
+ return 0;
+ }
+ }
+ a2aStatus() {
+ const sp = this._getScratchpad();
+ const counts = {};
+ for (const msg of sp.messages) {
+ if (!counts[msg.to]) counts[msg.to] = { pending: 0, delivered: 0, completed: 0, failed: 0 };
+ if (msg.status === 'pending') counts[msg.to].pending++;
+ else if (msg.status === 'delivered') counts[msg.to].delivered++;
+ else if (msg.status === 'completed') counts[msg.to].completed++;
+ else if (msg.status === 'failed') counts[msg.to].failed++;
+ }
+ this.terminal.print('📊 A2A Scratchpad Status:', 'header');
+ this.terminal.print(` Total messages: ${sp.messages.length}`, 'info');
+ this.terminal.print(` Registered agents: ${Object.keys(sp.agents).length}`, 'info');
+ this.terminal.print(` Sequence: ${sp.seq}`, 'info');
+ const agentNames = Object.keys(counts);
+ if (agentNames.length === 0) {
+ this.terminal.print(' No messages in scratchpad', 'info');
+ return;
+ }
+ for (const agentId of agentNames) {
+ const c = counts[agentId];
+ const total = c.pending + c.delivered + c.completed + c.failed;
+ const caps = sp.agents[agentId] ? ` [${sp.agents[agentId].capabilities.join(', ')}]` : '';
+ this.terminal.print(` ${agentId}${caps}: ${total} msgs (⏳${c.pending} 📬${c.delivered} ✅${c.completed} ❌${c.failed})`, 'info');
+ }
+ }
+ _a2aCleanup() {
+ // Remove agent entries from scratchpad when agent directory is deleted from /agents/
+ try {
+ const sp = this._getScratchpad();
+ const agentDirs = {};
+ const dir = this.terminal.fs.readDir('/agents/');
+ if (dir) {
+ for (const entry of dir) {
+ if (entry.type === 'dir') agentDirs[entry.name] = true;
+ }
+ }
+ let changed = false;
+ for (const agentId of Object.keys(sp.agents)) {
+ if (!agentDirs[agentId]) {
+ delete sp.agents[agentId];
+ changed = true;
+ }
+ }
+ if (changed) this._saveScratchpad(sp);
+ } catch (e) {
+ console.warn('a2aCleanup error:', e.message);
+ }
+ }
+ // ─── A2A Command Handlers ───
+ a2aPostCommand(args) {
+ // a2a-post <to> <type> <capability> <payload...>
+ if (args.length < 3) {
+ return this.terminal.print('Usage: a2a-post <to> <type> <capability> <payload...>', 'warning');
+ }
+ const to = args[0];
+ const type = args[1];
+ const capability = args[2];
+ const payloadRaw = args.slice(3).join(' ');
+ let payload = {};
+ try {
+ // Try parsing as JSON
+ payload = JSON.parse(payloadRaw || '{}');
+ } catch (e) {
+ // Treat as plain text
+ payload = { text: payloadRaw };
+ }
+ const from = 'user';
+ const ttl = 300000;
+ const msg = this.a2aPost(from, to, type, capability, payload, ttl);
+ if (msg) {
+ this.terminal.print(`📨 A2A message ${msg.id} posted to ${to} (type: ${type})`, 'success');
+ }
+ }
+ a2aPollCommand(args) {
+ // Use explicit agent ID if provided, otherwise try to determine current agent
+ let agentId = (args && args[0]) || 'user';
+ if (!args || !args[0]) {
+ try {
+ const orchestrator = this.terminal.moduleManager.modules['OrchestratorModule'];
+ if (orchestrator && orchestrator._agents) {
+ const myAgents = Object.entries(orchestrator._agents).filter(([, a]) => a.status === 'busy');
+ if (myAgents.length > 0) agentId = myAgents[0][0];
+ }
+ } catch (e) {}
+ }
+ const msgs = this.a2aPoll(agentId);
+ if (msgs.length === 0) {
+ this.terminal.print(`📭 No pending messages for ${agentId}`, 'info');
+ return;
+ }
+ this.terminal.print(`📬 ${msgs.length} message(s) for ${agentId}:`, 'header');
+ for (const msg of msgs) {
+ const capInfo = msg.capability ? ` [${msg.capability}]` : '';
+ this.terminal.print(` ${msg.id} from ${msg.from} (${msg.type}${capInfo})`, 'info');
+ this.terminal.print(` Payload: ${JSON.stringify(msg.payload).substring(0, 120)}`, 'text-muted');
+ }
+ }
+ a2aAckCommand(args) {
+ if (args.length < 1) {
+ return this.terminal.print('Usage: a2a-ack <msgId> [result...]', 'warning');
+ }
+ const msgId = args[0];
+ const resultRaw = args.slice(1).join(' ');
+ let result = {};
+ try {
+ result = JSON.parse(resultRaw || '{}');
+ } catch (e) {
+ result = { text: resultRaw };
+ }
+ if (this.a2aAck(msgId, result)) {
+ this.terminal.print(`✅ A2A message ${msgId} completed`, 'success');
+ }
+ }
+ a2aRejectCommand(args) {
+ if (args.length < 1) {
+ return this.terminal.print('Usage: a2a-reject <msgId> [error...]', 'warning');
+ }
+ const msgId = args[0];
+ const error = args.slice(1).join(' ') || 'rejected by user';
+ if (this.a2aReject(msgId, error)) {
+ this.terminal.print(`❌ A2A message ${msgId} rejected: ${error}`, 'warning');
+ }
+ }
_loadContracts() {
try {
const data = this.terminal.fs.readFile('/.contracts/index.json');
@@ -11593,18 +12141,21 @@ class AgentRegistryModule {
} catch (e) { console.error('Contract save error:', e); }
}
registerAgent(args) {
- const id = args[1] || `agent-${Date.now()}`;
- const capabilities = args.slice(2).length ? args.slice(2) : ['list_files', 'read_file'];
+ const id = args[0] || `agent-${Date.now()}`;
+ const capabilities = args[1] ? args[1].split(',').map(s => s.trim()).filter(Boolean) : ['list_files', 'read_file'];
const agentDir = `agents/${id}`;
if (!this.terminal.fs.readDir('/' + agentDir + '/')) this.terminal.fs.mkdir(agentDir);
this.terminal.fs.writeFile(`${agentDir}/status.json`, JSON.stringify({
id,
status: 'idle',
+ model: null,
capabilities,
registered: new Date().toISOString(),
lastHeartbeat: new Date().toISOString(),
contract: null
}));
+ // Auto-register in A2A scratchpad
+ this.a2aRegister(id, capabilities);
this.terminal.print(`✅ Agent '${id}' registered with ${capabilities.length} capabilities`, 'success');
}
registerContract(args) {
@@ -11677,6 +12228,8 @@ class AgentRegistryModule {
});
}
unregisterAgent(id) {
+ // Unregister from A2A scratchpad
+ this.a2aUnregister(id);
const ok = this.terminal.fs.deleteFile('agents/' + id);
if (ok) this.terminal.print(`✅ Agent '${id}' unregistered`, 'success');
else this.terminal.print(`❌ Agent '${id}' not found`, 'error');
@@ -11764,6 +12317,19 @@ class LeaseModule {
};
this._leases[lease.id] = lease;
this.terminal.fs.writeFile(`leases/${lease.id}.json`, JSON.stringify(lease));
+ // Post A2A message to lease system agent
+ try {
+ const registry = this.terminal.moduleManager.modules['AgentRegistryModule'];
+ if (registry && typeof registry.a2aPost === 'function') {
+ registry.a2aPost('orchestrator-0', 'lease-system', 'lease.create', 'leases.create', {
+ leaseId: lease.id,
+ workerId,
+ task,
+ duration,
+ expiresAt: lease.expiresAt
+ });
+ }
+ } catch (e) {}
this.terminal.print(`📜 Lease ${lease.id} created for ${workerId} (${duration}s, task: ${task.substring(0, 40)})`, 'success');
return lease;
}
@@ -11783,6 +12349,18 @@ class LeaseModule {
this._leases[id].status = 'revoked';
this._leases[id].revokedAt = new Date().toISOString();
this.terminal.fs.writeFile(`leases/${id}.json`, JSON.stringify(this._leases[id]));
+ // Post A2A completion message
+ try {
+ const registry = this.terminal.moduleManager.modules['AgentRegistryModule'];
+ if (registry && typeof registry.a2aPost === 'function') {
+ registry.a2aPost('orchestrator-0', 'lease-system', 'lease.terminate', 'leases.terminate', {
+ leaseId: id,
+ status: 'revoked',
+ revokedAt: this._leases[id].revokedAt,
+ workerId: this._leases[id].workerId
+ });
+ }
+ } catch (e) {}
this.terminal.print(`✅ Lease ${id} revoked`, 'warning');
}
renewLease(id, additionalSeconds) {
@@ -11825,6 +12403,7 @@ class OrchestratorModule {
this._HEARTBEAT_TIMEOUT = 15000;
this._IDLE_TIMEOUT = 120000;
this._RESTART_POLICY = 'never';
+ this._AGENT_DEFAULT_MODEL = null;
this._A2A_POLLING = true;
this._A2A_POLL_INTERVAL = 500;
this._HEARTBEAT_CHECK = 3000;
@@ -11847,7 +12426,7 @@ class OrchestratorModule {
this._loadConfig();
}
_loadConfig() {
- try { const c = localStorage.getItem('webx11_orchestrator_config'); if (c) { const p = JSON.parse(c); if (p.spawnLimit) this._SPAWN_LIMIT = p.spawnLimit; if (p.heartbeatTimeout) this._HEARTBEAT_TIMEOUT = p.heartbeatTimeout; if (p.idleTimeout) this._IDLE_TIMEOUT = p.idleTimeout; if (p.restartPolicy) this._RESTART_POLICY = p.restartPolicy; if (p.a2aPollInterval) this._A2A_POLL_INTERVAL = p.a2aPollInterval; if (p.heartbeatCheck) this._HEARTBEAT_CHECK = p.heartbeatCheck; if (p.idleSweepInterval) this._IDLE_SWEEP_INTERVAL = p.idleSweepInterval; if (p.deadPurgeTimeout) this._DEAD_PURGE_TIMEOUT = p.deadPurgeTimeout; if (p.consensusSize) this._CONSENSUS_SIZE = p.consensusSize; if (p.consensusStrategy) this._CONSENSUS_STRATEGY = p.consensusStrategy; if (p.consensusTimeout) this._CONSENSUS_TIMEOUT = p.consensusTimeout; if (p.pipelineAutoFix !== undefined) this._PIPELINE_CONFIG.autoFix = p.pipelineAutoFix; if (p.pipelineStageTimeout) this._PIPELINE_CONFIG.stageTimeout = p.pipelineStageTimeout; } } catch (e) {}
+ try { const c = localStorage.getItem('webx11_orchestrator_config'); if (c) { const p = JSON.parse(c); if (p.spawnLimit) this._SPAWN_LIMIT = p.spawnLimit; if (p.heartbeatTimeout) this._HEARTBEAT_TIMEOUT = p.heartbeatTimeout; if (p.idleTimeout) this._IDLE_TIMEOUT = p.idleTimeout; if (p.restartPolicy) this._RESTART_POLICY = p.restartPolicy; if (p.agentDefaultModel !== undefined) this._AGENT_DEFAULT_MODEL = p.agentDefaultModel; if (p.a2aPollInterval) this._A2A_POLL_INTERVAL = p.a2aPollInterval; if (p.heartbeatCheck) this._HEARTBEAT_CHECK = p.heartbeatCheck; if (p.idleSweepInterval) this._IDLE_SWEEP_INTERVAL = p.idleSweepInterval; if (p.deadPurgeTimeout) this._DEAD_PURGE_TIMEOUT = p.deadPurgeTimeout; if (p.consensusSize) this._CONSENSUS_SIZE = p.consensusSize; if (p.consensusStrategy) this._CONSENSUS_STRATEGY = p.consensusStrategy; if (p.consensusTimeout) this._CONSENSUS_TIMEOUT = p.consensusTimeout; if (p.pipelineAutoFix !== undefined) this._PIPELINE_CONFIG.autoFix = p.pipelineAutoFix; if (p.pipelineStageTimeout) this._PIPELINE_CONFIG.stageTimeout = p.pipelineStageTimeout; } } catch (e) {}
}
_saveConfig() {
localStorage.setItem('webx11_orchestrator_config', JSON.stringify({
@@ -11855,6 +12434,7 @@ class OrchestratorModule {
heartbeatTimeout: this._HEARTBEAT_TIMEOUT,
idleTimeout: this._IDLE_TIMEOUT,
restartPolicy: this._RESTART_POLICY,
+ agentDefaultModel: this._AGENT_DEFAULT_MODEL,
a2aPollInterval: this._A2A_POLL_INTERVAL,
heartbeatCheck: this._HEARTBEAT_CHECK,
idleSweepInterval: this._IDLE_SWEEP_INTERVAL,
@@ -11869,7 +12449,16 @@ class OrchestratorModule {
registerCommands() {
this.commands['orchestrate'] = (args) => this.delegateTask(args.join(' '));
this.commands['orchestrator-status'] = () => this.showStatus();
- this.commands['orchestrator-spawn'] = (args) => this.spawnAgent(args[0], args.slice(1));
+ this.commands['orchestrator-spawn'] = (args) => {
+ // Parse --model flag before passing remaining as capabilities
+ const modelIdx = args.indexOf('--model');
+ let modelName = null;
+ if (modelIdx !== -1 && args[modelIdx + 1]) {
+ modelName = args[modelIdx + 1];
+ args.splice(modelIdx, 2);
+ }
+ this.spawnAgent(args[0], args.slice(1), modelName);
+ };
this.commands['orchestrator-kill'] = (args) => this.killAgent(args[0]);
this.commands['orchestrator-purge'] = () => this.purgeDeadAgents();
this.commands['orchestrator-config'] = (args) => this.setConfig(args);
@@ -12124,7 +12713,7 @@ class OrchestratorModule {
_sanitizeId(name) {
return (name || 'worker').replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').substring(0, 32) || 'worker';
}
- spawnAgent(role, capabilities) {
+ spawnAgent(role, capabilities, modelName = null) {
if (this._spawnLock) {
this.terminal.print('⚠️ Spawn in progress — queued', 'warning');
return null;
@@ -12140,6 +12729,7 @@ class OrchestratorModule {
const agent = {
id,
role: role || 'worker',
+ model: modelName || this._AGENT_DEFAULT_MODEL,
capabilities: capabilities.length ? capabilities : ['list_files', 'read_file', 'web_search'],
createdAt: new Date().toISOString(),
lastHeartbeat: Date.now(),
@@ -12157,7 +12747,13 @@ class OrchestratorModule {
this.terminal.fs.writeFile(`agents/${id}/status.json`, JSON.stringify(agent));
// Register in agent registry
const registry = this.terminal.moduleManager.modules['AgentRegistryModule'];
- if (registry) registry.registerAgent(['register', id, ...agent.capabilities]);
+ if (registry) {
+ registry.registerAgent(['register', id, ...agent.capabilities]);
+ // A2A Scratchpad registration
+ if (typeof registry.a2aRegister === 'function') {
+ registry.a2aRegister(id, agent.capabilities);
+ }
+ }
this.terminal.print(`🤖 Agent '${id}' spawned (role: ${role || 'worker'}, ${agent.capabilities.length} caps)`, 'success');
return agent;
} finally {
@@ -12176,7 +12772,13 @@ class OrchestratorModule {
}
// Unregister from registry
const registry = this.terminal.moduleManager.modules['AgentRegistryModule'];
- if (registry) registry.unregisterAgent(id);
+ if (registry) {
+ registry.unregisterAgent(id);
+ // A2A Scratchpad unregister
+ if (typeof registry.a2aUnregister === 'function') {
+ registry.a2aUnregister(id);
+ }
+ }
// Clean up VFS
this.terminal.fs.deleteFile('agents/' + id);
delete this._agents[id];
@@ -12365,7 +12967,8 @@ class OrchestratorModule {
'consensus-strategy': () => { if (!['majority', 'unanimous', 'weighted'].includes(val)) return 'Must be majority, unanimous, or weighted'; this._CONSENSUS_STRATEGY = val; return `consensus-strategy set to ${val}`; },
'consensus-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 300000) return 'Must be 5000-300000ms'; this._CONSENSUS_TIMEOUT = n; return `consensus-timeout set to ${n}ms`; },
'pipeline-auto-fix': () => { this._PIPELINE_CONFIG.autoFix = val === 'on' || val === 'true'; return `auto-fix ${this._PIPELINE_CONFIG.autoFix ? 'on' : 'off'}`; },
- 'pipeline-stage-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 120000) return 'Must be 5000-120000ms'; this._PIPELINE_CONFIG.stageTimeout = n; return `stage-timeout set to ${n}ms`; }
+ 'pipeline-stage-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 120000) return 'Must be 5000-120000ms'; this._PIPELINE_CONFIG.stageTimeout = n; return `stage-timeout set to ${n}ms`; },
+ 'agent-default-model': () => { this._AGENT_DEFAULT_MODEL = val; this._saveConfig(); return `agent-default-model set to ${val}`; }
};
if (!configMap[key]) {
@@ -13020,11 +13623,13 @@ class OrchestratorModule {
this.terminal.print(` Agents: ${total} (${busy} busy, ${idle} idle, ${sleeping} sleeping, ${failed} failed)`, 'info');
this.terminal.print(` Spawn limit: ${this._SPAWN_LIMIT} (strategy: ${this._SPAWN_STRATEGY})`, 'info');
this.terminal.print(` Default restart: ${this._RESTART_POLICY}`, 'info');
+ this.terminal.print(` Default agent model: ${this._AGENT_DEFAULT_MODEL || 'none (uses main AI model)'}`, 'info');
this.terminal.print(` A2A Messages: ${a2aTotal} (${a2aPending} pending)`, 'info');
Object.values(this._agents).forEach(a => {
const task = a.currentTask ? ` (task: ${a.currentTask.substring(0, 40)})` : '';
+ const modelInfo = a.model ? ` [model: ${a.model}]` : '';
const backoff = a.nextRestartAt && a.nextRestartAt > Date.now() ? ` [restart in ${Math.round((a.nextRestartAt - Date.now()) / 1000)}s]` : '';
- this.terminal.print(` ${a.id} [${a.role}] — ${a.status}${task}${backoff}`, 'info');
+ this.terminal.print(` ${a.id} [${a.role}] — ${a.status}${modelInfo}${task}${backoff}`, 'info');
});
}
@@ -14206,7 +14811,22 @@ class WorldModelModule {
_deriveVersion(mInfo) {
// Single source of truth: the constant set at bootstrap
if (window.NEWOS4_VERSION) return window.NEWOS4_VERSION;
- // No fallback version string — if the constant is missing, report unknown
+ // Fallback: try to extract from script tag ?v= query param or LATEST_VER constant
+ try {
+ const scripts = document.querySelectorAll('script[src*="module-loader"]');
+ for (const s of scripts) {
+ const match = s.src.match(/[?&]v=([^&]+)/);
+ if (match) return match[1];
+ }
+ // Try to find LATEST_VER in any inline script
+ const allScripts = document.querySelectorAll('script:not([src])');
+ for (const s of allScripts) {
+ const match = s.textContent.match(/LATEST_VER\s*=\s*['"]([^'"]+)['"]/);
+ if (match) return match[1];
+ }
+ } catch (e) {
+ console.warn('[WorldModel] Version extraction error:', e.message);
+ }
return 'v?';
}
@@ -14216,14 +14836,20 @@ class WorldModelModule {
if (!mm) return 0;
// Count registry entries (all known modules) vs loaded
const registry = mm.moduleRegistry || [];
- return registry.length || Object.keys(mm.modules || {}).length + 5; // +5 for expected unloaded
+ return registry.length || Object.keys(mm.modules || {}).length;
} catch (e) {
+ console.warn('[WorldModel] Total modules gather error:', e.message);
return Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0;
}
}
_gatherSystemInfo(fs, moduleCount, version) {
- const healthData = fs.fsReadJSON('/system/metrics/health.json');
+ // Get real health data from ServerMonitorModule's _latencyLog and AI assistant's _errorLog
+ const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule'];
+ const ai = this.terminal?.moduleManager?.modules?.['WebX11AIAssistantModule'];
+ const errorCount = ai?._errorLog?.length || 0;
+ const latencyErrors = sm?._latencyLog ? Object.values(sm._latencyLog).filter(e => e.status === 'error').length : 0;
+ const totalErrors = errorCount + latencyErrors;
return {
version: `newOS4 ${version}`,
modules: moduleCount || Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0,
@@ -14232,8 +14858,8 @@ class WorldModelModule {
memory: typeof performance !== 'undefined' && performance.memory
? `${(performance.memory.usedJSHeapSize / 1048576).toFixed(1)}MB`
: 'N/A',
- health: healthData?.status || 'OK',
- lastHealthCheck: healthData?.timestamp || null
+ health: totalErrors > 0 ? `⚠️ ${totalErrors} errors` : 'OK',
+ lastHealthCheck: new Date().toISOString()
};
}
@@ -14285,6 +14911,7 @@ class WorldModelModule {
const count = idx ? Object.keys(idx).length : 0;
return { count, skills: idx ? Object.keys(idx) : [] };
} catch (e) {
+ console.warn('[WorldModel] Skill info gather error:', e.message);
return { count: 0, skills: [] };
}
}
@@ -14294,20 +14921,21 @@ class WorldModelModule {
if (mg && mg._nodes) {
const nodes = Object.keys(mg._nodes).length;
const edges = (mg._edges || []).length;
- return { nodes, edges, active: true };
+ return { nodes, edges, active: true, fallback: false };
}
// Fallback: count memory files
try {
const memIndex = this.terminal.fs?.fsReadJSON('/memory/index.json');
- return { nodes: memIndex ? Object.keys(memIndex).length : 0, edges: 0, active: false };
+ return { nodes: memIndex ? Object.keys(memIndex).length : 0, edges: 0, active: false, fallback: true };
} catch (e) {
- return { nodes: 0, edges: 0, active: false };
+ console.warn('[WorldModel] Memory info gather error:', e.message);
+ return { nodes: 0, edges: 0, active: false, fallback: true };
}
}
_gatherAgentInfo() {
const orch = this.terminal?.moduleManager?.modules?.['OrchestratorModule'];
- if (!orch || !orch._agents) return { agents: 0, active: 0 };
+ if (!orch || !orch._agents) return { agents: -1, active: -1, error: 'Orchestrator not loaded' };
const agents = Object.keys(orch._agents).length;
const active = Object.values(orch._agents).filter(a => a.status === 'busy').length;
return { agents, active, totalLeases: Object.keys(orch._a2aMessages || {}).length };
@@ -14315,13 +14943,17 @@ class WorldModelModule {
_gatherTelemetry(fs) {
try {
- const latency = fs.fsReadJSON('/system/metrics/tool-latency.json');
- const errors = fs.fsReadJSON('/system/metrics/error-log.json');
+ // Get real latency data from ServerMonitorModule and errors from AI assistant
+ const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule'];
+ const ai = this.terminal?.moduleManager?.modules?.['WebX11AIAssistantModule'];
+ const latencyDataPoints = sm?._latencyLog ? Object.keys(sm._latencyLog).length : 0;
+ const recentErrors = ai?._errorLog?.length || 0;
return {
- toolLatencyDataPoints: latency ? Object.keys(latency).length : 0,
- recentErrors: errors ? (Array.isArray(errors) ? errors.length : Object.keys(errors).length) : 0,
+ toolLatencyDataPoints: latencyDataPoints,
+ recentErrors: recentErrors,
};
} catch (e) {
+ console.warn('[WorldModel] Telemetry gather error:', e.message);
return { toolLatencyDataPoints: 0, recentErrors: 0 };
}
}
@@ -14334,19 +14966,20 @@ class WorldModelModule {
const tasks = Object.values(mg._nodes).filter(n => n.type === 'task');
const recentFiles = Object.values(mg._nodes)
.filter(n => n.type === 'file')
- .sort((a, b) => b.access_count - a.access_count)
+ .sort((a, b) => (b.access_count || 0) - (a.access_count || 0))
.slice(0, this._MAX_PROJECTS);
return {
totalTasks: tasks.length,
activeProjects: recentFiles.slice(0, 5).map(f => ({
name: f.label,
- accesses: f.access_count,
+ accesses: f.access_count || 0,
lastModified: f.updated
}))
};
}
return { totalTasks: 0, activeProjects: [] };
} catch (e) {
+ console.warn('[WorldModel] Projects gather error:', e.message);
return { totalTasks: 0, activeProjects: [] };
}
}
@@ -14368,7 +15001,25 @@ class WorldModelModule {
if (skills && skills.length > 0) {
recent.push(`Skills directory: ${skills.filter(s => s.type === 'file').length} files`);
}
- } catch (e) {}
+ // Check memory directory for recent changes
+ const memDir = fs.readDir('/memory/');
+ if (memDir && memDir.length > 0) {
+ const memFiles = memDir.filter(s => s.type === 'file');
+ if (memFiles.length > 0) {
+ recent.push(`Memory: ${memFiles.length} files in /memory/`);
+ }
+ }
+ // Check system config changes
+ const sysDir = fs.readDir('/system/');
+ if (sysDir && sysDir.length > 0) {
+ const configFiles = sysDir.filter(s => s.type === 'file' && s.name.endsWith('.json'));
+ if (configFiles.length > 0) {
+ recent.push(`System config: ${configFiles.length} config files`);
+ }
+ }
+ } catch (e) {
+ console.warn('[WorldModel] Recent gather error:', e.message);
+ }
return recent;
}
@@ -14490,7 +15141,10 @@ class WorldModelModule {
getContextForPrompt() {
if (!this._model) return '';
const m = this._model;
- return `[World Model Context]\\nSystem: newOS4 ${m.version} · ${m.modules.loaded} modules · ${m.skills.count} skills · ${m.files.total} files · ${m.agents.agents} agents${m.telemetry.recentErrors > 0 ? ` · ⚠️ ${m.telemetry.recentErrors} errors` : ' · ✅ OK'}${m.modules.modifiedCount > 0 ? `\\nModified modules: ${m.modules.modified.slice(0, 3).join(', ')}${m.modules.modifiedCount > 3 ? ` (+${m.modules.modifiedCount - 3} more)` : ''}` : ''}`;
+ const agentStr = m.agents.error
+ ? 'agents: N/A'
+ : `${m.agents.agents} agents`;
+ return `[World Model Context]\\nSystem: newOS4 ${m.version} · ${m.modules.loaded} modules · ${m.skills.count} skills · ${m.files.total} files · ${agentStr}${m.telemetry.recentErrors > 0 ? ` · ⚠️ ${m.telemetry.recentErrors} errors` : ' · ✅ OK'}${m.modules.modifiedCount > 0 ? `\\nModified modules: ${m.modules.modified.slice(0, 3).join(', ')}${m.modules.modifiedCount > 3 ? ` (+${m.modules.modifiedCount - 3} more)` : ''}` : ''}`;
}
// ─── Persistence ───
@@ -15052,7 +15706,7 @@ class NeuralVFSModule {
_isSystemFile(path) {
const normalized = path.startsWith('/') ? path : '/' + path;
- const sysPrefixes = ['/.trash', '/.semantic_index', '/.memory_graph', '/_snapshots', '/_sessions', '/logs', '/memory', '/system', '/sys', '/_mods', '/backups'];
+ const sysPrefixes = ['/.trash', '/.semantic_index', '/.memory_graph', '/_snapshots', '/_sessions', '/logs', '/memory', '/system', '/sys', '/_mods', '/backups', '/agents', '/_agents', '/leases'];
if (sysPrefixes.some(p => normalized.startsWith(p))) return true;
if (normalized.includes('/node_modules/')) return true;
return false;
@@ -15577,3 +16231,5 @@ Concepts:`;
if (typeof window !== 'undefined') {
window.NeuralVFSModule = NeuralVFSModule;
}
+
+/* === end core module bundle === */
diff --git a/modules/core/module-agent-registry.js b/modules/core/module-agent-registry.js
index 2439ab8..68dca66 100644
--- a/modules/core/module-agent-registry.js
+++ b/modules/core/module-agent-registry.js
@@ -1,10 +1,12 @@
// module-agent-registry.js — VFS-backed agent directory with A2A contracts
// Commands: agent-list, agent-register, agent-unregister, agent-status, contract-register, contract-list
+// A2A Scratchpad commands: a2a-post, a2a-poll, a2a-ack, a2a-reject, a2a-status, a2a-prune
class AgentRegistryModule {
constructor() {
this.commands = {};
this.terminal = null;
this._contracts = {};
+ this._a2aPruneTimer = null;
}
registerCommands() {
this.commands['agent-list'] = () => this.listAgents();
@@ -13,14 +15,321 @@ class AgentRegistryModule {
this.commands['agent-unregister'] = (args) => this.unregisterAgent(args[0]);
this.commands['contract-register'] = (args) => this.registerContract(args);
this.commands['contract-list'] = () => this.listContracts();
+ // A2A Scratchpad commands
+ this.commands['a2a-post'] = (args) => this.a2aPostCommand(args);
+ this.commands['a2a-poll'] = (args) => this.a2aPollCommand(args);
+ this.commands['a2a-ack'] = (args) => this.a2aAckCommand(args);
+ this.commands['a2a-reject'] = (args) => this.a2aRejectCommand(args);
+ this.commands['a2a-status'] = () => this.a2aStatus();
+ this.commands['a2a-prune'] = () => this.a2aPrune();
+ }
+ getHelpInfo() {
+ return {
+ 'a2a-post': 'a2a-post <to> <type> <capability> <payload...> — Post an A2A message',
+ 'a2a-poll': 'a2a-poll [agentId] — Poll for messages addressed to an agent (default: current or "user")',
+ 'a2a-ack': 'a2a-ack <msgId> <result...> — Acknowledge a message with result',
+ 'a2a-reject': 'a2a-reject <msgId> <error...> — Reject a message with error',
+ 'a2a-status': 'a2a-status — Show scratchpad stats per agent',
+ 'a2a-prune': 'a2a-prune — Clean up expired and old completed messages'
+ };
}
init(terminal) {
this.terminal = terminal;
if (!this.terminal.fs.readDir('/agents/')) this.terminal.fs.mkdir('agents');
if (!this.terminal.fs.readDir('/.contracts/')) this.terminal.fs.mkdir('.contracts');
this._loadContracts();
+ // Initialize scratchpad
+ this._ensureScratchpad();
+ // Auto-prune every 60 seconds
+ this._a2aPruneTimer = setInterval(() => this.a2aPrune(), 60000);
+ // Save the spec-vs-reality delta table
+ this._saveDeltaTable();
this.terminal.print('📋 Agent Registry loaded', 'info');
}
+ // ─── Task 1: Save spec-vs-reality delta table ───
+ _saveDeltaTable() {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs.readDir('/_docs/')) fs.mkdir('_docs');
+ const delta = `# Spec-vs-Reality Delta — 2026-06-17\n\n` +
+`| # | Whitepaper Concept | Spec'd Behavior | Current Reality | Δ | Priority |\n` +
+`|---|---|---|---|---|---|\n` +
+`| 1 | Hyper-Modular Kernel | Real-time self-rewriting drivers for task-specific optimization via WPM | WPM exists but is manual; no hot-patch daemon or runtime driver swap | Partial | Medium |\n` +
+`| 2 | A2A Protocol | Standardized neural communication layer for agent negotiation; typed messages, no central broker | VFS mailbox transport works (a2a-send/a2a-list/500ms polling) but orchestrator acts as central broker | Partial | High |\n` +
+`| 3 | Neural VFS | Semantic-based file storage (concept:// URIs) as primary access method; hierarchical paths are secondary | concept:// URIs work (nvfs-index/nvfs-resolve) but hierarchical paths remain primary access; concept resolution requires explicit index | Partial | High |\n` +
+`| 4 | Recursive Self-Optimization | Monitor → Profile → Diagnose → Patch → Test → Promote feedback loop | system-health telemetry captures latency/VFS stats but no auto-patch generation or hot-promotion pipeline | Partial | Medium |\n` +
+`| 5 | Agent Discovery | No central broker — agents discover each other via VFS registrations spontaneously | Orchestrator is central broker; all task delegation goes through it; agents don't autonomously discover each other | Critical Gap | Critical |\n` +
+`| 6 | Contract Schema | Every A2A message carries a contract schema defining capabilities and constraints | Contracts exist (contract-register/contract-list) but schema validation is basic substring matching; not enforced on every message | Partial | Medium |\n` +
+`| 7 | Semantic Addressing | Neural VFS as primary storage paradigm — users navigate by concept, not path | Hierarchical path access still primary; concept:// requires explicit nvfs-index; no transparent concept-to-path fallback | Critical Gap | Critical |\n` +
+`| 8 | Warm-start Guarantee | Kernel caches last N stable driver configs; boot in under 200ms | No golden config cache; full localStorage restore on every page load | Spec Violation | High |\n` +
+`| 9 | Self-rewrite Daemon | Monitors performance telemetry and generates optimized driver variants at runtime | No self-rewrite daemon exists; all module changes are manual | Missing | Low |\n` +
+`| 10 | Agent Lifecycle | spawn, sleep, wake, kill, restart policies with exponential backoff | Fully implemented in orchestrator with heartbeat, idle sweep, restart policies, VFS sleep serialization | Fully Aligned | — |\n\n` +
+`**Summary: 2 Critical gaps, 5 Partial, 1 Spec Violation, 1 Missing, 1 Fully Aligned**\n`;
+ fs.writeFile('_docs/spec-vs-reality-delta.md', delta);
+ this.terminal.print('📄 Spec-vs-Reality delta saved to /_docs/spec-vs-reality-delta.md', 'info');
+ } catch (e) {
+ console.warn('Failed to save delta table:', e.message);
+ }
+ }
+ // ─── A2A Scratchpad Helpers ───
+ _ensureScratchpad() {
+ try {
+ const data = this.terminal.fs.fsReadJSON('/_agents/scratchpad.json');
+ if (!data) throw new Error('no scratchpad');
+ } catch (e) {
+ this.terminal.fs.mkdir('_agents');
+ this.terminal.fs.fsWriteJSON('/_agents/scratchpad.json', {
+ messages: [],
+ seq: 0,
+ agents: {}
+ });
+ }
+ }
+ _getScratchpad() {
+ try {
+ const data = this.terminal.fs.fsReadJSON('/_agents/scratchpad.json');
+ if (data && data.messages) return data;
+ } catch (e) {}
+ return { messages: [], seq: 0, agents: {} };
+ }
+ _saveScratchpad(sp) {
+ try {
+ this.terminal.fs.mkdir('_agents');
+ this.terminal.fs.fsWriteJSON('/_agents/scratchpad.json', sp);
+ } catch (e) {
+ console.warn('Scratchpad save error:', e.message);
+ }
+ }
+ // ─── A2A Scratchpad Functions ───
+ a2aPost(from, to, type, capability, payload, ttl) {
+ if (!from || !to || !type) {
+ this.terminal.print('a2aPost: from, to, and type are required', 'warning');
+ return null;
+ }
+ const sp = this._getScratchpad();
+ sp.seq = (sp.seq || 0) + 1;
+ const msg = {
+ id: `msg_${String(sp.seq).padStart(3, '0')}`,
+ from: from,
+ to: to,
+ type: type,
+ capability: capability || null,
+ payload: payload || {},
+ ttl: (typeof ttl === 'number' ? ttl : 300000),
+ status: 'pending',
+ ts: Date.now(),
+ reply_to: null
+ };
+ sp.messages.push(msg);
+ this._saveScratchpad(sp);
+ return msg;
+ }
+ a2aPoll(agentId) {
+ if (!agentId) return [];
+ const sp = this._getScratchpad();
+ const pending = [];
+ for (const msg of sp.messages) {
+ if (msg.to === agentId && msg.status === 'pending') {
+ msg.status = 'delivered';
+ pending.push(msg);
+ }
+ }
+ if (pending.length > 0) this._saveScratchpad(sp);
+ return pending;
+ }
+ a2aAck(messageId, resultPayload) {
+ const sp = this._getScratchpad();
+ const msg = sp.messages.find(m => m.id === messageId);
+ if (!msg) {
+ this.terminal.print(`a2aAck: message ${messageId} not found`, 'warning');
+ return false;
+ }
+ if (msg.status === 'completed' || msg.status === 'failed') {
+ this.terminal.print(`a2aAck: message ${messageId} already ${msg.status}`, 'warning');
+ return false;
+ }
+ msg.status = 'completed';
+ msg.result = resultPayload || {};
+ msg.completedAt = Date.now();
+ this._saveScratchpad(sp);
+ return true;
+ }
+ a2aReject(messageId, error) {
+ const sp = this._getScratchpad();
+ const msg = sp.messages.find(m => m.id === messageId);
+ if (!msg) {
+ this.terminal.print(`a2aReject: message ${messageId} not found`, 'warning');
+ return false;
+ }
+ if (msg.status === 'completed' || msg.status === 'failed') {
+ this.terminal.print(`a2aReject: message ${messageId} already ${msg.status}`, 'warning');
+ return false;
+ }
+ msg.status = 'failed';
+ msg.error = error || 'unspecified error';
+ msg.completedAt = Date.now();
+ this._saveScratchpad(sp);
+ return true;
+ }
+ a2aRegister(agentId, capabilities) {
+ if (!agentId) return;
+ const sp = this._getScratchpad();
+ sp.agents[agentId] = {
+ last_poll: Date.now(),
+ capabilities: capabilities || []
+ };
+ this._saveScratchpad(sp);
+ }
+ a2aUnregister(agentId) {
+ if (!agentId) return;
+ const sp = this._getScratchpad();
+ delete sp.agents[agentId];
+ this._saveScratchpad(sp);
+ }
+ a2aPrune() {
+ try {
+ const sp = this._getScratchpad();
+ const now = Date.now();
+ const before = sp.messages.length;
+ sp.messages = sp.messages.filter(m => {
+ // Remove expired messages (past TTL)
+ if (m.status === 'pending' && (now - m.ts > m.ttl)) return false;
+ // Remove completed/failed messages older than 1 hour
+ if ((m.status === 'completed' || m.status === 'failed') && m.completedAt && (now - m.completedAt > 3600000)) return false;
+ return true;
+ });
+ const removed = before - sp.messages.length;
+ if (removed > 0) this._saveScratchpad(sp);
+ return removed;
+ } catch (e) {
+ console.warn('a2aPrune error:', e.message);
+ return 0;
+ }
+ }
+ a2aStatus() {
+ const sp = this._getScratchpad();
+ const counts = {};
+ for (const msg of sp.messages) {
+ if (!counts[msg.to]) counts[msg.to] = { pending: 0, delivered: 0, completed: 0, failed: 0 };
+ if (msg.status === 'pending') counts[msg.to].pending++;
+ else if (msg.status === 'delivered') counts[msg.to].delivered++;
+ else if (msg.status === 'completed') counts[msg.to].completed++;
+ else if (msg.status === 'failed') counts[msg.to].failed++;
+ }
+ this.terminal.print('📊 A2A Scratchpad Status:', 'header');
+ this.terminal.print(` Total messages: ${sp.messages.length}`, 'info');
+ this.terminal.print(` Registered agents: ${Object.keys(sp.agents).length}`, 'info');
+ this.terminal.print(` Sequence: ${sp.seq}`, 'info');
+ const agentNames = Object.keys(counts);
+ if (agentNames.length === 0) {
+ this.terminal.print(' No messages in scratchpad', 'info');
+ return;
+ }
+ for (const agentId of agentNames) {
+ const c = counts[agentId];
+ const total = c.pending + c.delivered + c.completed + c.failed;
+ const caps = sp.agents[agentId] ? ` [${sp.agents[agentId].capabilities.join(', ')}]` : '';
+ this.terminal.print(` ${agentId}${caps}: ${total} msgs (⏳${c.pending} 📬${c.delivered} ✅${c.completed} ❌${c.failed})`, 'info');
+ }
+ }
+ _a2aCleanup() {
+ // Remove agent entries from scratchpad when agent directory is deleted from /agents/
+ try {
+ const sp = this._getScratchpad();
+ const agentDirs = {};
+ const dir = this.terminal.fs.readDir('/agents/');
+ if (dir) {
+ for (const entry of dir) {
+ if (entry.type === 'dir') agentDirs[entry.name] = true;
+ }
+ }
+ let changed = false;
+ for (const agentId of Object.keys(sp.agents)) {
+ if (!agentDirs[agentId]) {
+ delete sp.agents[agentId];
+ changed = true;
+ }
+ }
+ if (changed) this._saveScratchpad(sp);
+ } catch (e) {
+ console.warn('a2aCleanup error:', e.message);
+ }
+ }
+ // ─── A2A Command Handlers ───
+ a2aPostCommand(args) {
+ // a2a-post <to> <type> <capability> <payload...>
+ if (args.length < 3) {
+ return this.terminal.print('Usage: a2a-post <to> <type> <capability> <payload...>', 'warning');
+ }
+ const to = args[0];
+ const type = args[1];
+ const capability = args[2];
+ const payloadRaw = args.slice(3).join(' ');
+ let payload = {};
+ try {
+ // Try parsing as JSON
+ payload = JSON.parse(payloadRaw || '{}');
+ } catch (e) {
+ // Treat as plain text
+ payload = { text: payloadRaw };
+ }
+ const from = 'user';
+ const ttl = 300000;
+ const msg = this.a2aPost(from, to, type, capability, payload, ttl);
+ if (msg) {
+ this.terminal.print(`📨 A2A message ${msg.id} posted to ${to} (type: ${type})`, 'success');
+ }
+ }
+ a2aPollCommand(args) {
+ // Use explicit agent ID if provided, otherwise try to determine current agent
+ let agentId = (args && args[0]) || 'user';
+ if (!args || !args[0]) {
+ try {
+ const orchestrator = this.terminal.moduleManager.modules['OrchestratorModule'];
+ if (orchestrator && orchestrator._agents) {
+ const myAgents = Object.entries(orchestrator._agents).filter(([, a]) => a.status === 'busy');
+ if (myAgents.length > 0) agentId = myAgents[0][0];
+ }
+ } catch (e) {}
+ }
+ const msgs = this.a2aPoll(agentId);
+ if (msgs.length === 0) {
+ this.terminal.print(`📭 No pending messages for ${agentId}`, 'info');
+ return;
+ }
+ this.terminal.print(`📬 ${msgs.length} message(s) for ${agentId}:`, 'header');
+ for (const msg of msgs) {
+ const capInfo = msg.capability ? ` [${msg.capability}]` : '';
+ this.terminal.print(` ${msg.id} from ${msg.from} (${msg.type}${capInfo})`, 'info');
+ this.terminal.print(` Payload: ${JSON.stringify(msg.payload).substring(0, 120)}`, 'text-muted');
+ }
+ }
+ a2aAckCommand(args) {
+ if (args.length < 1) {
+ return this.terminal.print('Usage: a2a-ack <msgId> [result...]', 'warning');
+ }
+ const msgId = args[0];
+ const resultRaw = args.slice(1).join(' ');
+ let result = {};
+ try {
+ result = JSON.parse(resultRaw || '{}');
+ } catch (e) {
+ result = { text: resultRaw };
+ }
+ if (this.a2aAck(msgId, result)) {
+ this.terminal.print(`✅ A2A message ${msgId} completed`, 'success');
+ }
+ }
+ a2aRejectCommand(args) {
+ if (args.length < 1) {
+ return this.terminal.print('Usage: a2a-reject <msgId> [error...]', 'warning');
+ }
+ const msgId = args[0];
+ const error = args.slice(1).join(' ') || 'rejected by user';
+ if (this.a2aReject(msgId, error)) {
+ this.terminal.print(`❌ A2A message ${msgId} rejected: ${error}`, 'warning');
+ }
+ }
_loadContracts() {
try {
const data = this.terminal.fs.readFile('/.contracts/index.json');
@@ -45,18 +354,21 @@ class AgentRegistryModule {
} catch (e) { console.error('Contract save error:', e); }
}
registerAgent(args) {
- const id = args[1] || `agent-${Date.now()}`;
- const capabilities = args.slice(2).length ? args.slice(2) : ['list_files', 'read_file'];
+ const id = args[0] || `agent-${Date.now()}`;
+ const capabilities = args[1] ? args[1].split(',').map(s => s.trim()).filter(Boolean) : ['list_files', 'read_file'];
const agentDir = `agents/${id}`;
if (!this.terminal.fs.readDir('/' + agentDir + '/')) this.terminal.fs.mkdir(agentDir);
this.terminal.fs.writeFile(`${agentDir}/status.json`, JSON.stringify({
id,
status: 'idle',
+ model: null,
capabilities,
registered: new Date().toISOString(),
lastHeartbeat: new Date().toISOString(),
contract: null
}));
+ // Auto-register in A2A scratchpad
+ this.a2aRegister(id, capabilities);
this.terminal.print(`✅ Agent '${id}' registered with ${capabilities.length} capabilities`, 'success');
}
registerContract(args) {
@@ -129,6 +441,8 @@ class AgentRegistryModule {
});
}
unregisterAgent(id) {
+ // Unregister from A2A scratchpad
+ this.a2aUnregister(id);
const ok = this.terminal.fs.deleteFile('agents/' + id);
if (ok) this.terminal.print(`✅ Agent '${id}' unregistered`, 'success');
else this.terminal.print(`❌ Agent '${id}' not found`, 'error');
@@ -156,4 +470,4 @@ class AgentRegistryModule {
this.terminal.fs.writeFile(`agents/${id}/status.json`, JSON.stringify(agent));
return true;
}
-} \ No newline at end of file
+}
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index c5a91c9..3cf8b65 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -57,6 +57,14 @@ buildSystemPrompt() {
return `Current date: ${dateStr}\nCurrent time: ${timeStr}\n\n${saved}`;
}
} catch (e) {}
+ // Check SmartContextModule for active profile system prompt
+ let profilePrompt = '';
+ try {
+ const scm = this.terminal?.moduleManager?.modules?.SmartContextModule;
+ if (scm && scm.activeProfile && scm.contextProfiles[scm.activeProfile]) {
+ profilePrompt = scm.contextProfiles[scm.activeProfile].systemPrompt || '';
+ }
+ } catch (e) {}
const now = new Date();
const dateStr = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0');
const nvfs = this.terminal?.moduleManager?.modules?.NeuralVFSModule;
@@ -67,11 +75,15 @@ buildSystemPrompt() {
if (ageMs > 14400000) nvfsNote = ' |nvfs ' + Math.floor(ageMs/3600000) + 'h stale';
}
} catch (e) {}
- return 'Current date: ' + dateStr + nvfsNote + '\n'
+ let base = 'Current date: ' + dateStr + nvfsNote + '\n'
+ 'VFS→Modules→AI. nvfs-index for commands. Tool→Observe→Answer. nvfs-resolve. Destructive cmds need confirm.\n'
- + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind: vaultmind_query(ep,msg,agent) — Hyperliquid prices/portfolio/chat\n'
+ + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind: vaultmind_connect(network) — connect wallet, vaultmind_status — check session, vaultmind_query(ep,msg,agent) — Hyperliquid prices/portfolio/chat | wallet_lookup(address) — read-only portfolio for any wallet\n'
+ 'web_fetch tool: GET for normal pages (returns title+content+word_count), POST for JSON APIs like Hyperliquid /info (pass method="POST" and body as JSON string).\n'
+ 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, guardrail, wget, tar, secret-* — discoverable via help';
+ if (profilePrompt) {
+ base = profilePrompt + '\n\n' + base;
+ }
+ return base;
}
getToolDefinitions() {
if (!this._toolDefs) this._toolDefs = this._buildToolDefs();
@@ -440,6 +452,43 @@ buildSystemPrompt() {
required: ['endpoint']
}
}
+ }, {
+ type: 'function',
+ function: {
+ name: 'vaultmind_connect',
+ description: 'Connect to VaultMind AI by reading Hyperliquid private key from SecretVault (stored as "hyperliquid_key") and authenticating with the backend. Stores session token in localStorage.',
+ parameters: {
+ type: 'object',
+ properties: {
+ network: { type: 'string', enum: ['testnet', 'mainnet'], description: 'Hyperliquid network (default testnet)' }
+ },
+ required: []
+ }
+ }
+ }, {
+ type: 'function',
+ function: {
+ name: 'vaultmind_status',
+ description: 'Check if VaultMind AI session token exists and is valid in localStorage.',
+ parameters: {
+ type: 'object',
+ properties: {},
+ required: []
+ }
+ }
+ }, {
+ type: 'function',
+ function: {
+ name: 'wallet_lookup',
+ description: 'Read-only Hyperliquid wallet lookup — returns portfolio, positions, and account value for any wallet address. No auth needed.',
+ parameters: {
+ type: 'object',
+ properties: {
+ address: { type: 'string', description: 'Hyperliquid wallet address (0x...)' }
+ },
+ required: ['address']
+ }
+ }
}];
}
getHelpInfo() {
@@ -456,7 +505,9 @@ buildSystemPrompt() {
'ai-model': 'ai-model [model] — Show or set AI model',
'ai-config': 'ai-config [key] [value] — View or set AI loop config (max-turns, max-context, confirm-timeout(0=disabled))',
'mem-debug': 'mem-debug — Show memory breakdown by component',
- 'reset-mem': 'reset-mem — Reset AI memory (logs, context, profile, guardrails) without losing VFS or config'
+ 'reset-mem': 'reset-mem — Reset AI memory (logs, context, profile, guardrails) without losing VFS or config',
+ 'vaultmind-connect': 'vaultmind-connect [testnet|mainnet] — Connect VaultMind wallet using hyperliquid_key from SecretVault',
+ 'vaultmind-status': 'vaultmind-status — Check VaultMind session status',
};
}
registerCommands() {
@@ -481,6 +532,8 @@ buildSystemPrompt() {
this.commands['shadow-rollback'] = () => this.shadowRollback();
this.commands['mem-debug'] = () => this.memDebug();
this.commands['reset-mem'] = () => this.resetMem();
+ this.commands['vaultmind-connect'] = (args) => this._vaultmindConnectCmd(args);
+ this.commands['vaultmind-status'] = () => this._vaultmindStatusCmd();
}
async chat(args) {
const userMessage = args.join(' ');
@@ -1846,6 +1899,8 @@ buildSystemPrompt() {
}
scm.activeProfile = name;
const profile = scm.contextProfiles[name];
+ // Rebuild system prompt to reflect new context profile
+ this.systemPrompt = this.buildSystemPrompt();
return JSON.stringify({ success: true, name, displayName: profile.name, systemPrompt: profile.systemPrompt, temperature: profile.temperature });
}
case 'create_mindmap': {
@@ -1876,6 +1931,53 @@ buildSystemPrompt() {
case 'list_modified_modules':
case 'get_module_status':
return await this.runModuleTool(name, argsStr);
+ case 'vaultmind_connect': {
+ const network = a.network || 'testnet';
+ try {
+ // Read Hyperliquid key from SecretVault
+ const vault = window.webx11_secretVault;
+ if (!vault) return JSON.stringify({ error: 'SecretVault not available — unlock it first with secret-unlock <pin>' });
+ const privateKey = await vault.decrypt('hyperliquid_key');
+ if (!privateKey) return JSON.stringify({ error: 'No hyperliquid_key found in SecretVault. Store it first: secret-set hyperliquid_key 0x...' });
+ // Authenticate with VaultMind backend
+ const resp = await fetch('/vmai-api/auth/wallet', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ private_key: privateKey, account_address: null, network: network })
+ });
+ if (!resp.ok) {
+ const errText = await resp.text();
+ return JSON.stringify({ error: 'VaultMind auth failed (' + resp.status + '): ' + errText });
+ }
+ const data = await resp.json();
+ if (data.token) {
+ localStorage.setItem('vaultmind_token', data.token);
+ return JSON.stringify({ success: true, status: data.status || 'connected', network: data.network || network, message: 'Connected to VaultMind on ' + (data.network || network) });
+ }
+ return JSON.stringify({ error: 'No token returned from VaultMind', data: data });
+ } catch (e) {
+ return JSON.stringify({ error: 'VaultMind connect error: ' + e.message });
+ }
+ }
+ case 'vaultmind_status': {
+ try {
+ const token = localStorage.getItem('vaultmind_token');
+ if (!token) return JSON.stringify({ connected: false, message: 'No VaultMind session token found. Use vaultmind_connect to authenticate.' });
+ // Try to verify by calling a lightweight endpoint
+ try {
+ const resp = await fetch('/vmai-api/portfolio', {
+ headers: { 'Authorization': 'Bearer ' + token }
+ });
+ if (resp.ok) return JSON.stringify({ connected: true, token_exists: true, token_prefix: token.substring(0, 12) + '...', message: 'VaultMind session is active and valid.' });
+ if (resp.status === 401) return JSON.stringify({ connected: false, token_exists: true, message: 'Token exists but is expired or invalid. Reconnect with vaultmind_connect.' });
+ return JSON.stringify({ connected: true, token_exists: true, message: 'Token exists (status: ' + resp.status + ').' });
+ } catch (e) {
+ return JSON.stringify({ connected: true, token_exists: true, message: 'Token exists but backend unreachable (' + e.message + ').' });
+ }
+ } catch (e) {
+ return JSON.stringify({ error: 'VaultMind status error: ' + e.message });
+ }
+ }
case 'vaultmind_query': {
const ep = a.endpoint;
const validEps = ['prices', 'portfolio', 'transactions', 'copy-trade', 'chat'];
@@ -1883,11 +1985,20 @@ buildSystemPrompt() {
return JSON.stringify({ error: 'Invalid endpoint. Valid: ' + validEps.join(', ') });
}
try {
- let url = '/vmai/' + ep;
+ const epMap = { prices: '/vmai/market/prices', portfolio: '/vmai/portfolio', transactions: '/vmai/transactions/summary', 'copy-trade': '/vmai/copy-trading/traders', chat: '/vmai/chat' };
+ let url = epMap[ep];
let opts = { method: 'GET', headers: { 'Accept': 'application/json' } };
+ // Portfolio, transactions, copy-trade need auth token
+ const authEps = ['portfolio', 'transactions', 'copy-trade'];
+ if (authEps.includes(ep)) {
+ const token = localStorage.getItem('vaultmind_token');
+ if (token) opts.headers['Authorization'] = 'Bearer ' + token;
+ }
if (ep === 'chat') {
opts.method = 'POST';
opts.headers['Content-Type'] = 'application/json';
+ const token = localStorage.getItem('vaultmind_token');
+ if (token) opts.headers['Authorization'] = 'Bearer ' + token;
opts.body = JSON.stringify({
message: a.message || 'What is the current market status?',
agent: a.agent || 'market'
@@ -1900,6 +2011,17 @@ buildSystemPrompt() {
return JSON.stringify({ error: 'VaultMind API error: ' + e.message });
}
}
+ case 'wallet_lookup': {
+ const addr = a.address;
+ if (!addr) return JSON.stringify({ error: 'Wallet address required' });
+ try {
+ const resp = await fetch('/wallet/' + encodeURIComponent(addr));
+ const data = await resp.json();
+ return JSON.stringify({ success: true, data });
+ } catch (e) {
+ return JSON.stringify({ error: 'Wallet lookup error: ' + e.message });
+ }
+ }
default:
return JSON.stringify({ error: `Unknown tool: ${name}` });
}
@@ -2226,7 +2348,7 @@ buildSystemPrompt() {
}
}
_getAiConfigKeys() {
- return ['max-turns', 'max-context', 'max-context-tokens', 'confirm-timeout', 'chat-lock-timeout', 'skill-debounce', 'trivial-optimization'];
+ return ['max-turns', 'max-context', 'max-context-tokens', 'confirm-timeout', 'chat-lock-timeout', 'skill-debounce', 'trivial-optimization', 'temperature', 'top-p', 'max-tokens', 'thinking'];
}
aiConfig(args) {
if (args.length === 0) {
@@ -2238,8 +2360,15 @@ buildSystemPrompt() {
this.terminal.print(`confirm-timeout: ${this._confirmTimeoutMs > 0 ? this._confirmTimeoutMs / 1000 + 's' : 'disabled (no auto-deny)'} (default: 120s)`, '');
this.terminal.print(`chat-lock-timeout: ${this._chatLockTimeoutMs / 1000}s (default: 10s)`, '');
this.terminal.print(`skill-debounce: ${this._skillDebounceMs / 60000}min (default: 30min)`, '');
+ const om = this.terminal?.ollamaManager;
+ if (om && om.modelParams) {
+ this.terminal.print(`temperature: ${om.modelParams.temperature} (default: 0.7)`, '');
+ this.terminal.print(`top-p: ${om.modelParams.top_p} (default: 0.9)`, '');
+ this.terminal.print(`max-tokens: ${om.modelParams.max_tokens} (default: 4096)`, '');
+ this.terminal.print(`thinking: ${om.modelParams.thinking} (default: off)`, '');
+ }
this.terminal.print('Usage: ai-config <key> <value>', 'info');
- this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout(0=disabled), chat-lock-timeout, skill-debounce, trivial-optimization', 'info');
+ this.terminal.print('Keys: max-turns, max-context, max-context-tokens, confirm-timeout(0=disabled), chat-lock-timeout, skill-debounce, trivial-optimization, temperature, top-p, max-tokens, thinking', 'info');
return;
}
const key = args[0];
@@ -2272,8 +2401,28 @@ buildSystemPrompt() {
if (this.terminal?.ollamaManager) {
this.terminal.ollamaManager.optimizationEnabled = val === 'on';
}
+ } else if (key === 'temperature') {
+ const n = parseFloat(val);
+ if (isNaN(n) || n < 0 || n > 2) return this.terminal.print('temperature must be 0-2', 'warning');
+ const om = this.terminal?.ollamaManager;
+ if (om && om.modelParams) { om.modelParams.temperature = n; om._saveModelParams(); }
+ } else if (key === 'top-p') {
+ const n = parseFloat(val);
+ if (isNaN(n) || n < 0 || n > 1) return this.terminal.print('top-p must be 0-1', 'warning');
+ const om = this.terminal?.ollamaManager;
+ if (om && om.modelParams) { om.modelParams.top_p = n; om._saveModelParams(); }
+ } else if (key === 'max-tokens') {
+ const n = parseInt(val);
+ if (isNaN(n) || n < 1 || n > 131072) return this.terminal.print('max-tokens must be 1-131072', 'warning');
+ const om = this.terminal?.ollamaManager;
+ if (om && om.modelParams) { om.modelParams.max_tokens = n; om._saveModelParams(); }
+ } else if (key === 'thinking') {
+ const validModes = ['off', 'deepseek_reasoner', 'deepseek'];
+ if (!validModes.includes(val)) return this.terminal.print(`thinking must be one of: ${validModes.join(', ')}`, 'warning');
+ const om = this.terminal?.ollamaManager;
+ if (om && om.modelParams) { om.modelParams.thinking = val; om._saveModelParams(); }
} else {
- return this.terminal.print(`Unknown key: ${key}. Valid: max-turns, max-context, max-context-tokens, confirm-timeout, chat-lock-timeout, skill-debounce, trivial-optimization`, 'warning');
+ return this.terminal.print(`Unknown key: ${key}. Valid: max-turns, max-context, max-context-tokens, confirm-timeout, chat-lock-timeout, skill-debounce, trivial-optimization, temperature, top-p, max-tokens, thinking`, 'warning');
}
// Persist
localStorage.setItem('webx11_ai_config', JSON.stringify({
@@ -2431,4 +2580,37 @@ buildSystemPrompt() {
this.terminal.print('Compression error: ' + e.message, 'error');
});
}
+ async _vaultmindConnectCmd(args) {
+ const network = args[0] || 'testnet';
+ try {
+ const vault = window.webx11_secretVault;
+ if (!vault) { this.terminal.print('SecretVault not available', 'error'); return; }
+ const privateKey = await vault.decrypt('hyperliquid_key');
+ if (!privateKey) { this.terminal.print('No hyperliquid_key in SecretVault. Use: secret-set hyperliquid_key 0x...', 'error'); return; }
+ this.terminal.print('Connecting to VaultMind...', 'info');
+ const resp = await fetch('/vmai-api/auth/wallet', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ private_key: privateKey, account_address: null, network })
+ });
+ if (!resp.ok) { this.terminal.print('Auth failed: ' + resp.status, 'error'); return; }
+ const data = await resp.json();
+ if (data.token) {
+ localStorage.setItem('vaultmind_token', data.token);
+ this.terminal.print('✅ Connected to VaultMind (' + (data.network || network) + ')', 'success');
+ } else {
+ this.terminal.print('No token returned', 'error');
+ }
+ } catch (e) {
+ this.terminal.print('Error: ' + e.message, 'error');
+ }
+ }
+ _vaultmindStatusCmd() {
+ const token = localStorage.getItem('vaultmind_token');
+ if (token) {
+ this.terminal.print('✅ VaultMind session active (token: ' + token.substring(0, 12) + '...)', 'success');
+ } else {
+ this.terminal.print('❌ No VaultMind session. Use: vaultmind-connect [testnet|mainnet]', 'warning');
+ }
+ }
}
diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js
index 309010d..ab8fd99 100644
--- a/modules/core/module-fs.js
+++ b/modules/core/module-fs.js
@@ -403,24 +403,18 @@ class FileSystemModule {
this.commands['session-save'] = (args) => {
if (!args[0]) return this.terminal.print('Usage: session-save <name>', 'warning');
const name = args[0].replace(/[^a-zA-Z0-9_-]/g, '_');
- // Strip system dirs to avoid recursive bloat in snapshots
- const fsRoot = JSON.parse(JSON.stringify(this.terminal.fs.root));
- if (fsRoot && fsRoot.content) {
- delete fsRoot.content['_sessions'];
- delete fsRoot.content['_snapshots'];
- delete fsRoot.content['.trash'];
- }
- const snapshot = {
+ // v2: metadata-only — VFS is persisted independently in localStorage
+ const manifest = {
type: 'session',
+ version: 2,
savedAt: new Date().toISOString(),
- fsRoot: fsRoot,
currentPath: this.terminal.fs.currentPath,
- version: 1
+ terminalLines: this.terminal._historyBuffer ? this.terminal._historyBuffer.slice(-50) : []
};
const filePath = '_sessions/' + name + '.json';
this.terminal.fs.mkdirRecursive('_sessions');
- this.terminal.fs.writeFile(filePath, JSON.stringify(snapshot, null, 2));
- this.terminal.print('Session saved: ' + name, 'success');
+ this.terminal.fs.writeFile(filePath, JSON.stringify(manifest));
+ this.terminal.print('Session saved: ' + name + ' (' + (manifest.terminalLines.length || 0) + ' history lines)', 'success');
};
this.commands['session-load'] = (args) => {
if (!args[0]) return this.terminal.print('Usage: session-load <name>', 'warning');
@@ -429,11 +423,28 @@ class FileSystemModule {
if (content === null) return this.terminal.print('Session not found: ' + args[0], 'error');
try {
const session = JSON.parse(content);
- if (!session || session.type !== 'session' || !session.fsRoot) {
+ if (!session || session.type !== 'session') {
return this.terminal.print('Invalid session file: ' + args[0], 'error');
}
- this.terminal.fs.root = session.fsRoot;
- this.terminal.fs.currentPath = session.currentPath || [];
+ if (session.version === 2) {
+ // v2: metadata-only restore — keep existing VFS, just restore path + terminal history
+ this.terminal.fs.currentPath = session.currentPath || [];
+ if (session.terminalLines && Array.isArray(session.terminalLines) && this.terminal._historyBuffer) {
+ this.terminal._historyBuffer = session.terminalLines.slice(-50);
+ // Re-render last few lines for context
+ this.terminal.outputContainer.innerHTML = '';
+ session.terminalLines.forEach(line => {
+ const text = typeof line === 'string' ? line : (line.l || '');
+ const type = typeof line === 'object' ? (line.t || '') : '';
+ this.terminal.print(text, type);
+ });
+ }
+ } else {
+ // v1: full VFS restore (legacy)
+ if (!session.fsRoot) return this.terminal.print('Invalid session file', 'error');
+ this.terminal.fs.root = session.fsRoot;
+ this.terminal.fs.currentPath = session.currentPath || [];
+ }
this.terminal.fs.saveToStorage();
this.terminal.updatePrompt();
this.terminal.print('Session loaded: ' + args[0] + ' (' + new Date(session.savedAt).toLocaleString() + ')', 'success');
@@ -453,8 +464,13 @@ class FileSystemModule {
const content = JSON.parse(this.terminal.fs.readFile('_sessions/' + f.name));
const label = f.name.replace('.json', '');
const date = content.savedAt ? new Date(content.savedAt).toLocaleString() : 'unknown';
- const fileCount = content.fsRoot ? Object.keys(content.fsRoot.content || {}).length : 0;
- this.terminal.print(' ' + label + ' (' + fileCount + ' items, ' + date + ')');
+ if (content.version === 2) {
+ const lines = content.terminalLines ? content.terminalLines.length : 0;
+ this.terminal.print(' ' + label + ' (' + lines + ' history lines, ' + date + ')');
+ } else {
+ const fileCount = content.fsRoot ? Object.keys(content.fsRoot.content || {}).length : 0;
+ this.terminal.print(' ' + label + ' (' + fileCount + ' items, ' + date + ')');
+ }
} catch (e) {
this.terminal.print(' ' + f.name.replace('.json', '') + ' (corrupt)');
}
@@ -497,12 +513,30 @@ class FileSystemModule {
reader.onload = (evt) => {
try {
const session = JSON.parse(evt.target.result);
- if (!session || session.type !== 'session' || !session.fsRoot) {
+ if (!session || session.type !== 'session') {
this.terminal.print('Invalid session file (not a valid session export)', 'error');
return;
}
- this.terminal.fs.root = session.fsRoot;
- this.terminal.fs.currentPath = session.currentPath || [];
+ if (session.version === 2) {
+ // v2: metadata-only — keep existing VFS, restore path
+ this.terminal.fs.currentPath = session.currentPath || [];
+ if (session.terminalLines && Array.isArray(session.terminalLines) && this.terminal._historyBuffer) {
+ this.terminal._historyBuffer = session.terminalLines.slice(-50);
+ this.terminal.outputContainer.innerHTML = '';
+ session.terminalLines.forEach(line => {
+ const text = typeof line === 'string' ? line : (line.l || '');
+ const type = typeof line === 'object' ? (line.t || '') : '';
+ this.terminal.print(text, type);
+ });
+ }
+ } else {
+ if (!session.fsRoot) {
+ this.terminal.print('Invalid session file (not a valid session export)', 'error');
+ return;
+ }
+ this.terminal.fs.root = session.fsRoot;
+ this.terminal.fs.currentPath = session.currentPath || [];
+ }
this.terminal.fs.saveToStorage();
this.terminal.updatePrompt();
this.terminal.print('Session imported: ' + file.name + ' (' + (session.savedAt ? new Date(session.savedAt).toLocaleString() : 'unknown date') + ')', 'success');
diff --git a/modules/core/module-leases.js b/modules/core/module-leases.js
index b79870c..7489c92 100644
--- a/modules/core/module-leases.js
+++ b/modules/core/module-leases.js
@@ -55,6 +55,19 @@ class LeaseModule {
};
this._leases[lease.id] = lease;
this.terminal.fs.writeFile(`leases/${lease.id}.json`, JSON.stringify(lease));
+ // Post A2A message to lease system agent
+ try {
+ const registry = this.terminal.moduleManager.modules['AgentRegistryModule'];
+ if (registry && typeof registry.a2aPost === 'function') {
+ registry.a2aPost('orchestrator-0', 'lease-system', 'lease.create', 'leases.create', {
+ leaseId: lease.id,
+ workerId,
+ task,
+ duration,
+ expiresAt: lease.expiresAt
+ });
+ }
+ } catch (e) {}
this.terminal.print(`📜 Lease ${lease.id} created for ${workerId} (${duration}s, task: ${task.substring(0, 40)})`, 'success');
return lease;
}
@@ -74,6 +87,18 @@ class LeaseModule {
this._leases[id].status = 'revoked';
this._leases[id].revokedAt = new Date().toISOString();
this.terminal.fs.writeFile(`leases/${id}.json`, JSON.stringify(this._leases[id]));
+ // Post A2A completion message
+ try {
+ const registry = this.terminal.moduleManager.modules['AgentRegistryModule'];
+ if (registry && typeof registry.a2aPost === 'function') {
+ registry.a2aPost('orchestrator-0', 'lease-system', 'lease.terminate', 'leases.terminate', {
+ leaseId: id,
+ status: 'revoked',
+ revokedAt: this._leases[id].revokedAt,
+ workerId: this._leases[id].workerId
+ });
+ }
+ } catch (e) {}
this.terminal.print(`✅ Lease ${id} revoked`, 'warning');
}
renewLease(id, additionalSeconds) {
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index bd02873..cfd8a68 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -4,7 +4,7 @@
// Core module bundle loaded synchronously in main.html.
// This loader only handles version cache-busting, splash, terminal init, and optional ext modules.
- const LATEST_VER = '2026-06-16-v4.5.61';
+ const LATEST_VER = '2026-06-17-v4.5.63';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
diff --git a/modules/core/module-neural-vfs.js b/modules/core/module-neural-vfs.js
index f9bc7f4..32fb0eb 100644
--- a/modules/core/module-neural-vfs.js
+++ b/modules/core/module-neural-vfs.js
@@ -525,7 +525,7 @@ class NeuralVFSModule {
_isSystemFile(path) {
const normalized = path.startsWith('/') ? path : '/' + path;
- const sysPrefixes = ['/.trash', '/.semantic_index', '/.memory_graph', '/_snapshots', '/_sessions', '/logs', '/memory', '/system', '/sys', '/_mods', '/backups'];
+ const sysPrefixes = ['/.trash', '/.semantic_index', '/.memory_graph', '/_snapshots', '/_sessions', '/logs', '/memory', '/system', '/sys', '/_mods', '/backups', '/agents', '/_agents', '/leases'];
if (sysPrefixes.some(p => normalized.startsWith(p))) return true;
if (normalized.includes('/node_modules/')) return true;
return false;
diff --git a/modules/core/module-orchestrator.js b/modules/core/module-orchestrator.js
index bcbd00d..ba43dfd 100644
--- a/modules/core/module-orchestrator.js
+++ b/modules/core/module-orchestrator.js
@@ -12,6 +12,7 @@ class OrchestratorModule {
this._HEARTBEAT_TIMEOUT = 15000;
this._IDLE_TIMEOUT = 120000;
this._RESTART_POLICY = 'never';
+ this._AGENT_DEFAULT_MODEL = null;
this._A2A_POLLING = true;
this._A2A_POLL_INTERVAL = 500;
this._HEARTBEAT_CHECK = 3000;
@@ -34,7 +35,7 @@ class OrchestratorModule {
this._loadConfig();
}
_loadConfig() {
- try { const c = localStorage.getItem('webx11_orchestrator_config'); if (c) { const p = JSON.parse(c); if (p.spawnLimit) this._SPAWN_LIMIT = p.spawnLimit; if (p.heartbeatTimeout) this._HEARTBEAT_TIMEOUT = p.heartbeatTimeout; if (p.idleTimeout) this._IDLE_TIMEOUT = p.idleTimeout; if (p.restartPolicy) this._RESTART_POLICY = p.restartPolicy; if (p.a2aPollInterval) this._A2A_POLL_INTERVAL = p.a2aPollInterval; if (p.heartbeatCheck) this._HEARTBEAT_CHECK = p.heartbeatCheck; if (p.idleSweepInterval) this._IDLE_SWEEP_INTERVAL = p.idleSweepInterval; if (p.deadPurgeTimeout) this._DEAD_PURGE_TIMEOUT = p.deadPurgeTimeout; if (p.consensusSize) this._CONSENSUS_SIZE = p.consensusSize; if (p.consensusStrategy) this._CONSENSUS_STRATEGY = p.consensusStrategy; if (p.consensusTimeout) this._CONSENSUS_TIMEOUT = p.consensusTimeout; if (p.pipelineAutoFix !== undefined) this._PIPELINE_CONFIG.autoFix = p.pipelineAutoFix; if (p.pipelineStageTimeout) this._PIPELINE_CONFIG.stageTimeout = p.pipelineStageTimeout; } } catch (e) {}
+ try { const c = localStorage.getItem('webx11_orchestrator_config'); if (c) { const p = JSON.parse(c); if (p.spawnLimit) this._SPAWN_LIMIT = p.spawnLimit; if (p.heartbeatTimeout) this._HEARTBEAT_TIMEOUT = p.heartbeatTimeout; if (p.idleTimeout) this._IDLE_TIMEOUT = p.idleTimeout; if (p.restartPolicy) this._RESTART_POLICY = p.restartPolicy; if (p.agentDefaultModel !== undefined) this._AGENT_DEFAULT_MODEL = p.agentDefaultModel; if (p.a2aPollInterval) this._A2A_POLL_INTERVAL = p.a2aPollInterval; if (p.heartbeatCheck) this._HEARTBEAT_CHECK = p.heartbeatCheck; if (p.idleSweepInterval) this._IDLE_SWEEP_INTERVAL = p.idleSweepInterval; if (p.deadPurgeTimeout) this._DEAD_PURGE_TIMEOUT = p.deadPurgeTimeout; if (p.consensusSize) this._CONSENSUS_SIZE = p.consensusSize; if (p.consensusStrategy) this._CONSENSUS_STRATEGY = p.consensusStrategy; if (p.consensusTimeout) this._CONSENSUS_TIMEOUT = p.consensusTimeout; if (p.pipelineAutoFix !== undefined) this._PIPELINE_CONFIG.autoFix = p.pipelineAutoFix; if (p.pipelineStageTimeout) this._PIPELINE_CONFIG.stageTimeout = p.pipelineStageTimeout; } } catch (e) {}
}
_saveConfig() {
localStorage.setItem('webx11_orchestrator_config', JSON.stringify({
@@ -42,6 +43,7 @@ class OrchestratorModule {
heartbeatTimeout: this._HEARTBEAT_TIMEOUT,
idleTimeout: this._IDLE_TIMEOUT,
restartPolicy: this._RESTART_POLICY,
+ agentDefaultModel: this._AGENT_DEFAULT_MODEL,
a2aPollInterval: this._A2A_POLL_INTERVAL,
heartbeatCheck: this._HEARTBEAT_CHECK,
idleSweepInterval: this._IDLE_SWEEP_INTERVAL,
@@ -56,7 +58,16 @@ class OrchestratorModule {
registerCommands() {
this.commands['orchestrate'] = (args) => this.delegateTask(args.join(' '));
this.commands['orchestrator-status'] = () => this.showStatus();
- this.commands['orchestrator-spawn'] = (args) => this.spawnAgent(args[0], args.slice(1));
+ this.commands['orchestrator-spawn'] = (args) => {
+ // Parse --model flag before passing remaining as capabilities
+ const modelIdx = args.indexOf('--model');
+ let modelName = null;
+ if (modelIdx !== -1 && args[modelIdx + 1]) {
+ modelName = args[modelIdx + 1];
+ args.splice(modelIdx, 2);
+ }
+ this.spawnAgent(args[0], args.slice(1), modelName);
+ };
this.commands['orchestrator-kill'] = (args) => this.killAgent(args[0]);
this.commands['orchestrator-purge'] = () => this.purgeDeadAgents();
this.commands['orchestrator-config'] = (args) => this.setConfig(args);
@@ -311,7 +322,7 @@ class OrchestratorModule {
_sanitizeId(name) {
return (name || 'worker').replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').substring(0, 32) || 'worker';
}
- spawnAgent(role, capabilities) {
+ spawnAgent(role, capabilities, modelName = null) {
if (this._spawnLock) {
this.terminal.print('⚠️ Spawn in progress — queued', 'warning');
return null;
@@ -327,6 +338,7 @@ class OrchestratorModule {
const agent = {
id,
role: role || 'worker',
+ model: modelName || this._AGENT_DEFAULT_MODEL,
capabilities: capabilities.length ? capabilities : ['list_files', 'read_file', 'web_search'],
createdAt: new Date().toISOString(),
lastHeartbeat: Date.now(),
@@ -344,7 +356,13 @@ class OrchestratorModule {
this.terminal.fs.writeFile(`agents/${id}/status.json`, JSON.stringify(agent));
// Register in agent registry
const registry = this.terminal.moduleManager.modules['AgentRegistryModule'];
- if (registry) registry.registerAgent(['register', id, ...agent.capabilities]);
+ if (registry) {
+ registry.registerAgent(['register', id, ...agent.capabilities]);
+ // A2A Scratchpad registration
+ if (typeof registry.a2aRegister === 'function') {
+ registry.a2aRegister(id, agent.capabilities);
+ }
+ }
this.terminal.print(`🤖 Agent '${id}' spawned (role: ${role || 'worker'}, ${agent.capabilities.length} caps)`, 'success');
return agent;
} finally {
@@ -363,7 +381,13 @@ class OrchestratorModule {
}
// Unregister from registry
const registry = this.terminal.moduleManager.modules['AgentRegistryModule'];
- if (registry) registry.unregisterAgent(id);
+ if (registry) {
+ registry.unregisterAgent(id);
+ // A2A Scratchpad unregister
+ if (typeof registry.a2aUnregister === 'function') {
+ registry.a2aUnregister(id);
+ }
+ }
// Clean up VFS
this.terminal.fs.deleteFile('agents/' + id);
delete this._agents[id];
@@ -552,7 +576,8 @@ class OrchestratorModule {
'consensus-strategy': () => { if (!['majority', 'unanimous', 'weighted'].includes(val)) return 'Must be majority, unanimous, or weighted'; this._CONSENSUS_STRATEGY = val; return `consensus-strategy set to ${val}`; },
'consensus-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 300000) return 'Must be 5000-300000ms'; this._CONSENSUS_TIMEOUT = n; return `consensus-timeout set to ${n}ms`; },
'pipeline-auto-fix': () => { this._PIPELINE_CONFIG.autoFix = val === 'on' || val === 'true'; return `auto-fix ${this._PIPELINE_CONFIG.autoFix ? 'on' : 'off'}`; },
- 'pipeline-stage-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 120000) return 'Must be 5000-120000ms'; this._PIPELINE_CONFIG.stageTimeout = n; return `stage-timeout set to ${n}ms`; }
+ 'pipeline-stage-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 120000) return 'Must be 5000-120000ms'; this._PIPELINE_CONFIG.stageTimeout = n; return `stage-timeout set to ${n}ms`; },
+ 'agent-default-model': () => { this._AGENT_DEFAULT_MODEL = val; this._saveConfig(); return `agent-default-model set to ${val}`; }
};
if (!configMap[key]) {
@@ -1207,11 +1232,13 @@ class OrchestratorModule {
this.terminal.print(` Agents: ${total} (${busy} busy, ${idle} idle, ${sleeping} sleeping, ${failed} failed)`, 'info');
this.terminal.print(` Spawn limit: ${this._SPAWN_LIMIT} (strategy: ${this._SPAWN_STRATEGY})`, 'info');
this.terminal.print(` Default restart: ${this._RESTART_POLICY}`, 'info');
+ this.terminal.print(` Default agent model: ${this._AGENT_DEFAULT_MODEL || 'none (uses main AI model)'}`, 'info');
this.terminal.print(` A2A Messages: ${a2aTotal} (${a2aPending} pending)`, 'info');
Object.values(this._agents).forEach(a => {
const task = a.currentTask ? ` (task: ${a.currentTask.substring(0, 40)})` : '';
+ const modelInfo = a.model ? ` [model: ${a.model}]` : '';
const backoff = a.nextRestartAt && a.nextRestartAt > Date.now() ? ` [restart in ${Math.round((a.nextRestartAt - Date.now()) / 1000)}s]` : '';
- this.terminal.print(` ${a.id} [${a.role}] — ${a.status}${task}${backoff}`, 'info');
+ this.terminal.print(` ${a.id} [${a.role}] — ${a.status}${modelInfo}${task}${backoff}`, 'info');
});
}
diff --git a/modules/core/module-smartctx.js b/modules/core/module-smartctx.js
index 2b4712d..81929f8 100644
--- a/modules/core/module-smartctx.js
+++ b/modules/core/module-smartctx.js
@@ -2,7 +2,7 @@
class SmartContextModule {
constructor() {
this.commands = {}; this.terminal = null;
- this.contextProfiles = this.loadProfiles(); this.activeProfile = null;
+ this.contextProfiles = this.loadProfiles(); this.activeProfile = this.loadActiveProfile();
}
getHelpInfo() {
return {
@@ -22,6 +22,22 @@
saveProfiles() {
localStorage.setItem('webx11_context_profiles', JSON.stringify(this.contextProfiles));
}
+ loadActiveProfile() {
+ try {
+ const saved = localStorage.getItem('webx11_active_context_profile');
+ if (saved && this.contextProfiles[saved]) return saved;
+ } catch (e) {}
+ return null;
+ }
+ saveActiveProfile() {
+ try {
+ if (this.activeProfile) {
+ localStorage.setItem('webx11_active_context_profile', this.activeProfile);
+ } else {
+ localStorage.removeItem('webx11_active_context_profile');
+ }
+ } catch (e) {}
+ }
getDefaultProfiles() {
return {
'coding': { name: 'Coding Assistant', systemPrompt: "You are an expert programmer.", temperature: 0.7 },
@@ -44,6 +60,7 @@
}
if (this.contextProfiles[args[0]]) {
this.activeProfile = args[0];
+ this.saveActiveProfile();
this.terminal.print(`Context set to: ${this.contextProfiles[args[0]].name}`, "success");
} else {
this.terminal.print(`Profile '${args[0]}' not found`, "error");
@@ -83,11 +100,43 @@
this.terminal.print("Usage: smart-ask <your-question>", "warning"); return;
}
let systemPrompt = "You are a helpful assistant.";
+ let temperature = 0.7;
if (this.activeProfile && this.contextProfiles[this.activeProfile]) {
- systemPrompt = this.contextProfiles[this.activeProfile].systemPrompt;
- this.terminal.print(`Using context: ${this.contextProfiles[this.activeProfile].name}`, "info");
+ const profile = this.contextProfiles[this.activeProfile];
+ systemPrompt = profile.systemPrompt;
+ temperature = profile.temperature || 0.7;
+ this.terminal.print(`Using context: ${profile.name} (temp: ${temperature})`, "info");
+ }
+ // Use agentic loop via the AI assistant module for tool-enabled responses
+ const aiModule = this.terminal?.moduleManager?.modules?.WebX11AIAssistantModule;
+ if (aiModule) {
+ // Set temperature on ollamaManager modelParams temporarily
+ const om = this.terminal.ollamaManager;
+ const savedTemp = om.modelParams ? om.modelParams.temperature : undefined;
+ if (om.modelParams) {
+ om.modelParams.temperature = temperature;
+ }
+ try {
+ const messages = [
+ { role: 'system', content: systemPrompt },
+ { role: 'user', content: args.join(' ') }
+ ];
+ const result = await om.chatWithTools(messages, aiModule.getToolDefinitions());
+ if (result && result.message) {
+ this.terminal.printAIResponse(result.message.content || '(no response)');
+ } else if (result && result.error) {
+ this.terminal.print(`⚠️ AI error: ${result.error}`, 'error');
+ }
+ } finally {
+ if (om.modelParams && savedTemp !== undefined) {
+ om.modelParams.temperature = savedTemp;
+ }
+ }
+ } else {
+ // Fallback: simple chat with temperature
+ const om = this.terminal.ollamaManager;
+ const fullPrompt = `${systemPrompt}\n\nUser: ${args.join(' ')}`;
+ await om.chat(fullPrompt);
}
- const fullPrompt = `${systemPrompt}\n\nUser: ${args.join(' ')}`;
- await this.terminal.ollamaManager.chat(fullPrompt);
}
}
diff --git a/modules/core/module-world-model.js b/modules/core/module-world-model.js
index e8e3405..6f0c4e5 100644
--- a/modules/core/module-world-model.js
+++ b/modules/core/module-world-model.js
@@ -96,7 +96,22 @@ class WorldModelModule {
_deriveVersion(mInfo) {
// Single source of truth: the constant set at bootstrap
if (window.NEWOS4_VERSION) return window.NEWOS4_VERSION;
- // No fallback version string — if the constant is missing, report unknown
+ // Fallback: try to extract from script tag ?v= query param or LATEST_VER constant
+ try {
+ const scripts = document.querySelectorAll('script[src*="module-loader"]');
+ for (const s of scripts) {
+ const match = s.src.match(/[?&]v=([^&]+)/);
+ if (match) return match[1];
+ }
+ // Try to find LATEST_VER in any inline script
+ const allScripts = document.querySelectorAll('script:not([src])');
+ for (const s of allScripts) {
+ const match = s.textContent.match(/LATEST_VER\s*=\s*['"]([^'"]+)['"]/);
+ if (match) return match[1];
+ }
+ } catch (e) {
+ console.warn('[WorldModel] Version extraction error:', e.message);
+ }
return 'v?';
}
@@ -106,14 +121,20 @@ class WorldModelModule {
if (!mm) return 0;
// Count registry entries (all known modules) vs loaded
const registry = mm.moduleRegistry || [];
- return registry.length || Object.keys(mm.modules || {}).length + 5; // +5 for expected unloaded
+ return registry.length || Object.keys(mm.modules || {}).length;
} catch (e) {
+ console.warn('[WorldModel] Total modules gather error:', e.message);
return Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0;
}
}
_gatherSystemInfo(fs, moduleCount, version) {
- const healthData = fs.fsReadJSON('/system/metrics/health.json');
+ // Get real health data from ServerMonitorModule's _latencyLog and AI assistant's _errorLog
+ const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule'];
+ const ai = this.terminal?.moduleManager?.modules?.['WebX11AIAssistantModule'];
+ const errorCount = ai?._errorLog?.length || 0;
+ const latencyErrors = sm?._latencyLog ? Object.values(sm._latencyLog).filter(e => e.status === 'error').length : 0;
+ const totalErrors = errorCount + latencyErrors;
return {
version: `newOS4 ${version}`,
modules: moduleCount || Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0,
@@ -122,8 +143,8 @@ class WorldModelModule {
memory: typeof performance !== 'undefined' && performance.memory
? `${(performance.memory.usedJSHeapSize / 1048576).toFixed(1)}MB`
: 'N/A',
- health: healthData?.status || 'OK',
- lastHealthCheck: healthData?.timestamp || null
+ health: totalErrors > 0 ? `⚠️ ${totalErrors} errors` : 'OK',
+ lastHealthCheck: new Date().toISOString()
};
}
@@ -175,6 +196,7 @@ class WorldModelModule {
const count = idx ? Object.keys(idx).length : 0;
return { count, skills: idx ? Object.keys(idx) : [] };
} catch (e) {
+ console.warn('[WorldModel] Skill info gather error:', e.message);
return { count: 0, skills: [] };
}
}
@@ -184,20 +206,21 @@ class WorldModelModule {
if (mg && mg._nodes) {
const nodes = Object.keys(mg._nodes).length;
const edges = (mg._edges || []).length;
- return { nodes, edges, active: true };
+ return { nodes, edges, active: true, fallback: false };
}
// Fallback: count memory files
try {
const memIndex = this.terminal.fs?.fsReadJSON('/memory/index.json');
- return { nodes: memIndex ? Object.keys(memIndex).length : 0, edges: 0, active: false };
+ return { nodes: memIndex ? Object.keys(memIndex).length : 0, edges: 0, active: false, fallback: true };
} catch (e) {
- return { nodes: 0, edges: 0, active: false };
+ console.warn('[WorldModel] Memory info gather error:', e.message);
+ return { nodes: 0, edges: 0, active: false, fallback: true };
}
}
_gatherAgentInfo() {
const orch = this.terminal?.moduleManager?.modules?.['OrchestratorModule'];
- if (!orch || !orch._agents) return { agents: 0, active: 0 };
+ if (!orch || !orch._agents) return { agents: -1, active: -1, error: 'Orchestrator not loaded' };
const agents = Object.keys(orch._agents).length;
const active = Object.values(orch._agents).filter(a => a.status === 'busy').length;
return { agents, active, totalLeases: Object.keys(orch._a2aMessages || {}).length };
@@ -205,13 +228,17 @@ class WorldModelModule {
_gatherTelemetry(fs) {
try {
- const latency = fs.fsReadJSON('/system/metrics/tool-latency.json');
- const errors = fs.fsReadJSON('/system/metrics/error-log.json');
+ // Get real latency data from ServerMonitorModule and errors from AI assistant
+ const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule'];
+ const ai = this.terminal?.moduleManager?.modules?.['WebX11AIAssistantModule'];
+ const latencyDataPoints = sm?._latencyLog ? Object.keys(sm._latencyLog).length : 0;
+ const recentErrors = ai?._errorLog?.length || 0;
return {
- toolLatencyDataPoints: latency ? Object.keys(latency).length : 0,
- recentErrors: errors ? (Array.isArray(errors) ? errors.length : Object.keys(errors).length) : 0,
+ toolLatencyDataPoints: latencyDataPoints,
+ recentErrors: recentErrors,
};
} catch (e) {
+ console.warn('[WorldModel] Telemetry gather error:', e.message);
return { toolLatencyDataPoints: 0, recentErrors: 0 };
}
}
@@ -224,19 +251,20 @@ class WorldModelModule {
const tasks = Object.values(mg._nodes).filter(n => n.type === 'task');
const recentFiles = Object.values(mg._nodes)
.filter(n => n.type === 'file')
- .sort((a, b) => b.access_count - a.access_count)
+ .sort((a, b) => (b.access_count || 0) - (a.access_count || 0))
.slice(0, this._MAX_PROJECTS);
return {
totalTasks: tasks.length,
activeProjects: recentFiles.slice(0, 5).map(f => ({
name: f.label,
- accesses: f.access_count,
+ accesses: f.access_count || 0,
lastModified: f.updated
}))
};
}
return { totalTasks: 0, activeProjects: [] };
} catch (e) {
+ console.warn('[WorldModel] Projects gather error:', e.message);
return { totalTasks: 0, activeProjects: [] };
}
}
@@ -258,7 +286,25 @@ class WorldModelModule {
if (skills && skills.length > 0) {
recent.push(`Skills directory: ${skills.filter(s => s.type === 'file').length} files`);
}
- } catch (e) {}
+ // Check memory directory for recent changes
+ const memDir = fs.readDir('/memory/');
+ if (memDir && memDir.length > 0) {
+ const memFiles = memDir.filter(s => s.type === 'file');
+ if (memFiles.length > 0) {
+ recent.push(`Memory: ${memFiles.length} files in /memory/`);
+ }
+ }
+ // Check system config changes
+ const sysDir = fs.readDir('/system/');
+ if (sysDir && sysDir.length > 0) {
+ const configFiles = sysDir.filter(s => s.type === 'file' && s.name.endsWith('.json'));
+ if (configFiles.length > 0) {
+ recent.push(`System config: ${configFiles.length} config files`);
+ }
+ }
+ } catch (e) {
+ console.warn('[WorldModel] Recent gather error:', e.message);
+ }
return recent;
}
@@ -380,7 +426,10 @@ class WorldModelModule {
getContextForPrompt() {
if (!this._model) return '';
const m = this._model;
- return `[World Model Context]\\nSystem: newOS4 ${m.version} · ${m.modules.loaded} modules · ${m.skills.count} skills · ${m.files.total} files · ${m.agents.agents} agents${m.telemetry.recentErrors > 0 ? ` · ⚠️ ${m.telemetry.recentErrors} errors` : ' · ✅ OK'}${m.modules.modifiedCount > 0 ? `\\nModified modules: ${m.modules.modified.slice(0, 3).join(', ')}${m.modules.modifiedCount > 3 ? ` (+${m.modules.modifiedCount - 3} more)` : ''}` : ''}`;
+ const agentStr = m.agents.error
+ ? 'agents: N/A'
+ : `${m.agents.agents} agents`;
+ return `[World Model Context]\\nSystem: newOS4 ${m.version} · ${m.modules.loaded} modules · ${m.skills.count} skills · ${m.files.total} files · ${agentStr}${m.telemetry.recentErrors > 0 ? ` · ⚠️ ${m.telemetry.recentErrors} errors` : ' · ✅ OK'}${m.modules.modifiedCount > 0 ? `\\nModified modules: ${m.modules.modified.slice(0, 3).join(', ')}${m.modules.modifiedCount > 3 ? ` (+${m.modules.modifiedCount - 3} more)` : ''}` : ''}`;
}
// ─── Persistence ───
diff --git a/modules/core/ollama-manager.js b/modules/core/ollama-manager.js
index 253e319..2f14aa8 100644
--- a/modules/core/ollama-manager.js
+++ b/modules/core/ollama-manager.js
@@ -19,6 +19,8 @@ class OllamaManager {
this.validatorEnabled = localStorage.getItem('webx11_validator_enabled') === 'true';
this.validatorModel = localStorage.getItem('webx11_validator_model') || 'ministral-3:3b-cloud';
this.validatorEndpoint = localStorage.getItem('webx11_validator_endpoint') || '';
+ // --- Model Parameters (temperature, top_p, max_tokens, thinking) ---
+ this.modelParams = this._loadModelParams();
// Build fallback endpoints
this.fallbackEndpoints = [];
const origin = typeof window !== 'undefined' ? window.location.origin : '';
@@ -73,6 +75,28 @@ class OllamaManager {
this._setSecret('webx11_llm_providers', JSON.stringify(this.providers));
}
+ _loadModelParams() {
+ try {
+ const saved = localStorage.getItem('webx11_model_params');
+ if (saved) {
+ const parsed = JSON.parse(saved);
+ return {
+ temperature: parsed.temperature !== undefined ? parsed.temperature : 0.7,
+ top_p: parsed.top_p !== undefined ? parsed.top_p : 0.9,
+ max_tokens: parsed.max_tokens !== undefined ? parsed.max_tokens : 4096,
+ thinking: parsed.thinking || 'off'
+ };
+ }
+ } catch (e) {}
+ return { temperature: 0.7, top_p: 0.9, max_tokens: 4096, thinking: 'off' };
+ }
+
+ _saveModelParams() {
+ try {
+ localStorage.setItem('webx11_model_params', JSON.stringify(this.modelParams));
+ } catch (e) {}
+ }
+
_getSecret(key) {
if (typeof window !== 'undefined' && window.webx11_secretVault && typeof window.webx11_secretVault.decrypt === 'function') {
return window.webx11_secretVault.decrypt(key);
@@ -711,10 +735,21 @@ class OllamaManager {
messages: messages,
stream: false
};
+ // Apply model parameters (user-configured temperature, top_p, max_tokens, thinking)
+ if (this.modelParams) {
+ body.temperature = this.modelParams.temperature;
+ body.top_p = this.modelParams.top_p;
+ body.max_tokens = this.modelParams.max_tokens;
+ }
if (isDeepSeek) {
- body.temperature = 0.3;
- body.max_tokens = 4096;
+ // DeepSeek defaults: user params override hardcoded defaults
+ if (body.temperature === undefined) body.temperature = 0.3;
+ if (body.max_tokens === undefined) body.max_tokens = 4096;
body.stop = ['<|end|>'];
+ // DeepSeek thinking mode
+ if (this.modelParams && this.modelParams.thinking !== 'off') {
+ body.thinking = { type: this.modelParams.thinking };
+ }
}
if (tools && tools.length > 0) {
body.tools = tools;
@@ -800,7 +835,7 @@ class OllamaManager {
return {
message: { role: 'assistant', content: '' },
tool_calls: null,
- error: null
+ error: 'DeepSeek returned empty response after retry — model may be overloaded or context too long'
};
}
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index 504e08d..05aefef 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -10,7 +10,7 @@
this.destructiveCmds = new Set(['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']);
this.systemCmds = new Set(['reboot','shutdown','format','kill','halt','poweroff']);
this.suspiciousFlags = new Set(['--dump','--execute','--eval','--exec','--shell','--script']);
- this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem']);
+ this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register']);
// Load persisted overrides from localStorage
try { const d = localStorage.getItem('webx11_security_destructive'); if(d){JSON.parse(d).forEach(c=>this.destructiveCmds.add(c));} }catch(e){}
try { const s = localStorage.getItem('webx11_security_system'); if(s){JSON.parse(s).forEach(c=>this.systemCmds.add(c));} }catch(e){}
@@ -19,7 +19,7 @@
this._registerBuiltIn();
}
_registerBuiltIn() {
- const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg'];
+ const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register'];
internals.forEach(c => this.registerCommand(c, { category: 'builtin' }));
}
setLevel(n) {
@@ -854,35 +854,14 @@
_autoSaveSession() {
try {
this.fs.mkdirRecursive('_sessions');
- var rootClone;
- try {
- const json = JSON.stringify(this.fs.root);
- if (json.length > 52428800) { // 50MB cap
- rootClone = { type: 'directory', name: '/', content: {}, _truncated: true };
- this.print('⚠ Auto-save skipped (VFS too large)', 'warning');
- return;
- }
- rootClone = JSON.parse(json);
- } catch (e) {
- rootClone = { type: 'directory', name: '/', content: {}, _error: true };
- return;
- }
- if (rootClone && rootClone.content && rootClone.content['_sessions']) {
- delete rootClone.content['_sessions'];
- }
const snapshot = {
type: 'session',
+ version: 2,
savedAt: new Date().toISOString(),
- fsRoot: rootClone,
currentPath: this.fs.currentPath,
- version: 1,
terminalLines: this._historyBuffer.slice(-50)
};
const snapshotJson = JSON.stringify(snapshot);
- if (snapshotJson.length > 52428800) {
- this.print('⚠ Auto-save snapshot too large, skipping', 'warning');
- return;
- }
this.fs.writeFile('_sessions/_auto.json', snapshotJson);
try {
const ctx = localStorage.getItem('webx11_ai_context');
@@ -907,19 +886,40 @@
this.print('↻ Skipped stale auto-session backup (older than last reset)', 'info');
return;
}
- if (data && data.fsRoot) {
+ if (data.version === 2) {
+ // v2: metadata-only restore (VFS is persisted independently in localStorage)
+ this.fs.currentPath = data.currentPath || [];
+ this.fs.saveToStorage();
+ this.updatePrompt();
+ this.print('↻ Auto-restored session from ' + (data.savedAt ? new Date(data.savedAt).toLocaleTimeString() : 'recent'), 'success');
+ // Optionally re-render last few terminal lines
+ if (data.terminalLines && Array.isArray(data.terminalLines)) {
+ data.terminalLines.slice(-10).forEach(line => {
+ const text = typeof line === 'string' ? line : (line.l || '');
+ this.print(text, typeof line === 'object' ? (line.t || '') : '');
+ });
+ }
+ try {
+ const aiCtx = this.fs.readFile('_sessions/_auto-context.json');
+ if (aiCtx) {
+ localStorage.setItem('webx11_ai_context', aiCtx);
+ }
+ } catch (e) {}
+ this.fs.deleteFile('_sessions/_auto.json');
+ this.fs.deleteFile('_sessions/_auto-context.json');
+ } else if (data && data.fsRoot) {
+ // v1: full VFS restore (legacy)
this.fs.root = data.fsRoot;
this.fs.currentPath = data.currentPath || [];
this.fs.saveToStorage();
+ this.updatePrompt();
this.print('↻ Auto-restored session from ' + (data.savedAt ? new Date(data.savedAt).toLocaleTimeString() : 'recent'), 'success');
- // Defer AI context restoration (AI module loads after terminal init)
try {
const aiCtx = this.fs.readFile('_sessions/_auto-context.json');
if (aiCtx) {
localStorage.setItem('webx11_ai_context', aiCtx);
}
} catch (e) {}
- // v4.5.47: delete backup after successful restore so reset-mem doesn't get resurrected
this.fs.deleteFile('_sessions/_auto.json');
this.fs.deleteFile('_sessions/_auto-context.json');
}
@@ -1566,22 +1566,41 @@
return;
}
if (cmd === 'history') {
- const search = args.join(' ').toLowerCase();
const entries = this.history;
if (!entries.length) { this.print('No command history.', 'info'); return; }
- if (search) {
- const filtered = entries.map((c, i) => ({ i, c })).filter(e => e.c.toLowerCase().includes(search));
- if (!filtered.length) { this.print(`No history entries matching: ${search}`, 'info'); return; }
- this.print(`=== History (matching: ${search}) ===`, 'info');
- filtered.forEach(e => this.print(` ${e.i + 1} ${e.c}`));
- } else {
- this.print(`=== Command History (${entries.length} entries) ===`, 'info');
- const start = Math.max(0, entries.length - 50);
- for (let i = start; i < entries.length; i++) {
- this.print(` ${i + 1} ${entries[i]}`);
+ // Flags
+ if (args[0] === '--clear' || args[0] === '-c') {
+ this.history = [];
+ this.historyIndex = 0;
+ localStorage.removeItem('webx11_command_history');
+ this.print('History cleared.', 'success');
+ return;
+ }
+ if (args[0] === '--export' || args[0] === '-e') {
+ try {
+ this.fs.mkdirRecursive('system');
+ this.fs.writeFile('system/history.json', JSON.stringify(entries));
+ this.print('History exported to /system/history.json (' + entries.length + ' entries).', 'success');
+ } catch (e) {
+ this.print('Export failed: ' + e.message, 'error');
}
- if (entries.length > 50) this.print(`... (${entries.length - 50} older entries hidden, use history <search> to find)`);
+ return;
+ }
+ if (args[0] === '--search' || args[0] === '-s') {
+ const term = args.slice(1).join(' ').toLowerCase();
+ if (!term) { this.print('Usage: history --search <term>', 'warning'); return; }
+ const filtered = entries.filter(e => e.toLowerCase().includes(term));
+ if (!filtered.length) { this.print('No matches for: ' + term, 'info'); return; }
+ this.print('=== History (matching: ' + term + ') ===', 'info');
+ filtered.forEach(e => this.print(' ' + e));
+ return;
}
+ let count = 50;
+ if (args[0] === '-n' && args[1]) count = parseInt(args[1]) || 50;
+ const slice = entries.slice(-count);
+ this.print('=== Command History (' + entries.length + ' entries) ===', 'info');
+ slice.forEach(c => this.print(' ' + c));
+ if (entries.length > count) this.print('... (' + (entries.length - count) + ' older, use history --search <term> to find)');
return;
}
for (const modName in this.moduleManager.modules) {
diff --git a/modules/core/virtual-fs.js b/modules/core/virtual-fs.js
index 9d3ea79..827c446 100644
--- a/modules/core/virtual-fs.js
+++ b/modules/core/virtual-fs.js
@@ -6,6 +6,29 @@ class VirtualFileSystem {
this._virtualMounts = null; // lazy-init on first /sys/ access
// Register /sys/ in emit-change exclusion (also added below)
}
+ // ── Auto-purge trash older than 24 hours ──
+ _purgeTrash() {
+ try {
+ const trash = this.root.content && this.root.content['.trash'];
+ if (!trash || !trash.content) return;
+ const now = Date.now();
+ const maxAge = 86400000; // 24 hours
+ let purged = 0;
+ for (const name of Object.keys(trash.content)) {
+ const tsMatch = name.match(/^(\d+)/);
+ if (tsMatch) {
+ const ts = parseInt(tsMatch[1], 10);
+ if (now - ts > maxAge) {
+ delete trash.content[name];
+ purged++;
+ }
+ }
+ }
+ if (purged > 0) {
+ this.saveToStorage();
+ }
+ } catch (e) { /* silent */ }
+ }
// ── Virtual /sys/ filesystem (live runtime state, never persisted) ──
_initVirtualMounts() {
if (this._virtualMounts) return;
@@ -342,6 +365,7 @@ class VirtualFileSystem {
const data = JSON.parse(text);
this.root = data.root || this.root;
this.currentPath = data.currentPath || [];
+ this._purgeTrash();
return true;
} catch (e) {
console.error('FS v2 decompress error:', e);
@@ -826,10 +850,17 @@ class VirtualFileSystem {
checkpoint(name) {
if (!name) name = new Date().toISOString();
const createdAt = new Date().toISOString();
+ // Strip system dirs to prevent fractal bloat
+ const rootClone = JSON.parse(JSON.stringify(this.root));
+ if (rootClone && rootClone.content) {
+ delete rootClone.content['_sessions'];
+ delete rootClone.content['_snapshots'];
+ delete rootClone.content['.trash'];
+ }
const data = {
version: 1,
createdAt,
- root: JSON.parse(JSON.stringify(this.root)),
+ root: rootClone,
currentPath: [...this.currentPath]
};
if (!this.readDir('/_snapshots')) {
@@ -837,11 +868,11 @@ class VirtualFileSystem {
}
this.fsWriteJSON('/_snapshots/checkpoint-' + name + '.json', data);
this.saveToStorage();
- // Auto-manage: keep max 10 checkpoints, delete oldest
+ // Auto-manage: keep max 3 checkpoints, delete oldest
const all = this.listCheckpoints();
- if (all.length > 10) {
+ if (all.length > 3) {
const sorted = all.slice().sort();
- const toDelete = sorted.slice(0, all.length - 10);
+ const toDelete = sorted.slice(0, all.length - 3);
for (const old of toDelete) this.deleteCheckpoint(old);
}
return true;
diff --git a/modules/ext/critical-ext-bundle.js b/modules/ext/critical-ext-bundle.js
index f2cefbb..9def12d 100644
--- a/modules/ext/critical-ext-bundle.js
+++ b/modules/ext/critical-ext-bundle.js
@@ -1,4 +1,4 @@
-// Critical ext module bundle — generated for v4.5.49// Loaded synchronously in main.html before module-loader.js.
+// Critical ext module bundle// Generated from /var/www/webx11.duckdns.org/spaces/newos4/modules/ext
/* === editor.js === */
class EditorModule {
constructor() {
@@ -2361,3 +2361,5 @@ class BusyBoxModule {
if (typeof window.webx11 !== 'undefined' && window.webx11) {
webx11.moduleManager.registerModule(new BusyBoxModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/busybox.js');
}
+
+/* === end critical ext module bundle === */
diff --git a/modules/ext/webdav.js b/modules/ext/webdav.js
index 8096b2e..464db16 100644
--- a/modules/ext/webdav.js
+++ b/modules/ext/webdav.js
@@ -626,11 +626,23 @@ class WebDAVModule {
// Build structured export: root + currentPath + aliases + moduleRegistry
let exportData;
try {
+ // Deep-clone and strip system dirs to prevent fractal bloat
+ let rootForExport;
+ try {
+ rootForExport = JSON.parse(JSON.stringify(self.terminal.fs.root));
+ if (rootForExport && rootForExport.content) {
+ delete rootForExport.content['_sessions'];
+ delete rootForExport.content['_snapshots'];
+ delete rootForExport.content['.trash'];
+ }
+ } catch (e) {
+ rootForExport = self.terminal.fs.root;
+ }
exportData = {
type: 'webx11-backup',
version: 2,
savedAt: new Date().toISOString(),
- root: self.terminal.fs.root,
+ root: rootForExport,
currentPath: self.terminal.fs.currentPath,
aliases: self.terminal.moduleManager.aliases || {},
moduleRegistry: self.terminal.moduleManager.moduleRegistry || []
diff --git a/newos4_whitepaper.md b/newos4_whitepaper.md
index c884ed7..8838ddf 100644
--- a/newos4_whitepaper.md
+++ b/newos4_whitepaper.md
@@ -181,11 +181,11 @@ What the whitepaper originally listed as 4 items has grown into 25+ completed fe
- ✅ Skill-pack core — 24 skills auto-loaded on startup with seed/reload support
- ✅ Neural VFS — semantic concept indexing: nvfs-index, nvfs-resolve, nvfs-tree, nvfs-status, nvfs-save, nvfs-config. concept:// URI resolution in virtual-fs.js with fuzzy matching and cache. Deep indexing via LLM (nvfs-config deep-index on|off, nvfs-index --deep). LLM concepts now survive page refresh — auto-save on deep pass complete.
- ✅ A2A Protocol — agent contract system with schema validation, orchestrator validates tasks before delegation
-- ✅ Self-Optimization Telemetry — system-health command with tool latency, VFS stats, error rates
- ✅ A2A VFS mailbox transport — a2a-send, a2a-list, 500ms polling loop routing request/response/broadcast/heartbeat messages via inbox/outbox
- ✅ Autonomous sub-agent spawning — auto-spawns workers with task-derived role and capabilities (orchestrator-spawn, orchestrator-list, orchestrator-config)
- ✅ Agent lifecycle management — orchestrator-sleep (VFS serialization), orchestrator-wake (restore), auto-reanimate on page reload
- ✅ Agent health monitoring — heartbeat timeouts, idle sweeps, restart policies (on-failure, always, never) with exponential backoff
+- ✅ **A2A Scratchpad** — shared VFS message bus at `/_agents/scratchpad.json`. 8 functions (a2aPost/Poll/Ack/Reject/Register/Unregister/Prune/Status), 6 CLI commands (a2a-post/poll/ack/reject/status/prune), auto-register on spawn, auto-unregister on kill, A2A events on lease create/revoke, auto-prune every 60s, garbage collection for orphaned entries. Replaces per-agent inbox/outbox with a single brokerless scratchpad.
**Built:**
- ✅ Sandboxed execution contexts via WebWorker pool (1-4 workers, VFS snapshots)
diff --git a/session-resume-2026-06-17.md b/session-resume-2026-06-17.md
new file mode 100644
index 0000000..39b2f09
--- /dev/null
+++ b/session-resume-2026-06-17.md
@@ -0,0 +1,67 @@
+# Session Resume — 2026-06-17
+
+## Where We Are
+A2A protocol is **fully operational** (post → file → poll → ack round trip confirmed).
+About to verify multi-agent lifecycle on **fresh session** after kimi-k7's
+`_agents` mkdir fix in `module-agent-registry.js` + `core-bundle.js`.
+
+## Whitepaper Gap Status (as of session end)
+| # | Concept | Status |
+|---|---|---|
+| 1 | A2A Protocol | 🟢 Working (kimi-k7 mkdir fix just landed) |
+| 2 | Neural VFS | 🟡 Partial |
+| 3 | Recursive Self-Opt | 🔴 Missing — telemetry spec written, not implemented |
+| 4 | WPM Golden Config | 🟡 Partial |
+| 5 | Agent Leases | 🟡 Partial (emits A2A events) |
+| 6 | Self-Rewriting Drivers | 🟡 Partial |
+| 7 | Procedural Memory | 🟡 Manual (auto-inject spec written) |
+| 8 | Trinity of Cognition | 🟡 Partial |
+| 9 | No Central Broker | 🟢 Genuinely brokerless |
+| 10 | Cognitive Shell | 🟡 Partial |
+
+**Critical gaps: 1** (Self-Opt). **Spec violations: 0**.
+
+## Next Steps (in order)
+1. **Verify A2A lifecycle** (current task — kimi-k7 just shipped mkdir fix):
+ - `agent-register researcher coder,reader,writer`
+ - `a2a-post researcher request code.search '{"query":"test"}'`
+ - `a2a-poll researcher` → must return the message
+ - `a2a-ack msg_NNN` → must clear from inbox
+2. **Multi-agent spawn test** (was blocked by A2A poll bug, now should pass):
+ - `spawn_agent researcher "test"`
+ - `spawn_agent coder "test"`
+ - `a2a-post researcher request chat "find node release"` from main
+ - worker-side `a2a-poll researcher` should receive
+ - worker-side `a2a-post coder FOUND {...}` should handoff
+ - coder-side `a2a-poll coder` should receive
+ - kill both, verify auto-unregister
+3. **Telemetry hooks** (Gap #3) — depends on A2A being rock solid
+4. **Skill auto-injector runtime** (Gap #7)
+
+## Key Files & Concepts to Remember
+- A2A scratchpad: `/_agents/scratchpad.json` (VFS path)
+- A2A commands: `a2a-status`, `a2a-post`, `a2a-poll`, `a2a-ack`, `a2a-register`
+- Registry: agent capabilities stored as comma-separated strings
+- Two A2A bugs fixed this session:
+ 1. `() => this.a2aPollCommand()` → `(args) => this.a2aPollCommand(args)` in command registration
+ 2. Recipient: `args[0]` first, then auto-detect busy agent, then `"user"` fallback
+ 3. (kimi-k7, this session) `terminal.fs.mkdir('_agents')` before every `fsWriteJSON` in
+ `_ensureScratchpad()` and `_saveScratchpad()` in BOTH `module-agent-registry.js`
+ AND `core-bundle.js`
+
+## Environment
+- newOS4 v4.5.62 · 41 modules · 14 skills · 135 files · 1 agents (was 0 at session start)
+- VaultMind session: ACTIVE
+- Connected to: user wallet (0xb9b3...)
+
+## Architectural Notes
+- A2A is **brokerless** — no central dispatcher, agents coordinate via shared
+ scratchpad file. This was a spec violation before this session, now resolved.
+- Shadow VFS tracks all file writes — useful for telemetry hook design later.
+- Module edits go to `/_mods/` (local), reload to pull from original URL.
+
+## User Preferences Observed
+- Likes terse, well-structured responses with tables
+- Appreciates "real" verification (actual command output, not "should work")
+- Comfortable with delegation to kimi-k7 for routine fixes
+- Prefers depth over breadth when debugging
diff --git a/skills/agent-orchestration.md b/skills/agent-orchestration.md
index 060ec96..e5fbd7f 100644
--- a/skills/agent-orchestration.md
+++ b/skills/agent-orchestration.md
@@ -7,11 +7,21 @@ category: system
## Steps
1. **Agent Registry:**
- agent-list — list all registered agents (ID, type, capabilities, status)
- - agent-register <type> <capabilities> — register a new agent (capabilities: comma-separated like "code,research")
+ - agent-register <name> <capabilities> — register a new agent (capabilities: comma-separated like "code,research")
- agent-unregister <id> — remove an agent
- agent-status <id> — detailed status including uptime, task history
-2. **Leases (time-bound contracts):**
+2. **A2A Scratchpad (agent communication bus):**
+ - a2a-post <to> <type> <capability> <payload...> — post a message to an agent
+ - a2a-poll [agentId] — poll for pending messages (default: current agent or "user")
+ - a2a-ack <msgId> <result...> — acknowledge a message with result
+ - a2a-reject <msgId> <error...> — reject a message with error
+ - a2a-status — show scratchpad stats per agent
+ - a2a-prune — cleanup expired messages
+ - Messages live in /_agents/scratchpad.json (shared VFS file, no central broker)
+ - Agents auto-register on spawn, auto-unregister on kill
+
+3. **Leases (time-bound contracts):**
- lease-create <agent-id> <ttl-seconds> — create a lease with watchdog timer
- lease-list — show active leases with remaining TTL
- lease-revoke <lease-id> — terminate a lease early