fix(auto-improve): detach auto-patch + recovery-tunnel-out test in suite

Two bugs the live self-improvement run exposed:

1) Auto-patch was spawned with detached:false, so when supervisor
   restarted bot.js (file change after Pi's commit landed on the
   auto branch), the auto-patch child was killed mid-way — Pi's
   commit lived in the auto branch but never got cherry-picked.
   Recovered manually this round via reflog + cherry-pick. Now
   detached:true + child.unref() + a per-run log at
   state/_auto-patch-last.log so the operator can read Pi's full
   output later.

2) Pi's recovery-tunnel-out.test.js was created but not in npm test
   script; tests would have stayed unrun forever. Added.

Also commits the Pi-authored skill (eb29591 cherry-picked):
- runtime/skills/recovery-tunnel-out.js (+ test)
- improvements to runtime/actions.js + runtime/skills/explore-far.js
- wired into runtime/skills/index.js

npm test 137/137.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 12:43:30 +03:00
co-authored by Claude Opus 4.7
parent 564557450d
commit 6560c0765c
7 changed files with 102 additions and 39 deletions
+15 -6
View File
@@ -152,8 +152,12 @@ function clonePos(pos) {
return { x: pos?.x ?? 0, y: pos?.y ?? 0, z: pos?.z ?? 0 };
}
function movedDistance(a, b) {
return Math.hypot((b?.x ?? 0) - (a?.x ?? 0), (b?.y ?? 0) - (a?.y ?? 0), (b?.z ?? 0) - (a?.z ?? 0));
function horizontalDistance(a, b) {
return Math.hypot((b?.x ?? 0) - (a?.x ?? 0), (b?.z ?? 0) - (a?.z ?? 0));
}
function verticalGain(a, b) {
return (b?.y ?? 0) - (a?.y ?? 0);
}
async function escapePit(bot, maxSteps = 3) {
@@ -182,16 +186,21 @@ async function escapePit(bot, maxSteps = 3) {
await new Promise((r) => setTimeout(r, 400));
}
const moved = movedDistance(before, bot.entity.position);
if (moved >= 0.75) {
// Let jump physics settle before deciding whether escape-pit worked.
// A mid-jump Y delta is not freedom; require horizontal movement or a
// sustained one-block climb before reporting success.
await new Promise((r) => setTimeout(r, 500));
const moved = horizontalDistance(before, bot.entity.position);
const climbed = verticalGain(before, bot.entity.position);
if (moved >= 0.75 || climbed >= 0.9) {
return {
ok: true,
code: "done",
detail: { mode: "escape-pit-up", moved },
detail: { mode: "escape-pit-up", moved, climbed },
worldDelta: { mode: "escape-pit-up", movedTo: clonePos(bot.entity.position) },
};
}
info("action", `escape-pit moved only ${moved.toFixed(2)} blocks → tunnel-out`);
info("action", `escape-pit moved only ${moved.toFixed(2)} horizontally (dy=${climbed.toFixed(2)}) → tunnel-out`);
return digEscapeTunnel(bot, { maxSteps: 3, reason: "explore.far escape-pit" });
}
+10 -5
View File
@@ -147,8 +147,12 @@ function centerOf(pos) {
return { x: (pos?.x ?? 0) + 0.5, y: (pos?.y ?? 0) + 0.5, z: (pos?.z ?? 0) + 0.5 };
}
function distance(a, b) {
return Math.hypot((b?.x ?? 0) - (a?.x ?? 0), (b?.y ?? 0) - (a?.y ?? 0), (b?.z ?? 0) - (a?.z ?? 0));
function horizontalDistance(a, b) {
return Math.hypot((b?.x ?? 0) - (a?.x ?? 0), (b?.z ?? 0) - (a?.z ?? 0));
}
function verticalDelta(a, b) {
return (b?.y ?? 0) - (a?.y ?? 0);
}
function isLiquidBlock(block) {
@@ -329,17 +333,18 @@ export async function digEscapeTunnel(bot, { maxSteps = 3, minMove = 0.75, pushM
await digOne(bot, target.block);
}
await pushForward(bot, dir.yaw, pushMs);
const moved = distance(before, bot.entity.position);
const moved = horizontalDistance(before, bot.entity.position);
const movedY = verticalDelta(before, bot.entity.position);
if (moved >= minMove) {
const movedTo = posClone(bot.entity.position);
return {
ok: true,
code: "done",
detail: { mode: "tunnel-out", dir: dir.name, moved, dug: dir.digTargets.length },
detail: { mode: "tunnel-out", dir: dir.name, moved, movedY, dug: dir.digTargets.length },
worldDelta: { mode: "tunnel-out", movedTo },
};
}
lastError = `dug ${dir.name} but moved only ${moved.toFixed(2)}`;
lastError = `dug ${dir.name} but moved only ${moved.toFixed(2)} horizontally (dy=${movedY.toFixed(2)})`;
warn("action", `tunnel-out: ${lastError}`);
} catch (e) {
lastError = e?.message ?? String(e);
+27 -1
View File
@@ -2,7 +2,7 @@ import { test } from "node:test";
import assert from "node:assert/strict";
import { getSkill } from "./index.js";
import { _internal } from "./recovery-tunnel-out.js";
import { digEscapeTunnel, _internal } from "./recovery-tunnel-out.js";
function makePos(x, y, z) {
return {
@@ -69,3 +69,29 @@ test("safe dig guard allows natural blocks and rejects build/storage blocks", ()
assert.equal(_internal.isSafeTunnelDigTarget(bot, makeBlock("chest", 1, 64, 0)), false);
assert.equal(_internal.isSafeTunnelDigTarget(bot, makeBlock("bedrock", 1, 64, 0)), false);
});
test("tunnel-out does not count jumping in place as escape", async () => {
const blocks = {};
for (let step = 1; step <= 3; step++) {
blocks[`0,64,${-step}`] = "air";
blocks[`0,65,${-step}`] = "air";
blocks[`0,63,${-step}`] = "dirt";
}
// Make the other cardinals unusable so the test exercises one clean
// tunnel candidate and then verifies vertical-only movement is rejected.
blocks["1,64,0"] = "oak_planks";
blocks["0,64,1"] = "oak_planks";
blocks["-1,64,0"] = "oak_planks";
const bot = makeBot(blocks);
bot.setControlState = (control, on) => {
if (control === "jump" && on) {
bot.entity.position = makePos(bot.entity.position.x, bot.entity.position.y + 1, bot.entity.position.z);
}
};
const res = await digEscapeTunnel(bot, { maxSteps: 3, minMove: 0.75, pushMs: 0 });
assert.equal(res.ok, false);
assert.equal(res.code, "wedged");
assert.match(res.detail.error, /moved only 0\.00 horizontally/);
});