fix(daemon): relaunch after stopping the last seat (#325)

This commit is contained in:
Wu Shuwen
2026-10-01 08:51:21 -07:00
committed by GitHub
parent dc6ab9eed5
commit 6f7d6a4ee4
2 changed files with 48 additions and 1 deletions
@@ -501,10 +501,41 @@ export class SeatLifecycleService {
).all(node.id) as Array<{ id: string }>).map((row) => row.id);
const retiringGeneration = this.sessionRegistry.currentOccupantTenure(node.id)?.generationUuid ?? null;
const canonicalProbe = await this.probeLiveness(
let canonicalProbe = await this.probeLiveness(
canonicalSessionName,
"fresh launch refuses rather than overwrite a possibly-live canonical session",
);
// `rig seat stop` persists an exited row after killing its managed session.
// When that was the last tmux session, tmux exits too, so the next classified
// probe sees unavailable transport instead of positive absence. Reopen an
// empty server only when the persisted state proves this seat's managed
// occupant was deliberately stopped and no other managed row remains live.
// The probe below still decides absence; a recreated session or failed
// transport remains a refusal.
if ("code" in canonicalProbe) {
const latest = this.latestSession(node.id);
const hasCurrentBinding = this.sessionRegistry.getBindingForNode(node.id) !== null;
const stoppedManagedOccupant = latest !== null
&& latest.session_name === canonicalSessionName
&& latest.status === "exited"
&& latest.origin !== "claimed"
&& !hasCurrentBinding
&& this.nonTerminalSessions(node.id).length === 0;
if (stoppedManagedOccupant) {
try {
const restored = await this.tmuxAdapter.startServer();
if (restored.ok) {
canonicalProbe = await this.probeLiveness(
canonicalSessionName,
"fresh launch refuses rather than overwrite a possibly-live canonical session",
);
}
} catch {
// Keep the original classified refusal when the transport cannot be
// restored; never translate a failed start into absence.
}
}
}
if ("code" in canonicalProbe) return canonicalProbe;
if (canonicalProbe.state === "present") {
const currentSession = this.latestSession(node.id);
@@ -376,6 +376,22 @@ describe("SeatLifecycleService.launchFresh", () => {
expect(sessionRegistry.getBindingForNode(seat.node.id)?.tmuxPane).toBe("%fresh");
});
it("reopens an empty tmux server after an explicit stop before a fresh launch", async () => {
const seat = seedSeat();
const server = modelServerLifetime();
const stopped = await service.stopSeat({ seatRef: seat.sessionName, reason: "operator requested stop" });
expect(stopped).toMatchObject({ ok: true });
expect(server).toEqual({ up: false, starts: 0 });
const result = await service.launchFresh({ seatRef: seat.sessionName, fresh: true, reason: "start a new occupant" });
expect(result).toMatchObject({ ok: true, status: "ready", sessionName: seat.sessionName });
expect(server).toEqual({ up: true, starts: 1 });
expect(tmux.createSession).toHaveBeenCalledTimes(1);
expect(alive.has(seat.sessionName)).toBe(true);
});
it("proves older non-terminal rows absent on the restored server before superseding them", async () => {
const seat = seedSeat({ clean: true });
const older = sessionRegistry.registerSession(seat.node.id, "r00-dev-impl@fresh-rig");