feat(recovery): death handler + auto-defend reflex + bulk-collect rework + mc_stay shim
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) <noreply@anthropic.com>
This commit is contained in:
@@ -148,6 +148,20 @@ export function attachPluginsAndInit(bot) {
|
|||||||
bot.loadPlugin(pvp);
|
bot.loadPlugin(pvp);
|
||||||
bot.loadPlugin(collectblock);
|
bot.loadPlugin(collectblock);
|
||||||
bot.loadPlugin(armorManager);
|
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', () => {
|
bot.once('login', () => {
|
||||||
mc_version = bot.version;
|
mc_version = bot.version;
|
||||||
mcdata = minecraftData(mc_version);
|
mcdata = minecraftData(mc_version);
|
||||||
|
|||||||
@@ -268,14 +268,45 @@ export default async function mindcraftSkills(pi: ExtensionAPI) {
|
|||||||
async execute(_id, params: { blockType: string; count?: number }) {
|
async execute(_id, params: { blockType: string; count?: number }) {
|
||||||
const bot = getBot();
|
const bot = getBot();
|
||||||
const count = Math.max(1, Math.min(64, Math.floor(params.count ?? 1)));
|
const count = Math.max(1, Math.min(64, Math.floor(params.count ?? 1)));
|
||||||
// Timeout scales with count: ~30s per block plus 30s overhead.
|
// Bulk collection in dense terrain reliably times out on the
|
||||||
// Allows count=8 (about 4 min) without hanging forever on
|
// upstream mineflayer-collectblock plugin (observed live: count=1
|
||||||
// pathfinder-unreachable cases.
|
// works in ~25s, count=8 hangs past 270s with the same blocks
|
||||||
const timeoutMs = Math.min(600_000, 30_000 + count * 30_000);
|
// in range). Loop single-block collects instead — each iteration
|
||||||
const ok = await safeCall("collectBlock", () =>
|
// re-pathfinds from the bot's current position, which is robust
|
||||||
withTimeout(skills.collectBlock(bot, params.blockType, count), timeoutMs, `collectBlock(${params.blockType}, ${count})`),
|
// 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 });
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -419,6 +419,9 @@ export default function mineflayerBridge(pi: ExtensionAPI) {
|
|||||||
let lastHumanChatAt = Date.now();
|
let lastHumanChatAt = Date.now();
|
||||||
let lastAutonomyPromptAt = 0;
|
let lastAutonomyPromptAt = 0;
|
||||||
let startupMemoryReviewed = false;
|
let startupMemoryReviewed = false;
|
||||||
|
let lastDeathAt = 0;
|
||||||
|
let autoDefendTimer: ReturnType<typeof setInterval> | undefined;
|
||||||
|
let lastAutoDefendAt = 0;
|
||||||
|
|
||||||
function log(event: string, detail?: unknown) {
|
function log(event: string, detail?: unknown) {
|
||||||
const suffix = detail === undefined ? "" : `: ${truncate(redact(stringifyUnknown(detail), config))}`;
|
const suffix = detail === undefined ? "" : `: ${truncate(redact(stringifyUnknown(detail), config))}`;
|
||||||
@@ -912,6 +915,57 @@ export default function mineflayerBridge(pi: ExtensionAPI) {
|
|||||||
autonomyTimer = undefined;
|
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<any>;
|
||||||
|
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) {
|
function queueOperatorLearning(rawFrom: string, rawText: string, entry: ChatEntry) {
|
||||||
const recent = recentChat.slice(-10).map(formatChatEntry).join("\n") || "(no recent chat)";
|
const recent = recentChat.slice(-10).map(formatChatEntry).join("\n") || "(no recent chat)";
|
||||||
const prompt = [
|
const prompt = [
|
||||||
@@ -1122,6 +1176,26 @@ export default function mineflayerBridge(pi: ExtensionAPI) {
|
|||||||
reconnectPausedReason = undefined;
|
reconnectPausedReason = undefined;
|
||||||
log("spawn");
|
log("spawn");
|
||||||
startAuthDetection();
|
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) => {
|
nextBot.on("chat", (username, message) => {
|
||||||
if (username !== current.username) lastHumanChatAt = Date.now();
|
if (username !== current.username) lastHumanChatAt = Date.now();
|
||||||
@@ -1631,6 +1705,7 @@ export default function mineflayerBridge(pi: ExtensionAPI) {
|
|||||||
surfaceEscalationCount();
|
surfaceEscalationCount();
|
||||||
connect("startup");
|
connect("startup");
|
||||||
startAutonomyTimer();
|
startAutonomyTimer();
|
||||||
|
startAutoDefendTimer();
|
||||||
if (ctx.hasUI) ctx.ui.setStatus("mineflayer", "mc: connecting");
|
if (ctx.hasUI) ctx.ui.setStatus("mineflayer", "mc: connecting");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log("startup-error", error);
|
log("startup-error", error);
|
||||||
@@ -1641,6 +1716,7 @@ export default function mineflayerBridge(pi: ExtensionAPI) {
|
|||||||
pi.on("session_shutdown", async () => {
|
pi.on("session_shutdown", async () => {
|
||||||
shuttingDown = true;
|
shuttingDown = true;
|
||||||
stopAutonomyTimer();
|
stopAutonomyTimer();
|
||||||
|
stopAutoDefendTimer();
|
||||||
disconnect({ manual: true });
|
disconnect({ manual: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user