Files
Mortdecai/oracle-bot/server.js
T
Seth 5b28002001 0.6.0 training session: Oracle Bot, RL combat, Mind's Eye, multilingual pipeline
Major changes from this session:

Training:
- 0.6.0 training running: 9B on steel141 3090 Ti, 27B on rented H100 NVL
- 7,256 merged training examples (up from 3,183)
- New training data: failure modes (85), midloop messaging (27),
  prompt injection defense (29), personality (32), gold from quarantine
  bank (232), new tool examples (30), claude's own experience (10)
- All training data RCON-validated at 100% pass rate
- Bake-off: gemma3:27b 66%, qwen3.5:27b 61%, translategemma:27b 56%

Oracle Bot (Mind's Eye):
- Invisible spectator bot (mineflayer) streams world state via WebSocket
- HTML5 Canvas frontend at mind.mortdec.ai
- Real-time tool trace visualization with expandable entries
- Streaming model tokens during inference
- Gateway integration: fire-and-forget POST /trace on every tool call

Reinforcement Learning:
- Gymnasium environment wrapping mineflayer bot (minecraft_env.py)
- PPO training via Stable Baselines3 (10K param policy network)
- Behavioral cloning pretraining (97.5% accuracy on expert policy)
- Infinite training loop with auto-restart and checkpoint resume
- Bot learns combat, survival, navigation from raw experience

Bot Army:
- 8-soldier marching formation with autonomous combat
- Combat bots using mineflayer-pvp, pathfinder, armor-manager
- Multilingual prayer bots via translategemma:27b (18 languages)
- Frame-based AI architecture: LLM planner + reactive micro-scripts

Infrastructure:
- Fixed mattpc.sethpc.xyz billing gateway (API key + player list parser)
- Billing gateway now tracks all LAN traffic (LAN auto-auth)
- Gateway fallback for empty god-mode responses
- Updated mortdec.ai landing page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 20:22:50 -04:00

287 lines
9.5 KiB
JavaScript

'use strict';
/**
* server.js — OracleBot entrypoint.
*
* Express HTTP server + WebSocket server providing:
* POST /trace — receive tool traces from the gateway
* POST /command — accept follow/scan commands
* GET /health — liveness / status check
* Static files from public/
* WS /ws — live push to dashboard clients
*/
const http = require('http');
const path = require('path');
const express = require('express');
const { WebSocketServer } = require('ws');
const OracleBot = require('./bot.js');
const WorldState = require('./world-state.js');
// ──────────────────────────────────────────────
// Config
// ──────────────────────────────────────────────
const PORT = parseInt(process.env.PORT || '3333', 10);
const HEARTBEAT_INTERVAL_MS = 5000; // idle pulse
const ACTIVE_INTERVAL_MS = 1000; // active world-snapshot push
const MAX_WS_TOTAL = 100; // global WebSocket cap
const MAX_WS_PER_IP = 5; // per-IP WebSocket cap
// ──────────────────────────────────────────────
// Instances
// ──────────────────────────────────────────────
const bot = new OracleBot();
const state = new WorldState(bot);
// ──────────────────────────────────────────────
// Express app
// ──────────────────────────────────────────────
const app = express();
app.use(express.json({ limit: '64kb' }));
app.use(express.static(path.join(__dirname, 'public')));
// ── POST /trace ──────────────────────────────
app.post('/trace', (req, res) => {
const body = req.body;
if (!body || typeof body.tool !== 'string') {
return res.status(400).json({ ok: false, error: 'Missing required field: tool' });
}
const prevPlayer = state.activePlayer;
state.onTrace(body);
// Broadcast: trace event, mode change, then world snapshot
broadcast(state.buildTraceEvent(body));
broadcast(state.buildModeChange());
const snapshot = state.buildWorldSnapshot();
if (snapshot) broadcast(snapshot);
// If the active player changed, follow them
if (state.activePlayer && state.activePlayer !== prevPlayer) {
try {
bot.followPlayer(state.activePlayer);
} catch (err) {
// Bot may not be spawned yet — non-fatal
console.warn(`[server] followPlayer failed: ${err.message}`);
}
}
return res.json({ ok: true });
});
// ── POST /stream ──────────────────────────────
app.post('/stream', (req, res) => {
const data = req.body;
if (!data) return res.status(400).json({ ok: false });
// Broadcast stream token to all WebSocket clients
broadcast({
v: 1,
type: 'stream',
token: data.token || '',
accumulated: data.accumulated || '',
step: data.step,
mode: data.mode || state.mode,
player: data.player || state.activePlayer,
ts: Date.now(),
});
res.json({ ok: true });
});
// ── POST /command ─────────────────────────────
app.post('/command', (req, res) => {
const { action, target, center, radius } = req.body || {};
if (!action) {
return res.status(400).json({ ok: false, error: 'Missing required field: action' });
}
if (action === 'follow') {
if (!target || typeof target !== 'string') {
return res.status(400).json({ ok: false, error: 'follow requires target (player name)' });
}
try {
bot.followPlayer(target);
return res.json({ ok: true, action: 'follow', target });
} catch (err) {
return res.status(503).json({ ok: false, error: err.message });
}
}
if (action === 'scan') {
if (!center || typeof center.x !== 'number' || typeof center.y !== 'number' || typeof center.z !== 'number') {
return res.status(400).json({ ok: false, error: 'scan requires center {x, y, z}' });
}
const r = typeof radius === 'number' ? radius : 16;
try {
const blocks = bot.scanArea(center.x, center.y, center.z, r);
const entities = bot.getNearbyEntities(center.x, center.y, center.z, r);
return res.json({ ok: true, action: 'scan', center, radius: r, blocks, entities });
} catch (err) {
return res.status(503).json({ ok: false, error: err.message });
}
}
return res.status(400).json({ ok: false, error: `Unknown action: ${action}` });
});
// ── GET /health ───────────────────────────────
app.get('/health', (_req, res) => {
res.json({
ok: true,
botConnected: bot._spawned === true,
wsClients: wss.clients.size,
mode: state.mode,
activePlayer: state.activePlayer,
});
});
// ──────────────────────────────────────────────
// HTTP server
// ──────────────────────────────────────────────
const server = http.createServer(app);
// ──────────────────────────────────────────────
// WebSocket server
// ──────────────────────────────────────────────
const wss = new WebSocketServer({ server, path: '/ws' });
// Track connections per IP: Map<ip, number>
const ipCounts = new Map();
wss.on('connection', (ws, req) => {
// Resolve client IP (trust x-forwarded-for from Caddy)
const forwarded = req.headers['x-forwarded-for'];
const ip = forwarded ? forwarded.split(',')[0].trim() : (req.socket.remoteAddress || 'unknown');
// Enforce total cap
if (wss.clients.size > MAX_WS_TOTAL) {
ws.close(1008, 'Server at capacity');
return;
}
// Enforce per-IP cap
const currentCount = ipCounts.get(ip) || 0;
if (currentCount >= MAX_WS_PER_IP) {
ws.close(1008, 'Too many connections from your IP');
return;
}
ipCounts.set(ip, currentCount + 1);
// Store IP on the socket for cleanup
ws._remoteIp = ip;
// Send current state immediately on connect
ws.send(JSON.stringify(state.buildHeartbeat()));
// If we're not idle, also send current mode
if (state.mode !== 'idle') {
ws.send(JSON.stringify(state.buildModeChange()));
}
ws.on('close', () => {
const count = ipCounts.get(ws._remoteIp) || 1;
if (count <= 1) {
ipCounts.delete(ws._remoteIp);
} else {
ipCounts.set(ws._remoteIp, count - 1);
}
});
});
/**
* Broadcast a message object to all connected WebSocket clients.
* @param {Object} msg
*/
function broadcast(msg) {
if (wss.clients.size === 0) return;
const payload = JSON.stringify(msg);
for (const client of wss.clients) {
if (client.readyState === client.OPEN) {
client.send(payload);
}
}
}
// ──────────────────────────────────────────────
// Periodic updates
// ──────────────────────────────────────────────
// Heartbeat every 5s (idle mode pulse + idle timeout check)
setInterval(() => {
if (wss.clients.size === 0) return;
// Check if session has timed out
const wentIdle = state.checkIdle();
if (wentIdle) {
broadcast(state.buildModeChange());
}
if (state.mode === 'idle') {
broadcast(state.buildHeartbeat());
}
}, HEARTBEAT_INTERVAL_MS);
// World snapshot every 1s (active mode only)
setInterval(() => {
if (wss.clients.size === 0) return;
if (state.mode === 'idle') return;
const snapshot = state.buildWorldSnapshot();
if (snapshot) broadcast(snapshot);
}, ACTIVE_INTERVAL_MS);
// ──────────────────────────────────────────────
// Bot event handlers
// ──────────────────────────────────────────────
bot.on('connected', () => {
broadcast(state.buildStatus(true));
});
bot.on('spawned', () => {
// Spectator is handled by bot.js internally (self-sets via chat after 2s delay)
});
bot.on('disconnected', () => {
broadcast(state.buildStatus(false));
});
bot.on('playerJoined', () => {
broadcast(state.buildHeartbeat());
});
bot.on('playerLeft', () => {
broadcast(state.buildHeartbeat());
});
// ──────────────────────────────────────────────
// Startup
// ──────────────────────────────────────────────
bot.connect();
server.listen(PORT, () => {
console.log(`[oracle-bot] HTTP + WS server listening on port ${PORT}`);
console.log(`[oracle-bot] WebSocket path: ws://localhost:${PORT}/ws`);
console.log(`[oracle-bot] Health check: http://localhost:${PORT}/health`);
console.log(`[oracle-bot] Connecting bot to Minecraft at ${bot.host}:${bot.port} ...`);
});