Found during the operational end-to-end review (#218). Deliberately not fixed, because it needs an answer to an open decision and the reviewer refused to guess at it — correctly.
The mechanism
When claim finds an item whose attempts has reached the ceiling, it retires it to exhausted:
UPDATE work SET state = ?, owner = NULL, lease_until = 0,
last_error = ?, updated_at = ? ...
disposition and reason_kind are not set, so they keep their default empty value.
Everywhere else in this repository, an empty disposition means nobody has finished with this item yet. So the one state that means the harness has given up on this item permanently is recorded as the one thing it is not.
An operator reading the queue — or /api/work, or a projection over it — cannot distinguish "gave up after N attempts" from "in flight" by disposition, only by state. last_error carries the human sentence, which is better than nothing and is not a classification.
Why it was not fixed
It needs a new reason kind in outcomes.py, and D8 is open — whether third-party gates get a registration mechanism in that taxonomy. outcomes.py has a test that fails if it ever grows a registry, precisely so that D8 is not answered by accident.
Adding a kind here would be answering part of D8 sideways, in a PR about end-to-end tests. AGENTS.md says: do not guess at a blocked decision to unblock yourself — say it is blocked.
What is actually wanted
Probably a gave_up reason kind under the existing DECIDED group, since the harness has decided and no person is required. But that is the decision, not the implementation, and it belongs with D8 rather than ahead of it.
Blind spots
- Whether any consumer currently relies on
exhausted having an empty disposition has not been checked. The audit projections are the place to look.
reaper.py and the hold-expiry path may retire items on similar terms; only the claim scan was examined.
Found during the operational end-to-end review (#218). Deliberately not fixed, because it needs an answer to an open decision and the reviewer refused to guess at it — correctly.
The mechanism
When
claimfinds an item whoseattemptshas reached the ceiling, it retires it toexhausted:dispositionandreason_kindare not set, so they keep their default empty value.Everywhere else in this repository, an empty
dispositionmeans nobody has finished with this item yet. So the one state that means the harness has given up on this item permanently is recorded as the one thing it is not.An operator reading the queue — or
/api/work, or a projection over it — cannot distinguish "gave up after N attempts" from "in flight" by disposition, only by state.last_errorcarries the human sentence, which is better than nothing and is not a classification.Why it was not fixed
It needs a new reason kind in
outcomes.py, and D8 is open — whether third-party gates get a registration mechanism in that taxonomy.outcomes.pyhas a test that fails if it ever grows a registry, precisely so that D8 is not answered by accident.Adding a kind here would be answering part of D8 sideways, in a PR about end-to-end tests. AGENTS.md says: do not guess at a blocked decision to unblock yourself — say it is blocked.
What is actually wanted
Probably a
gave_upreason kind under the existingDECIDEDgroup, since the harness has decided and no person is required. But that is the decision, not the implementation, and it belongs with D8 rather than ahead of it.Blind spots
exhaustedhaving an empty disposition has not been checked. The audit projections are the place to look.reaper.pyand the hold-expiry path may retire items on similar terms; only the claim scan was examined.