RS3B-05 — Registrar Replay / Idempotency / Attempt State Machine v0.1 — 2026-06-21
RS3B-05 — Registrar Replay / Idempotency / Attempt State Machine v0.1 — 2026-06-21
Macro: RS3B-REGISTRAR-HARDENING-DESIGN (read-only / KB-design)
Deliverable: 05 of 10 · Replay / idempotency / attempt (Mục tiêu E) · consumes Codex RS3-PATCH2 C1–C3
Date: 2026-06-21 · 0 mutations · NO_CODEX_LIVE_READ
Deliverable status: REPLAY_DOMAIN_FAIL_CLOSED_UNTIL_SURFACE_FIT_PROVEN · REPLAY_SURFACE_NOT_FIT — no existing surface fits; required surface defined as criteria, not created.
1. Three separated identities (Codex C1)
The conflated nonce | idempotency_key is split into three independent identities. One physical surface may carry all three only if it proves separate constraints and state transitions for each.
| Identity | Definition | Uniqueness rule | Must NOT |
|---|---|---|---|
logical_request_key |
stable across exact client retries; unique for one intended registration effect (one DOT artifact, one operation/target/run) | single-use axis: one durable consumed record per logical key | be re-satisfied by a new attempt or a fresh nonce |
authorization_nonce |
single-use authority credential, bound to the exact authority envelope (owner/approval row) + validity window | single-use; consumed on use; bound to envelope, not free-floating | authorize a duplicate logical effect just by being fresh |
attempt_id |
execution/retry identity for one logical key | never part of the single-use uniqueness key | bypass logical_request_key (the REPLAY_ATTEMPT_NO_BYPASS defect) |
Canonical binding digest (RS3-PATCH2 §6.1, carried as the authority-binding value, not the sole identity):
replay_key = H(protocol_version, logical_request_key, canonical_operation, canonical_target,
deployed_artifact_hash, owner_or_approval_binding, run_id) # attempt_id NON-keying
A fresh authorization_nonce must not permit a duplicate logical registration effect: the nonce authorizes; the logical_request_key (via replay_key) is what is single-use.
2. Why the live surface is NOT fit (source-fact, 2026-06-21)
iu_route_attempt (68 rows): columns idempotency_key (text NN), attempt_no (int NN, default 1, CHECK ≥ 1), route_kind ∈ {inbound,outbound}, status ∈ {pending,dry_run,sent,skipped,failed,disabled}; constraint UNIQUE(idempotency_key, attempt_no); no replay_key/nonce/run_id/operation/canonical_target/deployed_artifact_hash/owner-approval columns; no triggers.
UNIQUE(idempotency_key, attempt_no)is not single-use:(key,1),(key,2),… all insert → incrementingattempt_noreuses the same key (REPLAY_ATTEMPT_NO_BYPASS).- Wrong domain: IU message routing (inbound/outbound), not DOT registration.
- Writer authority unproven; no atomic-consume semantics for registration.
Verdict: iu_route_attempt is rejected as the registrar replay store (must-not-do #31). It is a retry-ledger precedent only.
3. Transaction / rollback state machine (Codex C2)
Single atomic Phase-1 transaction consumes the logical key and writes the inert registration result together. No "committed consume + failed same-txn registration" split (that contradiction is rejected, RS3-PATCH2 C2).
┌──────────────────────────── Phase-1 atomic txn ───────────────────────────┐
issue req │ check logical_request_key consumed? ── yes ─► return PRIOR durable result │ (exact retry)
(nonce, │ │ no │
attempt) │ ▼ │
│ validate nonce single-use + bound + fresh ── fail ─► REJECT (no consume) │
│ │ ok │
│ ▼ │
│ INSERT consume(logical_request_key) + INSERT inert registration row │
│ │ ON CONFLICT(logical_request_key) -> REPLAY_DUPLICATE (reject) │
│ ▼ │
│ COMMIT ── fail ─► (S1) PRE-COMMIT FAILURE: both roll back together │
└────────────┬────────────────────────────────────────────────────────────────┘
│ commit ok
▼
(S2) COMMIT SUCCESS: consume + inert registration durable together
│
▼
post-commit paired verifier (RS3B-03 §5)
│
┌──────────┴───────────┐
verify PASS verify FAIL
│ │
registered-closed (S3) POST-COMMIT VERIFY FAIL:
inert row exists; logical key STAYS consumed;
returns failed/compensating state; a NEW nonce may
authorize COMPENSATION but MUST NOT recreate the effect
State table
| State | Condition | Consume row | Registration row | Retry behavior |
|---|---|---|---|---|
| S0 issue | request arrives | — | — | check consumed first |
| S1 pre-commit failure | commit fails / crash before commit | rolled back | rolled back | uncertain-commit recovery: client must re-resolve commit status before issuing a new logical request (do not blindly re-issue) |
| S2 commit success | atomic commit | durable | durable inert | exact retry returns the durable prior result (idempotent) |
| S3 post-commit verify fail | verifier rejects committed row | durable (stays consumed) | durable inert (flagged) | same logical key returns failed/compensating; new nonce → compensation only, no new registration effect |
| S4 concurrent attempt | second attempt_id on same logical key during S0–S2 |
one wins via ON CONFLICT | one row | loser → ATTEMPT_COLLISION, reads winner's result |
4. Freshness vs consumed-state (Codex C3)
Request/envelope TTL governs whether a new request is admissible; it does not make an already-consumed logical_request_key reusable. The consumed record is retained / tombstoned for the replay horizon (not deleted when a request goes stale). An old consume row still proves prior consumption.
P2-RP-07 REPLAY_STALE_ROWis reinterpreted: a stale request is rejected as inadmissible; a stale consume row still blocks replay.- No authority-approved key-reuse policy exists here, so consumed = permanently blocking for the horizon.
CONSUMED_STATE_ERASED_BY_STALE_REQUESTis a fail-open condition (must-not-do #33).
5. Failure / adversarial table
| ID | Bad input/state | Expected | Layer | Fail-open condition (forbidden) |
|---|---|---|---|---|
| RP-01 | same logical_request_key, new attempt_id |
REPLAY_ATTEMPT_NO_BYPASS / return prior |
R | accepting because attempt differs |
| RP-02 | fresh authorization_nonce, same logical effect |
reject duplicate effect | R | nonce freshness re-authorizing effect |
| RP-03 | reused (already-consumed) nonce | REPLAY_NONCE_CONSUMED |
R | accepting consumed nonce |
| RP-04 | nonce not bound to envelope/window | NONCE_UNBOUND |
V/R | accepting free-floating nonce |
| RP-05 | exact client retry after S2 | return durable prior result | R | creating a 2nd effect |
| RP-06 | crash between insert and commit (S1) | both roll back; uncertain-commit recovery | R | re-issuing without recovery → double effect |
| RP-07 | post-commit verify fail (S3) | logical key stays consumed; compensation only | R | recreating registration under new nonce |
| RP-08 | stale request, consume row exists | request inadmissible; replay still blocked | V/R | erasing consumed meaning on staleness |
| RP-09 | two concurrent attempts (S4) | one wins (ON CONFLICT), other ATTEMPT_COLLISION |
R | both commit |
| RP-10 | use iu_route_attempt as the store |
REPLAY_SURFACE_NOT_FIT (reject surface) |
R | treating attempt-ledger as single-use |
6. Required future surface (defined, NOT created)
To reach REPLAY_DOMAIN_READY_AS_CRITERIA, the selected surface must prove ALL: (a) single-use UNIQUE on replay_key (one row per logical effect; attempt_no/attempt_id non-keying); (b) columns binding operation, canonical_target, run_id, deployed_artifact_hash, owner_or_approval_binding; (c) registration domain (not IU routing); (d) atomic in-Phase-1 consume with ON-CONFLICT reject; (e) proven registrar writer + append-only/immutability; (f) durable failure audit outside the rolled-back txn (RS3B-06); (g) exact-retry returns prior decision; (h) retention/tombstone for the replay horizon (C3). Surface selection is fail-closed; no table is created (reuse-first; evaluate existing surfaces before any new ledger).
7. Status block
- Deliverable status:
REPLAY_DOMAIN_FAIL_CLOSED_UNTIL_SURFACE_FIT_PROVEN·REPLAY_SURFACE_NOT_FIT - Consumes Codex C1 (3 identities), C2 (txn state model), C3 (freshness ≠ consumed-erasure)
iu_route_attemptrejected as store; pure validator is not the replay-state owner (must-not-do #28)- Registration gate:
REGISTRATION_HOLD·REGISTRATION_CAN_PROCEED = NO· 0 mutations