affaan-m/ECC · error · ClaimError
claim cannot grant another dispatch
Error message
claim cannot grant another dispatch
What it means
begin_dispatch raises this ClaimError when the claim row found for the given token is not in the 'claimed' state. A claim grants exactly one dispatch; once it has moved to 'dispatching', 'delivered', 'unknown', or 'cancelled', it can never start a new dispatch. This enforces at-most-once dispatch permission.
Solutions
- Do not reuse the token for a second attempt; create a fresh claim via claim(db, obligation_id, decision_id, now=...) for a new dispatch.
- Check the claim's current state first (query obligation_delivery_claims WHERE token=?) and route: 'dispatching' -> treat as uncertain, mark_unknown; 'delivered' -> nothing to do; 'cancelled' -> claim a new token.
- If the first dispatch crashed mid-send, call mark_unknown() then reconcile() with trusted evidence rather than re-dispatching the same token.
Example fix
// before: retry with the same token after a failure
token = claims.begin_dispatch(db, token, now=ts)
send(payload) # failed
token = claims.begin_dispatch(db, token, now=ts) # ClaimError
// after: mark uncertainty, then reconcile; new dispatch needs a new claim
try:
payload = claims.begin_dispatch(db, token, now=ts)
send(payload)
except DeliveryError:
claims.mark_unknown(db, token, now=ts)
claims.reconcile(db, token, coordinate, evidence, now=ts)
# a genuinely new dispatch:
new_token = claims.claim(db, obligation_id, decision_id, now=ts) Defensive patterns
Strategy: validation
Validate before calling
row = db.execute("SELECT state FROM obligation_delivery_claims WHERE token=?", (token,)).fetchone()
if row is None or row['state'] != 'claimed':
raise ValueError(f"token not dispatchable (state={row['state'] if row else 'missing'})") Try / catch
try:
payload = claims.begin_dispatch(db, token, now=ts)
except claims.ClaimError:
handle_already_dispatched(token) # new claim or reconcile path Prevention
- Treat the begin_dispatch return value as single-use; never cache it for retries
- Track claim state locally before each dispatch attempt
- Give each dispatch attempt its own claim token
When it happens
Trigger: Calling begin_dispatch(db, token) with a token whose obligation_delivery_claims row has any state other than 'claimed' — e.g. calling it twice with the same token, after cancel(), after mark_unknown(), or after complete()/reconcile() finished the claim.
Common situations: Retrying a dispatch after a crash or timeout using the cached token returned by a previous begin_dispatch (explicitly warned against in the docstring); worker restart that re-reads an old token; accidentally reusing a token after cancellation; two workers sharing one token.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- only a pre-dispatch claim can be cancelled
- baseline is not the active harness configuration
- completion requires the correct dispatch/reconciliation…
- dispatch transition lost
- only a dispatched attempt can become unknown
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/a3f7fdf29567b03b.
Report an issue: GitHub.
Appendix: source
Thrown at skills/operator-approval-loop/references/approval_claims.py:98
with _transaction(db, now):
_snapshot(db, obligation_id, decision_id)
token = secrets.token_hex(32)
db.execute('''INSERT INTO obligation_delivery_claims
(obligation_id,decision_id,token,state,created_ts,updated_ts)
VALUES (?,?,?,'claimed',?,?)''', (obligation_id, decision_id, token, now, now))
return token
def begin_dispatch(db, token, *, now):
"""Return bound payload once, only after dispatching state has committed.
A crash after this boundary is uncertain even if transport has not started.
Do not cache/reuse this return value for another attempt.
"""
with _transaction(db, now):
row = _claim_row(db, token)
if row['state'] != 'claimed':
raise ClaimError('claim cannot grant another dispatch')
payload = _snapshot(db, row['obligation_id'], row['decision_id'])
changed = db.execute('''UPDATE obligation_delivery_claims SET state='dispatching',updated_ts=?
WHERE token=? AND state='claimed' ''', (now, token)).rowcount
if changed != 1:
raise ClaimError('dispatch transition lost')
return payload
def cancel(db, token, *, now):
"""Cancel only a not-yet-dispatched claim. Never reopen its decision key."""
with _transaction(db, now):
row = _claim_row(db, token)
if row['state'] != 'claimed':
raise ClaimError('only a pre-dispatch claim can be cancelled')
db.execute("UPDATE obligation_delivery_claims SET state='cancelled',updated_ts=? WHERE token=?",
(now, token))
View on GitHub (pinned to 8321021c54)