feat(runtime/pathfinder): stuck-replan watchdog — react to mid-path obstacles

Problem (live 2026-05-26 screenshot): player drops a block in front of
the bot mid-path. mineflayer-pathfinder computes the path once when
goto() is called and never recomputes for world changes. Bot pushes
forward against the new block until the 30–60 s goto timeout fires,
visible as the bot just standing there pressing W.

Fix — runtime/pathfinder-watchdog.js: per-bot poll loop (2 s tick) that
runs while bot.pathfinder.goal is non-null. Tracks horizontal position.
If movement < 0.5 blocks for > 6 s after an initial 1.5 s grace,
forces a replan: setGoal(null) + setGoal(<same goal>) on a 250 ms
delay. That makes the planner rebuild the path against the current
world, so it routes around the new block — or, with canDig=true in our
profiles, digs through it. Capped at 3 replans per goal so a genuinely
unreachable target still bubbles up to the caller's timeout.

Side benefit: catches mineflayer-pathfinder issue #222 ("path hangs
on unreachable goal") much earlier than our 45 s goto wrappers.

Wired into bot.js on the "spawn" event and stopped on gracefulExit.
7 new unit tests cover the polling math + replan cap. 197/197 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 20:00:01 +03:00
co-authored by Claude Opus 4.7
parent d9f11b7d99
commit 01531c54c8
4 changed files with 223 additions and 1 deletions
+10
View File
@@ -40,6 +40,7 @@ import { startPlanner, isPlannerBusy, readNextMilestone, planExists } from "./pl
import { computeState, STATES } from "./state.js";
import { createNoProgressDetector } from "./no-progress.js";
import { maybeStartViewer } from "./viewer.js";
import { createPathfinderWatchdog } from "./pathfinder-watchdog.js";
import { nextMilestone as nextCurriculumMilestone } from "./curriculum.js";
import { listLocations } from "./locations.js";
import { runSkill } from "./skills/index.js";
@@ -66,6 +67,7 @@ const ESCALATE_AFTER_NOOPS = 20;
const ESCALATION_COOLDOWN_MS = 10 * 60 * 1000;
let bot = null;
let pathWatchdog = null;
let reflexPaused = false;
let tickTimer = null;
let reconnectTimer = null;
@@ -638,6 +640,12 @@ function connect() {
appendDiary(`spawned at ${bot.entity.position.x.toFixed(0)},${bot.entity.position.y.toFixed(0)},${bot.entity.position.z.toFixed(0)}`);
ipc?.broadcast(EVENT_TYPES.STATUS, buildSnapshot(bot));
maybeStartViewer(bot).catch((e) => warn("viewer", `start threw: ${e?.message ?? e}`));
// Pathfinder stuck-watchdog: replan when an obstacle appears mid-path
// (mineflayer-pathfinder doesn't recompute on world changes).
try {
pathWatchdog?.stop();
pathWatchdog = createPathfinderWatchdog(bot);
} catch (e) { warn("pathfinder", `watchdog start failed: ${e?.message ?? e}`); }
});
bot.on("messagestr", (text) => {
@@ -1033,6 +1041,8 @@ function gracefulExit(code) {
info("runtime", "shutting down");
if (tickTimer) clearInterval(tickTimer);
if (reconnectTimer) clearTimeout(reconnectTimer);
try { pathWatchdog?.stop(); } catch {}
pathWatchdog = null;
try {
bot?.quit("shutdown");
} catch {}