affaan-m/ECC · error · ClaimError

now must be a nonnegative integer

Error message

now must be a nonnegative integer

What it means

All claim APIs take a now keyword used as the created/updated timestamp for claim rows, and the library requires it to be a strict int (not bool, not float, not str) that is >= 0. This guards the audit trail against malformed or misleading timestamps. Passing anything else raises ClaimError before the transaction begins.

Solutions

  1. Pass an integer epoch value, e.g. now=int(time.time())
  2. Convert floats/strings explicitly with int() before the call, verifying the value is nonnegative
  3. If now may be absent, default it at the call site: now = now if isinstance(now, int) and now >= 0 else int(time.time())

Example fix

// before
claim(db, obligation_id, decision_id, now=time.time())  # float -> ClaimError

// after
claim(db, obligation_id, decision_id, now=int(time.time()))
Defensive patterns

Strategy: type-guard

Validate before calling

def valid_now(now):
    return type(now) is int and now >= 0

if not valid_now(ts): raise ValueError('now must be a nonnegative int')

Type guard

def is_valid_timestamp(v) -> bool:
    return type(v) is int and v >= 0  # excludes bool and float

Try / catch

try:
    token = claim(db, oid, did, now=ts)
except ClaimError as e:
    if 'now must be a nonnegative integer' in str(e):
        token = claim(db, oid, did, now=int(time.time()))
    else:
        raise

Prevention

When it happens

Trigger: claim(db, oid, did, now=None); passing now='1700000000' (string), now=time.time() (float), now=True, or now=-1 to any of claim/begin_dispatch/cancel/mark_unknown/_finish.

Common situations: Using time.time() instead of int(time.time()); passing a datetime or ISO string; None when the caller forgot to supply a clock value; JSON round-trips turning ints into floats or strings.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/89aaab2a3928041d. Report an issue: GitHub.

Appendix: source

Thrown at skills/operator-approval-loop/references/approval_claims.py:36


def connect(path):
    """Open an existing caller-selected database; never apply schema/migrations."""
    uri = Path(path).resolve().as_uri() + '?mode=rw'
    db = sqlite3.connect(uri, uri=True, isolation_level=None, timeout=5)
    db.row_factory = sqlite3.Row
    db.execute('PRAGMA foreign_keys=ON')
    db.execute('PRAGMA recursive_triggers=ON')
    return db


@contextmanager
def _transaction(db, now):
    # Never return permission whose commit belongs to an outer caller transaction.
    if db.in_transaction:
        raise ClaimError('a top-level committed transaction is required')
    if type(now) is not int or now < 0:
        raise ClaimError('now must be a nonnegative integer')
    if any(db.execute(f'PRAGMA {name}').fetchone()[0] != 1
           for name in ('foreign_keys', 'recursive_triggers')):
        raise ClaimError('required SQLite guards are disabled')
    try:
        db.execute('BEGIN IMMEDIATE')
        yield
        db.commit()
    except BaseException as error:
        db.rollback()
        if isinstance(error, sqlite3.Error):
            raise ClaimError('claim transaction failed; no permission granted') from error
        raise


def _snapshot(db, obligation_id, decision_id):
    row = db.execute('''SELECT * FROM approval_bound_drafts
        WHERE obligation_id=? AND decision_id=?''', (obligation_id, decision_id)).fetchone()
    if row is None:

View on GitHub (pinned to 8321021c54)