feat(runtime): survival-bot pivot — MC chat is dialog-only (Phase 0) #12
@@ -5,10 +5,24 @@
|
||||
> the hybrid one under [`runtime/`](./runtime/) — a script-driven reflex loop
|
||||
> with Pi invoked only on demand. See [`docs/runtime.md`](./docs/runtime.md)
|
||||
> for the new architecture. The principles below still apply to both modes:
|
||||
> tool catalog, safety rules, operator-trust model, and memory protocol are
|
||||
> shared. When you (the agent, in Pi-only mode) propose new code, prefer
|
||||
> writing it as a reflex/action in `runtime/` over an extension in
|
||||
> `extensions/` — the hybrid runtime is where new work lands going forward.
|
||||
> tool catalog, safety rules, and memory protocol are shared. When you (the
|
||||
> agent, in Pi-only mode) propose new code, prefer writing it as a
|
||||
> reflex/action in `runtime/` over an extension in `extensions/` — the
|
||||
> hybrid runtime is where new work lands going forward.
|
||||
|
||||
> **Product pivot (2026-05-25, Phase 0).** The bot is no longer a remote
|
||||
> control for operators or players. It is becoming a self-sufficient survival
|
||||
> resident of the configured Minecraft server. **Minecraft chat is
|
||||
> dialog-only.** Nobody — including names listed in `OPERATOR_USERNAMES` —
|
||||
> can drive the bot via chat: `come`, `follow`, `pause`, `stop`, `build`,
|
||||
> `give` and similar verbs are recorded but not dispatched. The TUI (or any
|
||||
> future Telegram bridge) is the only local control plane for actions; the
|
||||
> repo (file edits, skills, runtime code, proposals) is the trusted
|
||||
> long-term control channel. The full plan lives in
|
||||
> `plans/autonomous-survival-bot-prd.md` (local-only, gitignored). Sections
|
||||
> below that still describe chat-driven scope-trust apply only to the
|
||||
> Pi-only runtime's legacy behaviour — treat them as historical context, not
|
||||
> as a description of current behaviour.
|
||||
|
||||
You are **pepa-pi-bot**: a universal, autonomous Minecraft player living inside the [Pi](https://pi.dev) runtime.
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ The bot **proposes its own patches** when something repeatedly fails:
|
||||
|
||||
Supervisor (`npm run bot`) watches `runtime/*.js` and hot-restarts the child on file change, so during step 4 you can iterate quickly.
|
||||
|
||||
See [`docs/runtime.md`](./docs/runtime.md) for the full lifecycle and the operator-chat command list (`pepa_bot status`, `come`, `pause`, etc.).
|
||||
See [`docs/runtime.md`](./docs/runtime.md) for the full lifecycle. **Note (2026-05-25):** MC chat is now dialog-only — operator commands (`come`, `pause`, `stop`, …) are no longer dispatched from chat; use the TUI for local control. See `plans/autonomous-survival-bot-prd.md` for the survival-bot pivot.
|
||||
|
||||
## Authentication
|
||||
|
||||
@@ -198,11 +198,13 @@ These are mirrored in `AGENTS.md` and re-stated at the top of any system prompt
|
||||
|
||||
🌳 **Phase 0 — Body** done. Bridge online, AuthMe handled, `hello` sent. See `skills/server-onboarding.md`.
|
||||
|
||||
🌳 **Phase 1 — Presence** implemented and operator trust wired: the bridge stays online with bounded reconnects, keeps a rolling chat buffer, exposes status/recent-chat/operator/escalation tools, applies `OPERATOR_USERNAMES` as scope-only trust, and can prompt the Pi loop to reply sparingly.
|
||||
🌳 **Phase 1 — Presence** implemented: bridge stays online with bounded reconnects, rolling chat buffer, status/recent-chat tools, sparing replies.
|
||||
|
||||
🌿 **Phase 2 — Locomotion/build rails** in progress: `mineflayer-pathfinder` is wired with guarded `mc_goto`, plus `mc_build_pyramid_5x5` for the operator-approved empty-site pyramid task. Dynamic following is still pending. Phase 5 self-extension is documented and in progress; Phase 6 escalation logging is implemented.
|
||||
🌿 **Phase 2 — Locomotion/build rails** in progress: `mineflayer-pathfinder` is wired with guarded `mc_goto`, plus `mc_build_pyramid_5x5`. Phase 5 self-extension and Phase 6 escalation logging are implemented.
|
||||
|
||||
🌱 **Phase 3 — Goal-driven autonomy** seeded: [`docs/memory-model.md`](./docs/memory-model.md) defines shared-knowledge vs personal-memory; per-server `goal.md` / `plan.md` / `current-task.json` / `diary/` shape autonomous behaviour. Kickoff via [`prompts/live-your-life.md`](./prompts/live-your-life.md).
|
||||
🌱 **Phase 3 — Goal-driven autonomy** seeded: [`docs/memory-model.md`](./docs/memory-model.md) defines shared-knowledge vs personal-memory; per-server `goal.md` / `plan.md` / `current-task.json` / `diary/` shape autonomous behaviour.
|
||||
|
||||
🌿 **Survival-bot pivot (2026-05-25)** — the bot is becoming a self-sufficient survival resident of the configured server. **MC chat is dialog-only**; operator/player chat commands are recorded but not dispatched (TUI is the only local control plane). Full plan: `plans/autonomous-survival-bot-prd.md` (local-only, gitignored). Phase 0 (chat-control cleanup, version auto-detect) is done; Phase 1+ (observability, skill substrate, survival curriculum, base/village loop) is the next focus.
|
||||
|
||||
Full plan: [`docs/roadmap.md`](./docs/roadmap.md). Memory layout: [`docs/memory-model.md`](./docs/memory-model.md). Day-to-day judgement: "Operating principles" in [`AGENTS.md`](./AGENTS.md).
|
||||
|
||||
|
||||
+31
-13
@@ -2,6 +2,14 @@
|
||||
|
||||
> The bot grows by accretion. This file describes **the order in which it should grow**, not a set of features to build upfront. Each phase is something the agent itself can extend itself into, one skill at a time.
|
||||
|
||||
> **Product pivot (2026-05-25).** The original roadmap below assumed an
|
||||
> operator-driven bot (chat summons, follow-me, etc.). The new direction is
|
||||
> an autonomous survival resident — see `plans/autonomous-survival-bot-prd.md`
|
||||
> for the active plan and phase structure. Where the two disagree, **the PRD
|
||||
> wins.** Phases 2 (chat-summons) and 3 (operator-priority loop) below are
|
||||
> kept as historical context but no longer represent shipping work; the new
|
||||
> phases are tracked in the PRD.
|
||||
|
||||
Status legend: 🌱 not started · 🌿 in progress · 🌳 done · ⏸️ paused
|
||||
|
||||
## Phase 0 — Body 🌳
|
||||
@@ -21,7 +29,12 @@ The bot is **on the server, all the time** (except for a clean human-issued disc
|
||||
|
||||
Stretch: short-term chat memory (last N lines) so it can reference what was just said.
|
||||
|
||||
## Phase 2 — Locomotion with guard rails 🌿
|
||||
## Phase 2 — Locomotion with guard rails ⏸️ (superseded by PRD)
|
||||
|
||||
> Historical scope: chat-driven summons. As of the 2026-05-25 pivot, MC chat
|
||||
> cannot summon the bot. Pathfinder/safety primitives from this phase are
|
||||
> still useful for autonomous survival movement; the chat-driven summon
|
||||
> surface is removed. See PRD §5 (Target Architecture).
|
||||
|
||||
The bot can be **summoned** by trusted/sanctioned coordinate requests via `mc_goto`; dynamic follow is still pending. Goal: "come to 100 64 -200", "follow me", "go to spawn" with three hard rails:
|
||||
|
||||
@@ -31,28 +44,33 @@ The bot can be **summoned** by trusted/sanctioned coordinate requests via `mc_go
|
||||
|
||||
Stretch: `mc_position_share()` so the bot can answer "where are you?".
|
||||
|
||||
## Phase 3 — Best life when idle / goal-driven autonomy 🌿 (kickoff)
|
||||
## Phase 3 — Best life when idle / goal-driven autonomy 🌿 (revised by PRD)
|
||||
|
||||
When chat is quiet for some threshold (5-10 minutes of no addressed/non-trivial messages), the bot switches to **autonomous mode**.
|
||||
> The "live operator task → drop everything" step from the legacy priority
|
||||
> loop no longer applies — MC chat cannot create tasks. The revised
|
||||
> scheduler priority lives in PRD §5.3 (Task Scheduler Priority). The rest
|
||||
> of this section (goal/plan/current-task/diary structure) still applies
|
||||
> and is the foundation for the survival curriculum.
|
||||
|
||||
Now it is **goal-driven**, not just idle-active:
|
||||
The bot is **always autonomous**: it does not wait for operator chat to
|
||||
have something to do.
|
||||
|
||||
- A long-term goal lives in `state/<MC_HOST>/goal.md` (e.g. "build a small village and survive"). Personal memory, see `docs/memory-model.md`.
|
||||
- A long-term goal lives in `state/<MC_HOST>/goal.md`. Personal memory, see `docs/memory-model.md`.
|
||||
- Decomposed into milestones in `state/<MC_HOST>/plan.md`.
|
||||
- Current action is checkpointed in `state/<MC_HOST>/current-task.json` for resume-on-restart.
|
||||
- Daily journal in `state/<MC_HOST>/diary/YYYY-MM-DD.md`.
|
||||
|
||||
Priority loop (highest to lowest):
|
||||
1. Live operator task → drop everything, do it.
|
||||
2. Live non-operator chat → reply briefly.
|
||||
3. Resume `current-task.json` if interrupted.
|
||||
4. Next milestone in `plan.md`.
|
||||
5. Decompose `goal.md` → new plan.
|
||||
Revised priority loop (per PRD §5.3):
|
||||
1. Emergency survival (death/lava/low HP/starvation).
|
||||
2. Finish or recover current task.
|
||||
3. Maintain base safety (light, shelter, repair).
|
||||
4. Execute current milestone.
|
||||
5. Expand village/base.
|
||||
6. Social reply if there is a relevant chat event.
|
||||
7. Idle diary/status heartbeat.
|
||||
|
||||
Concrete activities while autonomous: scout/build modest base, farm food, store in chests, light area, defend at night, explore cautiously, build out toward the goal. Skills emerge: `farming-wheat`, `chest-organizer`, `careful-cave-mining`, `village-layout`, etc.
|
||||
|
||||
Kicked off via [`prompts/live-your-life.md`](../prompts/live-your-life.md).
|
||||
|
||||
## Phase 4 — Telegram bridge 🌱
|
||||
|
||||
Two-way ops channel without sitting in Pi TUI:
|
||||
|
||||
+40
-25
@@ -4,6 +4,12 @@
|
||||
> 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.
|
||||
|
||||
> **Phase 0 product pivot (2026-05-25).** MC chat is now **dialog-only** for
|
||||
> everyone, including `OPERATOR_USERNAMES`. The reflex loop no longer takes
|
||||
> commands from chat. Local control lives in the TUI (`p`/`s` hotkeys);
|
||||
> long-term control lives in the repo. See
|
||||
> `plans/autonomous-survival-bot-prd.md` for the survival-bot pivot.
|
||||
|
||||
## Why a hybrid runtime?
|
||||
|
||||
The original design ran every tick inside Pi — the LLM saw the world, picked
|
||||
@@ -41,8 +47,8 @@ new code for itself.
|
||||
│ │ - 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 ││
|
||||
│ │ (dialog-only) │ │ > sleep > tech │ │ chat events ││
|
||||
│ │ │ │ > autonomous │ │ - accepts ││
|
||||
│ │ │ │ - NO LLM in path │ │ commands ││
|
||||
│ └──────────────────┘ └─────────┬────────────┘ └────────────────┘│
|
||||
│ │ │
|
||||
@@ -100,18 +106,23 @@ TUI; the bot is unaffected.
|
||||
The chain (highest priority first), wired and dispatching real
|
||||
Mineflayer actions:
|
||||
|
||||
1. **`operatorGoalReflex`** — if `OPERATOR_USERNAMES` issued a `come` /
|
||||
`follow` command, satisfy it (walk to the operator's last known
|
||||
position, reply in chat on arrival or failure).
|
||||
2. **`defendReflex`** — closest hostile within 4 m → `attackNearest`
|
||||
1. **`defendReflex`** — closest hostile within 4 m → `attackNearest`
|
||||
(equips best melee). Within 12 m + low HP or ≥3 hostiles → `fleeFrom`
|
||||
along the away-vector.
|
||||
3. **`eatReflex`** — food < 16 → `eatBestFood` (picks from
|
||||
2. **`eatReflex`** — food < 16 → `eatBestFood` (picks from
|
||||
FOOD_PRIORITY list, equip + consume). 5 s cooldown.
|
||||
4. **`sleepReflex`** — night + no hostile within 8 m → `sleepInBed`
|
||||
3. **`sleepReflex`** — night + no hostile within 8 m → `sleepInBed`
|
||||
(finds nearest placed bed within 16 blocks, paths there, sleeps).
|
||||
30 s cooldown on failures.
|
||||
5. **`idleReflex`** — every 20th tick, log heartbeat (HP / food / pos).
|
||||
5 min cooldown on failures.
|
||||
4. **`techTreeReflex`** — deterministic crafting progression
|
||||
(planks → sticks → wooden axe → pickaxe → sword) when prerequisites
|
||||
are in inventory.
|
||||
5. **`autonomousReflex`** — when nothing reactive fires: chop trees until
|
||||
~16 logs, then wander to discover new chunks.
|
||||
6. **`idleReflex`** — every 20th tick, log heartbeat (HP / food / pos).
|
||||
|
||||
There is **no operator-goal reflex anymore.** MC chat does not create
|
||||
movement/build/mining tasks (Phase 0 of `plans/autonomous-survival-bot-prd.md`).
|
||||
|
||||
Adding a new reflex = a function `(ctx) => { action, ... }` in
|
||||
`runtime/reflex.js`, inserted at the right priority. Actions live in
|
||||
@@ -127,8 +138,8 @@ bot spawns `pi -p "<question>"` and streams its stdout into the Pi
|
||||
panel.
|
||||
|
||||
**2. Automatic** — every tick where the entire reflex chain returns
|
||||
`noop` (no operator goal, no hostiles in reach, food fine, day or no
|
||||
bed, etc.) increments a counter. When the counter hits
|
||||
`noop` (no hostiles in reach, food fine, day or no bed, nothing to
|
||||
craft, nowhere to wander) increments a counter. When the counter hits
|
||||
`ESCALATE_AFTER_NOOPS = 20` (≈1 min at `tick=3s`), the bot fires
|
||||
`askPi` with the current snapshot and a fixed system prompt telling Pi
|
||||
to suggest one next action. 10 min cooldown so a permanently-idle bot
|
||||
@@ -139,22 +150,26 @@ should only suggest what to do *with the existing tools*. If a deeper
|
||||
problem is happening, the failure-tracker (see Self-improvement) will
|
||||
file a proposal instead.
|
||||
|
||||
## Operator chat commands
|
||||
## In-game chat (dialog-only)
|
||||
|
||||
Players listed in `OPERATOR_USERNAMES` can address the bot in MC chat
|
||||
by prefixing the message with the bot's name:
|
||||
As of the Phase 0 survival-bot pivot, MC chat does **not** drive bot
|
||||
actions for anyone, including names listed in `OPERATOR_USERNAMES`.
|
||||
The bot will:
|
||||
|
||||
```
|
||||
pepa_bot status → bot replies with HP / food / pos / hostiles / busy
|
||||
pepa_bot come → bot pathfinds to the operator's current position
|
||||
pepa_bot pause → reflex loop stops
|
||||
pepa_bot resume → reflex loop resumes
|
||||
pepa_bot stop → graceful disconnect + process exit
|
||||
```
|
||||
- reply to greetings (`hi`, `привет`, etc.) and to being addressed by
|
||||
name, rate-limited;
|
||||
- answer status questions (`pepa_bot status`, `как дела`, `what are
|
||||
you doing?`) from the live snapshot;
|
||||
- detect command-like verbs (`come`, `follow`, `build`, `pause`,
|
||||
`stop`, `give`, …) when addressed, record them in the diary, and
|
||||
reply once per cooldown that MC chat is dialog-only.
|
||||
|
||||
Unrecognized commands get a polite "didn't recognize" reply. Operator
|
||||
identity verification is the server's job (AuthMe on cracked,
|
||||
online-mode on premium) — the bot trusts the nickname.
|
||||
Local control of the bot (pause/resume/stop, sending chat manually,
|
||||
escalating to Pi) lives in the TUI. Long-term control (skills,
|
||||
runtime code, proposals) lives in the repo. `OPERATOR_USERNAMES` is
|
||||
still used to **label** speakers in logs (`operator <name>` vs
|
||||
`player <name>`), and remains the right place to plug a future
|
||||
trusted control channel (e.g. Telegram bridge).
|
||||
|
||||
## IPC protocol
|
||||
|
||||
|
||||
+66
-96
@@ -3,7 +3,10 @@
|
||||
// - the reflex tick loop (no LLM in hot path)
|
||||
// - the IPC server for TUI clients
|
||||
// - on-demand Pi-headless escalation (manual or automatic)
|
||||
// - simple operator-chat commands from OPERATOR_USERNAMES
|
||||
//
|
||||
// MC chat is dialog-only (see plans/autonomous-survival-bot-prd.md, FR1).
|
||||
// Player/operator chat may produce a social reply but never dispatches a
|
||||
// movement/build/mining task. TUI is the only local control plane.
|
||||
//
|
||||
// Lifecycle: started by `npm run bot`. Connects to MC, spawns IPC server,
|
||||
// ticks every TICK_INTERVAL_SECONDS, broadcasts STATUS to clients each tick.
|
||||
@@ -58,16 +61,12 @@ const reflexCtx = {
|
||||
snapshot: lastSnapshot,
|
||||
busy: false,
|
||||
currentActionLabel: null,
|
||||
operatorGoal: null,
|
||||
idleCounter: 0,
|
||||
lastEatAt: 0,
|
||||
lastSleepAttemptAt: 0,
|
||||
// Tracks repeated failure of the same labelled action — triggers a proposal.
|
||||
recentFailures: [], // [{label, detail, ts}], capped at 10
|
||||
dispatch: dispatchAction,
|
||||
clearOperatorGoal: () => {
|
||||
reflexCtx.operatorGoal = null;
|
||||
},
|
||||
};
|
||||
|
||||
let chatTimestamps = [];
|
||||
@@ -300,96 +299,77 @@ function maybeFileProposal(label) {
|
||||
appendDiary(`proposal filed: ${filename} (${summary})`);
|
||||
}
|
||||
|
||||
// ---- player chat replies (non-operator) ------------------------------------
|
||||
// ---- chat replies (dialog-only) --------------------------------------------
|
||||
//
|
||||
// As of Phase 0 of the survival-bot PRD, MC chat is dialog-only for everyone,
|
||||
// including OPERATOR_USERNAMES. The bot may reply socially or answer status
|
||||
// questions, but it does NOT dispatch movement/build/mining tasks from chat.
|
||||
// If a player addresses the bot with a command-like verb (come, follow, build,
|
||||
// pause, stop, give), the bot acknowledges that chat is dialog-only and
|
||||
// records the ignored command in the diary.
|
||||
|
||||
// Rate-limited light replies to ordinary players. Spam guard is intentional:
|
||||
// the chat rate limit catches outbound flooding; this one prevents replying
|
||||
// to every greeting in a busy room.
|
||||
let lastPlayerReplyAt = 0;
|
||||
const PLAYER_REPLY_COOLDOWN_MS = 30_000;
|
||||
let lastChatReplyAt = 0;
|
||||
const CHAT_REPLY_COOLDOWN_MS = 30_000;
|
||||
|
||||
const GREETING_RE = /\b(hi|hello|hey|yo|sup|hola|привет|здаров|здарова|здорова|здравствуй|здравствуйте|салам)\b/i;
|
||||
|
||||
function maybeReplyToPlayer(username, text) {
|
||||
if (!bot) return;
|
||||
const lower = text.trim().toLowerCase();
|
||||
const botname = bot.username.toLowerCase();
|
||||
const addressed = lower.includes(botname);
|
||||
if (!addressed && !GREETING_RE.test(lower)) return;
|
||||
const since = Date.now() - lastPlayerReplyAt;
|
||||
if (since < PLAYER_REPLY_COOLDOWN_MS) return;
|
||||
lastPlayerReplyAt = Date.now();
|
||||
|
||||
// Pick a small canned response. Non-operator chat is dialog-only by design
|
||||
// — we don't act on player verbs, we just acknowledge presence.
|
||||
const replies = ["yo", "hey", "hi", "привет"];
|
||||
const reply = replies[Math.floor(Math.random() * replies.length)];
|
||||
botChat(`${username}: ${reply}`);
|
||||
}
|
||||
|
||||
// ---- operator chat commands ------------------------------------------------
|
||||
const STATUS_RE = /\b(status|how are you|what are you doing|whats up|what['’]?s up|чё делаешь|что делаешь|как ты|как дела|статус)\b/i;
|
||||
const COMMAND_LIKE_RE = /\b(come|follow|build|pause|resume|stop|go to|goto|tp|teleport|give|drop|attack|kill|dig|mine|chop|farm|harvest|sleep here|иди сюда|подойди|следуй|остановись|стоп|пауза|строй|копай|дай)\b/i;
|
||||
|
||||
function isOperator(username) {
|
||||
if (!username) return false;
|
||||
return config.operators.includes(username.toLowerCase());
|
||||
}
|
||||
|
||||
function handleOperatorChat(username, text) {
|
||||
// Two address formats accepted: prefix "<botname>," or "<botname>:" (case-insensitive),
|
||||
// or full chat starting with the bot's own name. We're permissive here.
|
||||
const lower = text.trim().toLowerCase();
|
||||
function buildStatusReply() {
|
||||
const s = lastSnapshot;
|
||||
const parts = [];
|
||||
if (reflexCtx.busy) parts.push(`busy=${reflexCtx.currentActionLabel}`);
|
||||
if (s.health !== undefined) parts.push(`hp=${s.health}/20`);
|
||||
if (s.food !== undefined) parts.push(`food=${s.food}/20`);
|
||||
if (s.position) parts.push(`pos=${s.position.x},${s.position.y},${s.position.z}`);
|
||||
if (s.hostileCount) parts.push(`hostiles=${s.hostileCount}`);
|
||||
return parts.join(" ") || "alive";
|
||||
}
|
||||
|
||||
function handleChat(username, text) {
|
||||
if (!bot) return;
|
||||
const trimmed = text.trim();
|
||||
const lower = trimmed.toLowerCase();
|
||||
const botname = bot.username.toLowerCase();
|
||||
const prefixed = lower.startsWith(botname + " ") || lower.startsWith(botname + ",") || lower.startsWith(botname + ":");
|
||||
const stripped = prefixed ? text.trim().slice(botname.length).replace(/^[,:\s]+/, "") : text.trim();
|
||||
const cmd = stripped.toLowerCase();
|
||||
const addressed = lower.includes(botname);
|
||||
const looksLikeCommand = addressed && COMMAND_LIKE_RE.test(lower);
|
||||
|
||||
// Unaddressed chat is fine — just don't treat it as a command.
|
||||
if (!prefixed) return;
|
||||
|
||||
info("operator", `${username} → "${cmd}"`);
|
||||
|
||||
if (cmd === "status" || cmd === "how are you?") {
|
||||
const s = lastSnapshot;
|
||||
botChat(
|
||||
`hp=${s.health}/20 food=${s.food}/20 pos=${s.position?.x},${s.position?.y},${s.position?.z}${
|
||||
s.hostileCount ? ` hostiles=${s.hostileCount}` : ""
|
||||
}${reflexCtx.busy ? ` busy=${reflexCtx.currentActionLabel}` : ""}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (cmd === "pause") {
|
||||
reflexPaused = true;
|
||||
botChat(`reflex paused, awaiting your call.`);
|
||||
return;
|
||||
}
|
||||
if (cmd === "resume") {
|
||||
reflexPaused = false;
|
||||
botChat(`reflex resumed.`);
|
||||
return;
|
||||
}
|
||||
if (cmd === "stop") {
|
||||
botChat(`bye.`);
|
||||
setTimeout(() => gracefulExit(0), 500);
|
||||
return;
|
||||
}
|
||||
if (cmd === "come" || cmd === "come here") {
|
||||
const op = Object.values(bot.entities).find((e) => e.username === username);
|
||||
if (!op) {
|
||||
botChat(`${username}: can't see you nearby.`);
|
||||
return;
|
||||
// Command-like chat (from anyone, including operators) is recorded but not
|
||||
// dispatched. We tell the speaker once per cooldown so they aren't left
|
||||
// wondering why nothing happened.
|
||||
if (looksLikeCommand) {
|
||||
const op = isOperator(username) ? "operator" : "player";
|
||||
info("chat", `ignored command-like chat from ${op} ${username}: ${trimmed.slice(0, 80)}`);
|
||||
appendDiary(`ignored command-like chat from ${username}: ${trimmed.slice(0, 120)}`);
|
||||
const since = Date.now() - lastChatReplyAt;
|
||||
if (since >= CHAT_REPLY_COOLDOWN_MS) {
|
||||
lastChatReplyAt = Date.now();
|
||||
botChat(`${username}: MC chat is dialog-only — operator uses the TUI to drive me.`);
|
||||
}
|
||||
reflexCtx.operatorGoal = {
|
||||
kind: "come",
|
||||
from: username,
|
||||
x: Math.round(op.position.x),
|
||||
y: Math.round(op.position.y),
|
||||
z: Math.round(op.position.z),
|
||||
};
|
||||
botChat(`on my way to ${reflexCtx.operatorGoal.x},${reflexCtx.operatorGoal.y},${reflexCtx.operatorGoal.z}`);
|
||||
return;
|
||||
}
|
||||
|
||||
botChat(`${username}: didn't recognize "${cmd}". I know: status, come, pause, resume, stop.`);
|
||||
// Dialog: greeting, status question, or addressed banter.
|
||||
const wantsStatus = addressed && STATUS_RE.test(lower);
|
||||
const isGreeting = GREETING_RE.test(lower);
|
||||
if (!addressed && !isGreeting) return;
|
||||
|
||||
const since = Date.now() - lastChatReplyAt;
|
||||
if (since < CHAT_REPLY_COOLDOWN_MS) return;
|
||||
lastChatReplyAt = Date.now();
|
||||
|
||||
if (wantsStatus) {
|
||||
botChat(`${username}: ${buildStatusReply()}`);
|
||||
return;
|
||||
}
|
||||
const greetings = ["yo", "hey", "hi", "привет"];
|
||||
const reply = greetings[Math.floor(Math.random() * greetings.length)];
|
||||
botChat(`${username}: ${reply}`);
|
||||
}
|
||||
|
||||
// ---- connect ---------------------------------------------------------------
|
||||
@@ -402,7 +382,7 @@ function connect() {
|
||||
port: config.port,
|
||||
username: config.username,
|
||||
auth: config.authMode === "microsoft" ? "microsoft" : "offline",
|
||||
version: config.version,
|
||||
version: config.mineflayerVersion,
|
||||
hideErrors: false,
|
||||
});
|
||||
reflexCtx.bot = bot;
|
||||
@@ -421,18 +401,10 @@ function connect() {
|
||||
bot.on("chat", (username, message) => {
|
||||
if (username === bot.username) return;
|
||||
ipc?.broadcast(EVENT_TYPES.CHAT, { from: username, text: message, kind: "player" });
|
||||
if (isOperator(username)) {
|
||||
try {
|
||||
handleOperatorChat(username, message);
|
||||
} catch (e) {
|
||||
warn("operator", `handler threw: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
maybeReplyToPlayer(username, message);
|
||||
} catch (e) {
|
||||
warn("chat", `player reply handler threw: ${e.message}`);
|
||||
}
|
||||
try {
|
||||
handleChat(username, message);
|
||||
} catch (e) {
|
||||
warn("chat", `chat handler threw: ${e.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -441,8 +413,6 @@ function connect() {
|
||||
warn("mc", `died at ${JSON.stringify(pos)}`);
|
||||
appendDiary(`died at ${pos?.x.toFixed(0)},${pos?.y.toFixed(0)},${pos?.z.toFixed(0)}`);
|
||||
ipc?.broadcast(EVENT_TYPES.DEATH, { reason: "unknown", position: pos });
|
||||
// On death, drop any operator goal — they need to ask again.
|
||||
reflexCtx.operatorGoal = null;
|
||||
clearCurrentTask();
|
||||
});
|
||||
|
||||
@@ -488,7 +458,7 @@ function maybeAutoEscalate() {
|
||||
`You are the escalation cortex for an autonomous Minecraft bot. The bot's`,
|
||||
`script-driven reflex loop has produced no useful action for ${ESCALATE_AFTER_NOOPS} consecutive ticks`,
|
||||
`(~${Math.round((ESCALATE_AFTER_NOOPS * config.tickIntervalMs) / 1000)}s). The reflex chain is:`,
|
||||
` operator-goal > defend > eat > sleep > idle`,
|
||||
` defend > eat > sleep > tech-tree > autonomous > idle`,
|
||||
`Snapshot:`,
|
||||
"```json",
|
||||
promptCtx,
|
||||
|
||||
+10
-1
@@ -22,11 +22,20 @@ const host = req("MC_HOST");
|
||||
const port = Number.parseInt(opt("MC_PORT", "25565"), 10);
|
||||
const username = req("MC_USERNAME");
|
||||
|
||||
// MC_VERSION: "auto" (or empty) lets mineflayer auto-detect from the server
|
||||
// handshake — the right default per the survival-bot PRD (no hard-coded modern
|
||||
// version unless the server requires pinning). Mineflayer accepts `false` to
|
||||
// auto-detect, so we translate "auto" into false at the boundary; the original
|
||||
// string is preserved for logging.
|
||||
const rawVersion = opt("MC_VERSION", "auto");
|
||||
const mineflayerVersion = rawVersion.toLowerCase() === "auto" ? false : rawVersion;
|
||||
|
||||
export const config = Object.freeze({
|
||||
host,
|
||||
port,
|
||||
username,
|
||||
version: opt("MC_VERSION", "1.21.5"),
|
||||
version: rawVersion,
|
||||
mineflayerVersion,
|
||||
authMode: opt("MC_AUTH_MODE", "offline"),
|
||||
authmePassword: opt("MC_AUTHME_PASSWORD", ""),
|
||||
operators: opt("OPERATOR_USERNAMES", "")
|
||||
|
||||
+4
-29
@@ -12,7 +12,6 @@ import {
|
||||
fleeFrom,
|
||||
eatBestFood,
|
||||
sleepInBed,
|
||||
goTo,
|
||||
chopNearestTree,
|
||||
wander,
|
||||
craftPlanks,
|
||||
@@ -32,34 +31,11 @@ const REFLEX_LOG = "reflex";
|
||||
// { action: "completed", kind, detail } — fully sync, already done
|
||||
// Reflexes must NEVER throw — they log and return noop on failure.
|
||||
|
||||
// ---- operator goal reflex --------------------------------------------------
|
||||
|
||||
// Highest priority: if the operator gave a "come here" / "follow me" command,
|
||||
// satisfy that before anything else (except defending if HP is critical).
|
||||
function operatorGoalReflex(ctx) {
|
||||
const goal = ctx.operatorGoal;
|
||||
if (!goal) return { action: "noop" };
|
||||
if (goal.kind === "come") {
|
||||
ctx.dispatch(
|
||||
() => goTo(ctx.bot, goal.x, goal.y, goal.z, 2),
|
||||
`operator-come(${goal.from})`,
|
||||
{
|
||||
onComplete: (res) => {
|
||||
if (res.ok) {
|
||||
ctx.bot.chat(`${goal.from}: arrived.`);
|
||||
} else {
|
||||
ctx.bot.chat(`${goal.from}: couldn't reach you (${res.detail}).`);
|
||||
}
|
||||
ctx.clearOperatorGoal();
|
||||
},
|
||||
},
|
||||
);
|
||||
return { action: "dispatched", kind: "operator-come", label: `come ${goal.x},${goal.y},${goal.z}` };
|
||||
}
|
||||
return { action: "noop" };
|
||||
}
|
||||
|
||||
// ---- defend ----------------------------------------------------------------
|
||||
//
|
||||
// Note: MC chat is dialog-only as of Phase 0 of the survival-bot PRD. There is
|
||||
// no operator-goal reflex anymore — operator/player chat cannot create a
|
||||
// movement/build/mining task. TUI is the only local control plane.
|
||||
|
||||
function defendReflex(ctx) {
|
||||
const s = ctx.snapshot;
|
||||
@@ -298,7 +274,6 @@ function idleReflex(ctx) {
|
||||
}
|
||||
|
||||
const REFLEXES = [
|
||||
{ name: "operator-goal", fn: operatorGoalReflex },
|
||||
{ name: "defend", fn: defendReflex },
|
||||
{ name: "eat", fn: eatReflex },
|
||||
{ name: "sleep", fn: sleepReflex },
|
||||
|
||||
Reference in New Issue
Block a user