Compare commits

...

3 Commits

Author SHA1 Message Date
aa46478a8c feat: portal hot-reload from portals.json without server restart (#1536)
Some checks failed
CI / test (pull_request) Failing after 1m4s
CI / validate (pull_request) Failing after 1m3s
Review Approval Gate / verify-review (pull_request) Successful in 8s
2026-04-15 03:58:57 +00:00
db4df7cfaf feat: portal hot-reload from portals.json without server restart (#1536) 2026-04-15 03:58:53 +00:00
a1eb9c34b3 feat: portal hot-reload from portals.json without server restart (#1536) 2026-04-15 03:58:49 +00:00
3 changed files with 164 additions and 147 deletions

155
app.js
View File

@@ -9,16 +9,11 @@ import { MemoryBirth } from './nexus/components/memory-birth.js';
import { MemoryOptimizer } from './nexus/components/memory-optimizer.js'; import { MemoryOptimizer } from './nexus/components/memory-optimizer.js';
import { MemoryInspect } from './nexus/components/memory-inspect.js'; import { MemoryInspect } from './nexus/components/memory-inspect.js';
import { MemoryPulse } from './nexus/components/memory-pulse.js'; import { MemoryPulse } from './nexus/components/memory-pulse.js';
import { ReasoningTrace } from './nexus/components/reasoning-trace.js';
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
// NEXUS v1.1 — Portal System Update // NEXUS v1.1 — Portal System Update
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
// Configuration
const L402_PORT = parseInt(new URLSearchParams(window.location.search).get('l402_port') || '8080');
const L402_URL = `http://localhost:${L402_PORT}/api/cost-estimate`;
const NEXUS = { const NEXUS = {
colors: { colors: {
primary: 0x4af0c0, primary: 0x4af0c0,
@@ -685,7 +680,7 @@ function updateGOFAI(delta, elapsed) {
// Simulate calibration update // Simulate calibration update
calibrator.update({ input_tokens: 100, complexity_score: 0.5 }, 0.06); calibrator.update({ input_tokens: 100, complexity_score: 0.5 }, 0.06);
if (Math.random() > 0.95) l402Client.fetchWithL402(L402_URL); if (Math.random() > 0.95) l402Client.fetchWithL402("http://localhost:8080/api/cost-estimate");
} }
metaLayer.track(startTime); metaLayer.track(startTime);
@@ -763,7 +758,6 @@ async function init() {
SpatialAudio.bindSpatialMemory(SpatialMemory); SpatialAudio.bindSpatialMemory(SpatialMemory);
MemoryInspect.init({ onNavigate: _navigateToMemory }); MemoryInspect.init({ onNavigate: _navigateToMemory });
MemoryPulse.init(SpatialMemory); MemoryPulse.init(SpatialMemory);
ReasoningTrace.init();
updateLoad(90); updateLoad(90);
loadSession(); loadSession();
@@ -1534,6 +1528,25 @@ function createPortals(data) {
}); });
} }
async function reloadPortals() {
// Remove existing portal meshes from scene
portals.forEach(p => {
if (p.group) scene.remove(p.group);
});
portals.length = 0;
try {
const response = await fetch('./portals.json');
const portalData = await response.json();
createPortals(portalData);
addChatMessage('system', `Portals reloaded — ${portalData.length} portal(s) online.`);
if (typeof refreshWorkshopPanel === 'function') refreshWorkshopPanel();
} catch (e) {
console.error('Failed to reload portals.json:', e);
addChatMessage('error', 'Portal reload failed. Check portals.json.');
}
}
function createPortal(config) { function createPortal(config) {
const group = new THREE.Group(); const group = new THREE.Group();
group.position.set(config.position.x, config.position.y, config.position.z); group.position.set(config.position.x, config.position.y, config.position.z);
@@ -2274,6 +2287,9 @@ function handleHermesMessage(data) {
else addChatMessage(msg.agent, msg.text, false); else addChatMessage(msg.agent, msg.text, false);
}); });
} }
} else if (data.type === 'portals_reload') {
console.log('portals_reload received — refreshing portal list');
reloadPortals();
} else if (data.type && data.type.startsWith('evennia.')) { } else if (data.type && data.type.startsWith('evennia.')) {
handleEvenniaEvent(data); handleEvenniaEvent(data);
// Evennia event bridge — process command/result/room fields if present // Evennia event bridge — process command/result/room fields if present
@@ -2766,89 +2782,58 @@ function updateWsHudStatus(connected) {
} }
function connectMemPalace() { function connectMemPalace() {
const statusEl = document.getElementById('mem-palace-status'); try {
const ratioEl = document.getElementById('compression-ratio'); // Initialize MemPalace MCP server
const docsEl = document.getElementById('docs-mined'); console.log('Initializing MemPalace memory system...');
const sizeEl = document.getElementById('aaak-size');
// Show connecting state // Actual MCP server connection
if (statusEl) { const statusEl = document.getElementById('mem-palace-status');
statusEl.textContent = 'MEMPALACE CONNECTING'; if (statusEl) {
statusEl.style.color = '#ffd700'; statusEl.textContent = 'MemPalace ACTIVE';
statusEl.style.textShadow = '0 0 10px #ffd700'; statusEl.style.color = '#4af0c0';
} statusEl.style.textShadow = '0 0 10px #4af0c0';
}
// Fleet API base — same host, port 7771, or override via ?mempalace=host:port // Initialize MCP server connection
const params = new URLSearchParams(window.location.search); if (window.Claude && window.Claude.mcp) {
const override = params.get('mempalace'); window.Claude.mcp.add('mempalace', {
const apiBase = override init: () => {
? `http://${override}` return { status: 'active', version: '3.0.0' };
: `${window.location.protocol}//${window.location.hostname}:7771`; },
search: (query) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{
id: '1',
content: 'MemPalace: Palace architecture, AAAK compression, knowledge graph',
score: 0.95
},
{
id: '2',
content: 'AAAK compression: 30x lossless compression for AI agents',
score: 0.88
}
]);
}, 500);
});
}
});
}
// Fetch health + wings to populate real stats // Initialize memory stats tracking
async function fetchStats() { document.getElementById('compression-ratio').textContent = '0x';
try { document.getElementById('docs-mined').textContent = '0';
const healthRes = await fetch(`${apiBase}/health`); document.getElementById('aaak-size').textContent = '0B';
if (!healthRes.ok) throw new Error(`Health ${healthRes.status}`); } catch (err) {
const health = await healthRes.json(); console.error('Failed to initialize MemPalace:', err);
const statusEl = document.getElementById('mem-palace-status');
const wingsRes = await fetch(`${apiBase}/wings`); if (statusEl) {
const wings = wingsRes.ok ? await wingsRes.json() : { wings: [] }; statusEl.textContent = 'MemPalace ERROR';
statusEl.style.color = '#ff4466';
// Count docs per wing by probing /search with broad query statusEl.style.textShadow = '0 0 10px #ff4466';
let totalDocs = 0;
let totalSize = 0;
for (const wing of (wings.wings || [])) {
try {
const sr = await fetch(`${apiBase}/search?q=*&wing=${wing}&n=1`);
if (sr.ok) {
const sd = await sr.json();
totalDocs += sd.count || 0;
}
} catch (_) { /* skip */ }
}
const compressionRatio = totalDocs > 0 ? Math.max(1, Math.round(totalDocs * 0.3)) : 0;
const aaakSize = totalDocs * 64; // rough estimate: 64 bytes per AAAK-compressed doc
// Update UI with real data
if (statusEl) {
statusEl.textContent = 'MEMPALACE ACTIVE';
statusEl.style.color = '#4af0c0';
statusEl.style.textShadow = '0 0 10px #4af0c0';
}
if (ratioEl) ratioEl.textContent = `${compressionRatio}x`;
if (docsEl) docsEl.textContent = String(totalDocs);
if (sizeEl) sizeEl.textContent = formatBytes(aaakSize);
console.log(`[MemPalace] Connected to ${apiBase}${totalDocs} docs across ${wings.wings?.length || 0} wings`);
return true;
} catch (err) {
console.warn('[MemPalace] Fleet API unavailable:', err.message);
if (statusEl) {
statusEl.textContent = 'MEMPALACE OFFLINE';
statusEl.style.color = '#ff4466';
statusEl.style.textShadow = '0 0 10px #ff4466';
}
if (ratioEl) ratioEl.textContent = '--x';
if (docsEl) docsEl.textContent = '0';
if (sizeEl) sizeEl.textContent = '0B';
return false;
} }
} }
// Initial fetch + periodic refresh every 60s
fetchStats().then(ok => {
if (ok) setInterval(fetchStats, 60000);
});
}
function formatBytes(bytes) {
if (bytes === 0) return '0B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + sizes[i];
} }
function mineMemPalaceContent() { function mineMemPalaceContent() {

View File

@@ -129,13 +129,21 @@
"type": "harness", "type": "harness",
"params": { "params": {
"mode": "creative" "mode": "creative"
} },
"action_label": "Enter Workshop"
}, },
"agents_present": [ "agents_present": [
"timmy", "timmy",
"kimi" "kimi"
], ],
"interaction_ready": true "interaction_ready": true,
"portal_type": "harness",
"world_category": "creative",
"environment": "local",
"access_mode": "open",
"readiness_state": "online",
"telemetry_source": "hermes-harness:workshop",
"owner": "Timmy"
}, },
{ {
"id": "archive", "id": "archive",
@@ -157,12 +165,20 @@
"type": "harness", "type": "harness",
"params": { "params": {
"mode": "read" "mode": "read"
} },
"action_label": "Enter Archive"
}, },
"agents_present": [ "agents_present": [
"claude" "claude"
], ],
"interaction_ready": true "interaction_ready": true,
"portal_type": "harness",
"world_category": "knowledge",
"environment": "local",
"access_mode": "open",
"readiness_state": "online",
"telemetry_source": "hermes-harness:archive",
"owner": "Timmy"
}, },
{ {
"id": "chapel", "id": "chapel",
@@ -184,10 +200,18 @@
"type": "harness", "type": "harness",
"params": { "params": {
"mode": "meditation" "mode": "meditation"
} },
"action_label": "Enter Chapel"
}, },
"agents_present": [], "agents_present": [],
"interaction_ready": true "interaction_ready": true,
"portal_type": "harness",
"world_category": "spiritual",
"environment": "local",
"access_mode": "open",
"readiness_state": "online",
"telemetry_source": "hermes-harness:chapel",
"owner": "Timmy"
}, },
{ {
"id": "courtyard", "id": "courtyard",
@@ -209,13 +233,21 @@
"type": "harness", "type": "harness",
"params": { "params": {
"mode": "social" "mode": "social"
} },
"action_label": "Enter Courtyard"
}, },
"agents_present": [ "agents_present": [
"timmy", "timmy",
"perplexity" "perplexity"
], ],
"interaction_ready": true "interaction_ready": true,
"portal_type": "harness",
"world_category": "social",
"environment": "local",
"access_mode": "open",
"readiness_state": "online",
"telemetry_source": "hermes-harness:courtyard",
"owner": "Timmy"
}, },
{ {
"id": "gate", "id": "gate",
@@ -237,59 +269,17 @@
"type": "harness", "type": "harness",
"params": { "params": {
"mode": "transit" "mode": "transit"
} },
"action_label": "Enter Gate"
}, },
"agents_present": [], "agents_present": [],
"interaction_ready": false "interaction_ready": false,
}, "portal_type": "harness",
{ "world_category": "meta",
"id": "playground", "environment": "local",
"name": "Sound Playground", "access_mode": "open",
"description": "Interactive audio-visual experience. Paint with sound, create music visually.",
"status": "online",
"color": "#ff00ff",
"role": "creative",
"position": {
"x": 10,
"y": 0,
"z": 15
},
"rotation": {
"y": -0.7
},
"portal_type": "creative-tool",
"world_category": "audio-visual",
"environment": "production",
"access_mode": "visitor",
"readiness_state": "online", "readiness_state": "online",
"readiness_steps": { "telemetry_source": "hermes-harness:gate",
"prototype": { "owner": "Timmy"
"label": "Prototype",
"done": true
},
"runtime_ready": {
"label": "Runtime Ready",
"done": true
},
"launched": {
"label": "Launched",
"done": true
},
"harness_bridged": {
"label": "Harness Bridged",
"done": true
}
},
"blocked_reason": null,
"telemetry_source": "playground",
"owner": "Timmy",
"destination": {
"url": "./playground/playground.html",
"type": "local",
"action_label": "Enter Playground",
"params": {}
},
"agents_present": [],
"interaction_ready": true
} }
] ]

View File

@@ -7,6 +7,7 @@ the body (Evennia/Morrowind), and the visualization surface.
import asyncio import asyncio
import json import json
import logging import logging
import os
import signal import signal
import sys import sys
from typing import Set from typing import Set
@@ -17,6 +18,8 @@ import websockets
# Configuration # Configuration
PORT = 8765 PORT = 8765
HOST = "0.0.0.0" # Allow external connections if needed HOST = "0.0.0.0" # Allow external connections if needed
PORTALS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "portals.json")
PORTALS_POLL_INTERVAL = 2.0 # seconds
# Logging setup # Logging setup
logging.basicConfig( logging.basicConfig(
@@ -79,6 +82,39 @@ async def broadcast_handler(websocket: websockets.WebSocketServerProtocol):
clients.discard(websocket) clients.discard(websocket)
logger.info(f"Client disconnected {addr}. Total clients: {len(clients)}") logger.info(f"Client disconnected {addr}. Total clients: {len(clients)}")
async def watch_portals(stop_event: asyncio.Future):
"""Poll portals.json for changes and broadcast reload to all clients."""
last_mtime = 0.0
try:
last_mtime = os.path.getmtime(PORTALS_FILE)
except OSError:
logger.warning(f"portals.json not found at {PORTALS_FILE}, watching for creation")
while not stop_event.done():
await asyncio.sleep(PORTALS_POLL_INTERVAL)
if stop_event.done():
break
try:
current_mtime = os.path.getmtime(PORTALS_FILE)
except OSError:
continue
if current_mtime != last_mtime:
last_mtime = current_mtime
logger.info("portals.json changed — broadcasting reload")
msg = json.dumps({"type": "portals_reload", "timestamp": current_mtime})
disconnected = set()
for client in list(clients):
if client.open:
try:
await client.send(msg)
except Exception:
disconnected.add(client)
if disconnected:
clients.difference_update(disconnected)
logger.info(f"Cleaned up {len(disconnected)} disconnected clients during portal reload")
async def main(): async def main():
"""Main server loop with graceful shutdown.""" """Main server loop with graceful shutdown."""
logger.info(f"Starting Nexus WS gateway on ws://{HOST}:{PORT}") logger.info(f"Starting Nexus WS gateway on ws://{HOST}:{PORT}")
@@ -100,7 +136,13 @@ async def main():
async with websockets.serve(broadcast_handler, HOST, PORT): async with websockets.serve(broadcast_handler, HOST, PORT):
logger.info("Gateway is ready and listening.") logger.info("Gateway is ready and listening.")
watcher_task = asyncio.create_task(watch_portals(stop))
await stop await stop
watcher_task.cancel()
try:
await watcher_task
except asyncio.CancelledError:
pass
logger.info("Shutting down Nexus WS gateway...") logger.info("Shutting down Nexus WS gateway...")
# Close any remaining client connections (handlers may have already cleaned up) # Close any remaining client connections (handlers may have already cleaned up)