fix(daemon): don't crash-loop on headless Linux when a routine is overdue#1409
Conversation
…rdue The overdue-routine desktop notification spawns notify-send (Linux) / osascript (macOS) best-effort. A missing notifier binary — the default on a headless box without libnotify-bin — surfaces as an async spawn 'error' event, not the synchronous throw the surrounding try/catch expected, so Node re-threw it as an uncaught exception and killed the daemon. systemd then restart-looped it every ~10s, which also tore down the browser IPC socket (agents browser start -> 'Timeout waiting for browser daemon socket'). Attach an 'error' listener to both notifier spawns so the failure is swallowed as the best-effort contract already documented. Add overdue.test.ts reproducing the crash with an emptied PATH (deterministic ENOENT on every platform).
Code ReviewerVerdict: Ready to merge Build: Pass — Read Changes that work well
Things to verify manually
Reviewed by Code Reviewer — actually ran the build and tests on this branch. |
What
The
agents-daemonsystemd user service crash-loops on headless Linux whenever a routine is overdue, which also takes down the browser IPC socket —agents browser startthen fails withError: Timeout waiting for browser daemon socket.Root cause
On startup the daemon detects overdue routines and fires a best-effort desktop notification (
notifyOverdueinapps/cli/src/lib/overdue.ts) vianotify-sendon Linux /osascripton macOS. A headless box has nonotify-send(nolibnotify-bin), so the spawn fails withENOENT.spawn()reports a missing binary as an asynchronous'error'event, not the synchronous throw the surroundingtry/catchwas written to catch. With no'error'listener, Node re-throws it as an uncaught exception and the whole daemon exits (status=1). systemdRestart=brings it back every ~10s, it re-detects the same overdue routine, and crashes again — a permanent loop. The daemon never lives long enough to bindbrowser.sock.Observed on
yosemite-m0(Ubuntu 24.04, headless):Fix
Attach an
'error'listener to both notifier spawns so the failure is swallowed — which is exactly what the function'sBest-effort … failures … are swalloweddoc comment already promised. The macOS branch had the same latent flaw (never hit becauseosascriptalways exists there).Test
apps/cli/src/lib/overdue.test.tsreproduces the crash by emptyingPATH(deterministicENOENTon every platform) and asserting the process survives. Verified it fails without the fix — the asyncspawn osascript ENOENTsurfaces as an Uncaught Exception and the run exits non-zero — and passes with it.Notes
yosemite-*worker fleet) once any routine goes overdue.--no-sandbox(Ubuntu 24.04 AppArmor blocks unprivileged user namespaces) — that's environment config, out of scope for this PR.