Skip to content

feat(agents): report a crash, an OOM and a permanent start failure from ExecStopPost (DIVE-3965) - #934

Merged
5dive-bot merged 3 commits into
mainfrom
dive-3965-stop-notify
Sep 13, 2026
Merged

feat(agents): report a crash, an OOM and a permanent start failure from ExecStopPost (DIVE-3965)#934
5dive-bot merged 3 commits into
mainfrom
dive-3965-stop-notify

Conversation

@5dive-bot

Copy link
Copy Markdown
Collaborator

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.md has
carried 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 could
have sent the message dies with the unit. The chat's last word is then whatever the agent
happened to be saying. ExecStopPost is the only observer left.

What changed

  • 5dive-agent-stop-notify <name> (new, repo root, installed next to
    5dive-agent-start): classifies the stop from SERVICE_RESULT / EXIT_CODE /
    EXIT_STATUS and says which one happened in plain words — out of memory, killed by
    signal, 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.
  • Silent for a deliberate stop, on two independent readings and either suffices:
    systemd's own success, and the desiredState=stopped intent DIVE-857 already records at
    5dive agent stop. The second is load-bearing — a launcher that dies on SIGTERM reports
    signal/SIGTERM, byte-identical to a kill, so without the intent flag every clean
    agent stop would page the chat.
  • Deduplicated by CAUSE, not by unit. Repeats of the same cause inside 15 minutes are
    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.
  • bash + curl + jq only, and always exit 0. An agent is most likely to be dying
    exactly when the 5dive bundle is mid-self-update, and an ExecStopPost that exits
    non-zero turns a clean stop into a failed unit. The unit prefixes it with - for the same
    reason, belt and braces.
  • systemd/5dive-agent@.service gains the ExecStopPost=- line; install.sh fetches and
    removes the script alongside the launcher.

Tests

tests/agent_stop_notify_unit.sh — 15 arms, graded end to end through the SHIPPED script
with the environment systemd actually hands an ExecStopPost, against a stub sender that
records its argv. Not by sourcing the classifier: what breaks here is the wiring between the
classifier, the dedup mark and the allowFrom read, and a harness that calls one function
reaches none of it.

Every negative arm pins the absence of a send and rc=0 — for an ExecStopPost,
"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 ExecStopPost environment rather
than 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 the
unconditional exit 0: the worst case is a notification that does not arrive, never a unit
that will not stop. Installed boxes get the new unit line on their next install.sh run
plus daemon-reload, the ordinary launcher-artifact path.

tests/lib/tier.sh defaults a new harness to core; DIVE-4374 records core at 101-103% of
its 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

lodar and others added 2 commits September 13, 2026 08:49
…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>
@5dive-bot

Copy link
Copy Markdown
Collaborator Author

The "no live systemd run" residual is CLOSED — measured on this host, 2026-09-13

The PR body and the section above both signed a residual: "nothing was exercised
by an actual unit stop/crash; the harness feeds the documented ExecStopPost
environment rather than provoking it."
That is no longer true. A throwaway unit
with the same shape as 5dive-agent@.service (User=agent-dev, Group=claude,
an EnvironmentFile= carrying a sentinel, ExecStopPost=-/usr/local/bin/<probe>)
was started, killed, and stopped, and the probe recorded what systemd handed it:

SIGKILL of the main process   whoami=agent-dev  result=signal   code=killed  status=KILL  token=sentinel  home=/home/agent-dev
deliberate `systemctl stop`   whoami=agent-dev  result=success  code=killed  status=TERM  token=sentinel  home=/home/agent-dev

Four assumptions the notifier rests on, all confirmed:

  1. It runs as the unit's User=, so the real one runs as agent-%i and
    ${HOME}//home/agent-<name> is the right place to look for access.json.
  2. EnvironmentFile= variables are inherited, so the per-agent
    TELEGRAM_BOT_TOKEN reaches it without the script reading any file.
  3. SERVICE_RESULT/EXIT_CODE/EXIT_STATUS arrive exactly as
    systemd.service(5) documents; the SIGKILL arm classifies to signal-KILL and
    notifies, the stop arm to silent.
  4. The SIGTERM ambiguity is real, not theoretical. A deliberate stop reports
    code=killed status=TERM — byte-identical to a kill. It is only result=success
    and DIVE-857's recorded desiredState that separate them, which is why the
    notifier reads both.

Unrelated finding, worth knowing: /run is mounted noexec on this host. The
first probe put its script in /run/... and ExecStopPost SILENTLY never ran —
systemctl show -p ExecStopPost reported start_time=[n/a] pid=0, nothing in the
journal, and the - prefix swallowed the exec failure. An ExecStopPost that
cannot be executed is indistinguishable from one that ran and decided to say
nothing. 5dive-agent-stop-notify installs to /usr/local/bin (with the launcher)
and is unaffected.

What is still uncovered: the delivery leg itself — no message was sent to a real
Telegram chat from an ExecStopPost on this box, only through the harness's stub
sender. That residual is bounded the same way as before: the worst case is a
notification that does not arrive.

@5dive-bot
5dive-bot enabled auto-merge September 13, 2026 10:41
…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 5dive-reviewer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@5dive-bot
5dive-bot added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit e2d95b3 Sep 13, 2026
33 checks passed
@5dive-bot
5dive-bot deleted the dive-3965-stop-notify branch September 13, 2026 12:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants