Skip to content

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.