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

  1. 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.
  2. 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.
  3. 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

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


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)