SCOPE 2.2 unifies grant lifecycle status (exhausted replaces used/consumed)
and scopes consume idempotency to (tenant_id, grant_id, idempotency_key) with
action_hash conflict detection.
Writers must emit:
| State | Meaning |
|---|---|
active |
Valid; may have use_count > 0 while uses remain |
exhausted |
Cardinality fully consumed |
revoked / expired / superseded / invalid |
Terminal |
Status payloads include use_count, maximum_uses, remaining_uses
(alias cardinality_remaining), last_use_event_id, and last_use_sequence.
Legacy reads of used / consumed normalize to exhausted.
Consume events store action_hash (column and/or event metadata). Uniqueness:
- Same grant + key + action → replay original consume
- Same grant + key + different action →
idempotency_conflict - Different grant + same key → independent
Failed preflight never inserts or reserves a consume idempotency key.
Concurrent retries remain serialized (BEGIN IMMEDIATE / advisory lock +
FOR UPDATE).
Schema version bumps to 3:
- Add nullable
grant_idandaction_hashcolumns onledger_events - Drop legacy
idx_ledger_idempotency(UNIQUE(tenant_id, idempotency_key)) - Create
idx_ledger_consume_idempotencyon(tenant_id, grant_id, idempotency_key)whereevent_type = 'grant_used'and key/grant are present - Backfill columns from
event_json/ metadata
Opening an existing DB with SqliteScopeLedger applies this automatically.
On connect, PostgresScopeLedger:
ADD COLUMN IF NOT EXISTS grant_id/action_hashDROP INDEX IF EXISTS idx_pg_ledger_idempotency- Create
idx_pg_ledger_consume_idempotency(same domain as SQLite) - Backfill from JSONB
- Pre-2.2 DBs that relied on tenant-global idempotency keys across grants must expect cross-grant keys to become independent after upgrade (intended).
- Duplicate historical
(tenant, grant, key)consume rows must be cleaned before the new unique index can be created; fresh installs are unaffected.