Skip to content

Flip a task from queued to in_progress in a single kernel-atomic write — so two polling triggers racing the same task produce one winner, one loser, never two duplicate spawns.

Pattern

Work Claim

The shape

One task file line. Four possible states. One winning writer.

Before

[ ]Add OAuth2 provider{opus, adw_feature}

/mark_in_progress  ·  adw_id=cc73faf1

Claimed

🟡cc73faf1Add OAuth2 providerclaimed_at=<ts>

terminal state  ·  release

Success

✅a3f91ccc73faf1

Failure

❌cc73faf1// Failed: <reason>

The claim carries who (adw_id) and when (timestamp). Release writes the terminal outcome back to the same line.

Why two triggers can't both win

POSIX writes under PIPE_BUF are kernel-atomic.

One write() syscall, <4 KB. The kernel serializes; one trigger replaces the line, the other sees "already claimed" and moves on.

Trigger A · t=0ms

read → status: queued

write → status: in_progress

✓ claim granted

write()

ATOMIC · < PIPE_BUF

4096 B · POSIX

Trigger B · t=0ms

read → status: queued

write → status: in_progress

✗ re-read: already claimed → skip

macOS, Linux, BSD guarantee it. Windows is weaker but usable at small scale. NFS is where the guarantee dies.

If you skip atomicity, both triggers succeed at "claim" — and the task list itself ends up corrupted, not just double-spawned.

The graduation ladder

Markdown now. Row lock later. Not the other way around.

MARKDOWN FILE

NFS DEAD ZONE

DB / LOCK / QUEUE

1 hostN hosts

✓ Markdown file

Kernel-atomic, debuggable with cat, costs nothing to operate.

single machine

⚠ NFS

Caches aggressively, writes don't propagate synchronously — atomicity is lost.

don't do this

→ Row lock / advisory / MQ

UPDATE … WHERE status='queued', pg_advisory_lock, or a queue.

multi-host

Graduate when multi-host polling is actually required. Until then, the markdown file is cheaper, simpler, debuggable.

What the claim doesn't solve

Work Claim alone wedges on the first crash.

A claim never expires on its own. One crashed agent = one permanent lock unless something reaps it.

💥

Agent crashes

mid-task, claim still in file

🔒

Claim stays

no release, no TTL, forever in_progress

Orphan Reaper

TTL check; reverts stale claim to queued

Work Claim without [[Orphan Reaper]] — one crash per worktree eventually blocks the whole worktree. The claim is the lock; the reaper is the key-under-the-mat.

If you ship Work Claim without Orphan Reaper, you have a correctness bug waiting on your next SIGKILL.

The discipline

Transition to in-progress is one atomic write — or it's a race.

Read-then-write without kernel-level atomicity is a double-spawn waiting on a poll collision. Small scale: markdown file under PIPE_BUF. Larger scale: row lock. Either way — pair it with an Orphan Reaper or the first crash wedges the worktree.