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
+54 -5
View File
@@ -14,9 +14,11 @@ CHAT-03 conversation controller exists.
```
scripts/discord.sh check <binding>
scripts/discord.sh run <binding>
scripts/discord.sh run <binding> [--supervised]
scripts/discord.sh stop <binding>
scripts/discord.sh unlock <binding>
scripts/discord.sh recover <binding>
scripts/discord-service.sh render | install | uninstall | status <binding>
```
`<binding>` names `<dataRoot>/discord/<binding>.json`. The wrapper passes
@@ -29,7 +31,8 @@ scripts/discord.sh unlock <binding>
as the message-content intent not being granted in the developer portal.
- `run` refuses when `STOP` exists or an unresolved delivery cannot be
reconciled. Otherwise it starts pi, connects, and serves turns until
SIGTERM, SIGINT or `stop`. Run it in a tmux window; there is no service unit.
SIGTERM, SIGINT or `stop`. Run it under the service unit below, or by hand
in a tmux window.
- `stop` writes `STOP` and sends SIGTERM to the process in `run.lock`, only
when that pid is alive and both its start time and the boot id match the
recorded ones; a reused pid, a pid from a previous boot, or a pid whose
@@ -48,8 +51,54 @@ scripts/discord.sh unlock <binding>
and releases itself if it is there, so nothing that starts during an
unlock can hold the binding. `run` never reclaims a stale lock on its own;
it refuses and names this command. Remove `STOP` to run again.
- `recover` is the supervised pre-start; `run --supervised` performs it
first, in the same process, and that is the form the service unit uses,
because systemd honours a never-retry exit status only from the main
process. It refuses, with exit 3 and
touching nothing, while `STOP` is present or the binding is held by a live
process, an alive pid whose identity cannot be verified, or an unreadable
record. A lock whose owner is gone, or that has no record, it clears the
way `unlock` does, then it removes the `STOP` it wrote for that so the run
that follows can claim. It removes only a `STOP` that consists of the one
line it wrote itself; a brake an operator wrote at any point, even during
the recovery, stays and the start is refused. Nothing automatic ever
removes an operator's `STOP`.
Exit codes: 0 ok, 1 operation failed, 2 invalid data or configuration, 4 usage.
Exit codes: 0 ok, 1 operation failed, 2 invalid data or configuration, 3
refused by a brake (`STOP` present or the binding held; a supervisor must
not retry), 4 usage.
## Service unit
`scripts/discord-service.sh install` renders
`packages/discord/systemd/[email protected]` with the repository
path and the directory of `node`, writes it to
`~/.config/systemd/user/[email protected]` (temp file, then rename;
`--dir DIR` for another place, `--no-reload` to skip `daemon-reload`) and
prints the commands that follow. One instance per binding:
```
systemctl --user enable --now mosaic-discord@<binding> start now and at login
systemctl --user status mosaic-discord@<binding>
journalctl --user -u mosaic-discord@<binding> -f the log (stderr of `run`)
systemctl --user stop mosaic-discord@<binding> SIGTERM; the turn in flight finishes; restartable
scripts/discord.sh stop <binding> the brake: writes STOP; the unit stays down until STOP is removed
scripts/discord-service.sh status <binding> unit state, STOP, run.lock
```
What the unit does: `ExecStart` runs `run --supervised`,
`Restart=on-failure` with 15 seconds between tries and at most five in ten
minutes, and `RestartPreventExitStatus=3` so a brake is never retried. A
crash (any other non-zero exit, a signal, an engine that died) restarts,
and the supervised run clears the dead lock on its way in. `systemctl --user stop`
sends SIGTERM only; the connector exits 0 and no `STOP` is written, so the
next start needs no hand. The stop timeout is 3700 seconds, the largest turn
timeout a binding may set plus margin; a normal stop takes as long as the
turn in flight. The unit never reads the binding or the token; `run` does,
at runtime, as before. Surviving logout and reboot needs
`loginctl enable-linger`. `render` prints the unit without writing it;
`uninstall` refuses while an instance is active. The log goes to journald:
`run` writes no message text to stderr, only ids, counts and state.
## The binding
@@ -142,5 +191,5 @@ malformed line), restart replay, stop and ceiling.
## Not in this piece
Tools, repository writes, announcements, attachments, slash commands, DMs,
per-thread sessions, more than one server or seat, a service unit, a
control-board row. Section 8 of the brief keeps the list.
per-thread sessions, more than one server or seat, a control-board row.
Section 8 of the brief keeps the list.