Skip to content

ADR-033: Per-key execution control

Status

Accepted. Tracked in #340. The worker-local baseline and the fleet-exact protocol remain experiment-gated by E5 in the 0.7 roadmap; acceptance fixes the contract and candidate constraints, not evidence that either tier has shipped.

Context

Awa controls execution at queue granularity. Multi-tenant and resource-oriented workloads also need a limit such as "at most one in-flight event for this provider operation" or "at most four jobs for this tenant across the fleet."

InsertOpts::ordering_key does not provide that guarantee. It chooses an enqueue shard and therefore preserves FIFO claim order inside that shard, but it does not prevent two already claimed jobs from executing concurrently. It is also a routing input rather than durable job identity: retry, callback-resume, and DLQ-retry paths do not currently retain the original key.

Three constraints make fleet-exact control a storage protocol rather than another local semaphore:

  1. A read-only "count live leases, then claim" check is racy. Two claimers can both observe zero and commit a claim unless the check and grant are serialized.
  2. The claim cursor may only advance over work for which committed attempt evidence exists. Skipping a capacity-blocked row as if it were claimed would violate the cursor-monotonicity and prune proofs in ADR-019/023.
  3. The #169 lesson remains binding: a per-key counter row updated on every claim and completion becomes a high-cardinality hot mutable table under a pinned MVCC horizon.

The public delivery contract also remains at-least-once. A durable Awa lease can be rescued while the old process is partitioned and still executing application code. Per-key control can make the database's active-attempt set exact; it cannot fence an external provider that does not accept a fencing token.

Decision

Deliver per-key control in two explicit tiers and keep fairness separate from the concurrency safety invariant.

Tier 1: worker-local control

Tier 1 uses an in-process KeyPolicy in each dispatcher. It is exact within one runtime and approximate across a fleet:

fleet upper bound ~= per-runtime limit x runtimes able to claim the queue

It is useful for noisy-neighbour damping and per-runtime API budgets, but its API and metrics must always name the approximation. Worker-local rate limiting remains in this tier; this ADR does not define an exact distributed rate-window protocol. The concurrency-key digest still travels durably with the job so claim, retry, callback, and DLQ paths retain the dispatch identity. Tier 1's storage claim is that it adds no fleet-wide grant, closure, counter, or permit family, not that the key occupies no bytes in existing job representations.

Tier 2: fleet-exact execution grants

Tier 2 defines an execution grant as the database authority for one keyed active attempt. For each (key_policy_id, concurrency_key_digest):

count(unclosed execution grants) <= max_in_flight

That invariant is fleet-wide and independent of the number of worker processes or claimers.

Durable key identity and policy authority

InsertOpts::concurrency_key is distinct from ordering_key. When it is absent and an ordering_key is present, the enqueue path copies the ordering key into the concurrency-key input before storage. A domain-separated SHA-256 digest, rather than the raw caller key, is stored on every live representation and every wide representation that can later be re-enqueued: ready, deferred, lease, retry, and DLQ rows. SHA-256 rather than ADR-002's BLAKE3 is deliberate: the public SQL contract computes this digest authoritatively inside PostgreSQL, and sha256() is a PostgreSQL built-in, while BLAKE3 would require an extension or an impractical procedural implementation. unique_key remains client-side BLAKE3 under ADR-002; the two key families intentionally use different algorithms and computation venues, and the #342 vectors cover both. Narrow terminal rows and compact successful completions hydrate the digest from their retained ready backing row under ADR-026 instead of widening the terminal hot path. The fallback is evaluated once at enqueue; it is not recomputed from routing state later.

Enqueue-shard routing has explicit precedence:

  1. an explicit ordering_key selects the shard;
  2. otherwise a concurrency_key selects a stable shard using its stored digest; and
  3. only jobs with neither key use the enqueue rotor.

The second rule localizes keyed-but-unordered work instead of scattering one saturated key across every enqueue shard. It does not promote concurrency_key to an ordering API: Awa still makes no completion-order promise, and jobs with explicit, differing ordering keys may share one concurrency key across several lanes. Physical PartitionedQueue routing is unchanged; the policy id remains the cross-partition concurrency namespace.

Shard locality is part of the producer contract, and the public SQL boundary enforces it rather than trusting each polyglot client. Every supported producer path uses the same domain-separated concurrency-key digest and routing precedence above. The awa.insert_job contract in #342 receives the logical keys and computes the digest and enqueue_shard authoritatively in PostgreSQL; it does not accept a client digest or shard as authoritative. If a conformance/debug request supplies an expected value, a mismatch fails with a stable contract error. Binary-coupled and bulk paths perform the equivalent validation before publishing staged rows. The contract freezes the digest and concurrency_key-to-shard vectors alongside the existing ordering_key vectors, converting a non-compliant producer from a silent queue-wide probe-cost regression into a boundary error.

The selected enqueue_shard becomes durable routing metadata on deferred/retry, callback, and DLQ representations and is reused when those rows return to ready. It is not recomputed through the unkeyed rotor. This also closes the current retry-path gap where an original ordering-key route is not retained. Lowering enqueue_shards follows ADR-025's drain/compatibility rules for lanes that were selected under the old width. A same-queue priority move retains the shard. A cross-queue administrative move resolves the retained shard against the destination width as retained_enqueue_shard % destination_enqueue_shards; it must not copy an out-of-range shard or fall back to the rotor. Jobs that shared a source route therefore remain co-located after the move.

The limit and scope are database-authoritative, represented by a bounded policy catalog rather than independently configured worker values. A policy has a stable key_policy_id and a max_in_flight value. Multiple physical partitions of one PartitionedQueue may reference the same policy, so a width change or cross-partition administration cannot accidentally create a second concurrency namespace. Jobs without a concurrency key do not consume a grant.

Lowering a limit must preserve the invariant at the instant the new value becomes authoritative, without holding a claim-conflicting row lock during an unbounded all-key scan. A successful change uses a three-step, epoch-guarded transition:

  1. A short transaction exclusively locks the policy row, records the requested target, increments its change epoch, and moves admission to draining. Acquiring that lock waits for claimers that already hold the shared policy lock; after commit, no new grant can open under that policy. A policy carries at most one pending transition: step 1 refuses while another change is already draining.
  2. Outside the exclusive window, Awa scans the fixed claim-ring children for the maximum open count under a bounded, operator-visible verification timeout. Closures may continue and can only lower the result. A timeout or resource-limit failure returns verification_incomplete, keeps the old limit authoritative, and leaves admission drained until the operator resumes or cancels.
  3. A final short transaction locks the policy row, verifies the same epoch, target, and drained state, and activates the lower value only if the observed maximum is within it. Otherwise it reports the observed maximum, leaves the old limit unchanged, and keeps the transition pending and drained so open grants keep falling toward the target.

Retrying after verification_incomplete is a resume of the pending transition, not a new step 1 — step 1 refuses while a change is draining, so resume is the only forward path. Resume carries the change epoch and target returned by step 1, validates them against the pending transition in a short policy-row transaction without incrementing the epoch, keeps admission closed throughout, and reruns step 2's bounded verification followed by step 3. A resume with a stale epoch or mismatched target is rejected without changing policy state; resume never creates a second pending transition.

Cancellation is also an epoch-guarded state transition, not an out-of-band flag clear. The request carries the change epoch and target returned by step 1. A short transaction exclusively locks the policy row, requires that exact epoch/target and draining state, increments the epoch, clears the pending target, and restores the prior active limit/state atomically. Admission remains closed until that transaction commits. A stale cancel is rejected without changing policy state; an in-flight verifier or activator from the cancelled operation observes the new epoch and cannot commit. After commit, new claims may use only the restored prior limit.

This protocol never exposes a committed policy state where the lower limit is authoritative while an over-limit governed set exists, and claimers do not queue behind the verification scan. The fail-closed residue is that an abandoned transition — an operator tool that crashed after step 1 — leaves the policy draining indefinitely; nothing re-opens admission automatically. Draining state, its pending target/epoch, and its age are therefore first-class health and metrics signals, not just rows an operator can query. The operator waits for open grants to fall within the target and resumes; Awa never "grandfathers" an over-limit active set under a lower authoritative value. Disabling or changing a policy's identity while it has open grants uses the same drain/epoch discipline; silently changing namespace would weaken the established guarantee.

Append-only grant and closure evidence

An execution grant is a semantic role of the attempt's existing claim evidence, not necessarily a second ledger family. Grant open-ness and claim open-ness have the same lifecycle: materializing a receipt claim into leases or entering waiting_external does not close either, while completion, retry, snooze, cancellation, DLQ, and rescue close both.

The preferred physical prototype therefore routes keyed attempts through the retained row-local lease_claims shape and adds key_policy_id plus the full key digest to that row. Existing lease_claim_closures / lease_claim_closure_batches evidence closes the grant. The open set is keyed lease_claims anti-joined with those existing closure shapes. Per-child indexes on (key_policy_id, key_digest) limit the search to one key across the fixed claim-ring children; same-key history retained within those children remains an E5 cost to measure.

This row-local shape is necessary because compact lease_claim_batches stores members in arrays; PostgreSQL cannot provide the per-member policy/key index needed by the claim-time capacity query without unnesting retained batches. Reusing claim evidence avoids an extra close write on every transition, makes grant/claim divergence structurally impossible, and reuses the existing claim prune proof.

It is not free. Keyed throughput that would otherwise use compact claim batches writes one row-local claim per attempt, and closed rows remain until claim-ring rotation. E5 must compare this preferred shape with a separate indexed grant/closure ledger that lets the ordinary receipt claim remain compact. Both are append/truncate designs; neither may introduce a mutable per-key counter. The accepted physical shape is the one that passes the #246, pinned-MVCC, WAL, and claim-p99 gates.

Serialized claim decision

claim_ready_runtime is already a PL/pgSQL transaction-scoped allocator, so the protocol remains one database round trip. It probes eligible lane heads in scheduler order, bounded by the queue's finite (priority, enqueue_shard) lane registry. For each candidate lane it:

  1. reads the ordinary FIFO candidate window;
  2. takes a shared row lock on each referenced policy, compatible with other claimers but conflicting with an administrative policy update;
  3. on the first occurrence of each keyed value, attempts a transaction-scoped advisory lock;
  4. counts committed, unclosed grants for that key;
  5. chooses the longest contiguous candidate prefix that stays within every limit; and
  6. appends ordinary claim evidence, including the grant fields or separate grant prototype, in the same transaction.

The function keeps per-call memos for policy row locks and for (key_policy_id, key_digest): it locks each policy once, acquires and counts a key once, then tracks grants tentatively added by earlier admitted prefixes. This matters when an explicit ordering key, priority, or compatibility row places the same concurrency key in more than one probed lane. The database-authoritative count remains the source of truth; the memo only avoids repeating work inside one transaction.

The default prototype uses pg_try_advisory_xact_lock: contention ends the admissible prefix without waiting on another key decision. A 64-bit lock key is derived with a domain that includes the Awa schema identity, policy id, and full digest; advisory locks are database-scoped, so omitting the schema would make independent Awa schemas contend accidentally. A hash collision only causes conservative under-admission because capacity is still counted by the full stored digest. The existing per-attempt receipt-lock helper is the implementation precedent, while the exact blocking versus try-lock acquisition plan remains part of E5 and AwaStorageLockOrder validation.

If a lane head is at capacity or its key lock is busy, the function does not advance that lane's cursor or write a tombstone; it probes the next eligible lane. The current allocator selects only one lane per call, and SKIP LOCKED helps only when concurrent transactions overlap, so returning immediately here could repeatedly pick the same gated lane and make a ready queue appear idle. The new result contract must distinguish idle, key_gated, and key_lock_contended even when it returns no jobs. Dispatch idle backoff and idle-prune heuristics must treat the latter two as busy.

Relation locks for the eventual claim insert still interact with claim-ring prune; try-locking the key does not make that insert non-blocking. The existing bounded prune lock_timeout remains, and E5 plus the lock-order model must cover claim-holds-key-lock versus prune-holds-child-lock. If an implementation caps probes below the fixed lane count, it must rotate the starting lane across calls so lanes outside one probe window remain live.

Grant-close wakeup and gated backoff

Closing a keyed grant changes ready-work eligibility even when it does not insert a ready row. Every completion, retry/snooze, cancellation, DLQ, callback resolution, caller-owned completion, and rescue transaction that closes one or more keyed grants therefore emits the ordinary transactional queue notification for each distinct affected logical queue. PostgreSQL delivers the notification only after commit, so a woken claimer sees the closure. Batched closure paths dedupe queues before calling pg_notify.

key_gated is its own dispatcher wait class. It is busy for idle-prune/health purposes but does not hot-spin: the dispatcher waits for a queue notification plus a bounded safety poll for lost notifications. key_lock_contended uses a shorter jittered retry because the competing claim transaction should be brief. With a working session listener, post-closure pickup is notify-bounded; poll-only or transaction-pooler deployments use the explicit gated safety cadence. E5 measures pickup latency and notification wake amplification in both modes.

Grant lifetime

The grant opens with the committed claim and stays open while the attempt is running or waiting_external. It closes atomically when the attempt:

  • completes, fails terminally, enters the DLQ, or is cancelled;
  • snoozes or becomes retryable/deferred;
  • is rescued; or
  • otherwise leaves the active-attempt set.

A later retry is a new attempt and acquires a new grant. Consequently, max_in_flight = 1 prevents overlap in Awa's admitted active-attempt set but does not promise completion order across backoff: after event A becomes deferred, event B may run before A's retry. Holding a key barrier across retry is a different strict-serialization feature and is not implied by this ADR.

Callback parking and resumption within waiting_external retain the grant and do not append or reuse closure evidence. Only callback resolution that completes, retries/defers, cancels, enters the DLQ, or otherwise leaves the active-attempt set closes it. Normal executor finalization, batched completion, such callback resolution, admin cancellation, and maintenance rescue append or reuse the claim closure in the same transaction as their guarded state transition. ADR-042 extends the same rule to a caller-owned transaction: application rows, terminal evidence, and the grant closure commit together or all roll back.

Capacity-release latency is therefore part of policy sizing. A crashed attempt holds its grant until heartbeat/deadline rescue; waiting_external holds it until callback resolution or callback_timeout_at, extended by callback heartbeats. There is no independent timer that silently releases a live grant. Operators choose attempt deadlines and callback timeouts consistent with the key's availability objective, and health/metrics expose the oldest open grant. At limit 1, a crash blocks later admission for that key until rescue, and an unresolved one-hour callback can hold the key for the full hour. Forced rescue still cannot prove that a partitioned zombie stopped calling an external provider.

Policy and operator surface

max_in_flight must be at least 1. A zero value would pause every key attached to a policy, not one tenant, and queue pause already expresses that operation honestly. Per-key incident overrides are a future design because a high-cardinality mutable override table could recreate the hot-row problem this ADR avoids.

The admin API and CLI expose policy create/show/update, limit lowering, drain, and force-disable operations using the same preview/confirm conventions as other operator mutations. Limit changes are audited with an explicit authenticated actor supplied by the admin boundary, or with PostgreSQL session_user for direct operator SQL; a definer must not record current_user, which would name its execution owner. The UI and SQL inspection surface provide:

  • open grant count and oldest age by policy and key digest;
  • top saturated keys without exposing raw caller keys;
  • a derived key_gated reason on an available job when its current policy/key is saturated;
  • policies in draining, with their pending target, change epoch, and age, surfaced through health/metrics so an abandoned transition pages rather than lingers; and
  • queue metrics for gated outcomes, lock contention, closure notifications, wake amplification, and post-closure pickup latency.

These are derived views over authoritative evidence, not a new mutable live-counter family.

Fairness is a separate allocator decision

Issue #340 also asks for fairness across keys. Exact admission and cross-key fair scheduling have different proof obligations. The contiguous-prefix protocol above intentionally preserves FIFO and cursor monotonicity, so a saturated key at a lane head blocks other keys behind it in that lane. Bounded lane probing prevents that lane from masquerading as an idle physical queue, but it is not round-robin scheduling across keys.

Removing within-lane head-of-line blocking requires a later design: for example durable per-key sub-lanes or append-only hole/skip evidence that the allocator and prune model can prove. Tier 2 may ship without claiming cross-key fairness. Metrics must expose gated lanes, lock contention, oldest gated age, and ready-but-gated outcomes so the trade-off is visible.

Guarantees and non-guarantees

Tier 2 guarantees:

  • no more than the configured number of committed, unclosed Awa attempts for a key across the fleet;
  • rollback-safe admission: a failed claim transaction creates neither a claim nor a grant;
  • attempt-specific release: a stale completion cannot close a successor's grant;
  • no release before durable finalization acknowledgement; and
  • a gated lane alone cannot produce an idle result while an admissible lane is within the configured probe set.

It does not guarantee:

  • exactly-once handler execution;
  • that a rescued/zombie process has stopped running application code;
  • exactly-once effects in an external provider;
  • per-key completion order across retry/snooze; or
  • cross-key fairness.

Provider-facing handlers must still use provider idempotency keys and reconcile authoritative provider state. Where the provider accepts a fencing token, (job_id, run_lease) or a derived attempt token can be supplied, but acceptance is outside Awa's contract.

Rolling upgrade

The schema expansion is disabled by default. Older workers may operate the additive schema only while no exact policy is enabled; they cannot retain the new key through every retry/rescue path. Enabling a policy therefore acts as a capability-gated flip under ADR-041: every fresh runtime that can claim, retry, rescue, or administratively move the affected queues must advertise support first.

Runtime capability is necessary but not sufficient. Rows enqueued before the expansion may have an ordering_key-selected shard but no recoverable concurrency-key digest: many keys map to one shard, so enqueue_shard is not an authoritative backfill source. Before enabling complete coverage, the same guarded flip must prove that every affected ready, deferred, retry, callback, DLQ, receipt, and lease representation either carries the new policy/key identity or has drained. An application-supplied authoritative key mapping may backfill rows before the flip; otherwise the operator drains the affected queues. A legacy row without key identity never silently shares a policy with new keyed work, because Awa cannot know which user-intent key it should consume.

For a queue that cannot drain, the operator may instead select an explicit permit_legacy_ungoverned compatibility activation. Legacy rows then run without a grant while rows with authoritative identity receive exact Tier 2 admission. This does not violate OpenGrantsPerKey <= Limit for governed rows, but it weakens the end-to-end user-intent promise: a legacy row may run concurrently with governed key-mates. The policy reports coverage=partial_legacy, awa doctor remains non-green with representation counts and remediation, and health/metrics expose ungoverned admission totals and remaining backlog. No raw shard is treated as a key. Promotion to coverage=complete is a separate epoch-guarded flip after every representation has drained or been authoritatively backfilled.

The implementation release must either provide a compatible N-1 patch that recognizes the expand schema or defer the migration to the next minor. This ADR does not reopen the current 0.7 release gate by itself.

Validation

Before Tier 2 ships:

  • a focused TLA+ model proves OpenGrantsPerKey <= Limit, the drain/epoch/verify/resume/activate-or-cancel lowering protocol — including that resume validates the pending epoch/target, never opens admission, and never creates a second pending transition — the governed-only scope of partial-legacy coverage, attempt-specific closure, rollback safety, completion-versus-rescue, no cursor advance over a gated head, and conditional liveness when capacity eventually becomes available;
  • AwaStorageLockOrder covers the shared policy-row lock before key-lock acquisition, both short exclusive policy-transition windows with verification outside them, receipt completion, and both directions of the claim/prune partition-lock interaction;
  • AwaDeadTupleContract classifies the selected row-local or separate-ledger shape as append/truncate and forbids per-key UPDATE/DELETE;
  • integration races run at least two claimers on different runtimes against the same key and prove one winner at limit 1;
  • limit lowering fences admission in a short epoch transition, performs its all-key verification outside the exclusive policy-row window under a bounded timeout, refuses incomplete or over-target verification, and never exposes a committed policy state where OpenGrantsPerKey > Limit; concurrent claimers do not wait behind the verification scan. A matching cancel restores the prior limit atomically, stale cancel/activate/resume requests fail, and no grant opens while rollback still reports draining; a second transition refuses while one is pending; resume validates the pending epoch/target, keeps admission closed, and reruns verification to activation; and an abandoned draining policy is reported through health/metrics with its age;
  • a gated top-priority lane cannot make an admissible lane in the same queue report idle, and the storage result distinguishes idle, gated, and lock-contended outcomes;
  • keyed jobs without an ordering key remain shard-local across enqueue, retry, callback, and DLQ replay; differing explicit ordering keys exercise the per-call same-key memo across lanes;
  • complete-coverage enablement refuses while an affected legacy representation lacks authoritative key identity, including banked backlog and in-flight N-1 attempts; a drain or authoritative backfill plus the final capability/row reconciliation makes the flip succeed atomically. The explicit partial-legacy mode admits those rows ungoverned, reports non-green coverage and metrics, and cannot promote to complete coverage until the same reconciliation passes;
  • closing a grant wakes a gated session-listening dispatcher after commit without hot polling; poll-only mode stays within its documented gated safety interval;
  • completion, caller-owned completion, retry, snooze, cancel, DLQ, rescue, rotation, and prune each prove exact grant lifetime; callback park/resume retains the grant and only resolution that leaves the active-attempt set closes it;
  • E5 compares keyed row-local claim evidence with a separate grant/closure ledger under uniform and Zipf key distributions at limits 1 and N. It records claim p99, oldest gated age, throughput, WAL/job, retained rows, and storage footprint, and stays within the roadmap's 10% claim-p99 gate; and
  • the ADR-041 mixed-version rehearsal proves the disabled expand phase and the capability-gated enablement boundary with real N-1 artifacts.

Alternatives considered

BLAKE3 for the concurrency digest

Rejected. Server-authoritative digest computation is part of the producer contract, and PostgreSQL has no native BLAKE3 — it would need an extension or a procedural implementation on the insert hot path. Computing BLAKE3 client-side only would reopen the silent key-scatter hole the authoritative boundary closes.

Read live leases/receipts without serialization

Rejected. Two concurrent claimers can observe the same open count and both admit work.

Separate execution-grant and closure ledgers

Retained as an E5 physical-layout candidate, not the default. It preserves compact receipt claims but duplicates one grant and one closure fact for every keyed attempt and adds a second prune proof. It wins only if the row-local claim shape repeats the #246 regression badly enough to justify that write and correctness surface.

Put key arrays on compact claim batches

Rejected for the capacity-query index. It keeps the batch compact, but PostgreSQL cannot index each member by (key_policy_id, key_digest) without expanding retained arrays on the hot claim path.

Park gated jobs and promote them on grant closure

This is the strongest alternative to lane-head gating and is retained as E5 prototype (d). A committed park transition could tombstone the ready lane, advance its cursor with durable evidence, and move the job to a keyed deferred-like structure. Grant closure would promote parked work and reuse ordinary ready-row notification, removing repeated probes and within-lane head-of-line blocking.

The costs are substantial: every gated arrival adds a move; the parking table churns in proportion to the hot key's backlog; closure gains promotion work; and FIFO moves from lane order to parking order. More importantly, promotion without a reservation can lose the next capacity slot to a new ready arrival and re-park the same job, while reserving capacity before dispatch introduces the pre-start reservation state ADR-023 deliberately avoided. E5 may select parking only with an exact handoff/liveness proof, no hot mutable per-key row, and better measured results than the simpler lane-probe shapes. Solid Queue's concurrency controls are direct semaphore-parking prior art; GoodJob's concurrency controls and Oban Pro's Smart Engine are broader Postgres-queue comparisons. Their choices are inputs, not evidence that the same storage trade-offs fit Awa's claim ring.

Mutable per-key counter or permit row

Rejected as the default design. It gives a simple unique/atomic lock target but recreates the high-cardinality hot mutable state that #169 and ADR-026 require Awa to avoid.

Session advisory lock held for the whole handler

Rejected. It consumes one dedicated Postgres session per keyed execution and releases on database disconnect even if the handler process is still running, so it does not improve the zombie-worker boundary.

Shard pinning plus a per-shard cap

Rejected as fleet-exact per-key control. A cap of one serializes unrelated keys sharing the shard; a larger cap permits the same key to overlap.

Post-claim worker-local gating

Retained only for Tier 1. Across a fleet it is approximate and holds durable claims that are not executing, wasting claim/receipt capacity.

Relationship to other ADRs

  • ADR-005/010/011: priority aging, rate limiting, and worker capacity continue to decide when a dispatcher asks for work; the execution grant is an additional storage admission condition.
  • ADR-013: (job_id, run_lease) is the attempt identity attached to and allowed to close a grant.
  • ADR-019/023/026: the preferred grant representation reuses the append-only claim/closure ring discipline and prune proof; E5 may select a separate append/truncate ledger only with its added proof cost recorded.
  • ADR-025/031: ordering and concurrency keys are distinct. An explicit ordering key owns shard routing; otherwise the concurrency key provides shard locality without adding an ordering promise. The policy id defines the fleet-exact cross-partition namespace.
  • ADR-041: enabling exact enforcement is a capability-gated representation flip.
  • ADR-042: caller-owned completion closes the exact attempt's grant in the caller's atomic business/finalization transaction.