Day 19 of 29 · Thursday · Learning

Watchdogs & timeouts — why a 30-min cap killed a 34-min jobConcept

A watchdog is a wall-clock kill-switch wrapped around a job so a hang can't run forever. But a cap set too tight kills healthy work — on May 30 a 30-min cap killed a plan-day run that legitimately needed 34. Today you'll learn how to set a cap that catches hangs without strangling good runs.

Catch-up progress
19/29
Why this matters to you

Your Today list this morning is a wall of 🚨 alerts — plan-day failed 6/13, 6/14, and 6/17; learning + language renders failed; the relay errored. Several of these are timing failures. The watchdog is the one piece of automation hygiene that decides whether a stuck job screams for help or just silently dies. Getting the cap right is the difference between 'I knew within 30 seconds' and 'I found out three days later.'

A watchdog is just a timer with a weapon. You wrap your real job in a command that says: 'run this, but if it isn't done in N minutes, kill it.' On macOS the tool is `gtimeout` (from coreutils) or a plain `timeout`. Without one, a job that hangs — waiting on a network call that never returns, a TCC permission prompt nobody clicks — runs forever, holding its slot and never reporting failure. That's the worst outcome: not a crash, but a silent zombie.

The trap is the cap value. Too loose (2 hours) and a hung job wastes two hours before anyone notices. Too tight (30 min) and a job having a slightly slow but *healthy* day gets executed mid-sentence — which is exactly what happened on May 30: plan-day took 34 minutes against a 30-minute watchdog and got killed doing real work. The fix isn't 'remove the watchdog,' it's 'size the cap to the job's real p99 runtime, then add headroom.'

There's a second knob most people miss: `--kill-after`. A normal timeout sends a polite SIGTERM ('please wrap up'). A well-behaved job catches that and exits cleanly. But a truly hung job ignores SIGTERM. `--kill-after=30s` says: 'if it's still alive 30 seconds after I asked nicely, send SIGKILL and end it for real.' Without `--kill-after`, your watchdog can itself hang on a job that refuses to die.

Worked example

This is the actual pattern your plan-day wrapper uses. Run the first block to see your real wrapper; the second is the shape to copy.

# 1. Look at your own watchdog
grep -n -A6 'timeout' ~/Library/LaunchAgents/com.tom.pdb-plan-day.plist 2>/dev/null
cat ~/Claude/PDB/scripts/run_plan_day.sh | grep -n -A4 -i timeout

# 2. The shape of a correct watchdog
gtimeout --kill-after=30s 45m  /usr/bin/python3 my_job.py
#        ^^^^^^^^^^^^^^^^ ^^^                ^^^^^^^^^^^^^^^
#        SIGKILL escape   soft cap           the real work
▶ Do it now
  1. Run block 1 above and read what cap plan-day currently runs under. Confirm it's above 34 minutes (the May 30 healthy-run high-water mark).
  2. Open `~/Claude/PDB/scripts/run_plan_day.sh` and find the run-with-timeout line. In one sentence, say out loud what `--kill-after` buys you that the plain cap doesn't.
  3. Look at this morning's 🚨 'PDB plan-day failed' tasks — check one log under `~/Claude/PDB/logs/`. Was it a timeout kill (job ran long) or a real error (job crashed)? That single distinction tells you whether to raise the cap or fix the code.

Gotchas

Go deeper: GNU coreutils timeout manual
One-card takeaway

A watchdog catches hangs; a well-sized cap keeps it from catching healthy work. Size to the real p99 plus headroom, and always give it a `--kill-after` so it can finish what it started.