Files
pepa-pi-bot/docs/runtime.md
T
mayatnikovandClaude Opus 4.7 c9794e0bbf 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>
2026-05-25 15:59:33 +03:00

9.7 KiB
Raw Blame History

Runtime — hybrid script + LLM-on-demand

Status: active. This is the recommended way to run pepa-pi-bot since 2026-05-25. The pure Pi runtime (pi from repo root) still works and is documented as a fallback at the bottom of this file.

Why a hybrid runtime?

The original design ran every tick inside Pi — the LLM saw the world, picked one tool, executed it, looped. That gave full self-extension out of the box, but had three problems in practice:

  1. Slow. A "look around → defend yourself" round-trip took 2060 seconds because the LLM was in the hot path.
  2. Expensive. Hostile mob at 4 m? Cost of evasion = one full reasoning pass. Hungry? Same. Idle? Same.
  3. Invisible. With Pi as the only frontend, you had to tmux capture-pane to know what the bot was doing.

The hybrid runtime splits the bot into a script-driven layer that handles fast, well-understood things on its own, and a Pi (or Codex) headless escalation that's only invoked when the script gets stuck or needs to write new code for itself.

Architecture

┌────────────────────────────────────────────────────────────────────┐
│  operator                                                          │
│  ├── repo edits (.env, skills/, runtime/)                          │
│  ├── TUI (Ink) — see status, send chat, press [a] to escalate      │
│  └── (future) Telegram bridge                                      │
└─────────────┬────────────────────────────────────────────┬─────────┘
              │ Unix socket (newline-JSON)                 │ git
              ▼                                            ▼
┌────────────────────────────────────────────────────────────────────┐
│  runtime/bot.js — single long-running Node process                 │
│                                                                    │
│  ┌──────────────────┐  ┌──────────────────────┐  ┌────────────────┐│
│  │ Mineflayer       │  │ Reflex loop          │  │ IPC server     ││
│  │ - MC TCP         │  │ - tick every N sec   │  │ - Unix socket  ││
│  │ - AuthMe handler │◀─│ - priority order:    │─▶│ - broadcasts   ││
│  │ - chat / events  │  │   defend > eat       │  │   status/log/  ││
│  │                  │  │   > sleep > current  │  │   chat events  ││
│  │                  │  │   > idle             │  │ - accepts      ││
│  │                  │  │ - NO LLM in path     │  │   commands     ││
│  └──────────────────┘  └─────────┬────────────┘  └────────────────┘│
│                                  │                                 │
│                                  ▼ on stuck / new scenario         │
│                        ┌──────────────────────┐                    │
│                        │ pi-bridge.js         │                    │
│                        │ spawn `pi -p`        │                    │
│                        │ stream stdout to IPC │                    │
│                        └──────────────────────┘                    │
└────────────────────────────────────────────────────────────────────┘
              │
              │ TCP 25565
              ▼
       Minecraft server

The bot is one process. The TUI is a separate process you can connect and disconnect at will — the bot keeps running. Multiple TUI clients can attach to the same bot simultaneously.

Quickstart

# Once
cd ~/Projects/pepa-pi-bot
npm install

# Terminal 1 — the bot daemon
npm run bot
# Logs go to stdout AND state/<host>/logs/<YYYY-MM-DD>.log

# Terminal 2 — the dashboard
npm run tui

The TUI auto-reconnects to the bot if you restart it. Press q to leave the TUI; the bot is unaffected.

TUI hotkeys

Key Effect
p Pause / resume the reflex loop (MC connection stays).
s Stop the bot process gracefully (disconnect + cleanup + exit).
r Force-broadcast a status snapshot now.
c Enter chat mode — type a message, Enter sends it into MC chat.
a Enter ask-Pi mode — type a prompt, Enter spawns pi -p and streams output into the Pi panel.
q Quit TUI only. Bot keeps running.

Enter submits, blank submit cancels.

What the reflex loop does today

All reflex bodies are currently stubs — they log decisions but don't yet call into Mineflayer actions. The priority chain is wired:

  1. defendReflex — closest hostile within 6 m → log + decision (next: actually attack / flee).
  2. eatReflex — food ≤ 16 → log (next: equip food, eat).
  3. sleepReflex — night + bed in inventory → log (next: bot.sleep).
  4. idleReflex — every 10th tick, log heartbeat (HP / food / pos).

Adding a new reflex = a function (ctx) => { action, ... } in runtime/reflex.js, inserted at the right priority. Pure script, no LLM.

When the bot calls Pi

Reflexes that don't handle a situation simply return noop. After N consecutive tick cycles with no useful action — or when a reflex explicitly flags "stuck" — the bot will escalate by calling pi-bridge.js:

askPi({
  prompt: "I've been at the same position for 5 minutes, last reflex chain
  fell through, snapshot attached. What's a reasonable next action?",
  onChunk, onDone,
});

This is not wired into the reflex loop yet — the escalation is currently operator-driven via TUI hotkey a. Wiring it up as an automatic fallback is the next milestone.

IPC protocol

Socket: state/<MC_HOST>_<MC_PORT>/bot.sock (permissions 0600, removed on shutdown). Framing: one JSON object per line.

Server → client events (see runtime/ipc-protocol.js):

Type Payload
hello { snapshot, recentLogs } — sent on connect.
status full snapshot from perceive.js.
log { ts, level, source, text, details } — every log line.
chat { from, text, kind: "player" | "system" }.
death { reason, position }.
error { source, text }.
ask-pi-chunk { stream: "stdout" | "stderr", text }.
ask-pi-done { code, durationMs }.

Client → server commands:

Type Payload Effect
cmd:pause {} Reflex loop stops ticking.
cmd:resume {} Reflex loop resumes.
cmd:stop {} Graceful shutdown of the bot.
cmd:chat { text } Sends text into MC chat (rate-limited).
cmd:ask-pi { prompt } Spawns pi -p "<prompt>".
cmd:snapshot {} Force a status event now.

The protocol is intentionally tiny — anyone can write a second client (a Telegram bridge, a web UI, a one-shot CLI) by reading runtime/ipc-protocol.js.

Self-improvement loop (planned)

When a reflex repeatedly fails (e.g. "tried to navigate to base 3 times, pathfinder returned noPath each time"), the bot will:

  1. Write state/<host>/proposals/YYYY-MM-DD-<slug>.md describing the gap.
  2. Mark a flag in the next status event so the TUI surfaces it.
  3. Wait for operator approval (TUI key y on a proposal — not yet built).
  4. On approval: spawn Pi headless with the proposal text + repo context, ask it to write a new skill / patch, commit on a feature-branch.
  5. Hot-reload the affected module (reflex / actions) without dropping the MC connection.

This is the "bot writes its own code, asks permission, restarts itself" loop — the whole point of having Pi as an escalation rather than a runtime. Not wired yet; tracked under tasks #56#58 history.

File layout

runtime/
  bot.js              entrypoint — owns MC + tick + IPC + reconnect
  config.js           reads .env, exposes frozen config + redacted view
  log.js              ring buffer + stdout + daily file + IPC fan-out
  perceive.js         snapshot(bot) → JSON
  reflex.js           priority chain (defend / eat / sleep / idle, stubs)
  ipc-server.js       Unix-socket server
  ipc-protocol.js     shared contract (event types, command types, framer)
  pi-bridge.js        spawn `pi -p`, stream stdout

tui/
  tui.tsx             Ink dashboard (React)
  ipc-client.js       socket client → EventEmitter

Per-server state stays under state/<MC_HOST>_<MC_PORT>/, gitignored, same as before. The bot.sock lives there too.

Pi-only fallback

The original Pi-driven runtime still works if you prefer the single-process model — npm run agent from repo root loads AGENTS.md and the existing extensions in extensions/. The two runtimes share the .env, the mineflayer deps, and the state/ directory. They MUST NOT run simultaneously — both will try to claim the same MC nickname and the server will kick one of them.

If you switch between them frequently, kill one before starting the other:

# stop hybrid
# (in TUI press 's', or just kill `npm run bot`)

# start Pi
npm run agent