fleet-health-automation — the box's own self-heal and alarm layer
Mirrored from the fleet knowledge base: wi/components/fleet-health-automation.md
Updated
Mirrored verbatim from
utopia-models/knowledgewi/components/fleet-health-automation.mdby the allowlisted publish pipeline. Edit it there, not here.
component: fleet-health-automation
serves: all
runs-on: novaserver
adrs: [3562]
owns:
- _infra/scripts/fleet-notify.sh
- _infra/systemd/fleet-notify@.service
- _infra/systemd/dropin/50-onfailure.conf
- fleet-notify@.service
- disk-guard.service
- fleet-heal-critical.service
- sudo-health.service
- token-health.service
- nova-ops-integrity.service
- automerge-reenable-watchdog.service
- dmv-revenue-integrity.servicefleet-health-automation — the box's own self-heal and alarm layer
WHAT IT IS
The set of timers that watch the fleet's own operating conditions — disk, sudo, tokens, critical
services, the ops tree, auto-merge, DMV's revenue integrity — plus the one notifier every
one of them alarms through. It is a layer, not a service: no single unit named
fleet-health-automation exists, which is precisely why it had no component row until this doc.
Stop it and the box keeps working right up until it doesn't. The failure it exists against is silent degradation on shared infrastructure: a disk creeping to 100%, a token that expired, a critical service that died between dispatches.
HOW IT WORKS TODAY
Every alarm in the fleet takes one path: unit fails → OnFailure= → fleet-notify → ONE board
issue. That is the whole design. A health timer does not talk to the board itself; it exits non-zero
and lets systemd do the routing. When the unit's issue is already open and a line-start ⛔ sits after
its last line-start UNBLOCK, fleet-notify writes a journal line instead of another alert comment —
the sweeper reads only comments(last:25), and repeat alerts would scroll the ⛔ out of that window and
get the issue re-dispatched with nothing changed (#10020). It fails open: an unreadable comment list
means the alert goes out.
flowchart LR
subgraph timers["--user timers on nova (disk-guard · fleet-heal-critical · nova-ops-integrity) and svc-ops (the rest), each a oneshot — read the cadences from list-timers, never from here"]
DG["disk-guard"]
FHC["fleet-heal-critical"]
SH["sudo-health"]
TH["token-health --auto"]
NOI["nova-ops-integrity"]
AMW["automerge-reenable-watchdog"]
DRI["dmv-revenue-integrity"]
end
timers -->|"exit != 0"| DI["service.d/50-onfailure.conf<br/>OnFailure=fleet-notify@%N.service"]
DI --> FN["_infra/scripts/fleet-notify.sh <unit><br/>ALWAYS exits 0 — a failing notifier<br/>would trigger its own OnFailure"]
FN --> B["ONE board issue per unit, upserted"]
FN -.->|"task-<issue>-<epoch> unit"| X["EXEMPT (#3805) — task-runner.sh<br/>already posts a terminal on the work issue"]
style FN fill:#9cf,stroke:#09c,stroke-width:3px
style B fill:#ffd,stroke:#cc0ENROLMENT is the thing to check, not liveness. OnFailure= is not on the units; it comes from
~/.config/systemd/user/service.d/50-onfailure.conf, a top-level drop-in applied to every user
service on that account. A box without it fails silently — win and dmv were never enrolled and
their units failed unnoticed for weeks (#5821). Measured 2026-08-31: nova · win · cloud · dust all
enrolled.
Two consequences of that drop-in are easy to trip over. It applies to your probe unit too, so a
per-unit [Unit] OnFailure= reset does not disarm the production notifier — the top-level drop-in is
applied afterwards and re-adds itself. And RestartMode=direct suppresses OnFailure= entirely;
plain Restart= does not.
A deliberate-failure experiment opts out through its own environment, not through OnFailure=.
systemd-run --user -u <name> -E FLEET_NOTIFY=off … — fleet-notify reads the failed unit's
Environment and, on FLEET_NOTIFY=off, logs a line and files nothing (#10823). -p OnFailure= cannot
do it, for the reason above. An unreadable unit falls through and files, so a broken read cannot eat a
real alarm.
The scripts are not in this repo — ~/.claude/ops/{disk-guard,nova-ops-integrity}.sh in brain-nova
for the nova half, and copies under svc-ops' ~/.claude/ops/{sudo-health,dmv-revenue-integrity}.sh and
~/.claude/scripts/{token-health,automerge-reenable-watchdog}.sh for the svc-ops half (knowledge#9201), with fleet-heal-critical wrapping
fleet-ctl.sh start-critical through runjob.sh. Only the notifier half is committed here. So this
doc states no thresholds and no cadences: read them from the unit.
disk-guard is the one with published measurements, and they are measurements rather than caution
— _diag retention is 7 days because 3 d reclaims 8.97 GB against 7 d's 8.25 GB, i.e. four extra days
of CI post-mortem cost 0.72 GB; _work is only reclaimed at 90% because a wipe forces every repo to
re-clone. It alarms only when it cannot get below its ceiling, latched by a state file that a
recovery clears.
BOUND VALUES
The notifier and the enrolment drop-in are in-git and bound below. Every health check this layer runs
is a brain-nova file that cannot be bound from here — an absolute out-of-repo path is refused by
cog_machinery on purpose, because it stats differently per account on a 0750 home. Those are
covered by the units: binding instead, which verify-unit-execstart.py executes daily on the box.
- ✅
_infra/scripts/fleet-notify.sh - ✅
_infra/systemd/fleet-notify@.service - ✅
_infra/systemd/dropin/50-onfailure.conf - ⚙️
user@nova:fleet-notify@probe.service— ExecStart target proven daily byverify-unit-execstart.py(#4742) - ⚙️
user@nova:disk-guard.service— ExecStart target proven daily byverify-unit-execstart.py(#4742)
DETERMINE CURRENT STATE
The population and its schedules, then one unit's own config:
systemctl --user list-timers --all --no-legend --no-pager # as nova on novaserver
sudo -n -u svc-ops XDG_RUNTIME_DIR=/run/user/$(id -u svc-ops) systemctl --user list-timers --all --no-legend --no-pager
ssh nova@100.71.25.43 "systemctl --user cat disk-guard.service disk-guard.timer"
ssh nova@100.71.25.43 "cat ~/.claude/state/disk-guard.state; tail -5 ~/.claude/logs/disk-guard.log"
for u in nova svc-ops win cloud dust; do ssh nova@100.71.25.43 \
"sudo -n -u $u test -f /home/$u/.config/systemd/user/service.d/50-onfailure.conf && echo $u enrolled"; doneKNOWN BROKEN
- 🔴 Only 2 of this layer's 8 timers carry a
units:binding in the registry —fleet-notifyanddisk-guard.fleet-heal-critical,sudo-health,token-health,nova-ops-integrity,automerge-reenable-watchdoganddmv-revenue-integrityhave nocomponents.yamlrow at all, so nothing proves theirExecStarttargets still exist. That is exactly the gapverify-unit-execstart.pywas written to close and the shape of thereap-orphan-claudefailure — a timer firing for 24 days against a deleted script. - 🔴 This layer's alarm cannot report a fault in the box it lives on. Every timer here is a
--userunit on novaserver and every alarm path ends at a board issue filed from novaserver, so a partitioned or dead box produces silence, not an alert. The off-box half is a different component (novaserver-heartbeat, a GitHub-hosted runner driven by a Cloudflare Worker) and it is the only thing that can see this box go dark. - ⚠
fleet-notifyalways exits 0 by design, so a notifier that fails to file leaves no unit-level trace either — the failure is invisible on both sides. The task-unit exemption (task-<n>-<epoch>) widens that: those units never file, correctly, but the same silence covers a genuine notifier bug. - ⚠ The volume runs close to the reclaim threshold, and on 2026-09-08 it crossed it (#7628).
df -h /reached 95% (17 G free);disk-guardalarmed correctly — CRIT,exit 1, board issue. It was right about the emergency and wrong about where the space was: its per-agent cache list held 830 MB across all four master homes while 19.6 GB of the identical class sat beside it in~/.npm/_cacache(15,259 MB) and~/.bun/install/cache(~4,300 MB) — package-manager download caches, invisible only because they do not live under~/.cache/. It also still typed its home list asnova win cloud dmv, so the renameddusthad been skipped in silence since the rename. Both fixed (targets added, homes discovered from/home/*); the first run after the fix freed 7,118 MB. 🔴 The reading to take from this is not the percentage — it is that a sweep's target list and its subject list are each a claim about the world, and neither announces itself when it goes stale. The guard logged truthful percentages the entire time. Full account and the verification:wi/work/reference/win/disk-guard.md, "The list was the mechanism".