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:
@@ -59,7 +59,7 @@ async function main() {
|
||||
process.stdout.write(`${flag} ${s.state.padEnd(8)} ${s.project.padEnd(14)} ${s.agent.padEnd(16)} ${age.padStart(7)} ${s.lastAssistantText ? s.lastAssistantText.slice(0, 80) : ""}\n`);
|
||||
}
|
||||
}
|
||||
process.stdout.write(`board: ${boardDir} (${index.sessions.length} sessions, ${index.waitingOnYou.length} waiting on you, ${index.seen.length} seen, ${index.registered.length} registered)\n`);
|
||||
process.stdout.write(`board: ${boardDir} (${index.sessions.length} sessions, ${index.waitingOnYou.length} waiting on you, ${index.seen.length} seen, ${index.registered.length} registered, ${index.registrationStale.length} stale)\n`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
|
||||
@@ -197,6 +197,7 @@
|
||||
if (reg.pid) parts.push("pid " + esc(reg.pid));
|
||||
if (reg.tmux && reg.tmux.session) parts.push("tmux " + esc(reg.tmux.session) + (reg.tmux.socket ? " (socket " + esc(reg.tmux.socket) + ")" : ""));
|
||||
if (reg.layout) parts.push(esc(reg.layout) + " layout");
|
||||
if (reg.alive === false) parts.push("stale: pid " + esc(reg.pid) + " is gone, so the derived values are shown");
|
||||
return parts.join("; ");
|
||||
}
|
||||
|
||||
|
||||
@@ -276,6 +276,18 @@ export function matchRegistration(spec, registrations) {
|
||||
return registrations.find((r) => r.sessionsDir && samePath(r.sessionsDir, spec.sessionsDir)) ?? null;
|
||||
}
|
||||
|
||||
// True when a pid is running (a signal-0 probe; EPERM still means running),
|
||||
// false when it is gone, null when there is no pid to check.
|
||||
export function pidAlive(pid) {
|
||||
if (!(Number.isInteger(pid) && pid > 0)) return null;
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return err.code === "EPERM";
|
||||
}
|
||||
}
|
||||
|
||||
// One agent -> one status record.
|
||||
//
|
||||
// Three fields answer "what is this seat doing, and where" (Gate A ask,
|
||||
@@ -289,7 +301,11 @@ export function matchRegistration(spec, registrations) {
|
||||
// else the session log's cwd.
|
||||
// activeProject registration.project, else the basename of the nearest git
|
||||
// repo root above the workspace.
|
||||
export function scanAgent(spec, { isAlive = tmuxInspect, now = () => new Date(), seen = {}, registration = null } = {}) {
|
||||
// A registration is written before the launch script's own checks run, so a
|
||||
// refused launch leaves a record whose pid is gone. Such a record is stale:
|
||||
// it is still reported under `registered` (with alive false) but the derived
|
||||
// values win, because the record describes a launch that is not running.
|
||||
export function scanAgent(spec, { isAlive = tmuxInspect, now = () => new Date(), seen = {}, registration = null, isPidAlive = pidAlive } = {}) {
|
||||
const live = liveness(isAlive(spec.tmux));
|
||||
const alive = live.alive;
|
||||
const file = findNewestSession(spec.sessionsDir);
|
||||
@@ -301,7 +317,9 @@ export function scanAgent(spec, { isAlive = tmuxInspect, now = () => new Date(),
|
||||
const needsYou = state === "waiting" || state === "error";
|
||||
const isSeen = needsYou && lastActivity !== null && seen[seenKey(spec)] === lastActivity;
|
||||
const cwd = session?.cwd ?? null;
|
||||
const reg = registration && typeof registration === "object" ? registration : null;
|
||||
const record = registration && typeof registration === "object" ? registration : null;
|
||||
const registeredAlive = record ? isPidAlive(record.pid) : null;
|
||||
const reg = record && registeredAlive !== false ? record : null;
|
||||
const derivedWorkspace = live.workspace ?? cwd;
|
||||
const workspace = reg?.workspace ?? derivedWorkspace;
|
||||
const workspaceSource = reg?.workspace ? "registration" : live.workspace ? "tmux-pane" : cwd ? "session-cwd" : null;
|
||||
@@ -329,8 +347,8 @@ export function scanAgent(spec, { isAlive = tmuxInspect, now = () => new Date(),
|
||||
workspaceSource,
|
||||
activeProject,
|
||||
activeProjectSource,
|
||||
registered: reg
|
||||
? { startedAt: reg.startedAt, updatedAt: reg.updatedAt, harness: reg.harness, pid: reg.pid, tmux: reg.tmux, layout: reg.layout, launchScript: reg.launchScript }
|
||||
registered: record
|
||||
? { startedAt: record.startedAt, updatedAt: record.updatedAt, harness: record.harness, pid: record.pid, alive: registeredAlive, tmux: record.tmux, layout: record.layout, launchScript: record.launchScript }
|
||||
: null,
|
||||
lastActivity,
|
||||
ageSeconds,
|
||||
@@ -369,12 +387,12 @@ function writeAtomic(path, data) {
|
||||
|
||||
// Scan every spec and write <boardDir>/sessions/<project>/<agent>.json plus index.json.
|
||||
// seatsDir (optional): where `mosaic launch` registrations live; read only.
|
||||
export function scan(specs, { boardDir, isAlive, now, seatsDir = null } = {}) {
|
||||
export function scan(specs, { boardDir, isAlive, now, seatsDir = null, isPidAlive } = {}) {
|
||||
if (!boardDir || !isAbsolute(boardDir)) throw new ConfigError("boardDir must be an absolute path");
|
||||
if (seatsDir !== null && (typeof seatsDir !== "string" || !isAbsolute(seatsDir))) throw new ConfigError("seatsDir must be an absolute path or null");
|
||||
const seen = loadSeen(boardDir);
|
||||
const { registrations, errors: registrationErrors } = loadRegistrations(seatsDir);
|
||||
const records = specs.map((spec) => scanAgent(spec, { isAlive, now, seen, registration: matchRegistration(spec, registrations) }));
|
||||
const records = specs.map((spec) => scanAgent(spec, { isAlive, now, seen, registration: matchRegistration(spec, registrations), isPidAlive }));
|
||||
for (const rec of records) {
|
||||
const dir = join(boardDir, "sessions", rec.project);
|
||||
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
@@ -387,6 +405,7 @@ export function scan(specs, { boardDir, isAlive, now, seatsDir = null } = {}) {
|
||||
waitingOnYou: records.filter((r) => r.waitingOnYou).map(seenKey),
|
||||
seen: records.filter((r) => r.seen).map(seenKey),
|
||||
registered: records.filter((r) => r.registered).map(seenKey),
|
||||
registrationStale: records.filter((r) => r.registered && r.registered.alive === false).map(seenKey),
|
||||
registrationErrors,
|
||||
sessions: records,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user