Compare commits

..

4 Commits

Author SHA1 Message Date
2ba379f0b5 fix: restore Fleet API polling and formatBytes in connectMemPalace 2026-04-15 15:11:17 +00:00
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
6 changed files with 135 additions and 592 deletions

View File

@@ -1,49 +0,0 @@
#!/usr/bin/env bash
# Commit-msg hook: warn about shell injection risks
# Install: cp .githooks/commit-msg .git/hooks/commit-msg && chmod +x .git/hooks/commit-msg
COMMIT_MSG_FILE="$1"
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
# Check for dangerous patterns
DANGEROUS_PATTERNS=(
'`' # Backticks
'$(' # Command substitution
'${' # Variable expansion
'\\`' # Escaped backticks
'eval ' # eval command
'exec ' # exec command
'source ' # source command
'|' # Pipe
'&&' # AND operator
'||' # OR operator
';' # Semicolon
'>' # Redirect
'<' # Input redirect
)
FOUND_ISSUES=()
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
if echo "$COMMIT_MSG" | grep -q "$pattern"; then
FOUND_ISSUES+=("$pattern")
fi
done
if [ ${#FOUND_ISSUES[@]} -gt 0 ]; then
echo "⚠️ WARNING: Commit message contains potentially dangerous patterns:"
for issue in "${FOUND_ISSUES[@]}"; do
echo " - $issue"
done
echo ""
echo "This could trigger shell execution during git operations."
echo ""
echo "Safe alternatives:"
echo " 1. Use: git commit -F <file> instead of git commit -m"
echo " 2. Escape special characters in commit messages"
echo " 3. Use the safe_commit() function from bin/safe_commit.py"
echo ""
echo "To proceed anyway, use: git commit --no-verify"
exit 1
fi
exit 0

62
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 { MemoryInspect } from './nexus/components/memory-inspect.js';
import { MemoryPulse } from './nexus/components/memory-pulse.js';
import { ReasoningTrace } from './nexus/components/reasoning-trace.js';
// ═══════════════════════════════════════════
// 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 = {
colors: {
primary: 0x4af0c0,
@@ -685,7 +680,7 @@ function updateGOFAI(delta, elapsed) {
// Simulate calibration update
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);
@@ -763,7 +758,6 @@ async function init() {
SpatialAudio.bindSpatialMemory(SpatialMemory);
MemoryInspect.init({ onNavigate: _navigateToMemory });
MemoryPulse.init(SpatialMemory);
ReasoningTrace.init();
updateLoad(90);
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) {
const group = new THREE.Group();
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 if (data.type === 'portals_reload') {
console.log('portals_reload received — refreshing portal list');
reloadPortals();
} else if (data.type && data.type.startsWith('evennia.')) {
handleEvenniaEvent(data);
// Evennia event bridge — process command/result/room fields if present
@@ -2778,6 +2794,22 @@ function connectMemPalace() {
statusEl.style.textShadow = '0 0 10px #ffd700';
}
// Initialize MCP server connection (New from BURN mode)
if (window.Claude && window.Claude.mcp) {
console.log('Initializing MemPalace MCP server...');
window.Claude.mcp.add('mempalace', {
init: () => ({ status: 'active', version: '3.0.0' }),
search: (query) => 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);
})
});
}
// Fleet API base — same host, port 7771, or override via ?mempalace=host:port
const params = new URLSearchParams(window.location.search);
const override = params.get('mempalace');
@@ -2785,7 +2817,7 @@ function connectMemPalace() {
? `http://${override}`
: `${window.location.protocol}//${window.location.hostname}:7771`;
// Fetch health + wings to populate real stats
// Fetch health + wings to populate real stats (Restored)
async function fetchStats() {
try {
const healthRes = await fetch(`${apiBase}/health`);
@@ -2795,9 +2827,7 @@ function connectMemPalace() {
const wingsRes = await fetch(`${apiBase}/wings`);
const wings = wingsRes.ok ? await wingsRes.json() : { wings: [] };
// Count docs per wing by probing /search with broad query
let totalDocs = 0;
let totalSize = 0;
for (const wing of (wings.wings || [])) {
try {
const sr = await fetch(`${apiBase}/search?q=*&wing=${wing}&n=1`);
@@ -2809,9 +2839,8 @@ function connectMemPalace() {
}
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
const aaakSize = totalDocs * 64;
// Update UI with real data
if (statusEl) {
statusEl.textContent = 'MEMPALACE ACTIVE';
statusEl.style.color = '#4af0c0';
@@ -2821,28 +2850,24 @@ function connectMemPalace() {
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) {
if (statusEl && !window.Claude?.mcp) {
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;
@@ -2851,6 +2876,7 @@ function formatBytes(bytes) {
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + sizes[i];
}
function mineMemPalaceContent() {
const logs = document.getElementById('mem-palace-logs');
const now = new Date().toLocaleTimeString();

View File

@@ -1,307 +0,0 @@
#!/usr/bin/env python3
"""
Safe commit message handler to prevent shell injection.
Issue #1430: [IMPROVEMENT] memory_mine.py ran during git commit — shell injection from commit message
This script provides safe ways to commit with code-containing messages.
"""
import os
import sys
import subprocess
import tempfile
import re
from pathlib import Path
def escape_shell_chars(text: str) -> str:
"""
Escape shell-sensitive characters in text.
This prevents shell injection when text is used in shell commands.
"""
# Characters that need escaping in shell
shell_chars = ['$', '`', '\\', '"', "'", '!', '(', ')', '{', '}', '[', ']',
'|', '&', ';', '<', '>', '*', '?', '~', '#']
escaped = text
for char in shell_chars:
escaped = escaped.replace(char, '\\' + char)
return escaped
def safe_commit_message(message: str) -> str:
"""
Create a safe commit message by escaping shell-sensitive characters.
Args:
message: The commit message
Returns:
Escaped commit message safe for shell use
"""
return escape_shell_chars(message)
def commit_with_file(message: str, branch: str = None) -> bool:
"""
Commit using a temporary file instead of -m flag.
This is the safest way to commit messages containing code or special characters.
Args:
message: The commit message
branch: Optional branch name
Returns:
True if successful, False otherwise
"""
# Create temporary file for commit message
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
f.write(message)
temp_file = f.name
try:
# Build git command
cmd = ['git', 'commit', '-F', temp_file]
if branch:
cmd.extend(['-b', branch])
# Execute git commit
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print(f"✅ Committed successfully using file: {temp_file}")
return True
else:
print(f"❌ Commit failed: {result.stderr}")
return False
finally:
# Clean up temporary file
try:
os.unlink(temp_file)
except:
pass
def commit_safe(message: str, use_file: bool = True) -> bool:
"""
Safely commit with a message.
Args:
message: The commit message
use_file: If True, use -F <file> instead of -m
Returns:
True if successful, False otherwise
"""
if use_file:
return commit_with_file(message)
else:
# Use escaped message with -m flag
escaped_message = safe_commit_message(message)
cmd = ['git', 'commit', '-m', escaped_message]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print("✅ Committed successfully with escaped message")
return True
else:
print(f"❌ Commit failed: {result.stderr}")
return False
def check_commit_message_safety(message: str) -> dict:
"""
Check if a commit message contains potentially dangerous patterns.
Args:
message: The commit message to check
Returns:
Dictionary with safety analysis
"""
dangerous_patterns = [
(r'`[^`]*`', 'Backticks (shell command substitution)'),
(r'\$\([^)]*\)', 'Command substitution $(...)'),
(r'\$\{[^}]*\}', 'Variable expansion ${...}'),
(r'\\`', 'Escaped backticks'),
(r'eval\s+', 'eval command'),
(r'exec\s+', 'exec command'),
(r'source\s+', 'source command'),
(r'\.\s+', 'dot command'),
(r'\|\s*', 'Pipe character'),
(r'&&', 'AND operator'),
(r'\|\|', 'OR operator'),
(r';', 'Semicolon (command separator)'),
(r'>', 'Redirect operator'),
(r'<', 'Input redirect'),
]
findings = []
for pattern, description in dangerous_patterns:
matches = re.findall(pattern, message)
if matches:
findings.append({
'pattern': pattern,
'description': description,
'matches': matches,
'count': len(matches)
})
return {
'safe': len(findings) == 0,
'findings': findings,
'recommendation': 'Use commit_with_file() or escape_shell_chars()' if findings else 'Message appears safe'
}
def create_commit_hook_guard():
"""
Create a commit-msg hook that warns about dangerous patterns.
"""
hook_content = '''#!/usr/bin/env bash
# Commit-msg hook: warn about shell injection risks
# Install: cp .githooks/commit-msg .git/hooks/commit-msg && chmod +x .git/hooks/commit-msg
COMMIT_MSG_FILE="$1"
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
# Check for dangerous patterns
DANGEROUS_PATTERNS=(
'`' # Backticks
'$(' # Command substitution
'${' # Variable expansion
'\\`' # Escaped backticks
'eval ' # eval command
'exec ' # exec command
'source ' # source command
'|' # Pipe
'&&' # AND operator
'||' # OR operator
';' # Semicolon
'>' # Redirect
'<' # Input redirect
)
FOUND_ISSUES=()
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
if echo "$COMMIT_MSG" | grep -q "$pattern"; then
FOUND_ISSUES+=("$pattern")
fi
done
if [ ${#FOUND_ISSUES[@]} -gt 0 ]; then
echo "⚠️ WARNING: Commit message contains potentially dangerous patterns:"
for issue in "${FOUND_ISSUES[@]}"; do
echo " - $issue"
done
echo ""
echo "This could trigger shell execution during git operations."
echo ""
echo "Safe alternatives:"
echo " 1. Use: git commit -F <file> instead of git commit -m"
echo " 2. Escape special characters in commit messages"
echo " 3. Use the safe_commit() function from bin/safe_commit.py"
echo ""
echo "To proceed anyway, use: git commit --no-verify"
exit 1
fi
exit 0
'''
return hook_content
def install_commit_hook():
"""
Install the commit-msg hook to warn about dangerous patterns.
"""
hook_path = Path('.git/hooks/commit-msg')
hook_content = create_commit_hook_guard()
# Check if .git/hooks exists
if not hook_path.parent.exists():
print("❌ .git/hooks directory not found")
return False
# Write hook
with open(hook_path, 'w') as f:
f.write(hook_content)
# Make executable
os.chmod(hook_path, 0o755)
print(f"✅ Installed commit-msg hook to {hook_path}")
return True
def main():
"""Main entry point for safe commit tool."""
import argparse
parser = argparse.ArgumentParser(description="Safe commit message handling")
parser.add_argument("--message", "-m", help="Commit message")
parser.add_argument("--file", "-F", help="Read commit message from file")
parser.add_argument("--check", action="store_true", help="Check message safety")
parser.add_argument("--install-hook", action="store_true", help="Install commit-msg hook")
parser.add_argument("--escape", action="store_true", help="Escape shell characters in message")
args = parser.parse_args()
if args.install_hook:
if install_commit_hook():
print("Commit hook installed successfully")
else:
print("Failed to install commit hook")
sys.exit(1)
return
if args.check:
if args.message:
safety = check_commit_message_safety(args.message)
print(f"Message safety check:")
print(f" Safe: {safety['safe']}")
print(f" Recommendation: {safety['recommendation']}")
if safety['findings']:
print(f" Findings:")
for finding in safety['findings']:
print(f" - {finding['description']}: {finding['count']} matches")
else:
print("Please provide a message with --message")
return
if args.escape:
if args.message:
escaped = safe_commit_message(args.message)
print(f"Escaped message:")
print(escaped)
else:
print("Please provide a message with --message")
return
if args.file:
# Read message from file
with open(args.file, 'r') as f:
message = f.read()
commit_with_file(message)
elif args.message:
# Check if message has dangerous patterns
safety = check_commit_message_safety(args.message)
if safety['safe']:
commit_safe(args.message, use_file=False)
else:
print("⚠️ Message contains potentially dangerous patterns")
print("Using file-based commit for safety...")
commit_safe(args.message, use_file=True)
else:
parser.print_help()
if __name__ == "__main__":
main()

View File

@@ -1,159 +0,0 @@
# Safe Commit Practices
**Issue:** #1430 - [IMPROVEMENT] memory_mine.py ran during git commit — shell injection from commit message
## Problem
During commit for #1124, the commit message contained Python code examples that triggered shell execution of memory_mine.py. The backtick-wrapped code in the commit message was interpreted by the shell during git commit processing.
This is a potential vector for unintended code execution.
## Safe Commit Methods
### 1. Use `git commit -F <file>` (Recommended)
The safest way to commit messages containing code or special characters:
```bash
# Create a file with your commit message
echo "Fix: implement memory_mine.py with backtick example
Example: \`python3 bin/memory_mine.py --days 7\`
This commit adds memory mining functionality." > /tmp/commit-msg.txt
# Commit using the file
git commit -F /tmp/commit-msg.txt
```
### 2. Use the Safe Commit Tool
```bash
# Safe commit with automatic escaping
python3 bin/safe_commit.py -m "Fix: implement memory_mine.py with backtick example"
# Safe commit using file
python3 bin/safe_commit.py -F /tmp/commit-msg.txt
# Check if a message is safe
python3 bin/safe_commit.py --check -m "Example: \`python3 bin/memory_mine.py\`"
```
### 3. Escape Shell Characters Manually
If you must use `git commit -m`, escape special characters:
```bash
# Escape backticks and other shell characters
git commit -m "Fix: implement memory_mine.py with backtick example
Example: \\`python3 bin/memory_mine.py --days 7\\`
This commit adds memory mining functionality."
```
## Dangerous Patterns to Avoid
The following patterns in commit messages can trigger shell execution:
- **Backticks**: `` `command` `` → Executes command
- **Command substitution**: `$(command)` → Executes command
- **Variable expansion**: `${variable}` → Expands variable
- **Pipes**: `command1 | command2` → Pipes output
- **Operators**: `&&`, `||`, `;` → Command chaining
- **Redirects**: `>`, `<` → File operations
## Installation
### Install the Commit Hook
To automatically warn about dangerous patterns:
```bash
# Install the commit-msg hook
python3 bin/safe_commit.py --install-hook
# Or manually
cp .githooks/commit-msg .git/hooks/commit-msg
chmod +x .git/hooks/commit-msg
```
### Configure Git Hooks Path
If using the `.githooks` directory:
```bash
git config core.hooksPath .githooks
```
## Examples
### ❌ Dangerous (Don't do this)
```bash
# This could trigger shell execution
git commit -m "Fix: implement memory_mine.py
Example: \`python3 bin/memory_mine.py --days 7\`
This mines sessions into MemPalace."
```
### ✅ Safe (Do this instead)
```bash
# Method 1: Use file
echo "Fix: implement memory_mine.py
Example: \`python3 bin/memory_mine.py --days 7\`
This mines sessions into MemPalace." > /tmp/commit-msg.txt
git commit -F /tmp/commit-msg.txt
# Method 2: Use safe commit tool
python3 bin/safe_commit.py -m "Fix: implement memory_mine.py
Example: \`python3 bin/memory_mine.py --days 7\`
This mines sessions into MemPalace."
# Method 3: Escape manually
git commit -m "Fix: implement memory_mine.py
Example: \\`python3 bin/memory_mine.py --days 7\\`
This mines sessions into MemPalace."
```
## What Happened in Issue #1430
During commit for #1124, a commit message contained:
```
Example: \`python3 bin/memory_mine.py --days 7\`
```
The backticks were interpreted by the shell during git commit processing, causing memory_mine.py to execute. While the outcome was positive (26 sessions mined), this is a security risk.
## Prevention
1. **Always use `git commit -F <file>`** for messages containing code
2. **Install the commit-msg hook** to warn about dangerous patterns
3. **Use the safe_commit.py tool** for automatic escaping
4. **Document safe patterns** in team guidelines
## Related Issues
- **Issue #1430:** This improvement
- **Issue #1124:** Original issue that triggered the problem
## Files
- `bin/safe_commit.py` - Safe commit tool
- `.githooks/commit-msg` - Commit hook (to be installed)
- `docs/safe-commit-practices.md` - This documentation
## Conclusion
Shell injection in commit messages is a real security risk. By using safe commit practices, we can prevent unintended code execution while still allowing code examples in commit messages.
**Remember:** When in doubt, use `git commit -F <file>` instead of `git commit -m`.

View File

@@ -129,13 +129,21 @@
"type": "harness",
"params": {
"mode": "creative"
}
},
"action_label": "Enter Workshop"
},
"agents_present": [
"timmy",
"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",
@@ -157,12 +165,20 @@
"type": "harness",
"params": {
"mode": "read"
}
},
"action_label": "Enter Archive"
},
"agents_present": [
"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",
@@ -184,10 +200,18 @@
"type": "harness",
"params": {
"mode": "meditation"
}
},
"action_label": "Enter Chapel"
},
"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",
@@ -209,13 +233,21 @@
"type": "harness",
"params": {
"mode": "social"
}
},
"action_label": "Enter Courtyard"
},
"agents_present": [
"timmy",
"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",
@@ -237,59 +269,17 @@
"type": "harness",
"params": {
"mode": "transit"
}
},
"action_label": "Enter Gate"
},
"agents_present": [],
"interaction_ready": false
},
{
"id": "playground",
"name": "Sound Playground",
"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",
"interaction_ready": false,
"portal_type": "harness",
"world_category": "meta",
"environment": "local",
"access_mode": "open",
"readiness_state": "online",
"readiness_steps": {
"prototype": {
"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
"telemetry_source": "hermes-harness:gate",
"owner": "Timmy"
}
]

View File

@@ -7,6 +7,7 @@ the body (Evennia/Morrowind), and the visualization surface.
import asyncio
import json
import logging
import os
import signal
import sys
from typing import Set
@@ -17,6 +18,8 @@ import websockets
# Configuration
PORT = 8765
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.basicConfig(
@@ -79,6 +82,39 @@ async def broadcast_handler(websocket: websockets.WebSocketServerProtocol):
clients.discard(websocket)
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():
"""Main server loop with graceful shutdown."""
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):
logger.info("Gateway is ready and listening.")
watcher_task = asyncio.create_task(watch_portals(stop))
await stop
watcher_task.cancel()
try:
await watcher_task
except asyncio.CancelledError:
pass
logger.info("Shutting down Nexus WS gateway...")
# Close any remaining client connections (handlers may have already cleaned up)