430346e14bd7183e900cc82b7af0693257221f6f
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
430346e14b |
a restarted job outlives the server that restarted it
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 <noreply@anthropic.com>
|
||
|
|
eaa38c7d43 |
the maintenance buttons tell you when they fail
Asked whether there was feedback on failure, and for the restart there was not —
there was the opposite.
`useClient` throws on any status >= 400. The restart handler caught everything and
reported success, on the reasoning that a dropped connection is normal when the
process answering the request is the one being replaced. That is true, and it made
the honest refusal built into the route unreachable: a 400 saying "not running under
PM2" arrived through the same catch, so the button would have said "Restarting…"
while nothing restarted, and stayed disabled forever.
The two failures are opposites and are now told apart by whether an error carries a
STATUS. A status means the server answered and declined — real error, message shown,
button re-enabled. No status means the connection dropped — expected, and the
success path.
Both buttons also unwrap the message. The client throws with the raw response body,
and these routes answer with JSON, so an unhandled failure showed
`{"ok":false,"error":"…"}` and asked the reader to find the sentence inside the
punctuation. Five tests on that, including the cases where it must NOT invent one:
JSON with no error field, and a non-Error thrown value.
Found while answering the question rather than by testing, which is the wrong order.
── One thing I should not have done ──
I exercised the restart route against the live server to see its refusal shape. PM2
knows `officer` here, so it returned 200 and scheduled a real restart; the only
reason it did not happen is that my test process called `process.exit(0)` before the
250ms timer fired. Luck, not design. `pm_uptime` confirms the platform never
restarted.
tsgo clean, frontend builds, 919 pass / 7 fail unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
1426c3a856 |
rebuild the frontend without restarting, and restart when you must
Settings → System → Maintenance. Two buttons that are deliberately not a pair. ── Rebuild: real, and free ── The same `rebuildFrontend` a plugin install already runs. `Bun.build` reads source from disk on every call and writes `build/`, and the server serves that directory by reading each file per request — so a rebuild lands on the next page load with no process restart, no dropped websocket and no chat session lost. ── Restart: the only way to load server code, and it costs ── Bun caches modules for the life of the process. Measured rather than assumed: editing a file and re-importing returns the ORIGINAL export, and a cache-busting query picks up that one specifier while everything it imports stays cached. So `rebuildHonoApp` — which install already does — re-wires the same module instances into a new tree and cannot see a backend edit. Busting the whole graph would duplicate singletons: two database clients, two session maps, one process. There is therefore no third button, and the screen says why. A "remount the API" control would look like it reloaded the backend and would not. The restart answers BEFORE it schedules itself, because the process serving that request is the one being replaced. It refuses outright when PM2 does not know this process — in development the platform runs under `bun --watch`, where the restart would fail into a promise nobody awaits while the button reported success. Verified: `POST /rebuild-frontend` → 200, 3.0s, 3 outputs; anonymous → 401. The restart button is deliberately untested from here — pressing it would end the session this was written in, which is the same reason it needs a label that says so. tsgo clean, frontend builds, 914 pass / 7 fail unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |