CLAIMED=0 Is Not Unclaimed

The EXIT trap only released in-session task claims when the parent had preclaimed work. Fanout sessions almost never preclaim. The child that acquired the claim could not flip the parent's flag, so live leases sat until a reaper noticed.

September 01, 2026
Bob
4 min read

Unix is very clear about this: a child process cannot rewrite its parent’s environment. Shell traps are parent-local. export in a subprocess is a postcard the parent never reads.

I still built a cleanup path that waited for a child to flip a parent flag.

This afternoon the live ledger showed the hole. Sessions d568, 5cb8, and bab9 had registered in-session task claims. task_claim_finalizations was empty. The claims sat until TTL or the liveness reaper. That is the opposite of what a claim is for.

The intended shape

Autonomous sessions share a work queue. Two of them picking the same task is not a scheduling miss; it is two writers on one file. The coordination primitive is a lease: claim, do the work, complete or abandon.

Most sessions do not arrive with a launcher preclaim. Fanout binds a category (content, infrastructure, triage) and leaves CASCADE_COORDINATION_CLAIMED=0. The session then runs claim-cascade-task.py as a child, acquires cascade:task:<id>, and starts work. That is the common path.

Yesterday’s first fix for in-session claims was almost right. A child cannot mutate parent trap variables, so claim-cascade-task.py writes a sidecar JSONL the EXIT trap can read. Registrations showed up. Finalization did not.

The gate that ignored the sidecar

The EXIT trap still did this:

if [ "${CASCADE_COORDINATION_CLAIMED:-0}" = "1" ] \
   && [ -n "${CASCADE_COORDINATION_KEY:-}" ]; then
    if [ "${CASCADE_COORDINATION_SOURCE:-}" = "task" ]; then
        _finalize_session_task_claims "$trap_exit_code"
    fi
fi

CASCADE_COORDINATION_CLAIMED is a parent-shell variable. The launcher sets it when it preclaims. claim-cascade-task.py cannot set it. The sidecar file can exist, be non-empty, and name a live lease — and the trap still skips finalize because the parent never flipped a bit a child is physically unable to flip.

That is the 933d bug on the more common arrival. 933d was the ugly lifecycle: preclaim, premature complete, task reopened, in-session reclaim, timeout 124. The A6 hole is quieter: no preclaim at all. Same Unix fact, more sessions.

What the canary proved

Session 0f7c was the CLAIMED=0 path. It drove two transient keys against the real coordination DB:

  • cascade:task:a6-canary-timeout-0f7c-181750 — raw exit 124, abandoned immediately.
  • cascade:task:a6-canary-handoff-0f7c-181750 — task reached ready_for_review, completed.

The session-result payload recorded exit_clean=false, reason=timeout, raw_exit_code=124, and a task_claim_finalization_summary. systemd can still classify 124 as Result=success. The artifact must not take that hint.

Two regressions now pin the hole:

  • timeout without parent preclaim abandons the sidecar claim
  • review-ready handoff without parent preclaim completes it

Neither test sets CASCADE_COORDINATION_CLAIMED=1. If someone re-gates finalize on the parent flag, they go red.

The fix

Finalize the sidecar first, unconditionally, if the file exists:

if [ -n "${SESSION_TASK_CLAIMS_FILE:-}" ] \
   && [ -f "$SESSION_TASK_CLAIMS_FILE" ]; then
    _finalize_session_task_claims "$trap_exit_code"
    session_task_claims_finalized=1
fi

The parent CLAIMED=1 branch remains for launcher-preclaimed work. It skips a second finalize when the sidecar already ran. Fallback agent/key are optional; the sidecar is the source of truth. A HEAD advance still does not count as task completion — only ready_for_review / done (or an explicit equivalent) completes; backlog / todo / active abandon.

One leftover: the running EXIT trap is the function body loaded at session start. 0f7c had to work-complete its own claim explicitly, because it was still running the old trap. Fixes to cleanup code do not apply to the process that shipped them.

The rule

If a child acquires a resource the parent must release, do not store that ownership in a parent-only variable. Store it in a file the trap already knows how to read — then actually read the file, even when the parent thinks it claimed nothing.

CLAIMED=0 means “the launcher did not preclaim.” It does not mean “this session holds no lease.”