Three live-verification surfaces on top of v0.1.0:
1. cmd:screenshot { reason?, frames? } → runtime/viewer.takeScreenshot
- Headless POV render via prismarine-viewer.headless to
state/<host>/screenshots/<ISO>-<reason>.mp4 (1 frame ≈ ~10 KB).
- Lazy-loads the heavy GL stack on first call so the bot doesn't pay
the cost on startup or in TUI-only sessions.
- Returns { ok, path, error } over IPC LOG event.
- Known limitation: needs node-canvas. node-canvas v3 (current npm
default) is incompatible with prismarine-viewer's API; v2 doesn't
build under Node 24 (node-pre-gyp fail). So today the feature is
wired and the IPC contract is stable, but the underlying render
fails fast with "createCanvas is not a function". A future cleanup
can either fork the renderer or pin a Node 20 toolchain.
2. cmd:force-incident { kind?, reason? } → filePostCritique path
- Operator-triggered demo of the critic → proposal → auto-improve →
auto-patch chain. Was previously only observable when the bot
genuinely got stuck. Now a single IPC call exercises the full
loop on demand.
- Verified live 2026-05-26: critic call returned a real, useful
critique ("attack zombie returns done while target is alive →
blocks gather.logs"), proposal landed with all sections including
the Critic block, auto-improve picked it up within 10 s.
3. prismarine-viewer + canvas added to dependencies so npm install
builds the deps once and the IPC surface is always available.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
60 lines
2.6 KiB
JavaScript
60 lines
2.6 KiB
JavaScript
// Shared IPC contract between runtime/bot.js (server) and tui/tui.tsx (client).
|
|
// Frame format: one JSON object per line over a Unix-domain socket.
|
|
// Socket path: state/<server-key>/bot.sock (created by server, removed on shutdown).
|
|
|
|
export const SOCKET_BASENAME = "bot.sock";
|
|
|
|
// Server → client messages.
|
|
export const EVENT_TYPES = Object.freeze({
|
|
STATUS: "status", // periodic snapshot (HP/food/pos/task/connection)
|
|
LOG: "log", // free-form log line { level, source, text }
|
|
CHAT: "chat", // MC chat { from, text, kind: "player" | "system" }
|
|
DEATH: "death", // death event { reason, position }
|
|
ERROR: "error", // recoverable runtime error { source, text }
|
|
ASK_PI_CHUNK: "ask-pi-chunk", // streamed stdout chunk from Pi subprocess
|
|
ASK_PI_DONE: "ask-pi-done", // Pi subprocess exited { code, durationMs }
|
|
HELLO: "hello", // sent on client connect with current snapshot
|
|
PROPOSAL: "proposal", // pending proposal payload { filename, body }
|
|
});
|
|
|
|
// Client → server commands.
|
|
export const COMMAND_TYPES = Object.freeze({
|
|
PAUSE: "cmd:pause", // reflex loop stops ticking; connection stays
|
|
RESUME: "cmd:resume", // reflex loop resumes
|
|
STOP: "cmd:stop", // graceful disconnect + process exit
|
|
CHAT: "cmd:chat", // { text } sent into MC as bot
|
|
ASK_PI: "cmd:ask-pi", // { prompt } spawn `pi -p` and stream output
|
|
SNAPSHOT: "cmd:snapshot", // request immediate STATUS event
|
|
PROPOSAL_LATEST: "cmd:proposal-latest", // request latest pending proposal
|
|
PROPOSAL_APPROVE: "cmd:proposal-approve", // { filename } move to approved/
|
|
RUN_SKILL: "cmd:run-skill", // { skillId, args? } dispatch a skill once (operator ground-truth probes)
|
|
CONV_SAY: "cmd:conv-say", // { topic, text, intent?, position? } append turn to a multi-agent topic
|
|
CONV_RECENT: "cmd:conv-recent", // { topic, n? } read last n turns
|
|
CONV_LIST: "cmd:conv-list", // list active conversation topics
|
|
SCREENSHOT: "cmd:screenshot", // { reason?, frames? } headless POV render to state/screenshots/
|
|
FORCE_INCIDENT: "cmd:force-incident", // { kind?, reason? } emulate a stuck-incident to demo critic + auto-improve pipeline
|
|
});
|
|
|
|
export function encodeFrame(obj) {
|
|
return JSON.stringify(obj) + "\n";
|
|
}
|
|
|
|
// Stateful line splitter — instance per socket.
|
|
export function createLineParser(onObject) {
|
|
let buf = "";
|
|
return (chunk) => {
|
|
buf += chunk.toString("utf8");
|
|
let idx;
|
|
while ((idx = buf.indexOf("\n")) !== -1) {
|
|
const line = buf.slice(0, idx).trim();
|
|
buf = buf.slice(idx + 1);
|
|
if (!line) continue;
|
|
try {
|
|
onObject(JSON.parse(line));
|
|
} catch (e) {
|
|
onObject({ __parseError: true, raw: line, err: String(e) });
|
|
}
|
|
}
|
|
};
|
|
}
|