carlos / platform Public

plan: self-serve moves 1, rev 22 (Astra confirming review 21: the floor test refuses on a stale status report, not a stale edges file)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev b48a5d403ce6058c423d7408b2151008896dfbc2 parent 194a6a4
1 file changed, +2 −2
  • docs/superpowers/plans/2026-10-22-self-serve-moves-1-lifetimes-and-order.md +2 −2

CI passed — run details

CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (1 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.014s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
=== vet (2 s)
=== gofmt (1 s)
gofmt -l .: clean
=== rest (145 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	4.098s
ok  	github.com/carlosframework/platform/cmd/moveprobe	12.040s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.802s
ok  	github.com/carlosframework/platform/internal/activator	60.033s
ok  	github.com/carlosframework/platform/internal/agentsock	0.028s
ok  	github.com/carlosframework/platform/internal/alerting	0.006s
ok  	github.com/carlosframework/platform/internal/analytics	0.027s
ok  	github.com/carlosframework/platform/internal/appconfig	3.424s
ok  	github.com/carlosframework/platform/internal/apperr	0.177s
ok  	github.com/carlosframework/platform/internal/applog	0.398s
ok  	github.com/carlosframework/platform/internal/awsinv	0.007s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.012s
ok  	github.com/carlosframework/platform/internal/boxid	0.993s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.012s
ok  	github.com/carlosframework/platform/internal/caps	0.017s
ok  	github.com/carlosframework/platform/internal/certwarm	0.005s
ok  	github.com/carlosframework/platform/internal/cgroup	0.848s
ok  	github.com/carlosframework/platform/internal/changelog	0.589s
ok  	github.com/carlosframework/platform/internal/claimactivation	1.244s
ok  	github.com/carlosframework/platform/internal/clamd	0.080s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	6.540s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.016s
ok  	github.com/carlosframework/platform/internal/dbverify	0.006s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.010s
ok  	github.com/carlosframework/platform/internal/docsite	0.014s
ok  	github.com/carlosframework/platform/internal/domains	3.062s
ok  	github.com/carlosframework/platform/internal/doorkey	0.009s
ok  	github.com/carlosframework/platform/internal/economics	0.004s
ok  	github.com/carlosframework/platform/internal/edge	45.601s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.012s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.198s
ok  	github.com/carlosframework/platform/internal/gate	1.883s
ok  	github.com/carlosframework/platform/internal/genselect	0.031s
ok  	github.com/carlosframework/platform/internal/geo	0.013s
ok  	github.com/carlosframework/platform/internal/geoip	0.070s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.303s
ok  	github.com/carlosframework/platform/internal/host	141.999s
ok  	github.com/carlosframework/platform/internal/hostactors	6.940s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.008s
ok  	github.com/carlosframework/platform/internal/hostbox	3.397s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.833s
ok  	github.com/carlosframework/platform/internal/hostenv	0.006s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.009s
ok  	github.com/carlosframework/platform/internal/hostexec	5.991s
ok  	github.com/carlosframework/platform/internal/hostfence	0.060s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.452s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.023s
ok  	github.com/carlosframework/platform/internal/hostguard	1.093s
ok  	github.com/carlosframework/platform/internal/hostops	2.293s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.004s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.003s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.680s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.307s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.002s
ok  	github.com/carlosframework/platform/internal/infraview	0.002s
ok  	github.com/carlosframework/platform/internal/instancectl	5.269s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.845s
ok  	github.com/carlosframework/platform/internal/instancefs	0.008s
ok  	github.com/carlosframework/platform/internal/instancekey	0.006s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.012s
ok  	github.com/carlosframework/platform/internal/layout	2.048s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.006s
ok  	github.com/carlosframework/platform/internal/lease	10.191s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.679s
ok  	github.com/carlosframework/platform/internal/ledger	0.014s
ok  	github.com/carlosframework/platform/internal/mail	0.239s
ok  	github.com/carlosframework/platform/internal/maintenance	0.100s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.650s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.088s
ok  	github.com/carlosframework/platform/internal/metrics	0.009s
ok  	github.com/carlosframework/platform/internal/moves	22.577s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.158s
ok  	github.com/carlosframework/platform/internal/objstore	0.302s
ok  	github.com/carlosframework/platform/internal/ops	0.185s
ok  	github.com/carlosframework/platform/internal/park	0.295s
ok  	github.com/carlosframework/platform/internal/plans	0.010s
ok  	github.com/carlosframework/platform/internal/privatefile	0.005s
ok  	github.com/carlosframework/platform/internal/provision	14.469s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.011s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.742s
ok  	github.com/carlosframework/platform/internal/relay	7.197s
ok  	github.com/carlosframework/platform/internal/release	26.284s
ok  	github.com/carlosframework/platform/internal/rollout	3.953s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.008s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.063s
ok  	github.com/carlosframework/platform/internal/rootstate	0.028s
ok  	github.com/carlosframework/platform/internal/runs	0.607s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.840s
ok  	github.com/carlosframework/platform/internal/seeds	0.014s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	12.751s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.893s
ok  	github.com/carlosframework/platform/internal/status	0.093s
ok  	github.com/carlosframework/platform/internal/steering	0.007s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.156s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.006s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.005s
ok  	github.com/carlosframework/platform/internal/unixdial	0.003s
ok  	github.com/carlosframework/platform/internal/updatewords	0.007s
ok  	github.com/carlosframework/platform/internal/usage	0.009s
ok  	github.com/carlosframework/platform/internal/version	0.003s
ok  	github.com/carlosframework/platform/internal/vet	1.711s
=== internal_console@0 (77 s)
ok  	github.com/carlosframework/platform/internal/console	75.104s
=== internal_console@1 (38 s)
ok  	github.com/carlosframework/platform/internal/console	36.019s
=== internal_console@2 (44 s)
ok  	github.com/carlosframework/platform/internal/console	41.639s
=== internal_console@3 (40 s)
ok  	github.com/carlosframework/platform/internal/console	37.170s
=== internal_console@4 (39 s)
ok  	github.com/carlosframework/platform/internal/console	36.185s
=== internal_console@5 (37 s)
ok  	github.com/carlosframework/platform/internal/console	34.416s
=== internal_console@6 (38 s)
ok  	github.com/carlosframework/platform/internal/console	35.609s
=== internal_console@7 (46 s)
ok  	github.com/carlosframework/platform/internal/console	43.144s
=== root@0 (35 s)
ok  	github.com/carlosframework/platform	33.359s
=== root@1 (45 s)
ok  	github.com/carlosframework/platform	43.956s
=== root@2 (44 s)
ok  	github.com/carlosframework/platform	42.011s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 50069666 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.03s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.02s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.037s
=== RUN   TestT4StorageProbeMinio
2026/10/22 11:01:43 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E0D50CF52B0A61, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.030s
minio contract: PASS
diff --git a/docs/superpowers/plans/2026-10-22-self-serve-moves-1-lifetimes-and-order.md b/docs/superpowers/plans/2026-10-22-self-serve-moves-1-lifetimes-and-order.md
index 3f484b4d1..4f0609870 100644
--- a/docs/superpowers/plans/2026-10-22-self-serve-moves-1-lifetimes-and-order.md
+++ b/docs/superpowers/plans/2026-10-22-self-serve-moves-1-lifetimes-and-order.md
@@ -2,7 +2,7 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-Rev 21, 2026-10-22. Rev 20's review (`selfserve-plan1-review20.md`) was **READY WITH CHANGES**: the two changes are made: the cutover door's box check takes the capability from the edges file and **liveness from the box's status report** (`status/boxes/<label>.json` within 10 minutes, `Metrics.BoxID` equal to the file's box), never from the edges file's age, because an unchanged edges file is not rewritten; and Task 0's surviving sentence about boxes purging until they read the marker is removed (boxes purge nothing from this release on). Rev 19's review (`selfserve-plan1-review19.md`: 18, 20, 25 closed; 23, 26, 27, 28 open) is answered: the box floor is a **capability in the edges file** (`lifetimes-1`, the inventory the console already reads for every fleet decision), the cutover door refuses while any edges file lacks it or is stale, and a box whose file later loses it pages "box <label> regressed below the lifetimes floor" (23); **the return of an older box binary after the cutover, and retention's prune-versus-recreate race, are declared out of scope**: neither is introduced or widened by this plan, both are today's exposure on `main`, and a fleet-wide binary floor is its own piece of work, filed as an amadan discussion (23, 26); restore evidence keeps today's value for every ordinary app object and applies the three-valued lifetime judgement to instance-record keys alone (27); the cancellation plumbing is declared (`record_retirements.cancels`, `Retirement.Cancels`, `BeginCancellingRetirement`) (28); the surviving statements about boxes purging before the cutover are removed. Rev 18's review (`selfserve-plan1-review18.md`: 5, 19, 22, 24 closed; 18, 20 declarations; 23, 25 P1) is answered: **claims-off boxes no longer auto-purge at all** from this release (a check-then-delete can always pause between the two; no fence is possible on a box), the console's expiry pass owns the store trash sweep, and the cutover door **refuses while any box of the fleet reports a version older than this release or no fresh status**, so by the time a marker can exist no box purge code is running (23); the materialisation worker takes `s.lockApp` per app **before** collecting evidence, re-reads the row and the marker under it and holds it through the SQLite transition, candidates being hints only (25); the lifetime the door read travels to the create helpers as an argument, the schema, the `Allocation` struct, the real `AppLife(accountID, app)` and `release.ReadAppTombstone` names, and the SQL audit's patterns are reconciled (18); the lock table no longer lists `retireSlotOrphanLocked` as assuming the app lock (20). Rev 17's review (`selfserve-plan1-review17.md`: 10, 17, 21 closed; 5, 18, 19, 20, 22 narrowed; 23, 24 new) is answered: a box purge is fenced at **every** destructive step (`purgeApp` gains a `before func(ctx) error` hook the box fills with the marker re-read and the 30 s clock, called before each `Delete`, the final marker delete included) (23); the app lifetime is minted by **one insertion helper** every app-row insert uses (`CreateAppInRegion`, `CreateTemporaryAppInRegion`, `BackfillOpsMemberships`, the migration's `apps_new` copy, the open-time backfill), pinned by a test that scans every `INSERT INTO apps`, and the declarations agree (`TEXT`, `Allocation.AppLifetime`, the recovery wording) (18); the create's step 0 also requires a successful read of the store's trash marker as absent (5); the cross-key stale-body cleanup is **best effort within the running create** and the promise is narrowed to that (19); the initial migration pass takes app → hostname and the cross-owner orphan retirement needs only the hostname lock and its captured slot ETag, stated with its reason (20); startup materialisation runs as an admitted worker iteration, never before the gate has a state (22); restore evidence has an explicit **unknown** outcome that defers the decision (24). Rev 16's review (`selfserve-plan1-review16.md`: 5, 10, 17, 18, 19 narrowed; 20, 21, 22 new) is answered: the create protocol checks the app's lifecycle **under the app lock** (present, not trashed, the same `lifetime` the door read) before allocating (5); a box purge re-reads the cutover marker **immediately before every conditional delete** and abandons the purge when more than 30 s passed since that check, so a sweep paused across the cutover deletes nothing numbered (10); cancellation writes the orphan retirement row and the allocation's `void` in **one SQLite transaction** and the retirement names the allocation it cancels, so no two pending rows block each other (17); the app lifetime is a non-repeating `apps.lifetime` id captured by `BeginAllocation` (18); the stale-body promise is narrowed to the same key and the create's step 5 also deletes a stale body under the retired slot's owner key (19); a **lock table** states what every wrapper acquires and every `Locked` helper assumes, and the attach door takes the app lock outside its hostname exclusion (20); the purge's absent-record branch acts only on a slot **owned by the purged app** (21); `settleApp` and `MaterializeApps` count only judged-valid instance records as restore evidence (22). Rev 15's review (`selfserve-plan1-review15.md`: 13, 14, 15, 16 closed; 5, 10, 17, 18, 19 open) is answered with **one lock order** instead of a snapshot: the create protocol, allocation recovery and the console's purge all take `s.lockApp(acct, app)` and then `withHostname(host)`, so a purge and a create or recovery of the same app never interleave, and the purge observes and retires each instance **inside** the same locked step (no delayed callback can see a successor; a legacy record is retired by **ETag**); an allocation is bound to the **app lifetime** that authorised it (`app_lifetime` = the apps row's `created_at`), so a reused name never replays an old allocation; **boxes stop auto-purging once the cutover marker exists** and the console's expiry pass runs the trash sweep under the app lock, so after the cutover the only purger is the one that can see pending allocations; a slot-bound orphan entry point (`retireSlotOrphanLocked`) exists and the void path's cleanup is stated honestly (a late stale body stays until the next create of the hostname removes it in step 5; a late slot PUT leaves a live orphan the orphan rule reclaims). Rev 14's review (`selfserve-plan1-review14.md`: 9, 11, 12 closed; 5, 10, 13, 14, 15, 16 the purge protocol) is answered by **removing the purge protocol**: four rounds of a lifetime-aware purge each found the next race (snapshot vs recovery, in-flight box sweeps, legacy incarnation 0, the app lock, guard ordering, the schema), so purge goes back to **today's sweep** with one conditional step (an instance record is deleted by `DeleteIfMatch` on an ETag a `Get` captured just before), on the console under the existing `s.lockApp` mutex its callers already hold, on the box as today; and the lifetime protocol **absorbs the consequences** with two rules it already nearly had: a live slot whose record is absent and for which this console holds no pending operation is an **orphan**, retired as kind `orphan` by the next create that meets it (so a purged app's slots never block a hostname); and allocation recovery **never replays a create for a trashed or purged app** (it retires the slot, and anything that landed at its incarnation, as `orphan`, and voids the row), so a late record or slot PUT of a purged app's allocation is cleaned by the same recovery. No generation table, no slot fence, no on-marker, no console trash sweep. Rev 13's review (`selfserve-plan1-review13.md`: findings 1 and 8 closed; 5, 9, 10, 11, 12 the purge) is answered by **taking boxes out of purging once lifetimes are on**: a box that has seen `lifetimes-on.json` never purges an app again (the console's expiry pass gains the store-driven trash sweep the box runs today), so no purger exists that cannot see the console's pending allocations; before the on-marker a box purge deletes an instance record only by `DeleteIfMatch` on an ETag it captured **before** its final marker check. The console's purge runs under the app mutation lock, records the **generation** it observed for every host (slot incarnation or record ETag) before any destructive step, and every retirement states that expectation (never `retireAny`), so a delayed callback cannot touch a successor; a pending allocation is terminalised by **fencing its slot** (a CAS write of a retired slot at its incarnation, against absence or the predecessor's ETag) so an outstanding allocation PUT can never land, then `void`; the purge calls the **locked** retirement under its own `withHostname`; the box has no retirement code at all; the witnesses are split into console and box expectations with their real terminal states. Rev 12's review (`selfserve-plan1-review12.md`: findings 2, 3, 4, 6, 7 closed; 1 a witness contradiction; 5, 8, 9 the purge) is answered: the restart witness no longer activates "at once"; the console writes a durable **`control/lifetimes-on.json`** as the first act of `on`, and that marker, not judge availability, is a box's authority for any slot mutation; purge **excludes instance-record keys from its sweep** whenever a retire hook is installed; the console's purge **voids the app's pending allocations durably** (`purged_at`) and retires their slots before touching records, and recovery treats a purged pending row as `void`; the box's purge needs **no migration, no journal and no cross-process lock**: it retires by CAS alone (slot CAS to `retired`, then `DeleteIfMatch` on the observed record ETag; a marked record is skipped; any CAS loss returns an error and the next pass retries idempotently), and before the on-marker it deletes only by `DeleteIfMatch` on the listed ETag after re-reading the marker. Rev 12's review (`selfserve-plan1-review11.md`, findings 1 to 7) is answered: the storage settle is anchored **after the last `before` unit released admission** (and after the process start on a restart), so a legacy request that was fully received before its client timed out has completed before activation; activation requires a **validated marker read** (`markerSeen`), never uncertainty alone; app purge is **lifetime-aware** on every path (browser, expiry pass, box auto-purge) through a retirement core in `provision` that `release.PurgeApp` calls for every instance record before its sweep; an in-scope valid own record defeats only the record-gone reason and still reaches the app-trash decision; the dispatch of create, delete, detach and `readDeletedRecord` is stated per admitted unit; both detached continuations (`runImportUpload`, `createFrom → runFrom`) are wired; the Task 3 witness no longer says draining reads are legacy; `claimAliasUnbacked` is named in the audit. Rev 11, 2026-10-22. Rev 10's review (`selfserve-plan1-review10.md`, findings 1 to 4) is answered: the cutover gains an **admission gate**: every claims-off unit of record work (a request's handler, a detached continuation, a worker iteration) is admitted under a counted gate while the state is `before`; the cutover door closes admission synchronously, refuses new work with 503, **waits for the admitted count to reach zero**, then starts the storage settle, then activates; so no unit admitted before the marker ever runs beside the lifetime protocol, and a unit never changes mode mid-flight. `on` is **latched**: once observed it is never left; at startup the state is `unknown` until the first successful marker read and every claims-off record door (reads and mutations) answers 503 until then, so a marker-read failure never restores legacy interpretation. The removal rule separates lifetime validity from placement eligibility: a valid own record that is **in this pass's scope** protects the legacy collision; a valid own record **outside** the scope follows today's removal rules (`KeepParticipant`, the app-trash rule), so offline finishing removes the source row as today; absence or staleness authorises the new path. One worker lifecycle (start once, loop, check the state each iteration), one migration interval, one context contract (`recordCtx`), the CLI command, the retry error, the three root constructions and the copy inventory are stated. Rev 10, 2026-10-22. Rev 9's review (`selfserve-plan1-review9.md`, findings 1 to 5) is answered: the cutover is a **mode** with a drain: from the marker's time until the window ends the console refuses every claims-off record mutation (503), so no legacy write can be admitted into the window, and the gate, the judge's installation, the recovery pass and the marker's failure modes are one contract with an implementation task for the operator door; the removal loop decides from the row's **own** record (observed by its own key), so a valid legacy owner in a hostname collision is kept as today and only an established absent or stale own record retires the row, with the keep view bypassed on that evidence and replacement stated as two passes; `fenceRecord` returns a classification so an own tombstone or marker that won resumes the operation instead of terminating it; the audit's allowlist names the two remaining claims-on callers of `hostops.ReadRecord` and is pinned both ways; and the remaining contracts are settled (the detach sentence, cleanup recovery through C2, corrupt index candidates, the root restart readers' judge). Rev 8's review (`selfserve-plan1-review8.md`, findings 1 to 5) is answered: the rollout guarantee becomes an **enforced cutover** (an operator marker written after the old console has stopped, plus the settle window, with the storage assumption stated) rather than a timer alone; quarantine copies the corrupt bytes and **never deletes under a live slot**: the retirement converts durably to the cleanup kind and retires the slot first, and a conditional-delete conflict re-classifies instead of counting as absence; a provisioned row owned by another record key than the hostname's current record is retired as "record gone" so a cross-owner recreation cannot leave a lagging box serving the predecessor; `RecordIndex.Find` judges its candidate keys before deciding ambiguity; and the remaining contradictions are resolved (the root scope sentence, one detach chain, the locked retirement's callers, reconcile's one sequence, the audit's enumerated list, the fence witness split). Rev 7's review (`selfserve-plan1-review7.md`, findings 1 to 6) is answered: a stated rollout guarantee closes the late legacy delete (one console per deployment, a settle window before any termination, old consoles never reintroduced over lifetimes records); the first-route attach, its rollback and detach run **inside** the hostname exclusion from the claim write to the record write, through locked variants of the create and retirement helpers; root's proof and restart record readers take the box's judge (the scope statement is corrected: only `ClaimRootHTTP`'s claims-on readers are slot-free); a corrupt body is quarantined (its bytes kept under `control/quarantine/`, the key removed conditionally) so the retirement or migration proceeds as for absence instead of promising a fence that cannot parse it; `provision.WithLifetimes` is the one context helper that carries the judge and enables sequencing, the runs worker wraps its detached execution context, the audit distinguishes approved judged wrappers from raw readers, the fence witnesses state their timing, exhaustion is witnessed at every transition, and the sibling undo has one dispatch path. Earlier reviews: rev 1 (`after236/selfserve-plan1-review.md`, 16 findings), rev 2 (F1 to F12), rev 3 (R1 to R10), rev 4 (1 to 9), rev 5 (1 to 6), rev 6 (`selfserve-plan1-review6.md`, findings 1 to 5). Rev 7 answers rev 6's five: every claims-off record write runs with `WithRecordSequence`, so `record_seq` exists after a record's first write and an ETag never recurs, and recovery writes a **fence** (a sequencing rewrite) before it terminates an operation on evidence of a changed record; one migration dispatcher classifies the observation at every entry, so a resume can never supersede its own marker; the attach door's claim rollback runs inside the hostname exclusion and never releases a claim a pending operation or a valid record needs; the judge reaches every mutation context (the console's top-level handler wraps every request, browser and API alike; the seed callback wraps its own context; the full-record updater's re-read is judged; `RecordIndex.Find` and `FindRecordByHost` take the judge from the context), and the audit is by AST; the remaining contracts (the running N1 table, a slot-retirement helper, one expectation type, listed-key absence, exhaustion at read, the import witness's state, the producer-state publication tests) are answered. Rev 5 removed the slot cache (every judgement reads the slot fresh, after the record, in bounded rounds) and made a successor lifetime's hold one row write. Rev 6 answers rev 5's six: a retirement persists its **kind** (normal, orphan, cleanup) and each kind has its own steps and recovery table; a migration's marker-CAS conflict is classified by a re-read for both the bound and the unbound form, so an own marker that landed is resumed, never superseded; every `retireInstanceRecord` caller states its expectation (the sibling undo names the lifetime it created; detach deletes what is current; the attach door never asks for a record rollback over a lifetime failure); a judge carried on the context makes `updateRecordSeeding`, and so every `SetRecord*` writer, refuse a stale record, and every box reader (Phase 1's included) and the import journal's readers go through the judge; the provenance mark is dropped (the fold compares pairs between unstamped entries, a pair-less entry is `(0, 0)` and equal pairs fall to the existing rule, exactly §6.4); the remaining interface gaps are answered.
+Rev 22, 2026-10-22. Rev 21's confirming review (`selfserve-plan1-review21.md`) found one stale phrase in Task 4's test list ("a stale file → 409"), now "a stale or missing status report → 409"; otherwise the two READY WITH CHANGES items of review 20 are closed. Rev 20's review (`selfserve-plan1-review20.md`) was **READY WITH CHANGES**: the two changes are made: the cutover door's box check takes the capability from the edges file and **liveness from the box's status report** (`status/boxes/<label>.json` within 10 minutes, `Metrics.BoxID` equal to the file's box), never from the edges file's age, because an unchanged edges file is not rewritten; and Task 0's surviving sentence about boxes purging until they read the marker is removed (boxes purge nothing from this release on). Rev 19's review (`selfserve-plan1-review19.md`: 18, 20, 25 closed; 23, 26, 27, 28 open) is answered: the box floor is a **capability in the edges file** (`lifetimes-1`, the inventory the console already reads for every fleet decision), the cutover door refuses while any edges file lacks it or is stale, and a box whose file later loses it pages "box <label> regressed below the lifetimes floor" (23); **the return of an older box binary after the cutover, and retention's prune-versus-recreate race, are declared out of scope**: neither is introduced or widened by this plan, both are today's exposure on `main`, and a fleet-wide binary floor is its own piece of work, filed as an amadan discussion (23, 26); restore evidence keeps today's value for every ordinary app object and applies the three-valued lifetime judgement to instance-record keys alone (27); the cancellation plumbing is declared (`record_retirements.cancels`, `Retirement.Cancels`, `BeginCancellingRetirement`) (28); the surviving statements about boxes purging before the cutover are removed. Rev 18's review (`selfserve-plan1-review18.md`: 5, 19, 22, 24 closed; 18, 20 declarations; 23, 25 P1) is answered: **claims-off boxes no longer auto-purge at all** from this release (a check-then-delete can always pause between the two; no fence is possible on a box), the console's expiry pass owns the store trash sweep, and the cutover door **refuses while any box of the fleet reports a version older than this release or no fresh status**, so by the time a marker can exist no box purge code is running (23); the materialisation worker takes `s.lockApp` per app **before** collecting evidence, re-reads the row and the marker under it and holds it through the SQLite transition, candidates being hints only (25); the lifetime the door read travels to the create helpers as an argument, the schema, the `Allocation` struct, the real `AppLife(accountID, app)` and `release.ReadAppTombstone` names, and the SQL audit's patterns are reconciled (18); the lock table no longer lists `retireSlotOrphanLocked` as assuming the app lock (20). Rev 17's review (`selfserve-plan1-review17.md`: 10, 17, 21 closed; 5, 18, 19, 20, 22 narrowed; 23, 24 new) is answered: a box purge is fenced at **every** destructive step (`purgeApp` gains a `before func(ctx) error` hook the box fills with the marker re-read and the 30 s clock, called before each `Delete`, the final marker delete included) (23); the app lifetime is minted by **one insertion helper** every app-row insert uses (`CreateAppInRegion`, `CreateTemporaryAppInRegion`, `BackfillOpsMemberships`, the migration's `apps_new` copy, the open-time backfill), pinned by a test that scans every `INSERT INTO apps`, and the declarations agree (`TEXT`, `Allocation.AppLifetime`, the recovery wording) (18); the create's step 0 also requires a successful read of the store's trash marker as absent (5); the cross-key stale-body cleanup is **best effort within the running create** and the promise is narrowed to that (19); the initial migration pass takes app → hostname and the cross-owner orphan retirement needs only the hostname lock and its captured slot ETag, stated with its reason (20); startup materialisation runs as an admitted worker iteration, never before the gate has a state (22); restore evidence has an explicit **unknown** outcome that defers the decision (24). Rev 16's review (`selfserve-plan1-review16.md`: 5, 10, 17, 18, 19 narrowed; 20, 21, 22 new) is answered: the create protocol checks the app's lifecycle **under the app lock** (present, not trashed, the same `lifetime` the door read) before allocating (5); a box purge re-reads the cutover marker **immediately before every conditional delete** and abandons the purge when more than 30 s passed since that check, so a sweep paused across the cutover deletes nothing numbered (10); cancellation writes the orphan retirement row and the allocation's `void` in **one SQLite transaction** and the retirement names the allocation it cancels, so no two pending rows block each other (17); the app lifetime is a non-repeating `apps.lifetime` id captured by `BeginAllocation` (18); the stale-body promise is narrowed to the same key and the create's step 5 also deletes a stale body under the retired slot's owner key (19); a **lock table** states what every wrapper acquires and every `Locked` helper assumes, and the attach door takes the app lock outside its hostname exclusion (20); the purge's absent-record branch acts only on a slot **owned by the purged app** (21); `settleApp` and `MaterializeApps` count only judged-valid instance records as restore evidence (22). Rev 15's review (`selfserve-plan1-review15.md`: 13, 14, 15, 16 closed; 5, 10, 17, 18, 19 open) is answered with **one lock order** instead of a snapshot: the create protocol, allocation recovery and the console's purge all take `s.lockApp(acct, app)` and then `withHostname(host)`, so a purge and a create or recovery of the same app never interleave, and the purge observes and retires each instance **inside** the same locked step (no delayed callback can see a successor; a legacy record is retired by **ETag**); an allocation is bound to the **app lifetime** that authorised it (`app_lifetime` = the apps row's `created_at`), so a reused name never replays an old allocation; **boxes stop auto-purging once the cutover marker exists** and the console's expiry pass runs the trash sweep under the app lock, so after the cutover the only purger is the one that can see pending allocations; a slot-bound orphan entry point (`retireSlotOrphanLocked`) exists and the void path's cleanup is stated honestly (a late stale body stays until the next create of the hostname removes it in step 5; a late slot PUT leaves a live orphan the orphan rule reclaims). Rev 14's review (`selfserve-plan1-review14.md`: 9, 11, 12 closed; 5, 10, 13, 14, 15, 16 the purge protocol) is answered by **removing the purge protocol**: four rounds of a lifetime-aware purge each found the next race (snapshot vs recovery, in-flight box sweeps, legacy incarnation 0, the app lock, guard ordering, the schema), so purge goes back to **today's sweep** with one conditional step (an instance record is deleted by `DeleteIfMatch` on an ETag a `Get` captured just before), on the console under the existing `s.lockApp` mutex its callers already hold, on the box as today; and the lifetime protocol **absorbs the consequences** with two rules it already nearly had: a live slot whose record is absent and for which this console holds no pending operation is an **orphan**, retired as kind `orphan` by the next create that meets it (so a purged app's slots never block a hostname); and allocation recovery **never replays a create for a trashed or purged app** (it retires the slot, and anything that landed at its incarnation, as `orphan`, and voids the row), so a late record or slot PUT of a purged app's allocation is cleaned by the same recovery. No generation table, no slot fence, no on-marker, no console trash sweep. Rev 13's review (`selfserve-plan1-review13.md`: findings 1 and 8 closed; 5, 9, 10, 11, 12 the purge) is answered by **taking boxes out of purging once lifetimes are on**: a box that has seen `lifetimes-on.json` never purges an app again (the console's expiry pass gains the store-driven trash sweep the box runs today), so no purger exists that cannot see the console's pending allocations; before the on-marker a box purge deletes an instance record only by `DeleteIfMatch` on an ETag it captured **before** its final marker check. The console's purge runs under the app mutation lock, records the **generation** it observed for every host (slot incarnation or record ETag) before any destructive step, and every retirement states that expectation (never `retireAny`), so a delayed callback cannot touch a successor; a pending allocation is terminalised by **fencing its slot** (a CAS write of a retired slot at its incarnation, against absence or the predecessor's ETag) so an outstanding allocation PUT can never land, then `void`; the purge calls the **locked** retirement under its own `withHostname`; the box has no retirement code at all; the witnesses are split into console and box expectations with their real terminal states. Rev 12's review (`selfserve-plan1-review12.md`: findings 2, 3, 4, 6, 7 closed; 1 a witness contradiction; 5, 8, 9 the purge) is answered: the restart witness no longer activates "at once"; the console writes a durable **`control/lifetimes-on.json`** as the first act of `on`, and that marker, not judge availability, is a box's authority for any slot mutation; purge **excludes instance-record keys from its sweep** whenever a retire hook is installed; the console's purge **voids the app's pending allocations durably** (`purged_at`) and retires their slots before touching records, and recovery treats a purged pending row as `void`; the box's purge needs **no migration, no journal and no cross-process lock**: it retires by CAS alone (slot CAS to `retired`, then `DeleteIfMatch` on the observed record ETag; a marked record is skipped; any CAS loss returns an error and the next pass retries idempotently), and before the on-marker it deletes only by `DeleteIfMatch` on the listed ETag after re-reading the marker. Rev 12's review (`selfserve-plan1-review11.md`, findings 1 to 7) is answered: the storage settle is anchored **after the last `before` unit released admission** (and after the process start on a restart), so a legacy request that was fully received before its client timed out has completed before activation; activation requires a **validated marker read** (`markerSeen`), never uncertainty alone; app purge is **lifetime-aware** on every path (browser, expiry pass, box auto-purge) through a retirement core in `provision` that `release.PurgeApp` calls for every instance record before its sweep; an in-scope valid own record defeats only the record-gone reason and still reaches the app-trash decision; the dispatch of create, delete, detach and `readDeletedRecord` is stated per admitted unit; both detached continuations (`runImportUpload`, `createFrom → runFrom`) are wired; the Task 3 witness no longer says draining reads are legacy; `claimAliasUnbacked` is named in the audit. Rev 11, 2026-10-22. Rev 10's review (`selfserve-plan1-review10.md`, findings 1 to 4) is answered: the cutover gains an **admission gate**: every claims-off unit of record work (a request's handler, a detached continuation, a worker iteration) is admitted under a counted gate while the state is `before`; the cutover door closes admission synchronously, refuses new work with 503, **waits for the admitted count to reach zero**, then starts the storage settle, then activates; so no unit admitted before the marker ever runs beside the lifetime protocol, and a unit never changes mode mid-flight. `on` is **latched**: once observed it is never left; at startup the state is `unknown` until the first successful marker read and every claims-off record door (reads and mutations) answers 503 until then, so a marker-read failure never restores legacy interpretation. The removal rule separates lifetime validity from placement eligibility: a valid own record that is **in this pass's scope** protects the legacy collision; a valid own record **outside** the scope follows today's removal rules (`KeepParticipant`, the app-trash rule), so offline finishing removes the source row as today; absence or staleness authorises the new path. One worker lifecycle (start once, loop, check the state each iteration), one migration interval, one context contract (`recordCtx`), the CLI command, the retry error, the three root constructions and the copy inventory are stated. Rev 10, 2026-10-22. Rev 9's review (`selfserve-plan1-review9.md`, findings 1 to 5) is answered: the cutover is a **mode** with a drain: from the marker's time until the window ends the console refuses every claims-off record mutation (503), so no legacy write can be admitted into the window, and the gate, the judge's installation, the recovery pass and the marker's failure modes are one contract with an implementation task for the operator door; the removal loop decides from the row's **own** record (observed by its own key), so a valid legacy owner in a hostname collision is kept as today and only an established absent or stale own record retires the row, with the keep view bypassed on that evidence and replacement stated as two passes; `fenceRecord` returns a classification so an own tombstone or marker that won resumes the operation instead of terminating it; the audit's allowlist names the two remaining claims-on callers of `hostops.ReadRecord` and is pinned both ways; and the remaining contracts are settled (the detach sentence, cleanup recovery through C2, corrupt index candidates, the root restart readers' judge). Rev 8's review (`selfserve-plan1-review8.md`, findings 1 to 5) is answered: the rollout guarantee becomes an **enforced cutover** (an operator marker written after the old console has stopped, plus the settle window, with the storage assumption stated) rather than a timer alone; quarantine copies the corrupt bytes and **never deletes under a live slot**: the retirement converts durably to the cleanup kind and retires the slot first, and a conditional-delete conflict re-classifies instead of counting as absence; a provisioned row owned by another record key than the hostname's current record is retired as "record gone" so a cross-owner recreation cannot leave a lagging box serving the predecessor; `RecordIndex.Find` judges its candidate keys before deciding ambiguity; and the remaining contradictions are resolved (the root scope sentence, one detach chain, the locked retirement's callers, reconcile's one sequence, the audit's enumerated list, the fence witness split). Rev 7's review (`selfserve-plan1-review7.md`, findings 1 to 6) is answered: a stated rollout guarantee closes the late legacy delete (one console per deployment, a settle window before any termination, old consoles never reintroduced over lifetimes records); the first-route attach, its rollback and detach run **inside** the hostname exclusion from the claim write to the record write, through locked variants of the create and retirement helpers; root's proof and restart record readers take the box's judge (the scope statement is corrected: only `ClaimRootHTTP`'s claims-on readers are slot-free); a corrupt body is quarantined (its bytes kept under `control/quarantine/`, the key removed conditionally) so the retirement or migration proceeds as for absence instead of promising a fence that cannot parse it; `provision.WithLifetimes` is the one context helper that carries the judge and enables sequencing, the runs worker wraps its detached execution context, the audit distinguishes approved judged wrappers from raw readers, the fence witnesses state their timing, exhaustion is witnessed at every transition, and the sibling undo has one dispatch path. Earlier reviews: rev 1 (`after236/selfserve-plan1-review.md`, 16 findings), rev 2 (F1 to F12), rev 3 (R1 to R10), rev 4 (1 to 9), rev 5 (1 to 6), rev 6 (`selfserve-plan1-review6.md`, findings 1 to 5). Rev 7 answers rev 6's five: every claims-off record write runs with `WithRecordSequence`, so `record_seq` exists after a record's first write and an ETag never recurs, and recovery writes a **fence** (a sequencing rewrite) before it terminates an operation on evidence of a changed record; one migration dispatcher classifies the observation at every entry, so a resume can never supersede its own marker; the attach door's claim rollback runs inside the hostname exclusion and never releases a claim a pending operation or a valid record needs; the judge reaches every mutation context (the console's top-level handler wraps every request, browser and API alike; the seed callback wraps its own context; the full-record updater's re-read is judged; `RecordIndex.Find` and `FindRecordByHost` take the judge from the context), and the audit is by AST; the remaining contracts (the running N1 table, a slot-retirement helper, one expectation type, listed-key absence, exhaustion at read, the import witness's state, the producer-state publication tests) are answered. Rev 5 removed the slot cache (every judgement reads the slot fresh, after the record, in bounded rounds) and made a successor lifetime's hold one row write. Rev 6 answers rev 5's six: a retirement persists its **kind** (normal, orphan, cleanup) and each kind has its own steps and recovery table; a migration's marker-CAS conflict is classified by a re-read for both the bound and the unbound form, so an own marker that landed is resumed, never superseded; every `retireInstanceRecord` caller states its expectation (the sibling undo names the lifetime it created; detach deletes what is current; the attach door never asks for a record rollback over a lifetime failure); a judge carried on the context makes `updateRecordSeeding`, and so every `SetRecord*` writer, refuse a stale record, and every box reader (Phase 1's included) and the import journal's readers go through the judge; the provenance mark is dropped (the fold compares pairs between unstamped entries, a pair-less entry is `(0, 0)` and equal pairs fall to the existing rule, exactly §6.4); the remaining interface gaps are answered.
**Goal:** Give every claims-off instance record a durable lifetime (the host incarnation slot) and every served row a serving order `(incarnation, serving_seq)` that boxes publish and edges honour, with crash-safe create, retirement and migration, so a later plan's holding pages and moves can never be out-ordered by a stale publication.
@@ -209,7 +209,7 @@ Inside `withHostname(host)`: `admit(host, "")`; `obs := classifyLocked` (unreada
- kind **orphan**, `pred`: absent or any present body → O1 (the slot, then the body if present), `committed`.
- kind **cleanup**, `pred`: any observation (absent, corrupt, stale, or a valid body at N: the DELETE this operation answers was for this lifetime, and under the retired slot every body is stale) → C1 then C2 on the current body's ETag when present, `committed`.
-- [ ] **Step 1: Write the failing tests** through the real doors and the function: `TestConsolePurgeRetiresEveryLifetime` (browser purge, the expiry pass's row purge and its hourly store sweep (an app with no console row), all holding `s.lockApp`: the guards run before any lifetime step and a refused purge retires nothing; every record ends absent with a retired slot (numbered by incarnation, legacy by ETag); a pending allocation with a live slot and no record is voided and its slot retired; one with no slot is voided; a create of the same app blocked on the app lock during the purge finds the app trashed and refuses; a recovery pass blocked on the app lock finds the lifetime mismatch and voids; the hostname is then created by another app at N+1; an `errLifetimePending` mid-purge leaves the marker and the re-run completes); `TestOrphanSlotReclaimedByTheNextCreate` (a live slot with no record and no pending op, from a lost allocation row, is retired as `orphan` and the create allocates N+1; with a pending op it is `*hostHeldError`; a stale body under the owner key is deleted in step 5 by a create under another key); `TestCreateChecksTheAppUnderTheLock` (a create that read a live app, blocked on the app lock across a trash → 409 trash, including a trash whose SQLite row update failed after the marker landed; across a purge → 404; across a purge and a recreate under the same name, by the permanent and by the temporary constructor → 409 changed, nothing allocated; an unreadable marker → 502, nothing allocated); `TestCancellationIsOneTransaction` (a crash anywhere between the void and O1 leaves exactly one pending row, the retirement, which completes); `TestBoxesNoLongerPurge` (a claims-off box with an expired trash marker purges nothing; a claims-on box still requests its auto-purge); `TestCutoverRefusesBoxesBelowTheFloor` (a fleet with one edges file lacking `lifetimes-1` → 409 naming it; a stale file → 409; all present → accepted; a file losing the capability after the cutover → the hourly page); `TestRestoreEvidenceKeepsOrdinaryObjects` (an app with releases and channels and no instance records, restore's SQLite update failed → settlement restores the row, never purges it; the same app with only a stale instance body → released);
+- [ ] **Step 1: Write the failing tests** through the real doors and the function: `TestConsolePurgeRetiresEveryLifetime` (browser purge, the expiry pass's row purge and its hourly store sweep (an app with no console row), all holding `s.lockApp`: the guards run before any lifetime step and a refused purge retires nothing; every record ends absent with a retired slot (numbered by incarnation, legacy by ETag); a pending allocation with a live slot and no record is voided and its slot retired; one with no slot is voided; a create of the same app blocked on the app lock during the purge finds the app trashed and refuses; a recovery pass blocked on the app lock finds the lifetime mismatch and voids; the hostname is then created by another app at N+1; an `errLifetimePending` mid-purge leaves the marker and the re-run completes); `TestOrphanSlotReclaimedByTheNextCreate` (a live slot with no record and no pending op, from a lost allocation row, is retired as `orphan` and the create allocates N+1; with a pending op it is `*hostHeldError`; a stale body under the owner key is deleted in step 5 by a create under another key); `TestCreateChecksTheAppUnderTheLock` (a create that read a live app, blocked on the app lock across a trash → 409 trash, including a trash whose SQLite row update failed after the marker landed; across a purge → 404; across a purge and a recreate under the same name, by the permanent and by the temporary constructor → 409 changed, nothing allocated; an unreadable marker → 502, nothing allocated); `TestCancellationIsOneTransaction` (a crash anywhere between the void and O1 leaves exactly one pending row, the retirement, which completes); `TestBoxesNoLongerPurge` (a claims-off box with an expired trash marker purges nothing; a claims-on box still requests its auto-purge); `TestCutoverRefusesBoxesBelowTheFloor` (a fleet with one edges file lacking `lifetimes-1` → 409 naming it; a stale or missing **status report** → 409 (an old edges file with a fresh status report is accepted); all present → accepted; a file losing the capability after the cutover → the hourly page); `TestRestoreEvidenceKeepsOrdinaryObjects` (an app with releases and channels and no instance records, restore's SQLite update failed → settlement restores the row, never purges it; the same app with only a stale instance body → released);
- `TestDeleteDoorRetiresThroughTheTombstone`; `TestSlotRetiresBeforeTheKeyIsAbsent`.
- `TestDeleteDoorAbsentPathAdmitsAndRetiresOrphans` (initial absence; absence found on the judge's re-read; a record created since is untouched and the door still answers "deleted").
- `TestStaleCleanupRetiresALiveOwnedSlotFirst` (a fieldless late body beside `{1, live, this key}`: the slot is `{1, retired}` before the body is removed; a blocked incarnation-1 create-only write released afterwards lands stale; the same for a corrupt body).