feat(agents): report a crash, an OOM and a permanent start failure from ExecStopPost (DIVE-3965) - #934
Conversation
…om ExecStopPost (DIVE-3965) The channel plugins can only report an interruption from inside a process that is still alive, which excludes the three states that matter most: SIGKILL, OOM, and a permanent start failure that trips RestartPreventExitStatus. systemd is the only observer that outlives the unit. 5dive-agent-stop-notify is the ExecStopPost half: it classifies the stop from SERVICE_RESULT/EXIT_CODE/EXIT_STATUS, stays silent for a deliberate stop (both systemd's verdict and the DIVE-857 registry intent), deduplicates repeats of the same cause inside a 15-minute window while counting them, and always exits 0. tests/agent_stop_notify_unit.sh grades it end to end through the shipped script with the environment systemd actually hands an ExecStopPost; the dedup and cause-awareness arms were confirmed non-vacuous by mutation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… harness (DIVE-3965) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The "no live systemd run" residual is CLOSED — measured on this host, 2026-09-13The PR body and the section above both signed a residual: "nothing was exercised Four assumptions the notifier rests on, all confirmed:
Unrelated finding, worth knowing: What is still uncovered: the delivery leg itself — no message was sent to a real |
…n (DIVE-3965) install.sh fetched `5dive-agent-stop-notify` fail-closed. The file is additive and the fleet pin (v0.35.1) predates the tag that ships it, so every fresh install and every box's 04:00Z self-update 404s there and aborts the WHOLE install under `set -e` (DIVE-4349). scripts/install-pin-compat.sh caught it: `checked=23 missing=1`, and install-contract confirmed it end to end with install.sh exiting 37 on a fresh ubuntu:22.04. Three changes, one per red: - install.sh routes the fetch through `fetch_optional_at_pin`, which skips on a 404 ONLY and still fails closed on anything else. A box that does not get the notifier simply has no ExecStopPost target, which the unit's `-` prefix already tolerates. The else arm removes a copy left by a newer install, so the box matches the pin it is on — symmetric with the optional-hook loop below it. - docker/Dockerfile lists the notifier in the bundle COPY. That is the exit 37: the Dockerfile's own comment says every file install.sh curls must be listed or the build fails that way. Without it install-contract would now merely SKIP the notifier — green, and never exercising the install it exists to grade. - tests/agent_stop_notify_unit.sh writes its counter as `fails=$((fails+1))`. The spaced form is not a shape tests/lib/harness-verdict-detect.sh recognises, so the harness reported UNPROBEABLE and `changed-harnesses` exited 1 on it. That was the third red, whose cause ops did not assert. Behaviour of the notifier itself is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5dive-reviewer
left a comment
There was a problem hiding this comment.
CODEOWNERS attestation on /install.sh (DIVE-4334 rail). This signs the automated rail at head 648de0c, not a human eye on the diff.
PR-time rail green at this head: shellcheck, scan, supply-chain-guard, install-contract, pin-compat, changed-harnesses, check, test, test-confirm, title, actionlint, workflow-structure-guards, core-pristine x3, acp-graded-pristine, rails-pristine, hook-staleness, diff.
docker-install and test-installed-host(+confirm) are queue-time only by design (if: github.event_name != 'pull_request', DIVE-4186) — they grade on the merge group, which is where this merge is decided. No PR-time result for them is claimed here.
Verifier: quinn, PASS at iteration 2 on DIVE-3965, all four acceptance criteria re-confirmed. Clearance: the row's delegated push-for-review approval gate was answered 'Push it' (2026-09-13 08:52Z) and the board names ops as merge owner.
Signed residuals, named by the maker and not by implication: no live systemd run and no live Telegram send; both bounded by the ExecStopPost '-' prefix and the script's unconditional exit 0, so the worst case is a notification that does not arrive, never a unit that will not stop. The 5dive-agent@.service template change reaches installed boxes only on the next install.sh run + daemon-reload.
What this is
P06 of the Codex channels series (DIVE-3965), CLI half. The plugin half is
5dive-ai/5dive-plugins#69. This is also the item
plugins/telegram-codex/TODO.mdhascarried as open since v0.1.9: "ExecStopPost in 5dive's systemd unit for true crash-aware
notification."
Why systemd has to be the one that speaks
A channel plugin can only report an interruption from inside a process that is still
alive. That excludes the three states that matter most — SIGKILL, OOM, and a permanent
start failure that trips
RestartPreventExitStatus=2 3— because every process that couldhave sent the message dies with the unit. The chat's last word is then whatever the agent
happened to be saying.
ExecStopPostis the only observer left.What changed
5dive-agent-stop-notify <name>(new, repo root, installed next to5dive-agent-start): classifies the stop fromSERVICE_RESULT/EXIT_CODE/EXIT_STATUSand says which one happened in plain words — out of memory, killed bysignal, start-limit crash-loop ("systemd gave up"), stop timeout, core dump, resources,
and the two permanent conditions, which say "It will NOT be retried" in as many words.
systemd's own
success, and thedesiredState=stoppedintent DIVE-857 already records at5dive agent stop. The second is load-bearing — a launcher that dies on SIGTERM reportssignal/SIGTERM, byte-identical to a kill, so without the intent flag every cleanagent stopwould page the chat.suppressed and counted; the next message that goes out says how many were held back. A
DIFFERENT cause inside that window is always sent — a crash-loop that hid an OOM would be
worse than no dedup at all.
exit 0. An agent is most likely to be dyingexactly when the
5divebundle is mid-self-update, and anExecStopPostthat exitsnon-zero turns a clean stop into a failed unit. The unit prefixes it with
-for the samereason, belt and braces.
systemd/5dive-agent@.servicegains theExecStopPost=-line;install.shfetches andremoves the script alongside the launcher.
Tests
tests/agent_stop_notify_unit.sh— 15 arms, graded end to end through the SHIPPED scriptwith the environment systemd actually hands an
ExecStopPost, against a stub sender thatrecords its argv. Not by sourcing the classifier: what breaks here is the wiring between the
classifier, the dedup mark and the
allowFromread, and a harness that calls one functionreaches none of it.
Every negative arm pins the absence of a send and
rc=0— for anExecStopPost,"nothing was sent" is only half the claim.
Non-vacuity confirmed by mutation: disabling the dedup window reds exactly the two dedup
arms; collapsing the OOM sentence into the generic exit-code one reds exactly the three
cause-awareness arms.
What is not covered
No live systemd run — the harness feeds the documented
ExecStopPostenvironment ratherthan provoking a real crash. The residual is systemd handing it something other than what
systemd.service(5)documents. That residual is bounded by the-prefix and theunconditional
exit 0: the worst case is a notification that does not arrive, never a unitthat will not stop. Installed boxes get the new unit line on their next
install.shrunplus
daemon-reload, the ordinary launcher-artifact path.tests/lib/tier.shdefaults a new harness tocore; DIVE-4374 records core at 101-103% ofits 300s budget on installed-host. This harness costs ~1s. Named because it is a real (small)
contribution to a budget already over.
Fixtures use the reserved fake Telegram id
1234567890.🤖 Generated with Claude Code