feat(optout): the two opt-out tiers, and the fan-out that makes erasure real (17d) master
SOFT DISABLE composes what were two uncomposed writes. Marking the actor disabled and cancelling their queued work now ride the rev-gate transaction handleFederation was already handed and discarded, so atomicity is structural and the gate advance is coupled to it: a failure leaves the record replayable rather than half-applied. A user who asks us to stop no longer watches their queued posts keep arriving for the next hour. The cancellation is scoped to the ACTOR across every community they have work in, and a re-enable does NOT resurrect cancelled deliveries — those were withdrawn by the user's own request, and re-sending them would publish on their behalf something they had already taken back. The actor document keeps being served; that is what separates this tier from the next. THE FAN-OUT, flagged since task 15, was keyed on the wrong question. The enqueuer asked "does this ACTIVITY exist?" and returned — true for every leg after the first, so one activity meant one delivery. Delete{Person} is one activity to MANY inboxes, and the assumption bit on REDELIVERY rather than first send, which is the worst possible timing for the tier most likely to be retried. Now InsertTx still answers the activity question and EnqueueTx owns "does THIS delivery exist?" via ON CONFLICT (activity_id, target_inbox), reading the standing row back on the same transaction. The standing row always wins, so a replay can never revive a delivery since delivered, poisoned, or cancelled by an opt-out. DESTRUCTIVE tier: Delete{Person, removeData: true} to every inbox the actor's content reached. The delivery history IS the address book — there is no other record of which instances hold a user's content — enumerated DISTINCT ON the inbox because erasure is per INSTANCE, keeping a real ordering key so the withdrawal serializes on a line that instance's traffic already uses. It is deliberately blind to delivery state: a poisoned or cancelled row may still have reached the peer, and reaching an instance that does not hold the content is harmless while missing one that does is not. Instances that found the actor via search or WebFinger and never received a delivery are the honest boundary, stated in the code. Each live delivered vote gets an Undo and has its state flipped in one upsert, because a purged actor leaving standing tallies is a number 17b's reseed would keep subtracting forever. The flip is written when we decide to withdraw rather than when a peer confirms; if the Undo never lands we have stopped counting a vote the peer may still hold, which is the right side to err on. The actor document returns 410, not 404: "never heard of them" reads as a lookup failure, Gone lets a peer stop asking. Terminal, and it disables the actor in the same statement so a withdrawn identity cannot stay discoverable. DECISION 19's confirm step existed nowhere until now — AccountTerminator had no implementation and nothing could read a DID's live state. AccountStatus goes PLC directory -> #atproto_pds -> getRepoStatus, and only active=false with status=deleted is deleted. Every failure returns an ERROR, never a verdict: "we could not confirm" must not collapse into "confirmed not deleted", the same collapse that in 17c-3 turned an unparseable timestamp into a permanent ban. Confirmed-live advances the seq so a stale deletion cannot wedge the consumer; confirm-failed does not. federation_prefs.source gains 'account' (migration 028) because neither existing value is true for a deletion — the user wrote no record and there is none left to fetch — and that column is the one place an operator can tell an opt-out from a deletion. Its Down rewrites those rows to 'probe' rather than dropping them: losing provenance is recoverable, losing the preference resumes federating for a deleted account. Ordering in the handler is load-bearing and was found by a deadlock: the cancellation runs BEFORE the purge, or it cancels the Delete{Person} it just queued; the mirror flip runs LAST, because the purge writes that row on its own transaction. The purge committing separately means a later gate rollback replays the record, which is safe only because every part of it is idempotent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>