Day 31 of 31 · Tuesday · Learning

Fallback model chains + watchdogs — surviving an overloaded headless runCapability

Claude Code can fall back to a second model when the primary is overloaded, and a wall-clock watchdog kills a hung headless run before it eats your morning. Today's lesson closes the loop you accepted on 06-09 but never wired in — and it's the direct cause of the 🚨 pile in your Inbox.

Catch-up progress
31/31
Why this matters to you

Scroll your Today list: 'PDB plan-day failed 2026-06-25', 'PDB weekly-review failed 2026-06-21', 'Learning email failed' four times, 'Language pre-render failed' five times. These are headless runs dying on overload with no fallback, and hanging with no watchdog. You ACCEPTED fallback chains on 06-09 — it just never made it into settings.json or the cron flags. That gap is the backlog.

When you run a job headless (`claude -p ...` from a LaunchAgent), the model can come back 'overloaded' or 'unavailable' — a 529/503 from the server. With one model named, that's the end of the run: it errors, the LaunchAgent logs a non-zero exit, and you get a 🚨 task (if you're lucky) or silence (if you're not). `--fallback-model` lets you name a backup that's tried automatically when the primary fails over — you can chain up to three.

But a fallback only solves 'the server said no.' It does NOT solve 'the run hung for 34 minutes' — which actually happened to plan-day on 5/30. That's a different failure, and the fix is a wall-clock watchdog: wrap the whole command in `timeout` so it's killed at a hard ceiling. Your own CLAUDE.md automation-hygiene rule is blunt about this: 'every long job has a watchdog you've proved kills,' and 'an unverified watchdog is decorative.'

Both belong on the same jobs: `run_plan_day.sh`, `run_weekly_review.sh`, and the `language_lesson.py` / `learning_lesson.py` LaunchAgents. The 🚨 backlog is the symptom of neither being wired — a perfect case study for the day you're about to leave the system unattended for two weeks.

Worked example

The two-line hardening — a fallback chain plus a timeout watchdog around a headless run:

# BEFORE — one model, no ceiling. Overload or hang = failure.
claude -p "$(cat plan_day_prompt.txt)"

# AFTER — fallback chain + 25-min wall-clock watchdog
timeout --signal=TERM --kill-after=30s 25m \
  claude -p "$(cat plan_day_prompt.txt)" \
    --model claude-opus-4-8 \
    --fallback-model claude-sonnet-4-6 \
  || /usr/bin/python3 ~/Claude/PDB/scripts/notify_fail.py "plan-day"
▶ Do it now
  1. Open `~/Claude/PDB/scripts/run_weekly_review.sh` and find the line that invokes `claude -p`. Look at what wraps it — is there a `timeout`? Is there a `--fallback-model`? (Today's spoiler: neither — that's the gap behind the 'weekly-review failed' 🚨 tasks.)
  2. Note what model the job pins. Then name its natural fallback: if it runs Opus, `claude-sonnet-4-6` is the catch — it'll finish even when Opus is slammed.
  3. Don't edit it live on travel-eve. Drop a one-line header comment instead: `# TODO: wrap in timeout 25m + add --fallback-model claude-sonnet-4-6 (see learning 2026-06-30)`. Today's win is SEEING the gap; you wire it for real when you're back.

Gotchas

Go deeper: Claude Code CLI reference (--model, --fallback-model) · coreutils timeout(1) — the watchdog primitive
One-card takeaway

A fallback model saves you from 'the server said no'; a watchdog saves you from 'the run never came back.' Critical jobs need both — and a failure that doesn't surface as a 🚨 task isn't handled, it's hidden.