control board: stale registrations and a launch-test leak (#1504)

Two defects in 69f99323, reported by the professor session and verified.

The darkwing launch test's flock-contention spawn ran without the fixture
config, so launch.sh re-entered scripts/mosaic against the real data root
and wrote fixture records for darkwing, dewey and filbert there. That
spawn now names the fixture config, and both launch test files set
MOSAIC_CONFIG to a nonexistent path and clear MOSAIC_LAUNCH_REGISTERED
process-wide, so a spawn that forgets fails instead of polluting.

A registration is written before the launch script's own checks, so a
refused launch left a record with a dead pid that the board honoured. The
scanner now probes the recorded pid (pidAlive, signal 0); a gone pid makes
the record stale: still on the Registered line with alive false, derived
task, project and workspace win, index gains registrationStale, CLI
summary gains a stale count.

Fleet launchers marked not planned per Jason. Board 90/90, seat 15/15,
launch scripts 5/5. Sonnet review APPROVED.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-09-12 11:04:49 -05:00
co-authored by Claude Fable 5.1
parent 69f99323c7
commit 17153fe140
14 changed files with 166 additions and 18 deletions
+51 -2
View File
@@ -34,6 +34,7 @@ import {
markSeen,
loadRegistrations,
matchRegistration,
pidAlive,
} from "../src/scan.mjs";
import { writeRegistration, makeRegistration } from "../../seat/src/seat.mjs";
@@ -444,7 +445,7 @@ test("registration: overrides task, project and workspace; every source says reg
workspace: "/registered/workspace",
harness: "pi",
tmux: { socket: null, session: "a" },
pid: 4242,
pid: process.pid,
now: () => new Date("2026-09-12T13:00:00Z"),
});
@@ -459,7 +460,8 @@ test("registration: overrides task, project and workspace; every source says reg
assert.ok(rec.registered, "registered must be non-null once a matching registration is passed");
assert.equal(rec.registered.startedAt, registration.startedAt);
assert.equal(rec.registered.harness, "pi");
assert.equal(rec.registered.pid, 4242);
assert.equal(rec.registered.pid, process.pid);
assert.equal(rec.registered.alive, true, "the default pid probe sees this test process");
assert.deepEqual(rec.registered.tmux, { socket: null, session: "a" });
assert.equal(rec.registered.layout, "repo");
assert.equal(rec.registered.launchScript, join(root, "seat-dir", "launch.sh"));
@@ -501,6 +503,52 @@ test("registration: empty task and null project/workspace leave the derived valu
assert.notEqual(rec.registered, null, "an empty task and null project/workspace still leave a registration attached");
});
test("registration: a record whose pid is gone is stale; derived values win, sources say derived, registered stays with alive false; a pid the probe cannot decide is not stale; pidAlive itself", () => {
const root = makeRoot();
const sessionsDir = join(root, "sessions");
writeSessionFile(sessionsDir, "s.jsonl", [
sessionLine({ id: "s1", timestamp: "2026-09-12T14:00:00Z", cwd: "/derived/cwd" }),
messageLine({ timestamp: "2026-09-12T14:00:01Z", role: "user", texts: ["derived task"] }),
]);
const spec = { agent: "a", project: "p", sessionsDir, tmux: {} };
const registration = makeRegistration({
resolved: { seat: "a", project: "resolved-project", sessionsDir, seatDir: join(root, "seat-dir"), launchScript: join(root, "seat-dir", "launch.sh"), layout: "repo", defaultWorkspace: "/resolved/workspace" },
task: "registered task", project: "registered-project", workspace: "/registered/workspace", harness: "pi", pid: 4242,
});
const probed = [];
const rec = scanAgent(spec, { isAlive: () => ({ alive: true, workspace: null }), now: GATE_NOW, registration, isPidAlive: (pid) => { probed.push(pid); return false; } });
assert.deepEqual(probed, [4242], "the probe is asked about the recorded pid");
assert.equal(rec.task, "derived task");
assert.equal(rec.taskSource, "first-user-message");
assert.equal(rec.activeProject, null, "no git root above /derived/cwd");
assert.equal(rec.activeProjectSource, null);
assert.equal(rec.workspace, "/derived/cwd");
assert.equal(rec.workspaceSource, "session-cwd");
assert.ok(rec.registered, "a stale record is still reported, not hidden");
assert.equal(rec.registered.alive, false);
assert.equal(rec.registered.pid, 4242);
assert.equal(rec.registered.startedAt, registration.startedAt);
// null from the probe (no pid recorded) keeps the override.
const undecided = scanAgent(spec, { isAlive: () => true, now: GATE_NOW, registration, isPidAlive: () => null });
assert.equal(undecided.task, "registered task");
assert.equal(undecided.registered.alive, null);
// scan(): the stale row is listed under registrationStale and still under registered.
const boardDir = join(root, "board");
const seatsDir = join(root, "seats");
writeRegistration(seatsDir, registration);
const index = scan([spec], { boardDir, seatsDir, isAlive: () => true, now: GATE_NOW, isPidAlive: () => false });
assert.deepEqual(index.registered, ["p/a"]);
assert.deepEqual(index.registrationStale, ["p/a"]);
assert.equal(index.sessions[0].taskSource, "first-user-message");
assert.equal(pidAlive(process.pid), true);
assert.equal(pidAlive(null), null);
assert.equal(pidAlive(0), null);
assert.equal(pidAlive(-1), null);
});
test("registration: no registration leaves the Gate A fields exactly as before, and registered is null", () => {
const root = makeRoot();
const repo = join(root, "repo");
@@ -643,6 +691,7 @@ test("scan: writes the registration override to disk; index.json carries registe
const spec = { agent: "a", project: "p", sessionsDir, tmux: {} };
const index = scan([spec], { boardDir, seatsDir, isAlive: () => ({ alive: true, workspace: null }), now: GATE_NOW });
assert.deepEqual(index.registered, ["p/a"]);
assert.deepEqual(index.registrationStale, [], "a record with pid null is never stale");
assert.deepEqual(index.registrationErrors, []);
const onDisk = JSON.parse(readFileSync(join(boardDir, "sessions", "p", "a.json"), "utf8"));
assert.equal(onDisk.task, "registered task");