feat(runtime): hybrid script reflex + Ink TUI + Pi-on-demand escalation (#4)

* fix(mindcraft-skills): hard timeout on every skill call

mc_avoid_enemies (and 7 other tools) wrapped only in safeCall without a
withTimeout. When mindcraft's underlying pathfinder/pvp goal couldn't be
satisfied, the call never resolved — the Pi tick loop blocked forever.
Observed live: mc_avoid_enemies pending >10 minutes after one mc_observe.

safeCall now takes timeoutMs (default 30s) and wraps withTimeout itself,
so every tool gets a hard ceiling. Per-tool overrides:
  - goToPosition / goToNearestBlock: 120s / 90s (unchanged from before)
  - defendSelf / avoidEnemies: 45s
  - stay: secs*1000 + 10s
  - craft / consume / pickup / place: 30s
  - equip: 15s
collectBlock still uses its bespoke per-iter 75s loop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 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>

---------

Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #4.
This commit is contained in:
Yuriy Mayatnikov
2026-05-25 16:04:48 +03:00
committed by GitHub
co-authored by Claude Opus 4.7 mayatnikov
parent 989ddf335e
commit 1e3b36a9a1
16 changed files with 2464 additions and 75 deletions
+44
View File
@@ -0,0 +1,44 @@
// Spawn `pi -p "<prompt>"` as a one-shot subprocess and stream stdout/stderr
// to the caller. Used for headless escalation: the bot's reflex / TUI can ask
// Pi a single question without keeping a long-lived TUI session open.
import { spawn } from "node:child_process";
import { info, warn } from "./log.js";
// Resolve `pi` lazily — user has it on PATH via vite-plus shim.
const PI_BIN = process.env.PI_BIN || "pi";
export function askPi({ prompt, onChunk, onDone, cwd, signal }) {
const startedAt = Date.now();
info("pi-bridge", `spawning pi -p (${prompt.length} chars)`);
const child = spawn(PI_BIN, ["-p", prompt], {
cwd: cwd || process.cwd(),
env: { ...process.env, CI: "1" },
stdio: ["ignore", "pipe", "pipe"],
signal,
});
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
onChunk?.({ stream: "stdout", text: chunk });
});
child.stderr.on("data", (chunk) => {
onChunk?.({ stream: "stderr", text: chunk });
});
child.on("error", (err) => {
warn("pi-bridge", `pi subprocess failed to start: ${err?.message ?? err}`);
onDone?.({ code: -1, durationMs: Date.now() - startedAt, error: String(err) });
});
child.on("exit", (code) => {
const durationMs = Date.now() - startedAt;
info("pi-bridge", `pi exited code=${code} after ${durationMs}ms`);
onDone?.({ code: code ?? -1, durationMs });
});
return child;
}