fix(mindcraft-skills): hard timeout on every skill call

mc_avoid_enemies (and 7 other tools) wrapped only in safeCall without a
withTimeout. When mindcraft's underlying pathfinder/pvp goal couldn't be
satisfied, the call never resolved — the Pi tick loop blocked forever.
Observed live: mc_avoid_enemies pending >10 minutes after one mc_observe.

safeCall now takes timeoutMs (default 30s) and wraps withTimeout itself,
so every tool gets a hard ceiling. Per-tool overrides:
  - goToPosition / goToNearestBlock: 120s / 90s (unchanged from before)
  - defendSelf / avoidEnemies: 45s
  - stay: secs*1000 + 10s
  - craft / consume / pickup / place: 30s
  - equip: 15s
collectBlock still uses its bespoke per-iter 75s loop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 15:59:11 +03:00
co-authored by Claude Opus 4.7
parent 989ddf335e
commit 4da886c1d6
+36 -18
View File
@@ -165,9 +165,19 @@ export default async function mindcraftSkills(pi: ExtensionAPI) {
const skills = skillsMod as Record<string, (...args: any[]) => any>; const skills = skillsMod as Record<string, (...args: any[]) => any>;
const world = worldMod as Record<string, (...args: any[]) => any>; const world = worldMod as Record<string, (...args: any[]) => any>;
const safeCall = async <T>(label: string, fn: () => T | Promise<T>): Promise<T> => { // safeCall: wraps a skill call with error labeling AND a hard timeout.
// Without a timeout the Mindcraft skills (defendSelf / avoidEnemies / stay
// / craftRecipe / etc.) can hang forever inside pathfinder / pvp loops if
// the goal is unreachable, blocking the entire Pi loop. Observed live:
// mc_avoid_enemies pending >10 min with no progress. Default 30s; callers
// override per-tool (goToPosition gets 120s, etc).
const safeCall = async <T>(
label: string,
fn: () => T | Promise<T>,
timeoutMs: number = 30_000,
): Promise<T> => {
try { try {
return await fn(); return await withTimeout(Promise.resolve().then(fn), timeoutMs, label);
} catch (e: unknown) { } catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e); const msg = e instanceof Error ? e.message : String(e);
throw new Error(`${label}: ${msg}`); throw new Error(`${label}: ${msg}`);
@@ -319,7 +329,11 @@ export default async function mindcraftSkills(pi: ExtensionAPI) {
executionMode: "sequential", executionMode: "sequential",
async execute(_id, params: { blockType: string; x: number; y: number; z: number }) { async execute(_id, params: { blockType: string; x: number; y: number; z: number }) {
const bot = getBot(); const bot = getBot();
const ok = await safeCall("placeBlock", () => skills.placeBlock(bot, params.blockType, params.x, params.y, params.z)); const ok = await safeCall(
"placeBlock",
() => skills.placeBlock(bot, params.blockType, params.x, params.y, params.z),
30_000,
);
return textResult(ok ? `Placed ${params.blockType} at ${params.x},${params.y},${params.z}.` : `placeBlock returned false.`, { ok, ...params }); return textResult(ok ? `Placed ${params.blockType} at ${params.x},${params.y},${params.z}.` : `placeBlock returned false.`, { ok, ...params });
}, },
}); });
@@ -333,8 +347,10 @@ export default async function mindcraftSkills(pi: ExtensionAPI) {
executionMode: "sequential", executionMode: "sequential",
async execute(_id, params: { x: number; y: number; z: number; minDistance?: number }) { async execute(_id, params: { x: number; y: number; z: number; minDistance?: number }) {
const bot = getBot(); const bot = getBot();
const ok = await safeCall("goToPosition", () => const ok = await safeCall(
withTimeout(skills.goToPosition(bot, params.x, params.y, params.z, params.minDistance ?? 2), 120_000, `goToPosition(${params.x},${params.y},${params.z})`), `goToPosition(${params.x},${params.y},${params.z})`,
() => skills.goToPosition(bot, params.x, params.y, params.z, params.minDistance ?? 2),
120_000,
); );
return textResult(ok ? `Arrived near ${params.x},${params.y},${params.z}.` : `goToPosition returned false.`, { ok, ...params }); return textResult(ok ? `Arrived near ${params.x},${params.y},${params.z}.` : `goToPosition returned false.`, { ok, ...params });
}, },
@@ -349,12 +365,10 @@ export default async function mindcraftSkills(pi: ExtensionAPI) {
executionMode: "sequential", executionMode: "sequential",
async execute(_id, params: { blockType: string; minDistance?: number; range?: number }) { async execute(_id, params: { blockType: string; minDistance?: number; range?: number }) {
const bot = getBot(); const bot = getBot();
const ok = await safeCall("goToNearestBlock", () => const ok = await safeCall(
withTimeout( `goToNearestBlock(${params.blockType})`,
skills.goToNearestBlock(bot, params.blockType, params.minDistance ?? 2, params.range ?? 64), () => skills.goToNearestBlock(bot, params.blockType, params.minDistance ?? 2, params.range ?? 64),
90_000, 90_000,
`goToNearestBlock(${params.blockType})`,
),
); );
return textResult(ok ? `Arrived near nearest ${params.blockType}.` : `goToNearestBlock returned false for ${params.blockType}.`, { ok, ...params }); return textResult(ok ? `Arrived near nearest ${params.blockType}.` : `goToNearestBlock returned false for ${params.blockType}.`, { ok, ...params });
}, },
@@ -369,7 +383,11 @@ export default async function mindcraftSkills(pi: ExtensionAPI) {
executionMode: "sequential", executionMode: "sequential",
async execute(_id, params: { itemName: string; num?: number }) { async execute(_id, params: { itemName: string; num?: number }) {
const bot = getBot(); const bot = getBot();
const ok = await safeCall("craftRecipe", () => skills.craftRecipe(bot, params.itemName, Math.max(1, Math.floor(params.num ?? 1)))); const ok = await safeCall(
"craftRecipe",
() => skills.craftRecipe(bot, params.itemName, Math.max(1, Math.floor(params.num ?? 1))),
30_000,
);
return textResult(ok ? `Crafted ${params.itemName}.` : `craftRecipe returned false.`, { ok, ...params }); return textResult(ok ? `Crafted ${params.itemName}.` : `craftRecipe returned false.`, { ok, ...params });
}, },
}); });
@@ -383,7 +401,7 @@ export default async function mindcraftSkills(pi: ExtensionAPI) {
executionMode: "sequential", executionMode: "sequential",
async execute(_id, params: { itemName: string }) { async execute(_id, params: { itemName: string }) {
const bot = getBot(); const bot = getBot();
const ok = await safeCall("equip", () => skills.equip(bot, params.itemName)); const ok = await safeCall("equip", () => skills.equip(bot, params.itemName), 15_000);
return textResult(ok ? `Equipped ${params.itemName}.` : `equip returned false for ${params.itemName}.`, { ok, ...params }); return textResult(ok ? `Equipped ${params.itemName}.` : `equip returned false for ${params.itemName}.`, { ok, ...params });
}, },
}); });
@@ -397,7 +415,7 @@ export default async function mindcraftSkills(pi: ExtensionAPI) {
executionMode: "sequential", executionMode: "sequential",
async execute(_id, params: { itemName?: string }) { async execute(_id, params: { itemName?: string }) {
const bot = getBot(); const bot = getBot();
const ok = await safeCall("consume", () => skills.consume(bot, params.itemName ?? "")); const ok = await safeCall("consume", () => skills.consume(bot, params.itemName ?? ""), 30_000);
return textResult(ok ? `Ate ${params.itemName ?? "food"}.` : `consume returned false.`, { ok, ...params }); return textResult(ok ? `Ate ${params.itemName ?? "food"}.` : `consume returned false.`, { ok, ...params });
}, },
}); });
@@ -411,7 +429,7 @@ export default async function mindcraftSkills(pi: ExtensionAPI) {
executionMode: "sequential", executionMode: "sequential",
async execute(_id, params: { range?: number }) { async execute(_id, params: { range?: number }) {
const bot = getBot(); const bot = getBot();
const ok = await safeCall("defendSelf", () => skills.defendSelf(bot, params.range ?? 9)); const ok = await safeCall("defendSelf", () => skills.defendSelf(bot, params.range ?? 9), 45_000);
return textResult(ok ? `Defended against hostiles.` : `defendSelf returned false.`, { ok, ...params }); return textResult(ok ? `Defended against hostiles.` : `defendSelf returned false.`, { ok, ...params });
}, },
}); });
@@ -425,7 +443,7 @@ export default async function mindcraftSkills(pi: ExtensionAPI) {
executionMode: "sequential", executionMode: "sequential",
async execute(_id, params: { distance?: number }) { async execute(_id, params: { distance?: number }) {
const bot = getBot(); const bot = getBot();
const ok = await safeCall("avoidEnemies", () => skills.avoidEnemies(bot, params.distance ?? 16)); const ok = await safeCall("avoidEnemies", () => skills.avoidEnemies(bot, params.distance ?? 16), 45_000);
return textResult(ok ? `Avoided enemies.` : `avoidEnemies returned false.`, { ok, ...params }); return textResult(ok ? `Avoided enemies.` : `avoidEnemies returned false.`, { ok, ...params });
}, },
}); });
@@ -440,7 +458,7 @@ export default async function mindcraftSkills(pi: ExtensionAPI) {
async execute(_id, params: { seconds?: number }) { async execute(_id, params: { seconds?: number }) {
const bot = getBot(); const bot = getBot();
const secs = Math.max(1, Math.min(600, Math.floor(params.seconds ?? 30))); const secs = Math.max(1, Math.min(600, Math.floor(params.seconds ?? 30)));
await safeCall("stay", () => skills.stay(bot, secs)); await safeCall("stay", () => skills.stay(bot, secs), secs * 1000 + 10_000);
return textResult(`Stood still for ${secs}s.`, { seconds: secs }); return textResult(`Stood still for ${secs}s.`, { seconds: secs });
}, },
}); });
@@ -454,7 +472,7 @@ export default async function mindcraftSkills(pi: ExtensionAPI) {
executionMode: "sequential", executionMode: "sequential",
async execute() { async execute() {
const bot = getBot(); const bot = getBot();
const ok = await safeCall("pickupNearbyItems", () => skills.pickupNearbyItems(bot)); const ok = await safeCall("pickupNearbyItems", () => skills.pickupNearbyItems(bot), 30_000);
return textResult(ok ? `Picked up nearby items.` : `pickupNearbyItems returned false.`); return textResult(ok ? `Picked up nearby items.` : `pickupNearbyItems returned false.`);
}, },
}); });