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.
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 reachedready_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.”