From 68f59dfa4c7f884c83ead912f80b49a86828f628 Mon Sep 17 00:00:00 2001 From: Yuriy Mayatnikov Date: Mon, 25 May 2026 15:06:37 +0300 Subject: [PATCH] feat(recovery): death handler + auto-defend reflex + bulk-collect rework + mc_stay shim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four targeted fixes for issues observed during the live autonomous test: 1. **bot.modes shim** (extensions/lib/mcdata.js) Mindcraft skills.* call bot.modes.pause('cowardice') in 7+ places. `mineflayer-modes` does not exist on npm — it's internal to Mindcraft. attachPluginsAndInit now installs a no-op shim so skills.stay() / .consume() / .defendSelf() etc. stop crashing with "pause undefined". 2. **Death + respawn handlers** (extensions/mineflayer-bridge.ts) Subscribe to bot 'death' event: append diary line with position, clear current-task.json so Pi doesn't resume a stale task referencing inventory that no longer exists. On 'spawn' within 5s of death: log the new respawn position to diary. Live test had bot killed twice by zombies at night; the next Pi prompt was unable to recover. With this it's now an explicit diary line + clean task slate. 3. **Auto-defend reflex tick** (extensions/mineflayer-bridge.ts) New setInterval(2s) that, when bot.health < 18 AND a hostile mob (zombie/skeleton/creeper/spider/etc.) is within 6 blocks AND no active world task, fires `bot.pvp.attack(nearest)` in the background. No LLM call needed for instant self-defense — saves tokens and reacts on mineflayer timescale (sub-second) rather than Pi loop timescale (~10s+ to reason and dispatch). Throttled to once per 4s. 4. **mc_collect_block bulk rework** (extensions/mindcraft-skills.ts) Live test showed count=1 succeeds in ~25s but count=8 hangs past 270s with identical blocks in range. Upstream collectBlock plugin appears to drift on its block cache after the first dig in dense terrain. Loop single-block collects in-tool instead (75s per iter, re-pathfind on each iteration). Track per-iter success/failure, abort after 3 consecutive failures, return aggregate count to the agent so it can adapt instead of seeing a single failure. Smoke test: both extensions load, bot connects, perception confirms hostiles nearby and daylight safety check. No syntax/runtime errors. Co-Authored-By: Claude Opus 4.7 (1M context) --- extensions/lib/mcdata.js | 14 ++++++ extensions/mindcraft-skills.ts | 45 ++++++++++++++++--- extensions/mineflayer-bridge.ts | 76 +++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 7 deletions(-) diff --git a/extensions/lib/mcdata.js b/extensions/lib/mcdata.js index 918b917..c09fa3e 100644 --- a/extensions/lib/mcdata.js +++ b/extensions/lib/mcdata.js @@ -148,6 +148,20 @@ export function attachPluginsAndInit(bot) { bot.loadPlugin(pvp); bot.loadPlugin(collectblock); bot.loadPlugin(armorManager); + + // Shim bot.modes — Mindcraft's skills.* call bot.modes.pause('cowardice') + // etc., but `mineflayer-modes` doesn't exist on npm (it's internal to + // Mindcraft). Stub the API so skills.stay()/.consume()/.defendSelf() etc. + // don't crash with "pause undefined". + if (!bot.modes) { + bot.modes = { + pause: () => {}, + unpause: () => {}, + isOn: () => false, + getMiningCooldown: () => 0, + }; + } + bot.once('login', () => { mc_version = bot.version; mcdata = minecraftData(mc_version); diff --git a/extensions/mindcraft-skills.ts b/extensions/mindcraft-skills.ts index 9eb2c97..286d4ec 100644 --- a/extensions/mindcraft-skills.ts +++ b/extensions/mindcraft-skills.ts @@ -268,14 +268,45 @@ export default async function mindcraftSkills(pi: ExtensionAPI) { async execute(_id, params: { blockType: string; count?: number }) { const bot = getBot(); const count = Math.max(1, Math.min(64, Math.floor(params.count ?? 1))); - // Timeout scales with count: ~30s per block plus 30s overhead. - // Allows count=8 (about 4 min) without hanging forever on - // pathfinder-unreachable cases. - const timeoutMs = Math.min(600_000, 30_000 + count * 30_000); - const ok = await safeCall("collectBlock", () => - withTimeout(skills.collectBlock(bot, params.blockType, count), timeoutMs, `collectBlock(${params.blockType}, ${count})`), + // Bulk collection in dense terrain reliably times out on the + // upstream mineflayer-collectblock plugin (observed live: count=1 + // works in ~25s, count=8 hangs past 270s with the same blocks + // in range). Loop single-block collects instead — each iteration + // re-pathfinds from the bot's current position, which is robust + // to the block-cache and pathfinder drift problems that cause + // the hangs. Per-iter timeout 75s. + let collected = 0; + let consecutiveFails = 0; + const errors: string[] = []; + for (let i = 0; i < count; i++) { + try { + const ok = await withTimeout( + skills.collectBlock(bot, params.blockType, 1), + 75_000, + `collectBlock(${params.blockType}) iter ${i + 1}/${count}`, + ); + if (ok) { + collected++; + consecutiveFails = 0; + } else { + consecutiveFails++; + errors.push(`iter ${i + 1}: returned false`); + } + } catch (e: any) { + consecutiveFails++; + errors.push(`iter ${i + 1}: ${e?.message ?? String(e)}`); + } + if (consecutiveFails >= 3) { + return textResult( + `Collected ${collected}/${count} ${params.blockType}; aborted after 3 consecutive failures. Last errors: ${errors.slice(-3).join("; ")}`, + { collected, requested: count, errors: errors.slice(-3) }, + ); + } + } + return textResult( + `Collected ${collected}/${count} ${params.blockType}${errors.length ? ` (${errors.length} iters failed but recovered)` : ""}.`, + { collected, requested: count, errors }, ); - return textResult(ok ? `Collected ${count} of ${params.blockType}.` : `collectBlock returned false for ${params.blockType}.`, { ok, blockType: params.blockType, count }); }, }); diff --git a/extensions/mineflayer-bridge.ts b/extensions/mineflayer-bridge.ts index 47f8964..d69ce97 100644 --- a/extensions/mineflayer-bridge.ts +++ b/extensions/mineflayer-bridge.ts @@ -419,6 +419,9 @@ export default function mineflayerBridge(pi: ExtensionAPI) { let lastHumanChatAt = Date.now(); let lastAutonomyPromptAt = 0; let startupMemoryReviewed = false; + let lastDeathAt = 0; + let autoDefendTimer: ReturnType | undefined; + let lastAutoDefendAt = 0; function log(event: string, detail?: unknown) { const suffix = detail === undefined ? "" : `: ${truncate(redact(stringifyUnknown(detail), config))}`; @@ -912,6 +915,57 @@ export default function mineflayerBridge(pi: ExtensionAPI) { autonomyTimer = undefined; } + // Auto-defend reflex — every 2 seconds while connected, check for hostile + // mobs within 6 blocks. If health is risky (<18) AND a hostile is close AND + // no active world task is in progress, fire-and-forget bot.pvp.attack on + // the nearest hostile. No LLM call needed for instant self-defense. + const HOSTILE_TYPES = new Set([ + "zombie", "skeleton", "spider", "creeper", "enderman", "witch", "drowned", + "husk", "stray", "phantom", "pillager", "vindicator", "evoker", "ravager", + "zoglin", "vex", "warden", "guardian", + ]); + function tickAutoDefend() { + if (!bot || activeWorldTask) return; + if (Date.now() - lastAutoDefendAt < 4000) return; // throttle to once per 4s + if (!bot.entity) return; + if ((bot.health ?? 20) >= 18) return; // healthy → don't bother + const me = bot.entity.position; + const entities = Object.values((bot as any).entities ?? {}) as Array; + const threats = entities.filter((e) => { + if (!e || !e.position || !e.name) return false; + if (!HOSTILE_TYPES.has(String(e.name).toLowerCase())) return false; + try { return me.distanceTo(e.position) < 6; } catch { return false; } + }); + if (!threats.length) return; + const nearest = threats.reduce((a, b) => + me.distanceTo(a.position) <= me.distanceTo(b.position) ? a : b, + ); + lastAutoDefendAt = Date.now(); + log("auto-defend", `${nearest.name} at d=${Math.round(me.distanceTo(nearest.position))}, HP=${bot.health}`); + try { + const pvp = (bot as any).pvp; + if (pvp && typeof pvp.attack === "function") { + pvp.attack(nearest); + } else { + // Fallback: swing if entity is in reach. + if (me.distanceTo(nearest.position) < 3.5 && typeof (bot as any).attack === "function") { + (bot as any).attack(nearest); + } + } + } catch (e) { log("auto-defend-error", e); } + } + function startAutoDefendTimer() { + stopAutoDefendTimer(); + autoDefendTimer = setInterval(() => { + try { tickAutoDefend(); } catch (error) { log("auto-defend-tick-error", error); } + }, 2000); + (autoDefendTimer as any).unref?.(); + } + function stopAutoDefendTimer() { + if (autoDefendTimer) clearInterval(autoDefendTimer); + autoDefendTimer = undefined; + } + function queueOperatorLearning(rawFrom: string, rawText: string, entry: ChatEntry) { const recent = recentChat.slice(-10).map(formatChatEntry).join("\n") || "(no recent chat)"; const prompt = [ @@ -1122,6 +1176,26 @@ export default function mineflayerBridge(pi: ExtensionAPI) { reconnectPausedReason = undefined; log("spawn"); startAuthDetection(); + // If we just died, log the new spawn position in diary so the + // operator (and future restart) can see where the bot reset to. + if (lastDeathAt && Date.now() - lastDeathAt < 5000) { + try { + const pos = nextBot.entity?.position; + const where = pos ? `${Math.round(pos.x)},${Math.round(pos.y)},${Math.round(pos.z)}` : "unknown"; + appendDiary(current, `respawned at ${where}`); + } catch (e) { log("respawn-diary-error", e); } + lastDeathAt = 0; + } + }); + nextBot.on("death", () => { + const pos = nextBot.entity?.position; + const where = pos ? `${Math.round(pos.x)},${Math.round(pos.y)},${Math.round(pos.z)}` : "unknown"; + log("death", `at ${where}`); + try { + appendDiary(current, `died at ${where} — clearing current-task`); + clearCurrentTask(current); + } catch (e) { log("death-handler-error", e); } + lastDeathAt = Date.now(); }); nextBot.on("chat", (username, message) => { if (username !== current.username) lastHumanChatAt = Date.now(); @@ -1631,6 +1705,7 @@ export default function mineflayerBridge(pi: ExtensionAPI) { surfaceEscalationCount(); connect("startup"); startAutonomyTimer(); + startAutoDefendTimer(); if (ctx.hasUI) ctx.ui.setStatus("mineflayer", "mc: connecting"); } catch (error) { log("startup-error", error); @@ -1641,6 +1716,7 @@ export default function mineflayerBridge(pi: ExtensionAPI) { pi.on("session_shutdown", async () => { shuttingDown = true; stopAutonomyTimer(); + stopAutoDefendTimer(); disconnect({ manual: true }); }); -- 2.54.0