carlos / platform Public

plan: self-serve moves 1, rev 7 (Astra plan review 6: findings 1 to 5; record sequencing fence, one migration dispatcher, claim rollback under the exclusion, judge at every mutation context, AST audit)

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

CI passed — run details

CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (1 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.014s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
=== vet (2 s)
=== gofmt (1 s)
gofmt -l .: clean
=== rest (138 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.504s
ok  	github.com/carlosframework/platform/cmd/moveprobe	11.835s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.730s
ok  	github.com/carlosframework/platform/internal/activator	59.702s
ok  	github.com/carlosframework/platform/internal/agentsock	0.044s
ok  	github.com/carlosframework/platform/internal/alerting	0.005s
ok  	github.com/carlosframework/platform/internal/analytics	0.031s
ok  	github.com/carlosframework/platform/internal/appconfig	3.079s
ok  	github.com/carlosframework/platform/internal/apperr	0.217s
ok  	github.com/carlosframework/platform/internal/applog	0.347s
ok  	github.com/carlosframework/platform/internal/awsinv	0.007s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.948s
?   	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.008s
ok  	github.com/carlosframework/platform/internal/caps	0.036s
ok  	github.com/carlosframework/platform/internal/certwarm	0.003s
ok  	github.com/carlosframework/platform/internal/cgroup	0.760s
ok  	github.com/carlosframework/platform/internal/changelog	0.525s
ok  	github.com/carlosframework/platform/internal/claimactivation	1.174s
ok  	github.com/carlosframework/platform/internal/clamd	0.057s
?   	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	5.759s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.014s
ok  	github.com/carlosframework/platform/internal/dbverify	0.005s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.006s
ok  	github.com/carlosframework/platform/internal/docsite	0.011s
ok  	github.com/carlosframework/platform/internal/domains	2.887s
ok  	github.com/carlosframework/platform/internal/doorkey	0.009s
ok  	github.com/carlosframework/platform/internal/economics	0.005s
ok  	github.com/carlosframework/platform/internal/edge	45.063s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.008s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.166s
ok  	github.com/carlosframework/platform/internal/gate	1.838s
ok  	github.com/carlosframework/platform/internal/genselect	0.022s
ok  	github.com/carlosframework/platform/internal/geo	0.019s
ok  	github.com/carlosframework/platform/internal/geoip	0.036s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.230s
ok  	github.com/carlosframework/platform/internal/host	135.102s
ok  	github.com/carlosframework/platform/internal/hostactors	6.606s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.007s
ok  	github.com/carlosframework/platform/internal/hostbox	3.273s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.817s
ok  	github.com/carlosframework/platform/internal/hostenv	0.005s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.011s
ok  	github.com/carlosframework/platform/internal/hostexec	5.506s
ok  	github.com/carlosframework/platform/internal/hostfence	0.041s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.257s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.028s
ok  	github.com/carlosframework/platform/internal/hostguard	1.081s
ok  	github.com/carlosframework/platform/internal/hostops	2.199s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.009s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.003s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.678s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.269s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.004s
ok  	github.com/carlosframework/platform/internal/infraview	0.002s
ok  	github.com/carlosframework/platform/internal/instancectl	5.254s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.811s
ok  	github.com/carlosframework/platform/internal/instancefs	0.007s
ok  	github.com/carlosframework/platform/internal/instancekey	0.004s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.018s
ok  	github.com/carlosframework/platform/internal/layout	1.816s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.003s
ok  	github.com/carlosframework/platform/internal/lease	10.149s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.482s
ok  	github.com/carlosframework/platform/internal/ledger	0.012s
ok  	github.com/carlosframework/platform/internal/mail	0.211s
ok  	github.com/carlosframework/platform/internal/maintenance	0.082s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.591s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.127s
ok  	github.com/carlosframework/platform/internal/metrics	0.008s
ok  	github.com/carlosframework/platform/internal/moves	21.286s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.070s
ok  	github.com/carlosframework/platform/internal/objstore	0.267s
ok  	github.com/carlosframework/platform/internal/ops	0.180s
ok  	github.com/carlosframework/platform/internal/park	0.243s
ok  	github.com/carlosframework/platform/internal/plans	0.011s
ok  	github.com/carlosframework/platform/internal/privatefile	0.006s
ok  	github.com/carlosframework/platform/internal/provision	13.295s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.008s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.590s
ok  	github.com/carlosframework/platform/internal/relay	7.194s
ok  	github.com/carlosframework/platform/internal/release	24.315s
ok  	github.com/carlosframework/platform/internal/rollout	3.785s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.013s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.059s
ok  	github.com/carlosframework/platform/internal/rootstate	0.029s
ok  	github.com/carlosframework/platform/internal/runs	0.612s
ok  	github.com/carlosframework/platform/internal/s3compat	0.009s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.808s
ok  	github.com/carlosframework/platform/internal/seeds	0.013s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	10.821s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.057s
ok  	github.com/carlosframework/platform/internal/status	0.097s
ok  	github.com/carlosframework/platform/internal/steering	0.008s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.182s
ok  	github.com/carlosframework/platform/internal/svcrole	0.003s
ok  	github.com/carlosframework/platform/internal/telemetry	0.005s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.006s
ok  	github.com/carlosframework/platform/internal/unixdial	0.004s
ok  	github.com/carlosframework/platform/internal/updatewords	0.006s
ok  	github.com/carlosframework/platform/internal/usage	0.005s
ok  	github.com/carlosframework/platform/internal/version	0.003s
ok  	github.com/carlosframework/platform/internal/vet	1.663s
=== internal_console@0 (77 s)
ok  	github.com/carlosframework/platform/internal/console	74.931s
=== internal_console@1 (37 s)
ok  	github.com/carlosframework/platform/internal/console	35.184s
=== internal_console@2 (42 s)
ok  	github.com/carlosframework/platform/internal/console	39.834s
=== internal_console@3 (37 s)
ok  	github.com/carlosframework/platform/internal/console	34.868s
=== internal_console@4 (37 s)
ok  	github.com/carlosframework/platform/internal/console	35.017s
=== internal_console@5 (35 s)
ok  	github.com/carlosframework/platform/internal/console	32.649s
=== internal_console@6 (37 s)
ok  	github.com/carlosframework/platform/internal/console	34.427s
=== internal_console@7 (43 s)
ok  	github.com/carlosframework/platform/internal/console	41.080s
=== root@0 (32 s)
ok  	github.com/carlosframework/platform	30.280s
=== root@1 (44 s)
ok  	github.com/carlosframework/platform	42.242s
=== root@2 (41 s)
ok  	github.com/carlosframework/platform	38.963s
=== 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 08:08:40 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E0CB9B6EB61C89, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.021s
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 9aa2a6723..d81525ebd 100644
--- a/docs/superpowers/plans/2026-10-22-self-serve-moves-1-lifetimes-and-order.md
+++ b/docs/superpowers/plans/2026-10-22-self-serve-moves-1-lifetimes-and-order.md
@@ -2,7 +2,7 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-Rev 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.
+Rev 7, 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 (1 to 9), rev 5 (1 to 6), rev 6 (`selfserve-plan1-review6.md`, findings 1 to 5). Rev 7 answers rev 6's five: every claims-off record write runs with `WithRecordSequence`, so `record_seq` exists after a record's first write and an ETag never recurs, and recovery writes a **fence** (a sequencing rewrite) before it terminates an operation on evidence of a changed record; one migration dispatcher classifies the observation at every entry, so a resume can never supersede its own marker; the attach door's claim rollback runs inside the hostname exclusion and never releases a claim a pending operation or a valid record needs; the judge reaches every mutation context (the console's top-level handler wraps every request, browser and API alike; the seed callback wraps its own context; the full-record updater's re-read is judged; `RecordIndex.Find` and `FindRecordByHost` take the judge from the context), and the audit is by AST; the remaining contracts (the running N1 table, a slot-retirement helper, one expectation type, listed-key absence, exhaustion at read, the import witness's state, the producer-state publication tests) are answered. Rev 5 removed the slot cache (every judgement reads the slot fresh, after the record, in bounded rounds) and made a successor lifetime's hold one row write. Rev 6 answers rev 5's six: a retirement persists its **kind** (normal, orphan, cleanup) and each kind has its own steps and recovery table; a migration's marker-CAS conflict is classified by a re-read for both the bound and the unbound form, so an own marker that landed is resumed, never superseded; every `retireInstanceRecord` caller states its expectation (the sibling undo names the lifetime it created; detach deletes what is current; the attach door never asks for a record rollback over a lifetime failure); a judge carried on the context makes `updateRecordSeeding`, and so every `SetRecord*` writer, refuse a stale record, and every box reader (Phase 1's included) and the import journal's readers go through the judge; the provenance mark is dropped (the fold compares pairs between unstamped entries, a pair-less entry is `(0, 0)` and equal pairs fall to the existing rule, exactly §6.4); the remaining interface gaps are answered.
**Goal:** Give every claims-off instance record a durable lifetime (the host incarnation slot) and every served row a serving order `(incarnation, serving_seq)` that boxes publish and edges honour, with crash-safe create, retirement and migration, so a later plan's holding pages and moves can never be out-ordered by a stale publication.
@@ -25,7 +25,8 @@ Rev 6, 2026-10-22. Reviews: rev 1 (`after236/selfserve-plan1-review.md`, 16 find
- 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.
+- 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; a stored value above `math.MaxInt64` is `ErrLifetimeExhausted` at read (the doors answer 502, reconcile protects the row).
+- **Record sequencing.** Every claims-off record write runs with `provision.WithRecordSequence(ctx)`: the lifetime create introduces `record_seq` (1) and every later `WriteRecord` advances it, so once a record has been written by this code its ETag never recurs. A pending operation is terminated (`refused`, `void`, `superseded`) on evidence that the record changed **only after a fence**: `fenceRecord` (Task 3) rewrites the current body against its ETag with the sequence enabled, so an outstanding conditional write against any earlier ETag can never land; a fence that cannot be written leaves the operation pending.
- Local tests: only the touched packages with `-run`; CI runs the full suite. Go needs the sandbox off; never set `GOCACHE`.
## Review Focus
@@ -110,6 +111,7 @@ The verdict, exactly, with `owned := slot != nil && slot.Account == rec.Account
- `func (s *Server) admit(host, ownOpID string) error`: `errLifetimeSettling` when `PendingForHost(host, ownOpID) > 0`.
- `func (s *Server) classifyLocked(ctx context.Context, acct, app, host string) (provision.Observation, error)`: `s.judge.Observe(ctx, s.cfg.Releases, acct, app, host)` (Task 6: record first, slot after, bounded rounds); the error is non-nil only with `Verdict == LifetimeUnreadable`.
- `func (s *Server) createRecordWithLifetime(ctx context.Context, rec provision.Record) (*provision.Record, error)`: the one function every claims-off creation path calls; returns the record as written. Errors: `release.ErrCASConflict` (a valid record under this key), `*hostHeldError` (`errors.As`; carries `*apexHold` for `hostHeldSentence`: a live slot owned by another key gives `&apexHold{Holder: slot.App, OtherAccount: slot.Account if ≠ rec.Account}`), `errLifetimeSettling`, `errLifetimePending`, or a read error from step 2 (nothing persisted).
+ - `func (s *Server) fenceRecord(ctx context.Context, acct, app, host string) error`: `GetRecordRaw`; absent → nil (nothing to fence; the slot is the fence for an absent key); present → `WriteRecord(WithRecordSequence(ctx), s, key, raw, raw, etag)` (a rewrite whose only change is the introduced or advanced `record_seq`); `ErrCASConflict` → re-read and retry, at most 3 times; any other failure → the error (the caller leaves its operation pending). Every recovery branch below and in Tasks 4 and 5 that would mark an operation `refused`, `void` or `superseded` because the record is not what the operation captured calls `fenceRecord` first and stays pending if it fails.
- `func (s *Server) recoverLifetimes(ctx context.Context) error`, `func (s *Server) StartLifetimeRecoveryPass(ctx context.Context)` (returns at once unless `lifetimesOn()`; once at start, then every 60 s).
The protocol, inside `withHostname(host)`:
@@ -118,7 +120,7 @@ The protocol, inside `withHostname(host)`:
3. `N = max(LastIncarnation(host), slot.Incarnation) + 1`; `rec.Incarnation = N`; `body = json.Marshal(rec)`; `BeginAllocation({OpID, Account, App, Host, N, pending, body, PredecessorETag: slot's ETag or "", StaleETag})`. A SQLite failure here → the error (nothing durable).
4. `WriteHostIncarnation({N, live, rec.Account, rec.App, "create " + OpID}, PredecessorETag)`. On `ErrCASConflict`: re-read the slot (unreadable → `pending`, `errLifetimePending`); `{N, live, this key, "create " + OpID}` → continue at 5; a slot at an incarnation > N, or at N owned by another key, or at N and this key with another intent → `superseded`, `errLifetimeSettling`; the predecessor again → `pending`, `errLifetimePending`. Any other error → `pending`, `errLifetimePending`.
5. If `StaleETag != ""`: `DeleteIfMatch` the record on it; any error (conflict included) → `pending`, `errLifetimePending`.
-6. `WriteRecord(ctx, s, key, []byte("{}"), body, "")`. Success → 7. `ErrCASConflict` → re-read raw (unreadable → pending): a record at N under this key → 7 (a replay landed); otherwise `pending`, `errLifetimePending`. Any other error → `pending`, `errLifetimePending`.
+6. `WriteRecord(WithRecordSequence(ctx), s, key, []byte("{}"), body, "")` (the record lands with `record_seq: 1`). Success → 7. `ErrCASConflict` → re-read raw (unreadable → pending): a record at N under this key → 7 (a replay landed); otherwise `pending`, `errLifetimePending`. Any other error → `pending`, `errLifetimePending`.
7. `SetAllocationState(committed)`; a SQLite failure → `errLifetimePending` (the record stands; recovery marks it committed). Return the written record.
Recovery (`recoverLifetimes`), per pending allocation, inside `withHostname` with `admit(host, OpID)`, after `classifyLocked`, the first matching branch:
@@ -131,14 +133,17 @@ 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; 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.
+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, created)` with **`dropRecord` always false**: nothing a lifetime failure leaves behind is this call's to retire. The claims-off `rollbackAttach` changes: its claim release runs inside `withHostname(host)` and releases the claim **only** when `admit(host, "") == nil` (no pending allocation, retirement or migration for the hostname) and `classifyLocked` finds no valid record under this key (a record completed since the failing attempt began keeps its claim); otherwise it logs and keeps the claim (a later detach, under the same exclusion, releases it once the record is gone). The claims-off detach (`domainattach.go:349`, `apidomains.go:989`) likewise runs its retirement and its claim release inside `withHostname(host)`. The claims-on `rollbackAttach` and detach are untouched. 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); `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).
+ - `TestAttachKeepsItsClaimOverAPendingAllocation` (failure after the slot write; a SQLite failure at step 7); `TestAttachNeverRetiresOverALifetimeFailure` (every helper failure: a counting retirement sees no call); `TestCompetingAttachCannotReleaseAPendingAllocationsClaim` (A creates the claim and pauses; B reuses it and leaves a pending allocation; A resumes, gets settling, its rollback keeps the claim; recovery completes B's record, still claimed; and the mirror: A's rollback finds B's completed valid record and keeps the claim).
+ - `TestFenceMakesAnOutstandingWriteHarmless` (a record without `record_seq`; an outstanding tombstone PUT and, separately, a marker PUT against its ETag E paused; the record goes A→B→A so its bytes and ETag return to E; recovery fences before terminating; the released PUT fails CAS; with the fence disabled by a test hook the same PUT lands, which is the defect the fence closes).
+ - `TestCreatedRecordsCarryARecordSeq` and `TestEveryClaimsOffWriteAdvancesTheSequence` (`SetRecordRegion` on a legacy record under a judged context introduces `record_seq`; A→B→A yields three distinct ETags).
+ - `TestExhaustionIsRefusedBeforeAnyWrite` (an allocation at `MaxInt64` → `ErrLifetimeExhausted`, no row, no slot write; a slot body above it → unreadable).
- `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.**
@@ -155,7 +160,7 @@ Door mappings: the create door: `*hostHeldError` → 409 `hostHeldSentence(host,
**Interfaces:**
- Consumes: Tasks 1 to 3; Task 5's `migrateHostLifetimeLocked`; Task 6's `Observation`.
-- 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.
+- Produces: `type provision.RetireExpectation struct { Absent bool; ETag string; Incarnation uint64 }` (at most one of the three set; `RetireRef.Expect RetireExpectation`); the console's named values `retireExpectAbsent = provision.RetireExpectation{Absent: true}`, `retireAny = provision.RetireExpectation{}` (delete whatever is current: detach), `retireExpectETag(etag)` (panics on ""), `retireExpectIncarnation(n)`; `func (s *Server) retireRecordWithLifetime(ctx context.Context, acct, app, host string, expect provision.RetireExpectation) error` (two fields set → a programming error, 500); `func (s *Server) retireSlotLocked(ctx context.Context, r Retirement) (slotDisposition, error)` with `slotDisposition` one of `slotRetired` (the write landed or the slot is already `{N, retired, this key}`), `slotSuperseded` (`> N` or another key), `slotUnreadable`; `var errRetireRefused`. The sibling undo and detach reach this through `retireInstanceRecord(ctx, acct, app, host, by, incarnation, expect)` → `retireHostRecord` → the claims-off branch; the claims-on branch ignores `Expect` and is unchanged. 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; `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.
@@ -166,16 +171,16 @@ Inside `withHostname(host)`: `admit(host, "")`; `obs := classifyLocked` (unreada
- (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`.
+- **N1** `WriteRecord(WithRecordSequence(ctx), s, key, raw, TombstoneBody(raw), E)`. `ErrCASConflict` → re-classify (unreadable → pending): a non-tombstone record at N under this key → `fenceRecord`, then `refused` (`errRetireRefused`); a corrupt body → `fenceRecord`, then `refused`; a tombstone at N → N2; absent beneath the live predecessor slot (an older delayed deletion took it) → N2 then `committed` (there is no tombstone to remove); a record of another incarnation → `fenceRecord`, then `void`, nil; a successor slot → `void`, nil.
+- **N2** `retireSlotLocked` (`WriteHostIncarnation({N, retired, Account, App, "retire " + OpID}, S)`; on `ErrCASConflict` the slot is re-read for the disposition): `slotRetired` → the kind's next step (normal: N3 when a tombstone exists, else `committed`; orphan: the body if present, then `committed`; cleanup: C2); `slotSuperseded` → `void`; `slotUnreadable` → `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`.
+- **O1** `retireSlotLocked` with the orphan's S; `slotRetired` → a body present under the key (a late write) is deleted conditionally on its ETag (a conflict is logged; it is stale under the retired slot), `committed`.
+- **C1** `retireSlotLocked` with the cleanup's S; `slotRetired` → **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 **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 → `fenceRecord`, `refused`; a present record whose `Incarnation != N` (not a tombstone) → `fenceRecord`, `void`; a corrupt body → `fenceRecord`, `refused`.
- 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`.
@@ -188,9 +193,9 @@ Inside `withHostname(host)`: `admit(host, "")`; `obs := classifyLocked` (unreada
- `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).
+ - `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 import in state `ready`; the `again` re-decision refuses with the import rule's 409 and nothing is deleted).
- `TestDelayedDeleteCannotEraseASuccessor`.
- - `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).
+ - `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; N1 losing to a deletion then committing without N3); `TestTerminationFencesFirst` (every `refused`/`void` branch: the record's `record_seq` is present and advanced afterwards; a fence that fails leaves the row pending).
- `TestDetachMapsLifetimeErrors`.
- [ ] **Step 2: Run to verify they fail.**
- [ ] **Step 3: Implement.**
@@ -207,21 +212,15 @@ Inside `withHostname(host)`: `admit(host, "")`; `obs := classifyLocked` (unreada
**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`, **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.
+The protocol (inside the exclusion): `admit(host, "")`; `BeginMigration({OpID, Account, App, Host, ExpectETag, "", pending})`; then **the dispatcher**, `migrationDispatch(obs, m) action`, is the only way in, at the first entry, after every CAS conflict and at every recovery: `own marker` (the record carries `IncarnationMigrating == OpID`) → resume at `SetMigrationMarkedETag` then (2); `own slot and done` (record at `Incarnation 1`, slot `{1, live, this key, "migration " + OpID}`) → `committed`; `eligible` (present, fieldless, unmarked, slot absent; and for a bound call at `ExpectETag`) → (1); `bound and moved` (bound, eligible but another ETag) → `fenceRecord`, `superseded`, `ErrCASConflict`; `unreadable` → pending; anything else (gone, another lifetime, another op's marker, a slot not of this migration) → `fenceRecord` when a record is present, `superseded`. "Resume (1)" anywhere below means "dispatch again". (1): `WriteRecord(WithMigration(WithRecordSequence(ctx), OpID), s, key, raw, raw with IncarnationMigrating = OpID, E)` (the marked record carries `record_seq`); on `ErrCASConflict` → dispatch again (an unbound `eligible` result retries (1) from the fresh record, at most 5 times, then leaves 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.
-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`;
-- record marked with OpID, slot bound to the record's current ETag → (3);
-- record marked with OpID and `MarkedETag == ""` → `SetMigrationMarkedETag(OpID, current ETag)` then (2);
-- record marked with OpID, slot absent → (2);
-- record present, fieldless, unmarked, slot absent: **bound** (`ExpectETag != ""`) and the record's ETag equals `ExpectETag` → resume (1) against `ExpectETag` (the outstanding original write either lands first, in which case our CAS fails and the re-read shows our marker, or ours lands and the original fails: both harmless); bound and the ETag differs → `superseded` (the original conditional write can no longer land); unbound → (1);
-- record gone, another lifetime, a marker of another op, or a slot not of this migration → `superseded`.
+Recovery, per pending migration, inside the exclusion with `admit(host, OpID)`: `classifyLocked` then `migrationDispatch`; the `own marker` action continues at `SetMigrationMarkedETag` when `MarkedETag == ""` (then (2)), at (2) when the slot is absent, at (3) when the slot is bound to the record's current ETag. An `eligible` result resumes (1) against the observed ETag (bound: it equals `ExpectETag` by construction); the outstanding original write either lands first, in which case our CAS fails and the next dispatch sees our marker, or ours lands and the original fails: both harmless. Because the dispatcher runs again after every conflict and at every entry, a marker that lands between a classification and the write is always recognised as this operation's.
- [ ] **Step 1: Write the failing tests**:
- `record_sequence_test.go`: `TestWriteRecordRefusesAMarkedRecordWithoutTheMigrationContext`, `TestWriteRecordAllowsTheMigrationsOwnWrite`, `TestCreateOnlyWriteIgnoresTheMarker`, `TestClaimRawUpdateRecordSurfacesTheMarker`.
- `TestMigrationMarksThenSlotsThenSets`; `TestMigrationRestartsFromAnUpdateThatWon` (unbound); `TestBoundMigrationReturnsConflictToItsCaller`.
- `TestAnIssuedWriteLosesToTheMarkerAndRetries`; `TestWriterGivesUpAfterTheBound`.
- - `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.
+ - `TestLateMarkerWriteIsCompletedNotStranded` (for a bound and for an unbound migration, with the paused marker PUT released at each of four points: before recovery's classification, between recovery's classification and (1)'s dispatch, between (1)'s dispatch and its CAS, and after (1)'s CAS: in every case the migration reaches committed and no marker is left without an owner); `TestBoundMigrationFencesBeforeSuperseding` (the ETag-recurrence execution of Task 3's fence test, on the marker).
- `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.**
@@ -233,7 +232,7 @@ 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; `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)
+- 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`; **`ServeHTTP` at line 1683 wraps every request**: `r = r.WithContext(provision.WithJudge(r.Context(), s.judge))`, so browser handlers (`placeAppTarget`'s callers in `fleets.go` and `appsettings.go` included) and the API alike carry it; every pass start derives its context with `WithJudge(ctx, s.judge)`), `internal/console/*.go` (the inventory below; `placeSeedBlock` and `importInFlight` take a `*provision.LifetimeJudge` parameter), `internal/provision/provision.go` (`claimRawUpdateRecord`: its `s.Get` re-read becomes `Observe` under the context's judge; stale → `os.ErrNotExist`, corrupt → the decode error, unreadable → the error, and the caller's ETag condition is kept), `internal/provision/rootrecord.go` (`RecordIndex.Find` and `FindRecordByHost` read through `JudgeFrom(ctx)`: on a claims-on box or in the root process the context carries no judge and they behave as today; on a claims-off agent the callers' contexts (`proofnamed.go:166,176`, `rootinventory_install.go:62`, `rootrecord_claim.go:40`) are the agent's judged contexts, so a restored stale unit record is not proof evidence; "no root-side change" means the root process (`ClaimRootHTTP`), whose readers are claims-on and see a nil judge), `internal/host/seedwiring.go` (`seedPlacedHook(deploy, data, judge)`: the callback does `ctx = provision.WithJudge(ctx, judge)` first, whatever context the activator hands it; `wireSeedPlacement` passes `boxJudge`)
- 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:**
@@ -243,11 +242,11 @@ Recovery, per pending migration, inside the exclusion with `admit(host, OpID)`,
- `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`).
+ - `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)`: a **stale** record is dropped silently; a valid one is replaced by its observation; an **unreadable** observation is that host unreadable (strict: the listing error; complete: `skipped`; tolerant: `complete = false`); a listed key whose body is **absent** by the time it is read keeps today's behaviour exactly (strict: the read error; complete: `skipped`; tolerant: `complete = false`), with or without a judge: absence is not staleness.
- `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.
+ - 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 audit `TestNoUnjudgedRecordReads` parses `internal/console`, `internal/host` and `internal/provision` (non-test) with `go/ast` and `go/types`, resolves every call whose callee is one of `provision.GetRecord`, `GetRecordETag`, `GetRecordRaw`, `ListRecords`, `ListRecordsStrict`, `ListRecordsComplete`, `ListAllRecords`, `ListAllRecordsStrict`, `ListAllRecordsTolerant`, `DecodeRecord`, `FindRecordByHost`, `(*RecordIndex).Find`, or `release.Store.Get` with a `RecordKey(...)` argument, and fails unless the enclosing function is one of the judged readers, the judge itself, `updateRecordSeeding`, `claimRawUpdateRecord`, `WriteRecord`, `TombstoneBody` or `fenceRecord`; **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 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; the browser placement handler and `placeAppTarget` refuse to mutate it; 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, `FindRecordByHost` and `RecordIndex.Find` on the agent all ignore it; the seed callback installed by the real `wireSeedPlacement`, invoked with a plain context, refuses and the seed snapshot is **not** deleted; `claimRawUpdateRecord` under a judged context refuses it); `TestListedKeyAbsenceIsNotStaleness` (list, delete, read: strict errors, complete skips, tolerant reports incomplete, with and without a judge); `TestClaimsOnConsumersReadNoSlot`; `TestBoxJudgeIsSharedAndGated`; `TestNoUnjudgedRecordReads`.
- [ ] **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.
@@ -310,7 +309,7 @@ Each `+1` sets `rec.MoveSeq++` and `rec.Move.Seq = rec.MoveSeq` (nil-safe) in th
**Interfaces:**
- 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 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 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); `TestPublicationUnderEveryClaimState` (`lifetimes` on and off crossed with region HA on and off, and with the claim read absent, failed and undecided: the pair appears only with `lifetimes`, the stamp only with a positive claim, and the fold of each retained publication against each fresh one answers as the mixed-history statement says, in both file orders); `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.
@@ -333,6 +332,6 @@ 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 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`.
+- 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`, `provision.RetireExpectation` (`retireExpectAbsent`, `retireAny`, `retireExpectETag`, `retireExpectIncarnation`), `Retirement.Kind` (`normal`, `orphan`, `cleanup`), `WithJudge`, `JudgeFrom`, `ErrLifetimeExhausted`, `fenceRecord`, `retireSlotLocked`, `slotDisposition`, `migrationDispatch`, `WithRecordSequence` (existing), `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.