[object Object]

← back to Nas Setup

daemon-health: belt-and-suspenders model (Steve ruled 8/14 'Both', TK-10547)

4a447c8255c592fb82a5f6f62a340920388ce680 · 2026-08-14 10:18:30 -0700 · steve

Check BOTH redundant writers per DB — the -root SYSTEM daemon (Option B,
durable, needs FDA on /usr/local/bin/nas-backup-sh) AND the gui/ USER agent
(Option C, homebrew bash, proven writing). Verdict is outcome-based on Henry
freshness: both writers healthy + fresh = PASS; exactly one belt down = WARN
(degraded redundancy, data safe); both down or Henry missing/stale = FAIL.
Current reality = WARN until the FDA grant brings the root belt up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 4a447c8255c592fb82a5f6f62a340920388ce680
Author: steve <steve@designerwallcoverings.com>
Date:   Fri Aug 14 10:18:30 2026 -0700

    daemon-health: belt-and-suspenders model (Steve ruled 8/14 'Both', TK-10547)
    
    Check BOTH redundant writers per DB — the -root SYSTEM daemon (Option B,
    durable, needs FDA on /usr/local/bin/nas-backup-sh) AND the gui/ USER agent
    (Option C, homebrew bash, proven writing). Verdict is outcome-based on Henry
    freshness: both writers healthy + fresh = PASS; exactly one belt down = WARN
    (degraded redundancy, data safe); both down or Henry missing/stale = FAIL.
    Current reality = WARN until the FDA grant brings the root belt up.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/daemon-health.sh | 65 +++++++++++++++++++++++++++---------------------
 1 file changed, 36 insertions(+), 29 deletions(-)

diff --git a/scripts/daemon-health.sh b/scripts/daemon-health.sh
index ace052b..6d7258c 100755
--- a/scripts/daemon-health.sh
+++ b/scripts/daemon-health.sh
@@ -1,5 +1,6 @@
 #!/bin/bash
-# daemon-health.sh — HONEST read-only health of the on-prem backup ROOT DAEMONS.
+# daemon-health.sh — HONEST read-only health of the on-prem backup MIRROR (belt-and-suspenders:
+# the -root SYSTEM daemon + the gui/ USER agent, both redundant writers to /Volumes/Henry).
 #
 # Why (TK-10547): pull.log + latest.json reflect whoever ran the script LAST (incl. a MANUAL
 # user-context rescue run), so a root daemon exiting 1 every night (TCC can't write /Volumes/
@@ -25,17 +26,24 @@ worst="PASS"   # PASS < WARN < FAIL
 rank(){ case "$1" in FAIL) echo 2;; WARN) echo 1;; *) echo 0;; esac; }
 rows=()
 
-# check <label> <henry-dir> <dump-prefix>
+# check <db-name> <henry-dir> <dump-prefix> <base-label>
 check(){
-  local label="$1" dir="$2" prefix="$3"
-  local print exit_code runs state verdict reason newest age_h fresh
-  # Option B (Steve-approved, TK-10547): the canonical scheduled job is the -root SYSTEM LaunchDaemon
-  # (runs nightly 03:45 as root). Option C (a user LaunchAgent) was REJECTED — it hits the same SIP-bash
-  # TCC wall and only runs while logged in. So look up the actually-scheduled job in system/, not gui/.
-  print=$(launchctl print "system/$label" 2>/dev/null)
-  exit_code=$(printf '%s\n' "$print" | awk -F'= ' '/last exit code/{print $2; exit}')
-  runs=$(printf '%s\n' "$print" | awk -F'= ' '/^\truns/{print $2; exit}')
-  state=$(printf '%s\n' "$print" | awk -F'= ' '/^\tstate/{print $2; exit}')
+  local name="$1" dir="$2" prefix="$3" base="$4"
+  local rootp userp root_ec user_ec newest age_h fresh verdict reason
+  # BELT-AND-SUSPENDERS (Steve ruled 8/14 "Both", TK-10547): the Henry mirror is written by TWO
+  # redundant schedulers — the -root SYSTEM LaunchDaemon (Option B: durable, survives logout, needs
+  # FDA on /usr/local/bin/nas-backup-sh) AND the gui/ USER LaunchAgent via /opt/homebrew/bin/bash
+  # (Option C: runs while logged in, proven writing). So check BOTH; the verdict is OUTCOME-based on
+  # Henry freshness (fresh via EITHER writer = data safe) but we still WARN if one belt is down so a
+  # broken writer is never hidden — the ticket's whole point.
+  rootp=$(launchctl print "system/${base}-root" 2>/dev/null)
+  userp=$(launchctl print "gui/$MYUID/${base}" 2>/dev/null)
+  root_ec=$(printf '%s\n' "$rootp" | awk -F'= ' '/last exit code/{print $2; exit}')
+  user_ec=$(printf '%s\n' "$userp" | awk -F'= ' '/last exit code/{print $2; exit}')
+  local root_ok=0 user_ok=0
+  [ -n "$rootp" ] && [ "${root_ec:-1}" = "0" ] && root_ok=1
+  [ -n "$userp" ] && [ "${user_ec:-1}" = "0" ] && user_ok=1
+  local up=$(( root_ok + user_ok ))
 
   newest=$(ls -t "$dir/${prefix}"_*.dump 2>/dev/null | head -1)
   if [ -n "$newest" ]; then
@@ -44,28 +52,27 @@ check(){
     [ "$age_h" -le "$STALE_WARN_H" ] && fresh="fresh" || fresh="stale"
   else age_h=-1; fresh="missing"; fi
 
-  # MECHANISM-LOUD verdict (Steve ruled 8/14, TK-10547): a broken SCHEDULED daemon = FAIL even when a
-  # manual rescue left Henry fresh — the entire point of this ticket is that monitoring must NOT hide a
-  # broken daemon behind a rosy manual PARTIAL. Henry-freshness enriches the REASON (data-safe vs
-  # data-lost), it does NOT soften the verdict. Only a clean daemon (exit 0) + fresh Henry = PASS.
-  if [ -z "$print" ]; then verdict="FAIL"; reason="launchd state unreadable for $label — scheduler gone"
-  elif [ "${exit_code:-1}" != "0" ] && [ "$fresh" = "fresh" ]; then verdict="FAIL"; reason="daemon last_exit=${exit_code:-?} but Henry dump fresh (${age_h}h) — mechanism broken, data safe (launcher swap pending)"
-  elif [ "${exit_code:-1}" != "0" ]; then verdict="FAIL"; reason="daemon last_exit=${exit_code:-?} — mechanism broken AND Henry ${fresh} (${age_h}h)"
-  elif [ "$fresh" = "missing" ]; then verdict="FAIL"; reason="no Henry dump for $prefix — mirror never landed"
-  elif [ "$fresh" = "stale" ]; then verdict="WARN"; reason="Henry dump ${age_h}h old (> ${STALE_WARN_H}h) — daemon exit 0 but mirror not landing"
-  else verdict="PASS"; reason="daemon exit 0, Henry dump ${age_h}h old"; fi
+  local writers="root(sys)=exit:${root_ec:-NA}$([ -n "$rootp" ]||echo /unloaded) user(gui)=exit:${user_ec:-NA}$([ -n "$userp" ]||echo /unloaded)"
+  # Henry-freshness is authoritative. Fresh via at least one healthy writer = data safe; both writers
+  # healthy = PASS; exactly one down = WARN (degraded redundancy, fix the down belt); both down or
+  # Henry missing/stale = FAIL (no working writer → the silent-death setup).
+  if [ "$fresh" = "missing" ]; then verdict="FAIL"; reason="no Henry dump for $prefix — neither writer landed [$writers]"
+  elif [ "$fresh" = "stale" ]; then verdict="FAIL"; reason="Henry dump ${age_h}h old (> ${STALE_WARN_H}h) — neither writer is landing [$writers]"
+  elif [ "$up" -eq 0 ]; then verdict="FAIL"; reason="Henry fresh (${age_h}h) but BOTH writers broken — nothing will refresh it [$writers]"
+  elif [ "$up" -eq 1 ]; then verdict="WARN"; reason="Henry fresh (${age_h}h) via one writer — redundancy DEGRADED, other belt down [$writers]"
+  else verdict="PASS"; reason="Henry fresh (${age_h}h), both writers healthy [$writers]"; fi
   [ "$(rank "$verdict")" -gt "$(rank "$worst")" ] && worst="$verdict"
 
-  echo "  $label -> $verdict ($reason)"
-  echo "     daemon: state=${state:-?} runs=${runs:-?} last_exit=${exit_code:-?} | henry: $fresh ${newest:+$(basename "$newest")} (${age_h}h)"
-  rows+=("$(jq -n --arg l "$label" --arg v "$verdict" --arg r "$reason" \
-     --argjson ec "${exit_code:-null}" --argjson runs "${runs:-null}" --argjson age "${age_h:-null}" \
-     '{daemon:$l,verdict:$v,reason:$r,last_exit:$ec,runs:$runs,henry_dump_age_h:$age}')")
+  echo "  $name -> $verdict ($reason)"
+  echo "     henry: $fresh ${newest:+$(basename "$newest")} (${age_h}h) | writers: $writers"
+  rows+=("$(jq -n --arg l "$name" --arg v "$verdict" --arg r "$reason" \
+     --argjson rec "${root_ec:-null}" --argjson uec "${user_ec:-null}" --argjson age "${age_h:-null}" \
+     '{db:$l,verdict:$v,reason:$r,root_last_exit:$rec,user_last_exit:$uec,henry_dump_age_h:$age}')")
 }
 
 echo "== on-prem backup daemon health ($(date -Iseconds)) =="
-check com.steve.nas-dwdump-mirror-root          /Volumes/Henry/dw-backups/dw_unified  dw_unified
-check com.steve.nas-realestate-dump-mirror-root /Volumes/Henry/dw-backups/realestate  realestate
+check dw_unified  /Volumes/Henry/dw-backups/dw_unified  dw_unified  com.steve.nas-dwdump-mirror
+check realestate  /Volumes/Henry/dw-backups/realestate  realestate  com.steve.nas-realestate-dump-mirror
 echo "== overall: $worst =="
 
 # JSON heartbeat (PASS/WARN/FAIL vocab so fleet-health-rollup + meta-watchdog read it right)
@@ -91,7 +98,7 @@ if [ "$ALERT" = "1" ]; then
   fi
   if [ "$fire" = "1" ]; then
     CNCP="${CNCP_URL:-http://localhost:3333}"
-    note="[BACKUP DAEMON HEALTH $(date +%F)] Henry mirror $worst — $(printf '%s\n' "${rows[@]}" | jq -r 'select(.verdict!="PASS")|"\(.daemon): \(.reason)"' | paste -sd'; ' -). The nightly root daemon can't write /Volumes/Henry (TCC/FDA). Fix: grant FDA to the daemon's launcher (TK-10547)."
+    note="[BACKUP DAEMON HEALTH $(date +%F)] Henry mirror $worst — $(printf '%s\n' "${rows[@]}" | jq -r 'select(.verdict!="PASS")|"\(.db): \(.reason)"' | paste -sd'; ' -). Belt-and-suspenders: WARN = one of the two writers is down (data still safe via the other); FAIL = both down or Henry stale. To restore the root belt, grant FDA to /usr/local/bin/nas-backup-sh (TK-10547)."
     curl -sS --max-time 10 "$CNCP/api/parking-lot" -H 'Content-Type: application/json' \
       -d "$(jq -n --arg u "onprem://henry-mirror-daemon" --arg note "$note" '{url:$u,note:$note}')" >/dev/null 2>&1 || true
     if [ -f "$HOME/.claude/skills/_shared/george-send.sh" ]; then

← 272bd47 daemon-health: fix false-PASS regression — restore mechanism  ·  back to Nas Setup  ·  belt-and-suspenders: stagger user-agent belts past their roo d41f040 →