feat(discord): systemd user service with a supervised run; brakes exit 3 and are never retried (#1509)

QUEUE row 17, MVP iteration 2. scripts/discord-service.sh renders and
installs mosaic-discord@<binding> from packages/discord/systemd/. The
unit's main process is `run --supervised`, which applies the new recover
policy first: a lock whose owner is gone is cleared and only the STOP
written for that is removed; an operator STOP or a held binding refuses
with exit 3, which RestartPreventExitStatus never retries. `recover` is
also a CLI verb. First cut used ExecStartPre and looped live, since systemd
honours the never-retry status only from the main process; replaced and
re-verified before any message traffic. Suite 40/40, 95 node tests.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-09-13 14:39:11 -05:00
co-authored by Claude Fable 5.1
parent dc5902aafd
commit 436ba6ed6b
15 changed files with 630 additions and 30 deletions
+27
View File
@@ -2383,3 +2383,30 @@ node tests.
Live check, 19:21 UTC: Jason sent a message in #sage-admin, the eyes reaction
appeared on it before the reply, the turn record carries `receipt.ok` true,
and a 0600 receipt sits in the private evidence directory. Row 15 done.
## 2026-09-13 — Discord service unit (#1509, QUEUE row 17)
Before: the Sage connector ran in a tmux window; a crash or a reboot left a
stale `run.lock`, and every rerun after `stop` needed STOP removed by hand.
After: `scripts/discord-service.sh install` renders and writes a systemd
user unit, `mosaic-discord@<binding>`, whose main process is
`discord.sh run <binding> --supervised`. The supervised run first applies
the new `recover` policy (also a CLI verb): a lock whose owner is gone is
cleared the way `unlock` does and only the STOP written for that is
removed; an operator STOP, or a binding held by a live or unverifiable
process, refuses with the new exit code 3, which the unit never retries.
`systemctl --user stop` is the soft stop (SIGTERM, no STOP); `discord.sh
stop` stays the brake. Tests: recover policy (clean, dead lock, no record,
operator STOP untouched, own leftover STOP, foreign line, brake written
during the unlock wins, live and unknown and invalid owners refuse without
writing STOP), CLI exit codes, unit render and install and uninstall,
`systemd-analyze verify`. Suite 40/40, 95 node tests. Loosening the STOP
ownership rule fails three tests.
Correction, same day: the first cut used `ExecStartPre=discord.sh recover`.
Live, a refused pre-start went into systemd's auto-restart, because
`RestartPreventExitStatus` applies to the main process only. Caught on the
first start behind a brake, before any message traffic; replaced by the
supervised run as the main process and re-verified: SIGKILL recovered in
16 s with the dead lock cleared, brake held with no restart, released and
READY. The Sage seat now runs under the unit; the tmux session is gone.