#!/usr/bin/env bash # jarvis-email-watch-liveness.sh — T40 timer-owner liveness check (code-be-01). # # Owner: code-be-01 (Jason ruling, board T40; jarvis remains author of the # watcher code). Detection is SYSTEMD-STATE-ONLY — no pane scraping, no fixed # interval assumptions beyond the unit's own 30-min cadence. # # DEAD conditions (any one pages the owner): # 1. timer unit not found or not active # 2. NextElapse is infinity (timer will never fire again) # 3. service last result failed # 4. last service run older than 90 min (3 missed 30-min slots) # # Exit: 0 = alive (prints state line) · 1 = DEAD (prints reason) · 2 = cannot # assert (systemd unreachable — surface, don't guess). set -uo pipefail UNIT_TIMER="jarvis-email-watch.timer" UNIT_SERVICE="jarvis-email-watch.service" CADENCE_MIN=30 MISSED_SLOTS=3 STALE_SEC=$(( CADENCE_MIN * 60 * MISSED_SLOTS )) export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" active="$(systemctl --user show "$UNIT_TIMER" --property=ActiveState --value 2>/dev/null)" rc=$? if [ $rc -ne 0 ] || [ -z "$active" ]; then echo "DEAD(reason=systemd-unreachable-or-unit-missing unit=$UNIT_TIMER rc=$rc)" exit 2 fi [ "$active" = "active" ] || { echo "DEAD(reason=timer-not-active state=$active)"; exit 1; } # This timer is MONOTONIC-ONLY (OnBootSec/OnUnitActiveSec/OnUnitInactiveSec — no # realtime calendar), so NextElapseUSecRealtime is legitimately empty. The # unit file's own comment records the failure signature that killed brain-sync # for five days: monotonic timer + reboot => NextElapse becomes 'infinity' # while is-active/is-enabled stay green. Probe the monotonic property; then # take the AUTHORITATIVE next-fire from list-timers (it computes the min over # all timer bases; `show`'s aggregate includes the long-expired OnBootSec base # and reads as ~never on a healthy timer). mono="$(systemctl --user show "$UNIT_TIMER" --property=NextElapseUSecMonotonic --value 2>/dev/null)" [ "$mono" != "infinity" ] || { echo "DEAD(reason=next-elapse-infinity monotonic — reboot-killed-timer signature, see unit-file note)"; exit 1; } row="$(systemctl --user list-timers "$UNIT_TIMER" --no-pager 2>/dev/null | grep "$UNIT_TIMER")" [ -n "$row" ] || { echo "DEAD(reason=no-list-timers-row unit=$UNIT_TIMER)"; exit 1; } case "$row" in *"n/a"*) echo "DEAD(reason=list-timers-next-na)"; exit 1 ;; esac next="$row" result="$(systemctl --user show "$UNIT_SERVICE" --property=Result --value 2>/dev/null)" [ "$result" = "success" ] || [ "$result" = "" ] || { echo "DEAD(reason=service-result-failed result=$result)"; exit 1; } execstamp="$(systemctl --user show "$UNIT_SERVICE" --property=ExecMainExitTimestamp --value 2>/dev/null)" if [ -n "$execstamp" ] && [ "$execstamp" != "0" ]; then last_ep="$(date -d "$execstamp" +%s 2>/dev/null || echo 0)" now_ep="$(date +%s)" age=$(( now_ep - last_ep )) if [ "$age" -gt "$STALE_SEC" ]; then echo "DEAD(reason=stale-last-run age_min=$((age/60)) threshold_min=$((STALE_SEC/60)))" exit 1 fi fi echo "ALIVE(active=$active next=$next result=$result last_exit=$execstamp)" exit 0