Server: a system unit for --service, so an OOM kill can't strand the phone - #126
Merged
Conversation
…the phone A systemd --user service inherits OOMScoreAdjust=100 from user@.service, making the ~40MB engine daemon a more attractive kernel OOM victim than the 400MB agents it supervises (which sit at 0). It also dies with the systemd user manager, which linger does not resurrect — only a new login does. The desktop hides both, since it starts a daemon over SSH just by connecting; the phone cannot, so its WebSocket stays dead until someone opens a laptop. Observed on a real box: the daemon was OOM-killed, the user manager went with it, and iOS was down ~21h while the desktop kept working. --service now writes a system unit wherever passwordless sudo is available — which is every box Ateam provisions, since provision.ts bakes NOPASSWD into cloud-init — with OOMScoreAdjust=-500 (the convention distros use for udevd/dbus/journald) and StartLimitIntervalSec=0, so systemd never permanently gives up after a burst of kills. Without sudo it still writes the user unit, plus StartLimitIntervalSec=0 and a warning that says what remains unprotected. It deliberately does NOT write OOMScoreAdjust there: systemd silently ignores a negative value in a user unit rather than failing, so the line would read as protection while doing nothing. Also fixes a stale-socket false positive in the handover check — it tested for the socket FILE, which a crashed daemon leaves behind, so a stale socket made the installer decline to start the service.
pallaoro
enabled auto-merge (squash)
August 15, 2026 13:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
A box's iOS access went down for ~21 hours while the desktop kept working. Not an app bug — the daemon was dead, and only the phone had no way to say so.
From the box's journal:
The unit
install.sh --servicewrites has three defects that only bite under memory pressure:user@.serviceshipsOOMScoreAdjust=100, inherited by every user service. The kernel killed a 36 MB daemon (oom_score_adj:200) and spared the 442 MB agents it supervises (score 0).StartLimitBurst=5per 10s; a burst of OOM kills exhausts it and nothing restarts the daemon again.Linger=yesdoes not resurrect a killed manager — only a new login does.The desktop survives all three by starting a daemon over SSH on connect, which is exactly what masks the problem. The phone's WebSocket only exists while a daemon is already running, so the one client
--serviceexists to serve is the one that can never recover.The fix
--servicenow writes a system unit wherever passwordless sudo is available — every box Ateam provisions, sinceprovision.tsbakesNOPASSWD:ALLinto cloud-init:Without sudo it still writes the user unit, plus
StartLimitIntervalSec=0and a warning naming what stays unprotected. It deliberately does not writeOOMScoreAdjustthere — measured: systemd silently ignores a negative value in a user unit (the unit starts,/procstill reads100), so that line would read as protection while doing nothing.Also fixes a stale-socket false positive in the handover check: it tested for the socket file, which a crashed daemon leaves behind, so a stale socket made the installer decline to start the service. Now tests for a live process.
Verified
ATEAM_WS_ADDRfrom the existing unit, wrote the unit, service active with/proc/<pid>/oom_score_adj = -500.kill -9the daemon twice → back in ~3s, listening, WebSocket handshake answering{protocolVersion:2,agents:["claude"]}.oom_score=337vs biggest agent692.sudo→ correct user unit,StartLimitIntervalSec=0present, zeroOOMScoreAdjustlines, warning printed.ATEAM_WS_ADDRacross and leaves running agents untouched (they live outside the unit's cgroup; the PTY daemon is never signalled).Not in scope
This keeps the daemon alive; it does not stop a box from running out of memory. Agents are unbounded (200–450 MB each, weeks-long) and, separately, the PTY daemon and every agent live in whichever SSH login-session scope first started them rather than an Ateam-owned cgroup — so agent memory can't be bounded as a group, and reaping that scope would kill every agent at once. The fix is spawning the PTY daemon into an Ateam-owned slice, which is a design change with real UX stakes (throttling a live agent mid-task) and belongs in its own PR.