feat(runtime): script-driven reflex daemon + Ink TUI dashboard
Pure-Pi runtime had three failure modes in practice: - slow: 20-60s per decision because LLM was in the hot path - expensive: every tick (defend, eat, idle) paid for a reasoning pass - invisible: required tmux capture-pane to know what the bot was doing New runtime/ layer is a long-running Node daemon that owns the MC connection, ticks a priority-ordered reflex chain (defend > eat > sleep > idle) with NO LLM in the hot path, and exposes status + commands over a Unix-socket IPC. tui/ is an Ink dashboard that attaches over IPC and can detach freely — multiple TUI clients can connect at once. Pi/Codex are still available, but as on-demand escalation: TUI hotkey 'a' spawns `pi -p "<prompt>"` as a subprocess and streams stdout into the dashboard. The self-improvement loop (proposals → operator approval → Pi-driven patch → hot reload) is documented in docs/runtime.md but not yet wired. Reflex bodies are stubs today — they log decisions but don't drive Mineflayer actions yet. The priority chain, IPC contract, and TUI are fully working; subsequent commits will fill in defend/eat/sleep bodies and wire automatic escalation. Run with `npm run bot` + `npm run tui`. Pi-only fallback stays at `npm run agent`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
// Thin client wrapper around the Unix-socket IPC server. Emits events as a
|
||||
// regular EventEmitter so the React layer can subscribe.
|
||||
|
||||
import net from "node:net";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { createLineParser, encodeFrame } from "../runtime/ipc-protocol.js";
|
||||
import { socketPath } from "../runtime/config.js";
|
||||
|
||||
export function createIpcClient() {
|
||||
const ee = new EventEmitter();
|
||||
let socket = null;
|
||||
let reconnectTimer = null;
|
||||
let shuttingDown = false;
|
||||
|
||||
function connect() {
|
||||
if (socket || shuttingDown) return;
|
||||
socket = net.createConnection(socketPath);
|
||||
const parser = createLineParser((obj) => {
|
||||
if (obj.__parseError) {
|
||||
ee.emit("error", new Error(`bad frame: ${obj.err}`));
|
||||
return;
|
||||
}
|
||||
ee.emit("frame", obj);
|
||||
if (obj.type) ee.emit(obj.type, obj.payload, obj.ts);
|
||||
});
|
||||
socket.setEncoding("utf8");
|
||||
socket.on("connect", () => ee.emit("connected"));
|
||||
socket.on("data", parser);
|
||||
socket.on("close", () => {
|
||||
socket = null;
|
||||
ee.emit("disconnected");
|
||||
if (!shuttingDown) scheduleReconnect();
|
||||
});
|
||||
socket.on("error", (err) => {
|
||||
ee.emit("ipc-error", err);
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
if (reconnectTimer) return;
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectTimer = null;
|
||||
connect();
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function send(type, payload) {
|
||||
if (!socket || socket.destroyed) return false;
|
||||
try {
|
||||
socket.write(encodeFrame({ type, payload }));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
shuttingDown = true;
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||
socket?.destroy();
|
||||
}
|
||||
|
||||
connect();
|
||||
|
||||
return Object.assign(ee, { send, close });
|
||||
}
|
||||
Reference in New Issue
Block a user