From 430346e14bd7183e900cc82b7af0693257221f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 16 Aug 2026 17:41:36 +0000 Subject: [PATCH] a restarted job outlives the server that restarted it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The other half of the same report. The pill came back after a refresh, and then a `pm2 restart officer` killed the dev server outright — because `restartBackgroundTask` spawned it as a plain child of the platform. A job the AGENT starts is a child of the `officer-claude-code` sidecar, a PM2 peer, so those were never affected. The tray therefore showed two chips that looked identical and behaved differently, and the restart button's own help text promised the opposite of what happened. Measured against a throwaway PM2 app rather than reasoned about, because the two plausible causes call for different fixes — a signal to the process GROUP, or a walk of the process TREE: plain `sh -c` killed `setsid sh -c` killed <- a new session is NOT enough `setsid --fork sh -c` survived PM2 walks the tree by ppid, so a new session leaves the job a child and it is found anyway. `--fork` is what matters: setsid forks and its parent exits at once, so the job is reparented to init before any restart can enumerate it. ── What that costs, and how it is paid ── The handle `Bun.spawn` returns is now the setsid parent, which exits immediately. Waiting on it reported every job as finished the moment it started, so exit detection moved to `watchForExit`: poll who holds the output file, and when nobody does, read the status the wrapper recorded. That is the same liveness question Stop and Pause already ask, so it adds no second notion of "running" — and a paused job still holds its file, so SIGSTOP correctly reads as running rather than as an exit. Two details that would each have been a bug: - A GRACE PERIOD. Between the fork and the job opening its output there is a moment with no holders; polling into it announces a completion for a job that never ran. - The command is a POSITIONAL PARAMETER to the wrapper, not interpolated into it. Interpolation would let a command containing quotes terminate the wrapper's own quoting. Tested with `echo "quoted 'both' ways"`. A missing status file is reported as a clean finish, not a failure: that is what Stop looks like, and "you stopped it" is nearer the truth than "it crashed". Also corrected the button text. It said background jobs survive "because they are their own PM2 processes" — true of agent-started jobs, false of restarted ones, and now the wrong reason for both. They survive because they are outside this process's tree. ── Verified ── End-to-end through `buildHonoApp`, on a synthetic task: POST /restart -> 200 holders: pid 906186 (ppid 1, session leader), pid 906188 (ppid 906186) probe process was 906173 — in neither chain quotes survived the wrapper durable events: task:started -> task:notification (completed), exit file "0" My own assertion in that probe was wrong and the output is what corrected it: it demanded ppid 1 of EVERY holder, but only the session root is reparented — its own children hang off it, which is the point. Not in `bun test`: nothing in the suite touches Postgres or spawns processes. tsgo clean, 919 pass / 7 fail unchanged. Needs a server restart. Co-Authored-By: Claude Opus 5 --- .../ServerSettings/MaintenanceSection.tsx | 2 +- src/servers/api/chat/task-restart.ts | 130 +++++++++++++++--- 2 files changed, 113 insertions(+), 19 deletions(-) diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/MaintenanceSection.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/MaintenanceSection.tsx index 471b5fb9..0d6f88e2 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/MaintenanceSection.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/MaintenanceSection.tsx @@ -113,7 +113,7 @@ export const MaintenanceSection = () => {

Costs every open connection: websockets drop and in-memory chat sessions are lost. Background jobs survive — - they are their own PM2 processes. + they run outside this process’s tree.