carlos / platform Public

plan: self-serve moves 1, rev 6 (Astra plan review 5: findings 1 to 6; retirement kinds, own-marker classification, caller expectations, context judge, §6.4 fold rule)

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

CI passed — run details

CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (1 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.016s
?   	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 (144 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	6.490s
ok  	github.com/carlosframework/platform/cmd/moveprobe	13.242s
ok  	github.com/carlosframework/platform/cmd/zddprobe	1.015s
ok  	github.com/carlosframework/platform/internal/activator	61.945s
ok  	github.com/carlosframework/platform/internal/agentsock	0.207s
ok  	github.com/carlosframework/platform/internal/alerting	0.008s
ok  	github.com/carlosframework/platform/internal/analytics	0.145s
ok  	github.com/carlosframework/platform/internal/appconfig	6.276s
ok  	github.com/carlosframework/platform/internal/apperr	0.283s
ok  	github.com/carlosframework/platform/internal/applog	0.304s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.934s
?   	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.084s
ok  	github.com/carlosframework/platform/internal/caps	0.014s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.895s
ok  	github.com/carlosframework/platform/internal/changelog	0.826s
ok  	github.com/carlosframework/platform/internal/claimactivation	2.464s
ok  	github.com/carlosframework/platform/internal/clamd	0.069s
?   	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.158s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.017s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.047s
ok  	github.com/carlosframework/platform/internal/docsite	0.007s
ok  	github.com/carlosframework/platform/internal/domains	5.859s
ok  	github.com/carlosframework/platform/internal/doorkey	0.010s
ok  	github.com/carlosframework/platform/internal/economics	0.005s
ok  	github.com/carlosframework/platform/internal/edge	49.336s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.015s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.528s
ok  	github.com/carlosframework/platform/internal/gate	2.504s
ok  	github.com/carlosframework/platform/internal/genselect	0.030s
ok  	github.com/carlosframework/platform/internal/geo	0.018s
ok  	github.com/carlosframework/platform/internal/geoip	0.152s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.579s
ok  	github.com/carlosframework/platform/internal/host	139.131s
ok  	github.com/carlosframework/platform/internal/hostactors	11.256s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.017s
ok  	github.com/carlosframework/platform/internal/hostbox	6.798s
ok  	github.com/carlosframework/platform/internal/hostclaim	1.376s
ok  	github.com/carlosframework/platform/internal/hostenv	0.005s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.007s
ok  	github.com/carlosframework/platform/internal/hostexec	11.412s
ok  	github.com/carlosframework/platform/internal/hostfence	0.117s
ok  	github.com/carlosframework/platform/internal/hostfiles	11.337s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.019s
ok  	github.com/carlosframework/platform/internal/hostguard	1.255s
ok  	github.com/carlosframework/platform/internal/hostops	5.351s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.015s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.017s
ok  	github.com/carlosframework/platform/internal/hostrelocate	2.302s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.831s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.007s
ok  	github.com/carlosframework/platform/internal/infraview	0.005s
ok  	github.com/carlosframework/platform/internal/instancectl	5.282s
ok  	github.com/carlosframework/platform/internal/instanceexec	3.193s
ok  	github.com/carlosframework/platform/internal/instancefs	0.029s
ok  	github.com/carlosframework/platform/internal/instancekey	0.003s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.015s
ok  	github.com/carlosframework/platform/internal/layout	5.410s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.005s
ok  	github.com/carlosframework/platform/internal/lease	10.627s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.536s
ok  	github.com/carlosframework/platform/internal/ledger	0.050s
ok  	github.com/carlosframework/platform/internal/mail	1.211s
ok  	github.com/carlosframework/platform/internal/maintenance	0.151s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	6.369s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.550s
ok  	github.com/carlosframework/platform/internal/metrics	0.008s
ok  	github.com/carlosframework/platform/internal/moves	27.821s
ok  	github.com/carlosframework/platform/internal/moves/archive	2.825s
ok  	github.com/carlosframework/platform/internal/objstore	1.136s
ok  	github.com/carlosframework/platform/internal/ops	0.297s
ok  	github.com/carlosframework/platform/internal/park	1.027s
ok  	github.com/carlosframework/platform/internal/plans	0.009s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	23.241s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.014s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	8.935s
ok  	github.com/carlosframework/platform/internal/relay	7.303s
ok  	github.com/carlosframework/platform/internal/release	32.510s
ok  	github.com/carlosframework/platform/internal/rollout	6.121s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.008s
ok  	github.com/carlosframework/platform/internal/rootaws	0.009s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.066s
ok  	github.com/carlosframework/platform/internal/rootstate	0.027s
ok  	github.com/carlosframework/platform/internal/runs	0.640s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.031s
ok  	github.com/carlosframework/platform/internal/schedule	4.861s
ok  	github.com/carlosframework/platform/internal/seeds	0.011s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	24.676s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.083s
ok  	github.com/carlosframework/platform/internal/status	0.247s
ok  	github.com/carlosframework/platform/internal/steering	0.013s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.198s
ok  	github.com/carlosframework/platform/internal/svcrole	0.005s
ok  	github.com/carlosframework/platform/internal/telemetry	0.012s
ok  	github.com/carlosframework/platform/internal/termfence	0.005s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.006s
ok  	github.com/carlosframework/platform/internal/unixdial	0.003s
ok  	github.com/carlosframework/platform/internal/updatewords	0.006s
ok  	github.com/carlosframework/platform/internal/usage	0.007s
ok  	github.com/carlosframework/platform/internal/version	0.003s
ok  	github.com/carlosframework/platform/internal/vet	2.317s
=== internal_console@0 (83 s)
ok  	github.com/carlosframework/platform/internal/console	79.243s
=== internal_console@1 (43 s)
ok  	github.com/carlosframework/platform/internal/console	39.618s
=== internal_console@2 (49 s)
ok  	github.com/carlosframework/platform/internal/console	45.416s
=== internal_console@3 (44 s)
ok  	github.com/carlosframework/platform/internal/console	40.478s
=== internal_console@4 (44 s)
ok  	github.com/carlosframework/platform/internal/console	39.431s
=== internal_console@5 (43 s)
ok  	github.com/carlosframework/platform/internal/console	38.235s
=== internal_console@6 (43 s)
ok  	github.com/carlosframework/platform/internal/console	40.353s
=== internal_console@7 (49 s)
ok  	github.com/carlosframework/platform/internal/console	46.056s
=== root@0 (43 s)
ok  	github.com/carlosframework/platform	38.561s
=== root@1 (48 s)
ok  	github.com/carlosframework/platform	45.250s
=== root@2 (49 s)
ok  	github.com/carlosframework/platform	46.110s
=== 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.02s)
    --- 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.01s)
        --- 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.00s)
        --- 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.00s)
        --- 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.022s
=== RUN   TestT4StorageProbeMinio
2026/10/22 07:43:20 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E0CA39976039F9, 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.00s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.020s
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 7b8667404..9aa2a6723 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,11 +2,11 @@
> **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 5, 2026-10-22. Reviews: rev 1 (`after236/selfserve-plan1-review.md`, 16 findings), rev 2 (`selfserve-plan1-review2.md`, F1 to F12), rev 3 (`selfserve-plan1-review3.md`, R1 to R10), rev 4 (`selfserve-plan1-review4.md`, findings 1 to 9). Rev 5 answers 1 to 9 and removes two sources of interleavings rather than patching them: **there is no slot cache** (every judgement reads the slot fresh, after the record, in bounded rounds), and **a successor lifetime that needs a hold is installed with the hold in one row write** (the setters accept an older unheld lifetime). Also: a bound migration's recovery resumes against its persisted ETag and supersedes only when that ETag is gone; stale or corrupt cleanup under a live owned slot retires the slot before exposing absence; absence found on a re-read is an "expect absent" retirement; the edges writer takes the lifetimes mode explicitly and marks its entries' provenance; the runs, steering, retention and seed readers are judged; a positively stale record overrides the offline keep view.
+Rev 6, 2026-10-22. Reviews: rev 1 (`after236/selfserve-plan1-review.md`, 16 findings), rev 2 (F1 to F12), rev 3 (R1 to R10), rev 4 (findings 1 to 9), rev 5 (`selfserve-plan1-review5.md`, findings 1 to 6). 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.
-**Architecture:** A per-hostname slot object in the deployment bucket (`control/host-incarnations/<host>.json`), created once and changed only by conditional writes, is the authority for a record's lifetime and names the record key that owns it; the console allocates, retires and migrates through persisted intents in SQLite under a per-hostname exclusion and recovers them at start. Records gain `incarnation`, `incarnation_migrating`, `move_seq` and a tombstone form; registry rows gain the pair; the claims-off edges writer publishes it, with each alias's target and a provenance mark; the home map orders marked, unstamped entries of one lifetime by it before the existing claim stamp rule. Retirement replaces the record with a tombstone before retiring the slot. Lazy migration marks a record by CAS before binding a slot to it, and the write primitive refuses every other writer while the marker stands.
+**Architecture:** A per-hostname slot object in the deployment bucket (`control/host-incarnations/<host>.json`), created once and changed only by conditional writes, is the authority for a record's lifetime and names the record key that owns it; the console allocates, retires and migrates through persisted intents in SQLite under a per-hostname exclusion and recovers them at start. Records gain `incarnation`, `incarnation_migrating`, `move_seq` and a tombstone form; registry rows gain the pair; the claims-off edges writer publishes it with each alias's target; the home map orders unstamped entries of one lifetime by it (a pair-less entry is `(0, 0)`), after the existing claim stamp rule and before its first-wins fallback. Retirement replaces the record with a tombstone before retiring the slot. Lazy migration marks a record by CAS before binding a slot to it, and the write primitive refuses every other writer while the marker stands.
**Tech Stack:** Go. `internal/provision` (records, sequence, reconcile, offline transitions), `internal/registry`, `internal/console` (doors, store, store gate, passes), `internal/host` (edges file, offline hold and release, runs, steering, retention, seeds, the agent's config), `internal/edge` (home map). Tests use `release.NewDirStore`, real registries, the console's doors in-process, and the existing two-box offline witness `TestOfflineMoveE2E` (`internal/host/movesoffline_e2e_test.go`).
@@ -14,24 +14,25 @@ Rev 5, 2026-10-22. Reviews: rev 1 (`after236/selfserve-plan1-review.md`, 16 find
## Global Constraints
-- **Scope: claims-off, every record.** On the console the protocol runs only when `!s.cfg.HostClaim`; on a box only when `!hostwrite.Enabled(getenv)` (`CARLOS_HOST_CLAIM` unset). Where it runs it applies to **every** instance record, fieldless ones included. Claims-on records are created by `finishClaimCreate` and retired by the host-claim operations; this plan does not touch them, their consumers read no slot (the judge is nil there, pinned by `TestClaimsOnConsumersReadNoSlot`), the claims-on edges writer publishes no pair and no provenance mark, and their rows keep the claim stamp order (Task 8). Self-serve moves refuse on a claims-on console (MV-O20).
+- **Scope: claims-off, every record.** On the console the protocol runs only when `!s.cfg.HostClaim`; on a box only when `!hostwrite.Enabled(getenv)` (`CARLOS_HOST_CLAIM` unset). Where it runs it applies to **every** instance record, fieldless ones included. Claims-on records are created by `finishClaimCreate` and retired by the host-claim operations; this plan does not touch them, their consumers read no slot (the judge is nil there, pinned by `TestClaimsOnConsumersReadNoSlot`), the claims-on edges writer publishes no pair, and their rows keep the claim stamp order (Task 8). Self-serve moves refuse on a claims-on console (MV-O20).
- Every claims-off consumer acts on an **observation** (Task 6): a record and the slot read after it, judged `valid`, `stale`, `corrupt` or `unreadable`, with `Present` saying whether a body was there. `stale` is absent; `corrupt` is today's unreadable body (reconcile protects; the delete rule may remove it conditionally); `unreadable` cannot be judged this pass (reconcile protects the row, a door answers 502, a listing reports the host unreadable).
- The slot is never deleted and never advanced to abandon a create (spec §6.4). It never moves backwards: every slot write names the exact prior state it replaces. A malformed slot body is `unreadable`, never `stale`.
- One per-hostname exclusion (`withHostname`, Task 3) around create, retirement, migration and their recovery in the console process. A persisted pending operation for a hostname refuses any create or retirement of that hostname by another operation (409 "another change to this instance is still settling. try again in a minute") until recovery resolves it; unreadable evidence leaves it pending and keeps refusing. After an allocation, retirement or migration row is durable, no failure returns an ordinary error to the caller: the outcome is `errLifetimePending` (the intent stays pending; recovery owns it), a persisted terminal state, or success. In every recovery table a re-read that fails is `unreadable` and leaves the row pending; it never selects a terminal branch.
-- Every record write stays on `provision.WriteRecord` with the exact bytes read and the sequence discipline of `record_sequence.go`; no new raw `Put` of a record key. Every record removal on a claims-off console is `retireRecordWithLifetime` (Task 4) with an explicit expectation (an ETag, or "absent"); `provision.DeleteRecord` gains no new caller and loses its claims-off instance callers.
+- Every record write stays on `provision.WriteRecord` with the exact bytes read and the sequence discipline of `record_sequence.go`; no new raw `Put` of a record key. Every claims-off record **update** runs with a judge on its context (`provision.WithJudge`, Task 6), so `updateRecordSeeding` refuses a stale record before it mutates anything. Every record removal on a claims-off console is `retireRecordWithLifetime` (Task 4) with an explicit expectation; `provision.DeleteRecord` gains no new caller and loses its claims-off instance callers.
- `release.DeleteIfMatch` never falls back to a plain delete; absence counts as success.
- No console write to a bucket outside the deployment bucket; no root-side change.
- Census: `TestPlan14RootReadCensus` and `TestPlan9B8RecordWriteCensus` are the authority on what they cover. The root-read overlay is `internal/provision/testdata/plan14-root-reads-self-serve-moves-1.tsv`, loaded last (before the comparison) by a new block at the end of the test, `rev3=PROPOSED-self-serve-moves-1`, with an `@fields` line naming the new record fields and registry columns, rows for every changed unit and envelope the test names, reviewed dispositions for changed frozen units, and the note `docs/superpowers/notes/census-proposal-self-serve-moves-1.md`. The record-write inventory is `plan14SelfServeMoves1WriteCensusInventory()`, wrapping `plan14ClaimRosterBindingWriteCensusInventory()` with every added, changed and removed site the scanner reports, each reviewed. Both tests green.
- The new SQLite tables are classified in `claimSQLTables` (`internal/console/store_gate.go`) so a claims-on store still opens.
- Copy: two new sentences are copy rows pending Paul's review: "the instance changed. read it again and try the delete once more" and "another change to this instance is still settling. try again in a minute". Existing sentences are reused where the plan says so. No tenant names: `shop.example.com`, account `bab`, app `shop`.
- Boxes roll before the console (the roll order rule); the migration pass is behind `CARLOS_LIFETIMES_MIGRATE=1`, default off, until every box of a deployment runs this code.
+- Counters: `incarnation` and `move_seq` are `uint64` on the record and `INTEGER` (int64) in SQLite and the registry; a value that would exceed `math.MaxInt64` is refused before any write (`ErrLifetimeExhausted`), never wrapped.
- Local tests: only the touched packages with `-run`; CI runs the full suite. Go needs the sandbox off; never set `GOCACHE`.
## Review Focus
1. A console crash between the slot write and the record write, a second console start, then the late write; the mirror case where the original slot write lands after recovery read the predecessor; and a bound migration whose marker PUT lands after recovery observed the unmarked record: no intent is abandoned and every late write is harmless (Task 3 `TestRecoveryReplaysCreateAndTheLateWriteIsHarmless`, `TestLateSlotWriteDoesNotAbandonTheAllocation`; Task 5 `TestLateMarkerWriteIsCompletedNotStranded`).
2. A judgement whose reads straddle a retirement, a recreation or a migration, in every order: the result is the current lifetime's answer, with the record and ETag the caller may act on, and never a removal of a valid successor (Task 6, `TestJudgeAcrossConcurrentLifetimeChanges`).
-3. A legacy fieldless record beside a retired slot (an old console's late create): the strict listings, every console door, reconcile (a populated offline keep view notwithstanding), the box's reassert, staging, runs, steering, retention and seed readers all treat it as absent, the edges file stops publishing its row after the next reconcile, and the next create replaces it (Task 6 `TestEveryClaimsOffConsumerIgnoresAFieldlessRecordBesideASlot`, Task 3 `TestCreateReplacesStaleRecord`).
+3. A legacy fieldless record beside a retired slot (an old console's late create): the strict listings, every console door, reconcile (a populated offline keep view notwithstanding), the box's reassert, staging, runs, steering, retention, seed-placement and import-journal readers, and every `SetRecord*` writer all treat it as absent, the edges file stops publishing its row after the next reconcile, and the next create replaces it (Task 6 `TestEveryClaimsOffConsumerIgnoresAFieldlessRecordBesideASlot`, Task 3 `TestCreateReplacesStaleRecord`).
4. A reconstructed registry row after a finished move and after an abort from landed: reconcile installs `(1, 3)` on the minted row; an existing row that stayed through the finish keeps `(1, 2)` (Task 7, `TestReconcileInstallsTheRecordsPairOnAMintedRow`, `TestAnExistingRowKeepsOpenSequenceThroughFinish`).
5. Existing rows across lifetimes: a legacy row held at incarnation 0 whose record migrates is released by the move's open or abort; a ready legacy row takes the lifetime and the hold in one write; an unheld predecessor row whose successor already has a move in flight takes the successor's lifetime, hold and pair in one write (never a ready row at the hold's pair); a predecessor row held by a vanished move is retired by its op and the successor minted (Task 7, `TestExistingRowsAcrossLifetimes`).
@@ -81,12 +82,12 @@ The verdict, exactly, with `owned := slot != nil && slot.Account == rec.Account
**Interfaces:**
- Tables (`account` is the sqid):
- `host_allocations (op_id TEXT PRIMARY KEY, account TEXT, app TEXT, host TEXT, incarnation INTEGER, state TEXT, body BLOB, predecessor_etag TEXT, stale_etag TEXT, created_at INTEGER)`
- - `record_retirements (op_id TEXT PRIMARY KEY, account TEXT, app TEXT, host TEXT, incarnation INTEGER, record_etag TEXT, slot_etag TEXT, slot_intent TEXT, state TEXT, created_at INTEGER)`
+ - `record_retirements (op_id TEXT PRIMARY KEY, kind TEXT, account TEXT, app TEXT, host TEXT, incarnation INTEGER, record_etag TEXT, slot_etag TEXT, slot_intent TEXT, state TEXT, created_at INTEGER)` (`kind` is `normal`, `orphan` or `cleanup`)
- `record_migrations (op_id TEXT PRIMARY KEY, account TEXT, app TEXT, host TEXT, expect_etag TEXT, marked_etag TEXT, state TEXT, created_at INTEGER)`
- Classification in `claimSQLTables`: `{Sqid: "account", App: "app"}` for all three.
- States: `pending`, `committed`, `superseded`, `void`, `refused`.
- On `*Store`: `BeginAllocation(a Allocation) error`, `SetAllocationState(opID, state string) error`, `PendingAllocations() ([]Allocation, error)`, `LastIncarnation(host string) (uint64, error)` (the highest `incarnation` over every state but `void`; 0 when none); `BeginRetirement(r Retirement) error`, `SetRetirementState(opID, state string) error`, `PendingRetirements() ([]Retirement, error)`; `BeginMigration(m Migration) error`, `SetMigrationMarkedETag(opID, etag string) error`, `SetMigrationState(opID, state string) error`, `PendingMigrations() ([]Migration, error)`; `PendingForHost(host, exceptOpID string) (int, error)`.
-- `type Allocation struct { OpID, Account, App, Host string; Incarnation uint64; State string; Body []byte; PredecessorETag, StaleETag string }`, `type Retirement struct { OpID, Account, App, Host string; Incarnation uint64; RecordETag, SlotETag, SlotIntent, State string }`, `type Migration struct { OpID, Account, App, Host, ExpectETag, MarkedETag, State string }`.
+- `type Allocation struct { OpID, Account, App, Host string; Incarnation uint64; State string; Body []byte; PredecessorETag, StaleETag string }`, `type Retirement struct { OpID, Kind, Account, App, Host string; Incarnation uint64; RecordETag, SlotETag, SlotIntent, State string }`, `type Migration struct { OpID, Account, App, Host, ExpectETag, MarkedETag, State string }`.
- [ ] **Step 1: Write the failing tests**: `TestLifetimeTablesRoundTrip`; `TestLastIncarnationSkipsVoid` (pending 2 and void 3 → 2); `TestPendingForHostCountsAllThreeExceptOwn`; `TestClaimStoreOpensWithLifetimeTables`.
- [ ] **Step 2: Run to verify they fail.**
@@ -130,14 +131,15 @@ Recovery (`recoverLifetimes`), per pending allocation, inside `withHostname` wit
- anything else → leave pending and log once per hour.
Then the retirements (Task 4) and the migrations (Task 5), in that order.
-Door mappings: the create door: `*hostHeldError` → 409 `hostHeldSentence(host, hold)`; `errLifetimeSettling` → 409 settling; `errLifetimePending` → 502 "could not create the instance — try again", no undo. Seed and import: the same three onto their existing failure sentences; on `errLifetimePending` the door writes nothing further and the record recovery completes carries the seed state the body had, which the existing seed sweep then owns. Attach: `ErrCASConflict` keeps today's adoption branch; `errLifetimePending`, `errLifetimeSettling` and `*hostHeldError` → the answer for each (the ambiguous sentence; 409 settling; 409 held) with `rollbackAttach(ctx, acct, app, host, false, keepClaim)` where the claim is dropped only when this call minted it **and** nothing was persisted (settling and held); a pending allocation keeps the claim. `rollbackAttach` itself changes: when `dropRecord` and the retirement returns anything but nil (pending, settling, refused, an error), the claim is **kept** and the failure logged; only a nil retirement result proceeds to `detachDomainClaim`.
+Door mappings: the create door: `*hostHeldError` → 409 `hostHeldSentence(host, hold)`; `errLifetimeSettling` → 409 settling; `errLifetimePending` → 502 "could not create the instance — try again", no undo; the **sibling undo** at line 1684 (the apex was claimed mid-create) becomes `retireRecordWithLifetime(ctx, acct.Sqid, app, host, retireExpectIncarnation(created.Incarnation))` with the record the create returned: it retires exactly the lifetime this create made and nothing else (Task 4). Seed and import: the same three onto their existing failure sentences; on `errLifetimePending` the door writes nothing further and the record recovery completes carries the seed state the body had, which the existing seed sweep then owns. Attach: `ErrCASConflict` keeps today's adoption branch; every other lifetime-helper failure answers its sentence (`errLifetimePending` the ambiguous one; `errLifetimeSettling` 409 settling; `*hostHeldError` 409 held; a step-2 read error the ambiguous one) and calls `rollbackAttach(ctx, acct, app, host, false, drop)` with **`dropRecord` always false**: nothing a lifetime failure leaves behind is this call's to retire (a pending allocation is recovery-owned; a pre-allocation failure persisted nothing); `drop` (the claim) is `created && !pending` (settling, held and a read error drop a claim this call minted; a pending allocation keeps it). The claims-off `rollbackAttach` therefore never retires a record; its `dropRecord` branch stays for the claims-on path only, and a test pins that no claims-off attach failure calls retirement.
- [ ] **Step 1: Write the failing tests** against an in-process console with `release.NewDirStore` and `HostClaim` off:
- `TestCreateAllocatesSlotThenRecord`, `TestCreateAfterRetirementGetsNextIncarnation`, `TestCreateReplacesStaleRecord`, `TestCreateOfALiveInstanceIsTheExistingConflict`, `TestCreateRefusesWhileAnOperationIsPending`, `TestCreateRefusesALiveSlotOfAnotherApp`, `TestCreateRefusesWhenTheAccountIsUnknown` (`AccountBySqid` nil → 502, nothing persisted), `TestTwoAppsCreatingOneHostnameSerialise`.
- `TestEveryPostAllocationFailureIsPending` (non-CAS errors injected at steps 4, 5, 6 and a SQLite failure at 7: each `pending` + `errLifetimePending`; recovery completes each).
- `TestRecoveryReplaysCreateAndTheLateWriteIsHarmless`; `TestLateSlotWriteDoesNotAbandonTheAllocation` (both orders); `TestDeleteDuringCreateRecoveryIsRefused` (through the real DELETE door while the record is absent → 409 settling).
- `TestRecoveryWritesTheSlotWhenMissing`, `TestRecoveryContinuesFromAPredecessorSlot`, `TestRecoveryLeavesALandedCreateServing`, `TestRecoveryRemovesAStaleRecordThenWrites`, `TestRecoverySupersedesOnlyOnSuccessorEvidence`, `TestRecoveryLeavesPendingWhenUnreadable` (a failing slot read; a malformed slot body), `TestRecoverySurvivesASecondCrash`, `TestRecoveryUsesOnlyPersistedState`.
- - `TestAttachKeepsItsClaimOverAPendingAllocation` (failure after the slot write; a SQLite failure at step 7); `TestAttachRollbackKeepsTheClaimWhenRetirementIsNotProven` (the ambiguous branch's retirement returns pending, settling, refused and an injected error: the claim stands in each; a nil retirement drops it as today).
+ - `TestAttachKeepsItsClaimOverAPendingAllocation` (failure after the slot write; a SQLite failure at step 7); `TestAttachNeverRetiresOverALifetimeFailure` (every helper failure: a counting retirement sees no call; the claim is dropped only for settling, held and a read error when this call minted it).
+ - `TestSiblingUndoRetiresOnlyTheLifetimeItCreated` (pause the undo after the create returned incarnation 1; delete and recreate the host at incarnation 2; resume: the undo observes incarnation 2, answers nil without touching it, the successor serves).
- `TestSeedCreatePendingIsOwnedByTheSweep`.
- [ ] **Step 2: Run to verify they fail.**
- [ ] **Step 3: Implement**; route the four claims-off creation paths through `createRecordWithLifetime`.
@@ -148,53 +150,52 @@ Door mappings: the create door: `*hostHeldError` → 409 `hostHeldSentence(host,
**Files:**
- Create: `internal/console/lifetimes_retire.go`
-- Modify: `internal/console/hostclaimops.go` (claims-off `ref.Family == "instance"` → `retireRecordWithLifetime(ctx, ref.Account, ref.App, ref.Host, retireAny)`), `internal/console/apiinstanceimport.go` (`readDeletedRecord` returns `(provision.Observation, error)` through `s.judge.Observe`; `importDeleteRule`: `!obs.Present` → `retireRecordWithLifetime(..., retireExpectAbsent)`; `obs.Verdict` stale or corrupt → `deleteRead(..., retireExpectETag(obs.ETag))`; valid → the import logic as today, and its ordinary delete `deleteRead(..., retireExpectETag(obs.ETag))`; `deleteRead` calls `retireRecordWithLifetime` with the expectation it is given: `ErrCASConflict` keeps the `again` re-decision; `errLifetimeSettling` → 409 settling; `errRetireRefused` → 409 its sentence), `internal/console/seedfinish.go` (`finishSeedOnce`'s final delete → `retireRecordWithLifetime(ctx, sqid, app, host, retireExpectETag(etag))`), `internal/console/apiinstances.go` (`handleAPIInstanceDelete`: `errRetireRefused`, `errLifetimeSettling` → 409), `internal/console/domainattach.go` and `internal/console/apidomains.go` (detach: `errLifetimeSettling` → 409 `claimDetachInProgressMsg`; `errRetireRefused` → 409 its sentence), `internal/console/lifetimes_recovery.go`
+- Modify: `internal/console/hostclaimops.go` (claims-off `ref.Family == "instance"` → `retireRecordWithLifetime(ctx, ref.Account, ref.App, ref.Host, ref.Expect)`; `provision.RetireRef` gains `Expect retireExpectation`-shaped fields through a new `provision.RetireExpectation{Absent bool; ETag string; Incarnation uint64}` so each caller states its own), `internal/console/claim_retire.go` (`retireInstanceRecord(ctx, account, app, host, by, incarnation string)` gains a `provision.RetireExpectation` parameter; its callers: `apiinstances.go:1684` (the sibling undo) passes `{Incarnation: created.Incarnation}`; `apidomains.go:989` and `domainattach.go:349` (detach: the member asked for this hostname's route to go, whatever it is now) pass `{}` (`retireAny`); `domainattach.go:557` (`rollbackAttach`) is never reached on the claims-off path (Task 3); `apiinstances.go:2245` (the DELETE door's claims-off branch) is not reached either: `importDeleteRule` answers every claims-off delete), `internal/console/apiinstanceimport.go` (`readDeletedRecord` returns `(provision.Observation, error)` through `s.judge.Observe`; `importDeleteRule`: `!obs.Present` → `retireRecordWithLifetime(..., retireExpectAbsent)`; `obs.Verdict` stale or corrupt → `deleteRead(..., retireExpectETag(obs.ETag))`; valid → the import logic as today, and its ordinary delete `deleteRead(..., retireExpectETag(obs.ETag))`; `deleteRead` passes the expectation through: `ErrCASConflict` keeps the `again` re-decision; `errLifetimeSettling` → 409 settling; `errRetireRefused` → 409 its sentence), `internal/console/seedfinish.go` (`finishSeedOnce`'s final delete → `retireRecordWithLifetime(ctx, sqid, app, host, retireExpectETag(etag))`), `internal/console/apiinstances.go` (`handleAPIInstanceDelete`: `errRetireRefused`, `errLifetimeSettling` → 409), `internal/console/domainattach.go` and `internal/console/apidomains.go` (detach: `errLifetimeSettling` → 409 `claimDetachInProgressMsg`; `errRetireRefused` → 409 its sentence), `internal/console/lifetimes_recovery.go`
- Test: `internal/console/lifetimes_retire_test.go`
**Interfaces:**
- Consumes: Tasks 1 to 3; Task 5's `migrateHostLifetimeLocked`; Task 6's `Observation`.
-- Produces: `type retireExpectation struct { absent bool; etag string }`; `var retireExpectAbsent = retireExpectation{absent: true}`, `var retireAny = retireExpectation{}` (no precondition: the `retireInstanceRecord` callers, which decide from a judged read they just made), `func retireExpectETag(etag string) retireExpectation` (panics on ""); `func (s *Server) retireRecordWithLifetime(ctx context.Context, acct, app, host string, expect retireExpectation) error`; `var errRetireRefused`. A nil return means the key is absent (or was left untouched on purpose) and the slot, if any, is retired or someone else's.
+- Produces: `type retireExpectation struct { absent bool; etag string; incarnation uint64 }`; `var retireExpectAbsent = retireExpectation{absent: true}`; `var retireAny = retireExpectation{}` (delete whatever is current: detach, and the recovery of a pending retirement); `func retireExpectETag(etag string) retireExpectation` (panics on ""); `func retireExpectIncarnation(n uint64) retireExpectation` (a compensation: act only on the lifetime `n` under this key); `func (s *Server) retireRecordWithLifetime(ctx context.Context, acct, app, host string, expect retireExpectation) error`; `var errRetireRefused`. A nil return means the key is absent (or was left untouched on purpose) and the slot, if any, is retired or someone else's.
-Inside `withHostname(host)`: `admit(host, "")`; `obs := classifyLocked` (unreadable → the error). Preconditions: `expect.absent && obs.Present` → nil, nothing touched (a record created since the caller observed absence is not deleted); `expect.etag != "" && (!obs.Present || obs.ETag != expect.etag)` → `release.ErrCASConflict`. Then the first matching case:
+Inside `withHostname(host)`: `admit(host, "")`; `obs := classifyLocked` (unreadable → the error). **Preconditions:** `expect.absent && obs.Present` → nil, nothing touched; `expect.etag != "" && (!obs.Present || obs.ETag != expect.etag)` → `release.ErrCASConflict`; `expect.incarnation != 0 && !(obs.Present && obs.Rec != nil && obs.Rec.Incarnation == expect.incarnation && slot owned by this key at that incarnation)` → nil, nothing touched (the lifetime this caller made is already gone or superseded). Then the first matching case, each beginning a retirement of its **kind**:
- (a) absent, slot absent → nil.
-- (b) absent, slot `{N, live}` owned by this key → **orphan slot**: `BeginRetirement({..., N, RecordETag: "", SlotETag: S, SlotIntent})`, step (2), `committed`.
+- (b) absent, slot `{N, live}` owned by this key → kind **orphan**: `BeginRetirement({Kind: orphan, N, RecordETag: "", SlotETag: S, SlotIntent})`; steps O1 (retire the slot against S), done.
- (c) absent, slot owned by another key, or retired → nil.
-- (d)/(g) present and stale or corrupt: if the slot is `{N, live}` owned by this key → **cleanup under a live owned slot**: `BeginRetirement({..., N, RecordETag: obs.ETag, SlotETag: S, SlotIntent})`, then (2) retire the slot against S, then `DeleteIfMatch` the body on `obs.ETag` (a conflict is logged and left: the body is non-servable and stale under the retired slot), `committed`; otherwise (no slot, a retired slot, or another key's) → `DeleteIfMatch` on `obs.ETag` (a conflict → `errLifetimeSettling`), nil.
-- (e) present, fieldless, slot absent (legacy) → `rec, raw, E, state, err := s.migrateHostLifetimeLocked(ctx, acct, app, host, obs.ETag)`: `ErrCASConflict` → `ErrCASConflict`; `state != committed` → `errLifetimeSettling`; committed → continue at (f) with the returned `rec, raw, E` and a fresh slot read (S).
-- (f) present and valid at N under slot `{N, live, this key}` at S → `BeginRetirement({OpID, Account, App, Host, N, E, S, SlotIntent, pending})`, then:
- - (1) `WriteRecord(ctx, s, key, raw, TombstoneBody(raw), E)`. `ErrCASConflict` → re-classify (unreadable → `pending`, `errLifetimePending`): a non-tombstone record at N → `refused` (`errRetireRefused`); a tombstone at N → (2); anything else → `void`, nil. Any other error → `pending`, `errLifetimePending`.
- - (2) `WriteHostIncarnation({N, retired, Account, App, "retire " + OpID}, S)`. `ErrCASConflict` → re-read the slot (unreadable → pending): `{N, retired, this key}` → continue; `> N` or another key → `void`; otherwise `pending`, `errLifetimePending`. Any other error → `pending`, `errLifetimePending`.
- - (3) `DeleteIfMatch` the tombstone on its ETag (absence is success; any other error → `pending`, `errLifetimePending`).
- - `committed` (a SQLite failure → `errLifetimePending`).
-
-Recovery, per pending retirement, inside `withHostname` with `admit(host, OpID)`, after `classifyLocked` (unreadable → pending), with `owned := slot owned by this key`, `succ := slot != nil && (slot.Incarnation > N || !owned)`, `pred := slot != nil && owned && slot.Incarnation == N && slot.State == live && slot.Intent == SlotIntent`, `retired := slot != nil && owned && slot.Incarnation == N && slot.State == retired`, the first matching branch:
-- `succ` → `void`;
-- `retired` and absent → `committed`;
-- `retired` and a present body (a tombstone at N, a late record at N, a stale fieldless body, a corrupt body) → `DeleteIfMatch` on its ETag (a conflict leaves it for the next create), `committed`;
-- `pred` and a present record whose `Incarnation != N` and is not a tombstone (a different lifetime's body under this slot; §6.4's different-incarnation observation) → `void`;
-- `pred`, `RecordETag != ""`, a present non-tombstone record at `RecordETag` → (1), (2), (3);
-- `pred`, a tombstone at N → (2), (3);
-- `pred`, `RecordETag == ""` (an orphan retirement), absent or any present body → (2), then the body if present is deleted conditionally, `committed`;
-- `pred`, `RecordETag != ""`, a present non-tombstone record at N with another ETag → `refused`;
-- `pred`, `RecordETag != ""`, absent → (2), `committed`;
-- a slot that is neither `succ`, `pred` nor `retired` (an owned live slot at N with another intent, or absent) → leave pending and log once per hour.
+- (d)/(g) present and stale or corrupt: slot `{N, live}` owned by this key → kind **cleanup**: `BeginRetirement({Kind: cleanup, N, RecordETag: obs.ETag, SlotETag: S, SlotIntent})`; steps C1 (retire the slot against S), C2 (`DeleteIfMatch` the body on `RecordETag`; a conflict or absence is fine: the body is non-servable under the retired slot), done. Otherwise (no slot, a retired slot, or another key's) → `DeleteIfMatch` on `obs.ETag` with no intent (a conflict → `errLifetimeSettling`), nil.
+- (e) present, fieldless, slot absent (legacy) → `rec, raw, E, state, err := s.migrateHostLifetimeLocked(ctx, acct, app, host, obs.ETag)`: `ErrCASConflict` → `ErrCASConflict`; `state != committed` → `errLifetimeSettling`; committed → (f) with `rec, raw, E` and a fresh slot read (S).
+- (f) present and valid at N under slot `{N, live, this key}` at S → kind **normal**: `BeginRetirement({Kind: normal, OpID, N, E, S, SlotIntent, pending})`; steps N1, N2, N3, done.
+
+**The steps** (every write failure other than the conflicts named is `pending` + `errLifetimePending`; every re-read that fails is `pending` + `errLifetimePending`):
+- **N1** `WriteRecord(ctx, s, key, raw, TombstoneBody(raw), E)`. `ErrCASConflict` → re-classify: a non-tombstone record at N under this key → `refused` (`errRetireRefused`); a tombstone at N → N2; absent beneath the live predecessor slot (an older delayed deletion took it) → N2 (the slot must still retire); a record of another incarnation, or a successor slot → `void`, nil.
+- **N2** `WriteHostIncarnation({N, retired, Account, App, "retire " + OpID}, S)`. `ErrCASConflict` → re-read the slot: `{N, retired, this key}` → N3; `> N` or another key → `void`; otherwise → `pending`, `errLifetimePending`.
+- **N3** `DeleteIfMatch` the tombstone on its ETag (absence is success). `committed`.
+- **O1** = N2 with the orphan's S, then `committed`; if a body landed under the key meanwhile (a late write), it is deleted conditionally on its ETag after the slot write (a conflict is logged; the body is stale under the retired slot).
+- **C1** = N2 with the cleanup's S; **C2** `DeleteIfMatch` the body on `RecordETag` (conflict or absence fine); `committed`.
+- `committed` is a SQLite write; its failure → `errLifetimePending` (recovery re-derives the terminal state).
+
+**Recovery**, per pending retirement, inside `withHostname` with `admit(host, OpID)`, after `classifyLocked` (unreadable → pending), with `owned`, `succ := slot != nil && (slot.Incarnation > N || !owned)`, `pred := slot != nil && owned && slot.Incarnation == N && slot.State == live && slot.Intent == SlotIntent`, `retired := slot != nil && owned && slot.Incarnation == N && slot.State == retired`:
+- any kind: `succ` → `void`; `retired` and absent → `committed`; `retired` and any present body (tombstone, late record, stale, corrupt) → `DeleteIfMatch` on its ETag (a conflict is left for the next create), `committed`; a slot that is neither `succ`, `pred` nor `retired` → pending, logged once per hour.
+- kind **normal**, `pred`: a present non-tombstone record at `RecordETag` → N1, N2, N3; a tombstone at N → N2, N3; absent → N2, `committed`; a present non-tombstone record at N with another ETag → `refused`; a present record whose `Incarnation != N` (not a tombstone) → `void`; a corrupt body → `refused` (the lifetime changed under the read into something this operation cannot judge; the delete rule's next call takes the corrupt path).
+- kind **orphan**, `pred`: absent or any present body → O1 (the slot, then the body if present), `committed`.
+- kind **cleanup**, `pred`: any observation → 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:
- `TestDeleteDoorRetiresThroughTheTombstone`; `TestSlotRetiresBeforeTheKeyIsAbsent`.
- - `TestDeleteDoorAbsentPathAdmitsAndRetiresOrphans` (initial absence; and absence found on the judge's re-read: both admit and retire the orphan slot; a record created since is untouched and the door still answers "deleted").
+ - `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).
- - `TestDeleteDoorRemovesStaleAndCorruptRecordsConditionally` (under a retired or foreign slot: body removed, slot untouched).
+ - `TestDeleteDoorRemovesStaleAndCorruptRecordsConditionally` (under a retired or foreign slot).
- `TestSeedFinishRetiresThroughTheTombstone`.
- - `TestRetireRefusesWhenTheRecordChangedInItsLifetime`; `TestRetireVoidsAgainstASuccessor`; `TestRetireNeverMovesTheSlotBackwards`.
- - `TestOrphanSlotIsRetired`; `TestOrphanRetirementSurvivesARestoredRecord` (crash after Begin; a late record at N lands; recovery retires the slot and removes the body).
- - `TestLegacyRetirementIsBoundToTheCallersRead` (the helper: an ordinary update between the caller's read and the marker CAS → `ErrCASConflict`, the updated record intact, the migration row superseded; the door: the `again` re-decision reads the updated record and decides it afresh, pinned as today's behaviour).
+ - `TestRetireRefusesWhenTheRecordChangedInItsLifetime`; `TestRetireVoidsAgainstASuccessor`; `TestRetireNeverMovesTheSlotBackwards`; `TestTombstoneConflictWithAbsentRecordStillRetiresTheSlot` (N1 loses to a delayed deletion; the slot ends retired).
+ - `TestOrphanSlotIsRetired`; `TestOrphanRetirementSurvivesARestoredRecord`.
+ - `TestExpectationsGateRetirement` (`retireExpectIncarnation(1)` against a record at 2 → nil, untouched; `retireAny` retires what is current; `retireExpectETag` conflict → `ErrCASConflict`).
+ - `TestLegacyRetirementIsBoundToTheCallersRead` (the helper: an ordinary update between the caller's read and the marker CAS → `ErrCASConflict`, the updated record intact, the migration row superseded); `TestDeleteDoorRefusesAnImportThatAppearedAfterItsFirstRead` (the real DELETE door: the first read sees a legacy record; before the retirement the record becomes an uncommitted import; the `again` re-decision refuses with the import rule's 409 and nothing is deleted).
- `TestDelayedDeleteCannotEraseASuccessor`.
- - `TestRetirementRecoveryBranches` (one subtest per branch above, with realistic predecessor intents; a second crash during recovery in two of them).
+ - `TestRetirementRecoveryBranches` (one subtest per kind and branch above, crashing each kind immediately after `BeginRetirement` and after each step, restarting from SQLite; a corrupt body under a cleanup; a changed body; a delayed slot write).
- `TestDetachMapsLifetimeErrors`.
- [ ] **Step 2: Run to verify they fail.**
- [ ] **Step 3: Implement.**
-- [ ] **Step 4: Run** `go test ./internal/console -run 'Retire|Tombstone|Lifetime|InstanceDelete|SeedFinish|DeleteDoor|Detach|StaleCleanup|Orphan' -count=1` and `go test ./internal/provision -run 'Retire' -count=1`.
-- [ ] **Step 5: Commit** `console: every claims-off record removal retires through a tombstone, with an explicit expectation and the owned slot retired before any absence shows`.
+- [ ] **Step 4: Run** `go test ./internal/console -run 'Retire|Tombstone|Lifetime|InstanceDelete|SeedFinish|DeleteDoor|Detach|StaleCleanup|Orphan|Expectations' -count=1` and `go test ./internal/provision -run 'Retire' -count=1`.
+- [ ] **Step 5: Commit** `console: every claims-off record removal retires through a persisted retirement of a stated kind, with the caller's expectation and the owned slot retired before any absence shows`.
### Task 5: Lazy migration with a marker fenced in the write primitive
@@ -206,7 +207,7 @@ Recovery, per pending retirement, inside `withHostname` with `admit(host, OpID)`
**Interfaces:**
- Produces: `var ErrRecordMigrating = fmt.Errorf("%w: the record is being migrated; read it again", release.ErrCASConflict)`; `func WithMigration(ctx context.Context, id string) context.Context`; `func (s *Server) migrateHostLifetimeLocked(ctx context.Context, acct, app, host, expectETag string) (rec *provision.Record, raw []byte, etag string, state string, err error)` (`expectETag == ""` is unbound: retry from the current record; a real ETag binds the marker CAS to it); `func (s *Server) StartIncarnationMigrationPass(ctx context.Context)` (behind `CARLOS_LIFETIMES_MIGRATE=1` and `lifetimesOn()`, every 5 minutes, at most 50 records per pass, oldest first, each inside `withHostname` and `admit`, only fieldless unmarked records whose slot is absent, unbound).
-The protocol (inside the exclusion): `admit(host, "")`; `BeginMigration({OpID, Account, App, Host, ExpectETag, "", pending})`; (1) `classifyLocked`: the record must be present, fieldless, unmarked, slot absent, and for a bound call at `ExpectETag` (else `superseded`, and for a bound call `ErrCASConflict`); `WriteRecord(WithMigration(ctx, OpID), s, key, raw, raw with IncarnationMigrating = OpID, E)`; on `ErrCASConflict`: bound → re-read; a record marked with OpID (this write landed) → continue; otherwise `superseded`, `ErrCASConflict`; unbound → re-read and retry from the current record, at most 5 times, then leave pending; `SetMigrationMarkedETag(OpID, M)` (a SQLite failure → pending, `errLifetimePending`); (2) `WriteHostIncarnation({1, live, Account, App, "migration " + OpID, RecordETag: M}, "")` create-only, or `If-Match` when a slot `{1, live, this key, "migration " + OpID}` exists; a CAS conflict → re-read: this migration's slot → continue; otherwise `superseded`; (3) `WriteRecord(WithMigration(ctx, OpID), ..., Incarnation = 1, IncarnationMigrating = "", against M)` (a conflict → re-read: a record at `Incarnation 1` under this slot → continue; otherwise pending); `committed`. Every other error after `BeginMigration` → pending, `errLifetimePending`; every re-read that fails → pending.
+The protocol (inside the exclusion): `admit(host, "")`; `BeginMigration({OpID, Account, App, Host, ExpectETag, "", pending})`; (1) `classifyLocked`: the record must be present, fieldless, unmarked, slot absent, and for a bound call at `ExpectETag` (else `superseded`, and for a bound call `ErrCASConflict`); `WriteRecord(WithMigration(ctx, OpID), s, key, raw, raw with IncarnationMigrating = OpID, E)`; on `ErrCASConflict`, **for both forms**, re-read and classify first (unreadable → pending): a record marked with OpID (this operation's write landed) → continue at `SetMigrationMarkedETag`; a record at `Incarnation 1` under a slot `{1, live, this key, "migration " + OpID}` → `committed`; otherwise, an eligible record (present, fieldless, unmarked, slot absent): bound → `superseded`, `ErrCASConflict` (its ETag is no longer `ExpectETag`); unbound → retry (1) from it, at most 5 times, then leave pending; anything else (gone, another lifetime, another op's marker, a slot not of this migration) → `superseded`; `SetMigrationMarkedETag(OpID, M)` (a SQLite failure → pending, `errLifetimePending`); (2) `WriteHostIncarnation({1, live, Account, App, "migration " + OpID, RecordETag: M}, "")` create-only, or `If-Match` when a slot `{1, live, this key, "migration " + OpID}` exists; a CAS conflict → re-read: this migration's slot → continue; otherwise `superseded`; (3) `WriteRecord(WithMigration(ctx, OpID), ..., Incarnation = 1, IncarnationMigrating = "", against M)` (a conflict → re-read: a record at `Incarnation 1` under this slot → continue; otherwise pending); `committed`. Every other error after `BeginMigration` → pending, `errLifetimePending`; every re-read that fails → pending.
Recovery, per pending migration, inside the exclusion with `admit(host, OpID)`, after `classifyLocked` (unreadable → pending):
- record at `Incarnation 1` and slot `{1, live, this key, "migration " + OpID}` → `committed`;
@@ -220,7 +221,7 @@ Recovery, per pending migration, inside the exclusion with `admit(host, OpID)`,
- `record_sequence_test.go`: `TestWriteRecordRefusesAMarkedRecordWithoutTheMigrationContext`, `TestWriteRecordAllowsTheMigrationsOwnWrite`, `TestCreateOnlyWriteIgnoresTheMarker`, `TestClaimRawUpdateRecordSurfacesTheMarker`.
- `TestMigrationMarksThenSlotsThenSets`; `TestMigrationRestartsFromAnUpdateThatWon` (unbound); `TestBoundMigrationReturnsConflictToItsCaller`.
- `TestAnIssuedWriteLosesToTheMarkerAndRetries`; `TestWriterGivesUpAfterTheBound`.
- - `TestLateMarkerWriteIsCompletedNotStranded` (bound migration M against E; the marker PUT is paused; crash; recovery observes the unmarked record at E and resumes (1); release the paused PUT: whichever lands second fails CAS; the migration reaches committed; no marker is left without an owner); the same with the paused PUT landing before recovery's read.
+ - `TestLateMarkerWriteIsCompletedNotStranded` (for a bound and for an unbound migration: the marker PUT is paused; crash; recovery observes the unmarked record and resumes (1); release the paused PUT: whichever lands second fails CAS, the re-read finds this operation's marker, the migration reaches committed; no marker is left without an owner); the same with the paused PUT landing before recovery's read, and with it landing between the running pass's read and its CAS.
- `TestMigrationRecoveryBranches` (one subtest per branch, a restart after the marker write and before `MarkedETag` is persisted, a second crash during recovery).
- `TestMigrationPassIsOffWithoutTheFlag`, `TestMigrationSkipsAFieldlessRecordBesideAnUnrelatedSlot`.
- [ ] **Step 2: Run to verify they fail.**
@@ -232,29 +233,30 @@ Recovery, per pending migration, inside the exclusion with `admit(host, OpID)`,
**Files:**
- Create: `internal/provision/lifetime_judge.go`, `internal/console/lifetimes_read.go`
-- Modify: `internal/provision/reconcile.go` (the decode loop at lines 855–941: `getEach` and `fetched` gain `etag`; after the path check and before `ValidHostShape`, `cfg.Judge.ObserveWith(...)`, replacing `rec`/`raw` with the observation's; stale hosts are recorded in `stale map[string]bool`; the removal loop: a row whose host is in `stale` skips the `KeepParticipant` check (a positively stale record overrides an obsolete keep decision); `ListAllRecordsStrict` and `ListAllRecordsTolerant` gain judged siblings and `readAllRecords` takes the judge; `holdExisting` line 1716: `cfg.Judge.Observe`), `internal/provision/provision.go` (judged siblings of `GetRecordETag`, `ListRecordsStrict`, `ListRecordsComplete`, `ListRecords`), `internal/host/agent.go` (one `boxJudge *provision.LifetimeJudge`, built once beside `provisionTickConfig` when `!hostwrite.Enabled(getenv)` and nil otherwise, handed to `ReconcileConfig.Judge`, `moveCfg.Judge`, the offline view, the runs worker, the steering loop, retention and the seed wiring), `internal/host/movespass.go:1079`, `internal/host/movesoffline.go` (`refreshOfflineView`; `offlineReassert` line 420), `internal/host/movesoffline_stage.go` (lines 75 and 162), `internal/host/runs.go` (`bootPass` line 122: the judged tolerant listing; the pickup re-read at line 278), `internal/host/runexec.go` (lines 318 and 421), `internal/host/steering_loop.go` (line 100), `internal/host/retention.go` (line 308), `internal/host/seedwiring.go` (line 60), `internal/console/*.go` (the inventory below; `placeSeedBlock` and `importInFlight` gain a `*provision.LifetimeJudge` parameter from their callers)
-- Test: `internal/provision/reconcile_lifetime_test.go`, `internal/provision/lifetime_judge_test.go`, `internal/console/lifetimes_read_test.go`, `internal/host/lifetime_gating_test.go`
+- Modify: `internal/provision/reconcile.go` (the decode loop at lines 855–941: `getEach` and `fetched` gain `etag`; after the path check and before `ValidHostShape`, `cfg.Judge.ObserveWith(...)`, replacing `rec`/`raw` with the observation's; stale hosts are recorded in `stale map[string]bool`; the removal loop: a row whose host is in `stale` skips the `KeepParticipant` check; `ListAllRecordsStrict` and `ListAllRecordsTolerant` gain judged siblings and `readAllRecords` takes the judge; `holdExisting` line 1716: `cfg.Judge.Observe`), `internal/provision/seedrollback.go` (`journalRecordByKey(ctx, s, j, judge)`: `Observe`; stale → `(nil, nil)` as a record that is gone; corrupt → the decode error as today; unreadable → the error), `internal/provision/seedreconcile.go` (`journalRecord` passes `cfg.Judge`; a stale journal record is "gone" to the journal's own rules, which already handle an absent record, and `known` stays true; unreadable keeps `known = false`), `internal/provision/provision.go` (`WithJudge`; `updateRecordSeeding`: when the context carries a judge, the read becomes `Observe` and a stale or absent observation returns `os.ErrNotExist` before `mut` runs, a corrupt one the decode error, an unreadable one its error; judged siblings of `GetRecordETag`, `ListRecordsStrict`, `ListRecordsComplete`, `ListRecords`), `internal/host/agent.go` (one `boxJudge *provision.LifetimeJudge`, built once in `provisionTickConfig`'s caller when `!hostwrite.Enabled(getenv)` and nil otherwise, set on `ReconcileConfig.Judge`, `moveCfg.Judge`, the offline view, the runs worker, the steering loop, retention and the seed hook; every pass context on the box is `provision.WithJudge(ctx, boxJudge)`: the reconcile loop, the moves pass, the runs worker, the steering loop, retention, the status tick), `internal/host/movespass.go:1079`, `internal/host/movesoffline.go` (`refreshOfflineView`; `offlineReassert` line 420), `internal/host/movesoffline_stage.go` (75, 162), `internal/host/runs.go` (122: `ListAllRecordsStrictJudged` with today's strict error handling; 278), `internal/host/runexec.go` (318, 421), `internal/host/steering_loop.go` (100), `internal/host/retention.go` (`appRecords` line 308: `Observe`; stale → omitted; unreadable → the error, as every error is today), `internal/host/seedwiring.go` (`seedPlacedHook`: the ctx carries the judge, so `SetRecordSeed` itself refuses a stale record with `os.ErrNotExist`, which the hook returns; the conflict re-read at line 60 uses `ReadRecordJudged`), every other `provision.GetRecord`/`ListAllRecords` in `internal/host` (`movescut.go:102`, `moveslandcommit.go:73,197`, `moveshold.go:266`, `movesland.go:188`, `movesfinal.go:562,622,709`, `moveslandinstall.go:92`, `movespolicy.go:146`, `movesparks.go:62`: all become the judged readers with the box's judge; on a claims-on box the judge is nil and they behave as today, so no exemptions), `internal/console/server.go` (`s.judge`; `cliRoute` and the pass starts derive their contexts with `WithJudge(ctx, s.judge)`), `internal/console/*.go` (the inventory below; `placeSeedBlock` and `importInFlight` take a `*provision.LifetimeJudge` parameter)
+- Test: `internal/provision/reconcile_lifetime_test.go`, `internal/provision/lifetime_judge_test.go`, `internal/provision/seedreconcile_lifetime_test.go`, `internal/console/lifetimes_read_test.go`, `internal/host/lifetime_gating_test.go`
**Interfaces:**
- Produces:
- - `type Observation struct { Present bool; Rec *Record; Raw []byte; ETag string; Slot *HostIncarnation; SlotETag string; Verdict LifetimeVerdict; Err error }` (`Rec` is nil when absent or corrupt; `Raw`/`ETag` are set for a corrupt present body).
- - `type LifetimeJudge struct{}` (no state; nil means claims-on: every observation is `valid` with the inputs unchanged); `func NewLifetimeJudge() *LifetimeJudge`.
- - `func (j *LifetimeJudge) Observe(ctx context.Context, s release.Store, acct, app, host string) Observation` and `func (j *LifetimeJudge) ObserveWith(ctx context.Context, s release.Store, acct, app, host string, raw []byte, etag string, readErr error) Observation` (round 1 uses the given record read). The protocol, up to 3 rounds: read the record (`GetRecordRaw`; absence, corruption and errors noted), **then** read the slot; a read error in either → `unreadable` with the error; judge: a present, decodable record with `LifetimeOf == valid` → return `valid`; absent → return `{Present: false, Verdict: stale}` when a slot exists and `{Present: false, Verdict: valid}` when none (an absent record is simply absent; the verdict tells a caller whether a slot stands); corrupt → return `corrupt` with `Raw`, `ETag`; stale → if this is round 1, or the slot's ETag differs from the previous round's, loop; stale with the slot ETag unchanged since the previous round → return `stale` (the authority did not move between two observations, so the record is behind it). Exhausted (3 rounds, the slot still changing) → `unreadable` with an error naming the host. The slot is always read after the record, so a slot that validates a record is at least as new as it.
- - `func ReadRecordJudged(ctx, s, j *LifetimeJudge, acct, app, host string) (*Record, string, error)`: stale, absent or a tombstone → `os.ErrNotExist`; corrupt → `ErrRecordCorrupt`; unreadable → the error.
- - Listings (typed): `func ListRecordsStrictJudged(ctx context.Context, s release.Store, j *LifetimeJudge, acct, app string, max int) ([]Record, map[string]bool, error)`; `func ListRecordsCompleteJudged(ctx context.Context, s release.Store, j *LifetimeJudge, acct, app string, max int) (recs []Record, truncated bool, skipped []string, err error)`; `func ListRecordsJudged(ctx context.Context, s release.Store, j *LifetimeJudge, acct, app string, max int) (recs []Record, truncated bool, err error)`; `func ListAllRecordsStrictJudged(ctx context.Context, s release.Store, j *LifetimeJudge) (recs []Record, unreadable map[string]bool, err error)`; `func ListAllRecordsTolerantJudged(ctx context.Context, s release.Store, j *LifetimeJudge) (recs []Record, complete bool, err error)`: stale dropped, records replaced by their observation, an unreadable observation reported as that host unreadable (strict: the listing error; complete: `skipped`; tolerant: `complete = false`).
- - `ReconcileConfig.Judge *LifetimeJudge`; `moveCfg.Judge *provision.LifetimeJudge`; `runsWorker.judge`, the steering loop's and retention's parameters likewise.
- - Console: `s.judge`; `func (s *Server) readRecord(ctx context.Context, acct, app, host string) (*provision.Record, string, error)`; `func (s *Server) observeRecord(ctx context.Context, acct, app, host string) provision.Observation` (the delete rule); `listRecords`, `listRecordsStrict`, `listRecordsComplete`, `listAllRecordsStrict`, `listAllRecordsTolerant` wrappers. Inventory to route: every instance-record read in `apiinstancemove.go`, `apiinstances.go`, `seedfinish.go`, `channelmove.go`, `apiruns.go`, `apiinstancemove_offline.go`, `apiinstanceimport.go`, `updatescore.go`, `movesrecovery.go`, `domainattach.go`, `appsettings.go`, `apirunsfollow.go`, `apiretry.go`, `apidb.go`, `apex.go`, `apidomains.go`, `apiinstanceseed.go`, `apilegacyroutes.go`, `relayops.go`, `seedsweep.go`, `server.go`, `regiondrift.go`, `runsweep.go`, `seedmove.go`. `TestNoConsoleReadBypassesTheJudgedReaders` walks `internal/console` (non-test) and fails on `provision.GetRecord(`, `provision.GetRecordETag(`, `provision.GetRecordRaw(`, `provision.ListRecords`, `provision.ListAllRecords`, `provision.DecodeRecord(` or `.Get(ctx, provision.RecordKey(` outside `lifetimes*.go` and an explicit list of claims-on files (`claim_*.go`, `hostclaimops.go`, `movesrecovery.go`'s claims-on branches named by function). `TestNoBoxReadBypassesTheJudge` does the same for `internal/host` outside `lifetime*.go` and the claims-on files (`claim*.go`, `root*.go`, `hostlife*.go`).
-
-- [ ] **Step 1: Write the failing tests**: `TestJudgeReadsRecordThenSlot` (a counting store: one record read and one slot read for a valid record; order pinned); `TestJudgeAcrossConcurrentLifetimeChanges` (hooks between the reads: record read, then retirement completes, then slot read → round 2 → absent/stale with no record; record obsolete, slot `{N, retired}`, recreation at N+1 completes, round 2 reads the new record then the new slot → valid with the successor's record and ETag; record read before a migration's marker, slot after → round 2 valid with the migrated record and ETag; a slot that changes on every round → unreadable after 3); `TestNilJudgeIsClaimsOn`; `TestReconcileRemovesARecordBehindItsSlot`; `TestReconcileRemovesATombstone`; `TestReconcileKeepsALegacyRecordWithNoSlot`; `TestReconcileKeepsAMarkedRecordUnderItsMigrationSlot`; `TestReconcileProtectsWhenTheSlotIsUnreadable`; `TestReconcileUsesTheObservation`; `TestStaleOverridesAPopulatedKeepView` (the offline view holds a staging move for the host from an earlier listing; the record is now stale; the real `reconcileSerializedWith` path (no manual refresh) removes the row and the next edges write omits the host); `TestEveryClaimsOffConsumerIgnoresAFieldlessRecordBesideASlot` (slot `{1, retired}`, a fieldless record carrying an offline move and a queued run: `ReadRecordJudged` not found; the five judged listings omit it; the console GET answers 404; reconcile mints nothing and retires its row; the edges file stops listing it; `offlineReassert`, the staging deletion gate, the runs boot pass and pickup, `launchable`, the steering loop's desired state, retention's app records and the seed hook all ignore it); `TestClaimsOnConsumersReadNoSlot`; `TestBoxJudgeIsSharedAndGated`; `TestNoConsoleReadBypassesTheJudgedReaders`; `TestNoBoxReadBypassesTheJudge`.
+ - `type Observation struct { Present bool; Rec *Record; Raw []byte; ETag string; Slot *HostIncarnation; SlotETag string; Verdict LifetimeVerdict; Err error }` (`Rec` nil when absent or corrupt; `Raw`/`ETag` set for a present corrupt body).
+ - `type LifetimeJudge struct{}` (stateless); `func NewLifetimeJudge() *LifetimeJudge`. **A nil judge disables slot checking only**: `Observe` reads the record once and returns `Present`, `Rec`, `Raw`, `ETag` and `Verdict` `valid` for a decodable body, `corrupt` for one that does not decode or disagrees with its path, `unreadable` with the error for a failed read; absence is `Present: false, Verdict: valid`. Every judged reader and listing therefore behaves exactly as its unjudged sibling under a nil judge.
+ - `func WithJudge(ctx context.Context, j *LifetimeJudge) context.Context`; `func JudgeFrom(ctx context.Context) *LifetimeJudge`.
+ - `func (j *LifetimeJudge) Observe(ctx context.Context, s release.Store, acct, app, host string) Observation` and `func (j *LifetimeJudge) ObserveWith(ctx context.Context, s release.Store, acct, app, host string, raw []byte, etag string, readErr error) Observation` (round 1 uses the given record read). With a non-nil judge, up to 3 rounds: read the record (`GetRecordRaw`), **then** the slot; a read error in either → `unreadable`; a present decodable record with `LifetimeOf == valid` → `valid`; absent → `{Present: false, Verdict: stale}` when a slot exists, `{Present: false, Verdict: valid}` when none; corrupt → `corrupt` with `Raw`, `ETag`; stale → loop if this is round 1 or the slot's ETag differs from the previous round's; stale with the slot ETag unchanged → `stale`. Exhausted → `unreadable` with an error naming the host.
+ - `func ReadRecordJudged(ctx context.Context, s release.Store, j *LifetimeJudge, acct, app, host string) (*Record, string, error)`: stale, absent or a tombstone → `os.ErrNotExist`; corrupt → `ErrRecordCorrupt`; unreadable → the error.
+ - `func ListRecordsStrictJudged(ctx context.Context, s release.Store, j *LifetimeJudge, acct, app string, max int) (recs []Record, unreadable map[string]bool, err error)`; `func ListRecordsCompleteJudged(ctx context.Context, s release.Store, j *LifetimeJudge, acct, app string, max int) (recs []Record, truncated bool, skipped []string, err error)`; `func ListRecordsJudged(ctx context.Context, s release.Store, j *LifetimeJudge, acct, app string, max int) (recs []Record, truncated bool, err error)`; `func ListAllRecordsStrictJudged(ctx context.Context, s release.Store, j *LifetimeJudge) (recs []Record, unreadable map[string]bool, err error)`; `func ListAllRecordsTolerantJudged(ctx context.Context, s release.Store, j *LifetimeJudge) (recs []Record, complete bool, err error)`: stale dropped, records replaced by their observation, an unreadable observation reported as that host unreadable (strict: the listing error; complete: `skipped`; tolerant: `complete = false`).
+ - `ReconcileConfig.Judge *LifetimeJudge`; `moveCfg.Judge *provision.LifetimeJudge`.
+ - Console: `s.judge`; `func (s *Server) readRecord(ctx context.Context, acct, app, host string) (*provision.Record, string, error)`; `func (s *Server) observeRecord(ctx context.Context, acct, app, host string) provision.Observation`; `listRecords`, `listRecordsStrict`, `listRecordsComplete`, `listAllRecordsStrict`, `listAllRecordsTolerant` wrappers. Inventory to route: every instance-record read in `apiinstancemove.go`, `apiinstances.go`, `seedfinish.go`, `channelmove.go`, `apiruns.go`, `apiinstancemove_offline.go`, `apiinstanceimport.go`, `updatescore.go`, `movesrecovery.go`, `domainattach.go`, `appsettings.go`, `apirunsfollow.go`, `apiretry.go`, `apidb.go`, `apex.go`, `apidomains.go`, `apiinstanceseed.go`, `apilegacyroutes.go`, `relayops.go`, `seedsweep.go`, `server.go`, `regiondrift.go`, `runsweep.go`, `seedmove.go`. The walk tests `TestNoConsoleReadBypassesTheJudgedReaders`, `TestNoBoxReadBypassesTheJudge` and `TestNoProvisionReadBypassesTheJudge` fail on `provision.GetRecord(`, `provision.GetRecordETag(`, `provision.GetRecordRaw(`, `provision.ListRecords`, `provision.ListAllRecords`, `provision.DecodeRecord(` or `.Get(ctx, provision.RecordKey(` (and inside `internal/provision` on `s.Get(ctx, RecordKey(`/`DecodeRecord(` outside the judge, the listings' own decode loop, `updateRecordSeeding`, `claimRawUpdateRecord` and `WriteRecord`) in non-test files outside `lifetime*.go`; **no other exemptions**: a claims-on path reads through a nil judge.
+
+- [ ] **Step 1: Write the failing tests**: `TestJudgeReadsRecordThenSlot`; `TestJudgeAcrossConcurrentLifetimeChanges` (each subtest asserts `Present`, `Raw`, `ETag` and `SlotETag`, not only the verdict); `TestNilJudgeIsClaimsOn` (absence, a corrupt body and a read error behave exactly as the unjudged reader); `TestUpdateRecordRefusesAStaleRecordUnderAJudge` (`SetRecordSeed`, `SetRecordRegion` and `SetRecordMove` with `WithJudge` against a stale record → `os.ErrNotExist`, no write; without a judge → today); `TestReconcileRemovesARecordBehindItsSlot`; `TestReconcileRemovesATombstone`; `TestReconcileKeepsALegacyRecordWithNoSlot`; `TestReconcileKeepsAMarkedRecordUnderItsMigrationSlot`; `TestReconcileProtectsWhenTheSlotIsUnreadable`; `TestReconcileUsesTheObservation`; `TestStaleOverridesAPopulatedKeepView`; `TestImportJournalIgnoresAStaleRecord` (a real import journal in `swapped` beside a stale committed record: the gate is not opened, no swap replay; a stale `ready` record likewise; an unreadable observation leaves the import alone as today); `TestEveryClaimsOffConsumerIgnoresAFieldlessRecordBesideASlot` (slot `{1, retired}`, a fieldless record carrying an offline move, a queued run and a placeable seed: `ReadRecordJudged` not found; the five judged listings omit it; the console GET answers 404; reconcile mints nothing and retires its row; the edges file stops listing it; `offlineReassert`, the staging deletion gate, the runs boot pass and pickup, `launchable`, the steering loop's desired state, retention's app records all ignore it; the seed hook's `SetRecordSeed` refuses and the seed snapshot is **not** deleted); `TestClaimsOnConsumersReadNoSlot`; `TestBoxJudgeIsSharedAndGated`; the three walk tests.
- [ ] **Step 2: Run to verify they fail.**
- [ ] **Step 3: Implement.**
- [ ] **Step 4: Run** the touched packages' targeted tests, then the two census tests and add the rows they name.
-- [ ] **Step 5: Commit** `lifetimes: every claims-off read observes the record then the slot, in bounded rounds, and acts on the observation`.
+- [ ] **Step 5: Commit** `lifetimes: every claims-off read and update observes the record then the slot, in bounded rounds, and acts on the observation`.
### Task 7: `move_seq` on the record and the pair on rows
**Files:**
-- Modify: `internal/provision/offline_move.go` (the transition table below), `internal/provision/provision.go` (`MoveState.Seq uint64 \`json:"seq,omitempty"\``), `internal/provision/offline_move_test.go` (`TestOfflineRecordDiffersOnlyWhereTheMoveSaysSo`: `move_seq` joins the allowlist; `TestOfflineAbort` keeps pinning the repeated abort), `internal/registry/registry.go` (columns `incarnation INTEGER NOT NULL DEFAULT 0`, `serving_seq INTEGER NOT NULL DEFAULT 0`; `Route.Incarnation`, `Route.ServingSeq`; `routeCols`, `routeColCount` (+2), `scanRoute`, `routeArgs`, `normalizeRoute`; `Upsert`'s conflict clause gains `incarnation=routes.incarnation, serving_seq=routes.serving_seq`; `Fingerprint` gains both; `remote_dispatch.go` gains the new methods; `SetHold`, `SetHoldIf`, `HandHold`, `ReleaseHold`, `DeleteIfHold` untouched), `internal/provision/reconcile.go` (`RouteFor` copies `rec.Incarnation` and `rec.MoveSeq`; `ReconcileConfig.PairOf func(rec Record) registry.Pair` (nil → the untouched `SetHoldIf`/`ReleaseHold`) and `ReconcileConfig.DeleteIfHold func(host, op string) (bool, error)` (nil → rule (iv) logs and skips); the existing-row rules below), `internal/host/agent.go` (`provisionTickConfig`: `DeleteIfHold: reg.DeleteIfHoldWithTimers`), `internal/host/movesoffline.go` (`withOfflineHolds` sets `cfg.PairOf = func(rec) Pair{rec.Incarnation, rec.Move.Seq}` (nil-safe); `moveCfg.ReleaseHoldSeq func(host, op string, pair registry.Pair) (bool, error)` backed by `releaseHeldSeq(reg, written)`; the **source abort/revert** path at `movesoffline.go:1077`: under the existing locks, `cfg.Judge.Observe` the record; `Move != nil && Move.ID == m.ID && Move.State == OfflineReverting` (else skip this pass, logged); `ReleaseHoldSeq(host, m.ID, {rec.Incarnation, rec.Move.Seq})` from that observation), `internal/host/movesoffline_land.go` (the **target open** path at line 165 likewise, expecting `OfflineOpen`), `internal/host/movespass.go` (`armOffline`, line 371: `cfg.ReleaseHoldSeq = releaseHeldSeq(reg, holdStageRequester(dataDir))`), `internal/host/movesoffline_test.go:207` and `internal/host/movesoffline_land_test.go:206` (the overrides gain `cfg.ReleaseHoldSeq = releaseHeldSeq(b.reg, nil)`)
+- Modify: `internal/provision/offline_move.go` (the transition table below), `internal/provision/provision.go` (`MoveState.Seq uint64 \`json:"seq,omitempty"\``), `internal/provision/offline_move_test.go` (`TestOfflineRecordDiffersOnlyWhereTheMoveSaysSo`: `move_seq` joins the allowlist; `TestOfflineAbort` keeps pinning the repeated abort), `internal/registry/registry.go` (columns `incarnation INTEGER NOT NULL DEFAULT 0`, `serving_seq INTEGER NOT NULL DEFAULT 0`; `Route.Incarnation`, `Route.ServingSeq`; `routeCols`, `routeColCount` (+2), `scanRoute`, `routeArgs`, `normalizeRoute`; `Upsert`'s conflict clause gains `incarnation=routes.incarnation, serving_seq=routes.serving_seq`; `Fingerprint` gains both; `remote_dispatch.go` gains the new methods; `SetHold`, `SetHoldIf`, `HandHold`, `ReleaseHold`, `DeleteIfHold` untouched), `internal/provision/reconcile.go` (`RouteFor` copies `rec.Incarnation` and `rec.MoveSeq`; `ReconcileConfig.PairOf func(rec Record) registry.Pair` (nil → the untouched `SetHoldIf`/`ReleaseHold`) and `ReconcileConfig.DeleteIfHold func(host, op string) (bool, error)` (nil → rule (iv) logs and skips); the existing-row rules below), `internal/host/agent.go` (`reconcileSerializedWith`, which owns `reg`: `cfg.DeleteIfHold = reg.DeleteIfHoldWithTimers` when `reg != nil`, beside the existing `withOfflineHolds` wiring), `internal/host/movesoffline.go` (`withOfflineHolds` sets `cfg.PairOf = func(rec) Pair{rec.Incarnation, rec.Move.Seq}` (nil-safe); `moveCfg.ReleaseHoldSeq func(host, op string, pair registry.Pair) (bool, error)` backed by a new `func releaseHeldSeq(reg *registry.Registry, written func(host string)) func(host, op string, pair registry.Pair) (bool, error)` declared in `movesoffline.go` beside `offlineHold`, calling `reg.ReleaseHoldSeq` and then `written(host)` only when the release changed a row (as `releaseHeld` does); the **source abort/revert** path at `movesoffline.go:1077`: under the existing locks, `cfg.Judge.Observe` the record; `Move != nil && Move.ID == m.ID && Move.State == OfflineReverting` (else skip this pass, logged); `ReleaseHoldSeq(host, m.ID, {rec.Incarnation, rec.Move.Seq})` from that observation), `internal/host/movesoffline_land.go` (the **target open** path at line 165 likewise, expecting `OfflineOpen`), `internal/host/movespass.go` (`armOffline`, line 371: `cfg.ReleaseHoldSeq = releaseHeldSeq(reg, holdStageRequester(dataDir))`), `internal/host/movesoffline_test.go:207` and `internal/host/movesoffline_land_test.go:206` (the overrides gain `cfg.ReleaseHoldSeq = releaseHeldSeq(b.reg, nil)`)
- Test: `internal/provision/offline_move_test.go`, `internal/registry/registry_test.go`, `internal/provision/reconcile_lifetime_test.go`, `internal/host/movesoffline_e2e_test.go`
**The transition table (authoritative):**
@@ -273,7 +275,7 @@ Recovery, per pending migration, inside the exclusion with `admit(host, OpID)`,
| `OfflineFinish` with `StartedOffline()` | +1 | |
| `OfflineFinish` without (an abort from staging) | unchanged | |
-Each `+1` sets `rec.MoveSeq++` and `rec.Move.Seq = rec.MoveSeq` (nil-safe) in the same mutation.
+Each `+1` sets `rec.MoveSeq++` and `rec.Move.Seq = rec.MoveSeq` (nil-safe) in the same mutation; a `MoveSeq` already at `math.MaxInt64` makes the transition return `ErrLifetimeExhausted` before any write (the create's `N` likewise, Task 3).
**The setters:**
- `type registry.Pair struct { Incarnation, ServingSeq uint64 }`.
@@ -299,20 +301,20 @@ Each `+1` sets `rec.MoveSeq++` and `rec.Move.Seq = rec.MoveSeq` (nil-safe) in th
- [ ] **Step 4: Run** `go test ./internal/registry -run 'Pair|Hold|Lifetime|Fingerprint' -count=1`, `go test ./internal/provision -run 'MoveSeq|RouteFor|Offline|Reconcile|Lifetime' -count=1`, `go test ./internal/host -run 'OfflineMoveE2E|Offline' -count=1`, then both census tests.
- [ ] **Step 5: Commit** `moves: the offline hold, open, abort and finish advance move_seq; rows take a lifetime with its hold in one write and are never moved by another lifetime`.
-### Task 8: Publish the pair with its provenance and order marked entries by it
+### Task 8: Publish the pair and order unstamped entries by it
**Files:**
-- Modify: `internal/host/edgesfile.go` (`edgeHost` gains `Lifetimes bool \`json:"lifetimes,omitempty"\``, `Incarnation uint64 \`json:"incarnation,omitempty"\``, `ServingSeq uint64 \`json:"serving_seq,omitempty"\``, `TargetHost string \`json:"target_host,omitempty"\``; `edgesFileWriteClaims` gains `lifetimes bool`, passed from the agent as `!hostwrite.Enabled(getenv)` (**not** the `claims` argument, which is the region-HA switch); with `lifetimes` every entry this box publishes carries `Lifetimes: true`: a served route publishes its row's pair; the explicit alias entry at line 546 publishes `hosts[a.Host]`'s pair and `TargetHost: a.Host`; the wildcard dial entry at line 632 publishes `hosts[primary]`'s pair and `TargetHost: primary`; a certificate-only wildcard entry publishes none; without `lifetimes` no entry carries the mark or a pair, and `TargetHost` is still published), `internal/edge/homemap.go` (`edgesHostView` and `homeEntry` gain `lifetimes`, `incarnation`, `servingSeq`, `targetHost`; `foldHomes` copies all four with the dial winner; `dialBeats`: `!in.dial` → false; `!prev.dial` → true; `prev.alias != in.alias` → the real route wins (today); if either entry is stamped → `in.stamp > prev.stamp` (today); both unstamped: if exactly one carries `lifetimes` → the marked entry wins (a lifetimes publisher is newer than any unmarked, unstamped one); both marked and both real → the higher `(incarnation, servingSeq)`, equal → today's rule (`in.stamp > prev.stamp`, false); both marked aliases with `prev.targetHost != "" && prev.targetHost == in.targetHost` → the same pair rule; otherwise → today's rule)
+- Modify: `internal/host/edgesfile.go` (`edgeHost` gains `Incarnation uint64 \`json:"incarnation,omitempty"\``, `ServingSeq uint64 \`json:"serving_seq,omitempty"\``, `TargetHost string \`json:"target_host,omitempty"\``; `edgesFileWriteClaims` gains `lifetimes bool`, passed from the agent as `!hostwrite.Enabled(getenv)` (**not** the `claims` argument, which is the region-HA switch): with `lifetimes`, a served route publishes its row's pair; the explicit alias entry at line 546 publishes `hosts[a.Host]`'s pair and `TargetHost: a.Host`; the wildcard dial entry at line 632 publishes `hosts[primary]`'s pair and `TargetHost: primary`; a certificate-only wildcard entry publishes none; without `lifetimes` no entry carries a pair, and `TargetHost` is still published), `internal/edge/homemap.go` (`edgesHostView` and `homeEntry` gain `incarnation`, `servingSeq`, `targetHost`; `foldHomes` copies all three with the dial winner; `dialBeats`: `!in.dial` → false; `!prev.dial` → true; `prev.alias != in.alias` → the real route wins (today); if either entry is stamped → `in.stamp > prev.stamp` (today); both unstamped and both real → the higher `(incarnation, servingSeq)` wins, where an entry without the fields is `(0, 0)`, and equal pairs → today's rule (`in.stamp > prev.stamp`, false: the earlier file wins); both unstamped aliases with `prev.targetHost != "" && prev.targetHost == in.targetHost` → the same pair rule; otherwise → today's rule)
- Test: `internal/host/edgesfile_test.go`, `internal/edge/homemap_test.go`
**Interfaces:**
-- Produces: the four fields on the wire and in the fold; the rule above. Mixed history, stated: two claims-on publications fold as today (stamp when relocatable, first-wins otherwise); a claims-on publication of a relocatable row is stamped and beats any retained claims-off entry; a claims-on publication of a non-relocatable route (unstamped, unmarked) against a retained claims-off marked entry for the same name loses to it until the retained file is refreshed, which is the carry-forward window of a failed fetch and today's behaviour for a duplicate unstamped claim; a claims-off deployment mid-roll: marked entries beat unmarked retained ones, as the design asks (the first move's hold `(1, 1)` beats a retained pair-less `(0, 0)`).
+- Produces: the three fields on the wire and in the fold; the rule above, which is §6.4's ("publications without the pair are `(0, 0)`"; "equal pairs fall to the existing home-map rule"). Mixed history, stated: two stamped claims-on publications fold as today; a stamped claims-on publication beats any unstamped entry whatever its pair; two unstamped claims-on publications (non-relocatable routes, or a relocatable row whose claim read was absent, failed or undecided) are both `(0, 0)` and fold as today; an unstamped claims-on publication against a **retained** claims-off publication carrying a pair for the same name (a box whose file this edge could not refresh after the deployment turned claims on) loses to it until that file refreshes, which is the carry-forward window of a failed fetch and the one case where the pair outranks an unstamped claim; the design's cases (the first move's hold `(1, 1)` over a retained pair-less `(0, 0)`; open `(1, 2)` over the hold; a recreated host's `(2, 0)` over lifetime 1) follow from the pair rule.
-- [ ] **Step 1: Write the failing tests**: `TestEdgesFilePublishesThePairAndTargetHost` (with `lifetimes`: a row `(2, 3)`; its explicit alias; a wildcard dial entry whose parent differs from the primary; a certificate-only wildcard entry has no target, no pair, no mark); `TestLifetimesOffWriterPublishesNoMarkOrPair` (`lifetimes == false` with the region-HA `claims` both nil and non-nil: no mark, no pair, `TargetHost` present); `TestFoldPrefersTheHigherPairOfOneLifetime`; `TestFoldStampBeatsPair` (a retained marked `(1, 3)` vs a stamped unmarked `(0, 0)` → the stamped; two stamped → the higher stamp); `TestFoldMarkedBeatsUnmarkedUnstamped` (a marked `(1, 1)` hold-side route vs an unmarked `(0, 0)` → marked; a marked `(0, 0)` vs an unmarked `(0, 0)` → marked); `TestFoldTwoUnmarkedUnstampedIsToday`; `TestFoldKeepsRealRouteOverAHigherPairAlias`; `TestFoldComparesAliasesOnlyForOneTarget`; `TestFoldNeverDialsACertOnlyEntryForItsPair`; `TestFoldCopiesTheWinnersFields`; `TestFoldLegacyPublicationsUnchanged` (existing fixtures; `TestHomeMapPicksTheSameHomeOnADuplicateClaim` still passes).
+- [ ] **Step 1: Write the failing tests**: `TestEdgesFilePublishesThePairAndTargetHost` (with `lifetimes`: a row `(2, 3)`; its explicit alias; a wildcard dial entry whose parent differs from the primary; a certificate-only wildcard entry has no target and no pair); `TestLifetimesOffWriterPublishesNoPair` (`lifetimes == false` with the region-HA `claims` nil and non-nil: no pair, `TargetHost` present); `TestFoldPrefersTheHigherPairOfOneLifetime` (`(1, 2)` over `(1, 1)`; `(2, 0)` over `(1, 9)`; `(1, 1)` over a pair-less entry; equal pairs → today's rule, both file orders); `TestFoldStampBeatsPair` (a retained `(1, 3)` vs a stamped `(0, 0)` → the stamped, both orders; two stamped → the higher stamp whatever their pairs); `TestFoldUnstampedClaimsOnAgainstRetainedPair` (an unstamped `(0, 0)` vs a retained `(1, 3)` → the retained one, both orders, as stated); `TestFoldKeepsRealRouteOverAHigherPairAlias`; `TestFoldComparesAliasesOnlyForOneTarget`; `TestFoldNeverDialsACertOnlyEntryForItsPair`; `TestFoldCopiesTheWinnersFields`; `TestFoldLegacyPublicationsUnchanged` (existing all-pair-less fixtures fold exactly as before; `TestHomeMapPicksTheSameHomeOnADuplicateClaim` still passes).
- [ ] **Step 2: Run to verify they fail.**
- [ ] **Step 3: Implement.**
- [ ] **Step 4: Run** `go test ./internal/host -run 'EdgesFile|LifetimesOffWriter' -count=1`, `go test ./internal/edge -run 'Fold|HomeMap' -count=1`, then the census tests.
-- [ ] **Step 5: Commit** `edges: the lifetimes writer marks its entries and publishes (incarnation, serving_seq) and target_host; the home map orders marked, unstamped entries of one lifetime by the pair`.
+- [ ] **Step 5: Commit** `edges: the lifetimes writer publishes (incarnation, serving_seq) and target_host; the home map orders unstamped entries by the pair, pair-less entries counting as (0, 0)`.
### Task 9: Lifetimes end to end
@@ -330,7 +332,7 @@ These are §11's witnesses plan 1 owns. Every witness that needs a holding publi
## Self-review notes
-- §6.4 coverage: slot, owner key and tombstone form (T1, T4), create and recovery (T3), every claims-off removal with an explicit expectation and its recovery (T4), migration, its fence and recovery (T5), the observation protocol in every claims-off consumer (T1, T6), `move_seq`, the setters that install a lifetime with its hold and the existing-row rules (T7), publication with provenance and the fold order (T8), witnesses (T9). Deferred by name: holding entries and their five-minute validity, the probe mode, the different-target alias rule and every holding-dependent witness (plan 3); the door, token, job, reservations and reports (plan 2); cleanup (plan 4). The one stated deviation from §6.4's text is the uncached slot read.
-- Names used across tasks: `LifetimeOf`, `LifetimeVerdict`, `HostIncarnation.Valid`, `ErrSlotMalformed`, `ErrRecordCorrupt`, `ReadHostIncarnation`, `WriteHostIncarnation`, `TombstoneBody`, `GetRecordRaw`, `Observation{Present, Rec, Raw, ETag, Slot, SlotETag, Verdict, Err}`, `LifetimeJudge.Observe/ObserveWith`, `ReadRecordJudged`, `List*Judged`, `withHostname`, `admit`, `classifyLocked`, `createRecordWithLifetime`, `retireRecordWithLifetime`, `retireExpectation` (`retireExpectAbsent`, `retireAny`, `retireExpectETag`), `migrateHostLifetimeLocked`, `errLifetimeSettling`, `errLifetimePending`, `*hostHeldError`, `errRetireRefused`, `ErrRecordMigrating`, `WithMigration`, `ReconcileConfig.Judge/PairOf/DeleteIfHold`, `moveCfg.Judge/ReleaseHoldSeq`, `releaseHeldSeq`, `registry.Pair`, `SetHoldIfSeq`, `ReleaseHoldSeq`, `InstallLifetimeIf`, `Route.Incarnation/ServingSeq`, `MoveState.Seq`, `edgeHost.Lifetimes/Incarnation/ServingSeq/TargetHost`.
+- §6.4 coverage: slot, owner key and tombstone form (T1, T4), create and recovery (T3), every claims-off removal with an explicit expectation and its recovery (T4), migration, its fence and recovery (T5), the observation protocol in every claims-off consumer (T1, T6), `move_seq`, the setters that install a lifetime with its hold and the existing-row rules (T7), publication by the lifetimes writer and §6.4's fold order (T8), witnesses (T9). Deferred by name: holding entries and their five-minute validity, the probe mode, the different-target alias rule and every holding-dependent witness (plan 3); the door, token, job, reservations and reports (plan 2); cleanup (plan 4). The one stated deviation from §6.4's text is the uncached slot read; the fold rule is §6.4's own.
+- Names used across tasks: `LifetimeOf`, `LifetimeVerdict`, `HostIncarnation.Valid`, `ErrSlotMalformed`, `ErrRecordCorrupt`, `ReadHostIncarnation`, `WriteHostIncarnation`, `TombstoneBody`, `GetRecordRaw`, `Observation{Present, Rec, Raw, ETag, Slot, SlotETag, Verdict, Err}`, `LifetimeJudge.Observe/ObserveWith`, `ReadRecordJudged`, `List*Judged`, `withHostname`, `admit`, `classifyLocked`, `createRecordWithLifetime`, `retireRecordWithLifetime`, `retireExpectation` (`retireExpectAbsent`, `retireAny`, `retireExpectETag`, `retireExpectIncarnation`), `provision.RetireExpectation`, `Retirement.Kind` (`normal`, `orphan`, `cleanup`), `WithJudge`, `JudgeFrom`, `ErrLifetimeExhausted`, `migrateHostLifetimeLocked`, `errLifetimeSettling`, `errLifetimePending`, `*hostHeldError`, `errRetireRefused`, `ErrRecordMigrating`, `WithMigration`, `ReconcileConfig.Judge/PairOf/DeleteIfHold`, `moveCfg.Judge/ReleaseHoldSeq`, `releaseHeldSeq`, `registry.Pair`, `SetHoldIfSeq`, `ReleaseHoldSeq`, `InstallLifetimeIf`, `Route.Incarnation/ServingSeq`, `MoveState.Seq`, `edgeHost.Incarnation/ServingSeq/TargetHost`.
- Review Focus 1 to 5 are pinned by T3+T5, T6, T6+T3, T7, T7+T9.
- Store primitives, as verified in the reviews: `Store.Put(..., "")` is create-only on both backends; `WriteRecord` with `before={}` and `etag=""` is the create and needs the real CAS base otherwise; `DeleteIfMatch` treats absence as success and has no unconditional fallback; `OfflineAbort` on a reverting record returns `errRecordUnchanged`, a success without a write; `AccountBySqid` returns `(nil, nil)` for an unknown sqid.