Recover stale supervised daemons

Teach daemon stop to use lifecycle shutdown when the API is reachable even if the owner pid is stale, then wait for the API to disappear and clean the stale pidfile.

Launch desktop-managed daemons detached from desktop stdio, preserve stale reachable daemon ownership for version checks, and allow desktop stop/restart to use the CLI recovery path.

Add supervisor heartbeats so supervised workers shut down when their supervisor disappears instead of surviving as orphaned reachable daemons.
This commit is contained in:
Mohamed Boudra
2026-06-07 23:18:48 +07:00
parent 7c8b290e2f
commit 893e3376b0
7 changed files with 769 additions and 115 deletions
@@ -19,12 +19,28 @@ type SupervisorLifecycleMessage =
reason?: string;
};
interface SupervisorHeartbeatMessage {
type: "paseo:supervisor-heartbeat";
}
interface BootstrapResult {
paseoHome: string;
logger: ReturnType<typeof createRootLogger>;
config: ReturnType<typeof loadConfig>;
}
function isPidAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch (err) {
if (typeof err === "object" && err !== null && "code" in err && err.code === "EPERM") {
return true;
}
return false;
}
}
function bootstrapFromEnvironment(): BootstrapResult {
try {
const paseoHome = resolvePaseoHome();
@@ -150,6 +166,46 @@ async function main() {
beginShutdown("restart lifecycle intent", { successExitCode: 0 });
};
const installSupervisorLivenessGuard = () => {
if (typeof process.send !== "function") {
return;
}
const supervisorPid = process.ppid;
let lastSupervisorHeartbeatAt = Date.now();
let supervisorShutdownRequested = false;
const requestSupervisorShutdown = (reason: string) => {
if (supervisorShutdownRequested) {
return;
}
supervisorShutdownRequested = true;
beginShutdown(reason);
};
process.on("message", (message: unknown) => {
if (
typeof message === "object" &&
message !== null &&
"type" in message &&
(message as SupervisorHeartbeatMessage).type === "paseo:supervisor-heartbeat"
) {
lastSupervisorHeartbeatAt = Date.now();
}
});
process.on("disconnect", () => requestSupervisorShutdown("supervisor disconnect"));
const timer = setInterval(() => {
const ipcConnected = typeof process.connected === "boolean" ? process.connected : true;
const heartbeatExpired = Date.now() - lastSupervisorHeartbeatAt > 3500;
if (ipcConnected === false || !isPidAlive(supervisorPid) || heartbeatExpired) {
requestSupervisorShutdown("supervisor disconnect");
}
}, 1000);
timer.unref();
};
installSupervisorLivenessGuard();
try {
daemon = await createPaseoDaemon(
{