Appearance
A background job scans in_progress tasks past a TTL, confirms the owning process is dead, and resets them to queued — so a crashed agent never wedges a worktree forever.
Pattern
Orphan Reaper
The loop
Every N seconds: stale + dead → queued.
cron/reap_orphans.py
every 60s
for task intask_board.by_status("in_progress"):# 1. too fresh? leave aloneif task.claimed_at + TTL > now():continue# 2. slow but alive? leave aloneifagent_process_alive(task.adw_id):continue# 3. stale AND dead → reaptask.status = "queued"task.adw_id = Nonetask_board.save(task)log(f"Reaped orphan: {task.description}")
Two gates before any mutation — staleness and deadness. Skip either gate and the reaper itself becomes the bug.
The TTL budget
Long enough to survive a slow task. Short enough to recover before the queue jams.
TTL as multiple of median task duration
median_task ≈ 5–10 min · TTL lands at 60–90 min
TOO SHORT
✓ SWEET SPOT
TOO LONG
1×3×10×30×+
✗ Too short False-positive reaps. Mid-flight tasks reset and double-picked; work is redone by a second agent.
✓ 10× median Survives every honest task; reaps only on genuine termination. Tune from observed duration distribution.
✗ Too long Crashes wedge the worktree for hours. Downstream [⏰] tasks starve.
Tune to the observed tail of task durations — not a guess, not a round number.
The liveness gate
Stale ≠ dead. Check the PID before you touch the task.
TTL only proves time has passed — not that the agent is gone.
⏱
TTL expired
Check agent_process_alive(adw_id)
via PID, lock file, or heartbeat
→
ALIVEDo not reap. The task is slow, not orphaned. Reaping here causes a double-pick on completion.
DEADReap. The process is gone. Reset to queued; next poll re-picks it cleanly.
No liveness check → the reaper competes with its own workers. The original completes with a stale ADW ID while a fresh agent re-does the work.
Reset, don't fail
Recovery from accidents is not the same as permanent failure.
Default policyreset to queued
status = [ ]→ next poll re-picks
When: kernel panic, power blip, accidental kill -9, container eviction.
Treats termination as transient infrastructure noise. Task returns to the pool as if it never started — no human review required.
Strict policytag + hold for review
status = [❌ orphan]→ operator inspects
When: Stage 1–2 autonomy — every accident must be surfaced.
Use while calibrating. Permanent failure from deliberate flaws belongs to Correction Task, not the reaper.
The reaper is accidental-termination recovery. Deliberate task failure is a different primitive with different tagging.
What breaks without it
Trust is not a recovery mechanism.
Anti-patterns — observed in systems without a reaper
No reaper
One crash per worktree. Task stuck [🟡] forever. Every downstream [⏰] task behind it never runs.
No liveness check
Slow-but-alive tasks get reset. Original finishes with stale adw_id while a second agent redoes the work. Silent duplication.
TTL too short
False-positive reaps churn the board. Agents thrash through the same task repeatedly; throughput collapses under its own retry storm.
Reap → hard fail
Transient infra noise becomes permanent task death. The queue loses tasks to causes unrelated to the work itself.
Claims don't self-clear. A kernel panic mid-claim wedges the status until something external intervenes.
The discipline
A claim is a lease, not a guarantee.
Every task board that accepts claims needs a reaper that expires them on its own schedule. Without one, the first unsupervised crash is also the last work that worktree ever does — the out-loop dies silently the moment nothing is watching.