* 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> * feat(runtime): script-driven reflex daemon + Ink TUI dashboard Pure-Pi runtime had three failure modes in practice: - slow: 20-60s per decision because LLM was in the hot path - expensive: every tick (defend, eat, idle) paid for a reasoning pass - invisible: required tmux capture-pane to know what the bot was doing New runtime/ layer is a long-running Node daemon that owns the MC connection, ticks a priority-ordered reflex chain (defend > eat > sleep > idle) with NO LLM in the hot path, and exposes status + commands over a Unix-socket IPC. tui/ is an Ink dashboard that attaches over IPC and can detach freely — multiple TUI clients can connect at once. Pi/Codex are still available, but as on-demand escalation: TUI hotkey 'a' spawns `pi -p "<prompt>"` as a subprocess and streams stdout into the dashboard. The self-improvement loop (proposals → operator approval → Pi-driven patch → hot reload) is documented in docs/runtime.md but not yet wired. Reflex bodies are stubs today — they log decisions but don't drive Mineflayer actions yet. The priority chain, IPC contract, and TUI are fully working; subsequent commits will fill in defend/eat/sleep bodies and wire automatic escalation. Run with `npm run bot` + `npm run tui`. Pi-only fallback stays at `npm run agent`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
1.8 KiB
JavaScript
70 lines
1.8 KiB
JavaScript
// Build a compact, JSON-safe snapshot of the world around the bot. Used both
|
|
// for reflex decisions and for periodic IPC STATUS events.
|
|
|
|
function vec3ToObj(v) {
|
|
if (!v) return null;
|
|
return { x: Math.round(v.x * 100) / 100, y: Math.round(v.y * 100) / 100, z: Math.round(v.z * 100) / 100 };
|
|
}
|
|
|
|
const HOSTILE = new Set([
|
|
"zombie",
|
|
"skeleton",
|
|
"creeper",
|
|
"spider",
|
|
"witch",
|
|
"pillager",
|
|
"vindicator",
|
|
"husk",
|
|
"stray",
|
|
"drowned",
|
|
"phantom",
|
|
"enderman",
|
|
"slime",
|
|
"magma_cube",
|
|
"hoglin",
|
|
"piglin_brute",
|
|
"ravager",
|
|
"warden",
|
|
"breeze",
|
|
"bogged",
|
|
]);
|
|
|
|
export function snapshot(bot) {
|
|
if (!bot || !bot.entity) {
|
|
return { connected: false };
|
|
}
|
|
const pos = bot.entity.position;
|
|
const entities = Object.values(bot.entities || {});
|
|
const players = entities.filter((e) => e.type === "player" && e.username && e.username !== bot.username);
|
|
const hostiles = entities.filter((e) => HOSTILE.has((e.name || "").toLowerCase()));
|
|
const closestHostile = hostiles.reduce((best, e) => {
|
|
const d = e.position.distanceTo(pos);
|
|
return !best || d < best.d ? { d, e } : best;
|
|
}, null);
|
|
|
|
const inventory = (bot.inventory?.items?.() ?? []).reduce((acc, item) => {
|
|
acc[item.name] = (acc[item.name] ?? 0) + item.count;
|
|
return acc;
|
|
}, {});
|
|
|
|
return {
|
|
connected: true,
|
|
username: bot.username,
|
|
position: vec3ToObj(pos),
|
|
health: bot.health,
|
|
food: bot.food,
|
|
saturation: bot.foodSaturation,
|
|
experience: bot.experience?.level,
|
|
time: bot.time?.timeOfDay,
|
|
isDay: bot.time?.isDay,
|
|
weather: { rain: bot.isRaining, thunder: bot.thundering },
|
|
dimension: bot.game?.dimension,
|
|
inventory,
|
|
players: players.map((p) => ({ name: p.username, distance: Math.round(p.position.distanceTo(pos)) })),
|
|
hostileCount: hostiles.length,
|
|
closestHostile: closestHostile
|
|
? { name: closestHostile.e.name, distance: Math.round(closestHostile.d * 10) / 10 }
|
|
: null,
|
|
};
|
|
}
|