Work items whose execution kept dying without a durable verdict (crash
mid-dispatch, kill -9, cancel-before-harvest) were re-dispatched forever:
every exit path was responsible for remembering to write a terminal
phase, and any path that forgot left the card RUNNING and eligible again.
Replace that with structural accounting:
- claim CAS opens an attempt in the same UPDATE (attempt_seq+1,
attempt_settled=false) so no dispatch can start unaccounted (store.py)
- transition_work_item becomes the settlement authority: every
non-RUNNING transition settles the open attempt in the same write;
crashed/interrupted outcomes accumulate streaks, clean outcomes reset
them; claim release folds into the same write; settlement still lands
when the phase write loses a race (work_item_transition.py)
- dispatcher refuses cards over the streak limits (crash>=3,
interrupted>=5) in both is_dispatchable and _work_item_is_runnable,
and a per-tick reconcile pass back-fills dead attempts as interrupted
and terminalizes over-limit cards to FAILED with a visible
blocked_reason (dispatch_hold quarantine if even that write fails)
(phase.py, company_mode.py)
- crash exits now settle: cancellation unwind harvests coroutines that
died on a real exception before discarding them, the crashed-item
handler releases the claim and settles as crashed with a quarantine
fallback, and the timeout path settles as crashed (company_mode.py)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>