carlos / platform Public

plan: self-serve moves 1, rev 9 (Astra plan review 8: findings 1 to 5; enforced cutover, slot-first quarantine, cross-owner row removal, judged index candidates)

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

CI passed — run details

CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (0 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.013s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
=== vet (1 s)
=== gofmt (0 s)
gofmt -l .: clean
=== rest (140 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.778s
ok  	github.com/carlosframework/platform/cmd/moveprobe	11.943s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.715s
ok  	github.com/carlosframework/platform/internal/activator	59.183s
ok  	github.com/carlosframework/platform/internal/agentsock	0.028s
ok  	github.com/carlosframework/platform/internal/alerting	0.005s
ok  	github.com/carlosframework/platform/internal/analytics	0.028s
ok  	github.com/carlosframework/platform/internal/appconfig	3.159s
ok  	github.com/carlosframework/platform/internal/apperr	0.161s
ok  	github.com/carlosframework/platform/internal/applog	0.275s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.005s
ok  	github.com/carlosframework/platform/internal/boxid	1.174s
?   	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.018s
ok  	github.com/carlosframework/platform/internal/certwarm	0.006s
ok  	github.com/carlosframework/platform/internal/cgroup	0.745s
ok  	github.com/carlosframework/platform/internal/changelog	0.596s
ok  	github.com/carlosframework/platform/internal/claimactivation	1.398s
ok  	github.com/carlosframework/platform/internal/clamd	0.058s
?   	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.986s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.014s
ok  	github.com/carlosframework/platform/internal/dbverify	0.011s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.005s
ok  	github.com/carlosframework/platform/internal/docsite	0.008s
ok  	github.com/carlosframework/platform/internal/domains	2.892s
ok  	github.com/carlosframework/platform/internal/doorkey	0.012s
ok  	github.com/carlosframework/platform/internal/economics	0.006s
ok  	github.com/carlosframework/platform/internal/edge	45.233s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.011s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.206s
ok  	github.com/carlosframework/platform/internal/gate	1.918s
ok  	github.com/carlosframework/platform/internal/genselect	0.029s
ok  	github.com/carlosframework/platform/internal/geo	0.017s
ok  	github.com/carlosframework/platform/internal/geoip	0.043s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.223s
ok  	github.com/carlosframework/platform/internal/host	136.791s
ok  	github.com/carlosframework/platform/internal/hostactors	6.556s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.005s
ok  	github.com/carlosframework/platform/internal/hostbox	3.361s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.919s
ok  	github.com/carlosframework/platform/internal/hostenv	0.005s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.010s
ok  	github.com/carlosframework/platform/internal/hostexec	5.605s
ok  	github.com/carlosframework/platform/internal/hostfence	0.071s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.258s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.036s
ok  	github.com/carlosframework/platform/internal/hostguard	1.026s
ok  	github.com/carlosframework/platform/internal/hostops	2.372s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.006s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.006s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.637s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.227s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.004s
ok  	github.com/carlosframework/platform/internal/infraview	0.007s
ok  	github.com/carlosframework/platform/internal/instancectl	5.242s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.807s
ok  	github.com/carlosframework/platform/internal/instancefs	0.007s
ok  	github.com/carlosframework/platform/internal/instancekey	0.009s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.018s
ok  	github.com/carlosframework/platform/internal/layout	1.894s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.006s
ok  	github.com/carlosframework/platform/internal/lease	10.213s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.613s
ok  	github.com/carlosframework/platform/internal/ledger	0.022s
ok  	github.com/carlosframework/platform/internal/mail	0.285s
ok  	github.com/carlosframework/platform/internal/maintenance	0.090s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.617s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.115s
ok  	github.com/carlosframework/platform/internal/metrics	0.009s
ok  	github.com/carlosframework/platform/internal/moves	21.430s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.163s
ok  	github.com/carlosframework/platform/internal/objstore	0.224s
ok  	github.com/carlosframework/platform/internal/ops	0.196s
ok  	github.com/carlosframework/platform/internal/park	0.306s
ok  	github.com/carlosframework/platform/internal/plans	0.006s
ok  	github.com/carlosframework/platform/internal/privatefile	0.004s
ok  	github.com/carlosframework/platform/internal/provision	13.470s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.007s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.580s
ok  	github.com/carlosframework/platform/internal/relay	7.194s
ok  	github.com/carlosframework/platform/internal/release	24.524s
ok  	github.com/carlosframework/platform/internal/rollout	3.833s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.013s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.068s
ok  	github.com/carlosframework/platform/internal/rootstate	0.026s
ok  	github.com/carlosframework/platform/internal/runs	0.567s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.009s
ok  	github.com/carlosframework/platform/internal/schedule	2.758s
ok  	github.com/carlosframework/platform/internal/seeds	0.014s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	10.982s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.633s
ok  	github.com/carlosframework/platform/internal/status	0.079s
ok  	github.com/carlosframework/platform/internal/steering	0.010s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.180s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.009s
ok  	github.com/carlosframework/platform/internal/unixdial	0.004s
ok  	github.com/carlosframework/platform/internal/updatewords	0.005s
ok  	github.com/carlosframework/platform/internal/usage	0.005s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.708s
=== internal_console@0 (82 s)
ok  	github.com/carlosframework/platform/internal/console	79.508s
=== internal_console@1 (37 s)
ok  	github.com/carlosframework/platform/internal/console	34.912s
=== internal_console@2 (42 s)
ok  	github.com/carlosframework/platform/internal/console	39.849s
=== internal_console@3 (38 s)
ok  	github.com/carlosframework/platform/internal/console	35.517s
=== internal_console@4 (37 s)
ok  	github.com/carlosframework/platform/internal/console	35.059s
=== internal_console@5 (35 s)
ok  	github.com/carlosframework/platform/internal/console	32.980s
=== internal_console@6 (37 s)
ok  	github.com/carlosframework/platform/internal/console	34.496s
=== internal_console@7 (43 s)
ok  	github.com/carlosframework/platform/internal/console	40.862s
=== root@0 (32 s)
ok  	github.com/carlosframework/platform	30.511s
=== root@1 (44 s)
ok  	github.com/carlosframework/platform	42.390s
=== root@2 (41 s)
ok  	github.com/carlosframework/platform	39.317s
=== 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.024s
=== RUN   TestT4StorageProbeMinio
2026/10/22 08:45:35 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E0CD9F36628F75, 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 c82718c20..91b8aac04 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 8, 2026-10-22. Rev 7's review (`selfserve-plan1-review7.md`, findings 1 to 6) is answered: a stated rollout guarantee closes the late legacy delete (one console per deployment, a settle window before any termination, old consoles never reintroduced over lifetimes records); the first-route attach, its rollback and detach run **inside** the hostname exclusion from the claim write to the record write, through locked variants of the create and retirement helpers; root's proof and restart record readers take the box's judge (the scope statement is corrected: only `ClaimRootHTTP`'s claims-on readers are slot-free); a corrupt body is quarantined (its bytes kept under `control/quarantine/`, the key removed conditionally) so the retirement or migration proceeds as for absence instead of promising a fence that cannot parse it; `provision.WithLifetimes` is the one context helper that carries the judge and enables sequencing, the runs worker wraps its detached execution context, the audit distinguishes approved judged wrappers from raw readers, the fence witnesses state their timing, exhaustion is witnessed at every transition, and the sibling undo has one dispatch path. Earlier reviews: rev 1 (`after236/selfserve-plan1-review.md`, 16 findings), rev 2 (F1 to F12), rev 3 (R1 to R10), rev 4 (1 to 9), rev 5 (1 to 6), rev 6 (`selfserve-plan1-review6.md`, findings 1 to 5). Rev 7 answers rev 6's five: every claims-off record write runs with `WithRecordSequence`, so `record_seq` exists after a record's first write and an ETag never recurs, and recovery writes a **fence** (a sequencing rewrite) before it terminates an operation on evidence of a changed record; one migration dispatcher classifies the observation at every entry, so a resume can never supersede its own marker; the attach door's claim rollback runs inside the hostname exclusion and never releases a claim a pending operation or a valid record needs; the judge reaches every mutation context (the console's top-level handler wraps every request, browser and API alike; the seed callback wraps its own context; the full-record updater's re-read is judged; `RecordIndex.Find` and `FindRecordByHost` take the judge from the context), and the audit is by AST; the remaining contracts (the running N1 table, a slot-retirement helper, one expectation type, listed-key absence, exhaustion at read, the import witness's state, the producer-state publication tests) are answered. Rev 5 removed the slot cache (every judgement reads the slot fresh, after the record, in bounded rounds) and made a successor lifetime's hold one row write. Rev 6 answers rev 5's six: a retirement persists its **kind** (normal, orphan, cleanup) and each kind has its own steps and recovery table; a migration's marker-CAS conflict is classified by a re-read for both the bound and the unbound form, so an own marker that landed is resumed, never superseded; every `retireInstanceRecord` caller states its expectation (the sibling undo names the lifetime it created; detach deletes what is current; the attach door never asks for a record rollback over a lifetime failure); a judge carried on the context makes `updateRecordSeeding`, and so every `SetRecord*` writer, refuse a stale record, and every box reader (Phase 1's included) and the import journal's readers go through the judge; the provenance mark is dropped (the fold compares pairs between unstamped entries, a pair-less entry is `(0, 0)` and equal pairs fall to the existing rule, exactly §6.4); the remaining interface gaps are answered.
+Rev 9, 2026-10-22. Rev 8's review (`selfserve-plan1-review8.md`, findings 1 to 5) is answered: the rollout guarantee becomes an **enforced cutover** (an operator marker written after the old console has stopped, plus the settle window, with the storage assumption stated) rather than a timer alone; quarantine copies the corrupt bytes and **never deletes under a live slot**: the retirement converts durably to the cleanup kind and retires the slot first, and a conditional-delete conflict re-classifies instead of counting as absence; a provisioned row owned by another record key than the hostname's current record is retired as "record gone" so a cross-owner recreation cannot leave a lagging box serving the predecessor; `RecordIndex.Find` judges its candidate keys before deciding ambiguity; and the remaining contradictions are resolved (the root scope sentence, one detach chain, the locked retirement's callers, reconcile's one sequence, the audit's enumerated list, the fence witness split). Rev 7's review (`selfserve-plan1-review7.md`, findings 1 to 6) is answered: a stated rollout guarantee closes the late legacy delete (one console per deployment, a settle window before any termination, old consoles never reintroduced over lifetimes records); the first-route attach, its rollback and detach run **inside** the hostname exclusion from the claim write to the record write, through locked variants of the create and retirement helpers; root's proof and restart record readers take the box's judge (the scope statement is corrected: only `ClaimRootHTTP`'s claims-on readers are slot-free); a corrupt body is quarantined (its bytes kept under `control/quarantine/`, the key removed conditionally) so the retirement or migration proceeds as for absence instead of promising a fence that cannot parse it; `provision.WithLifetimes` is the one context helper that carries the judge and enables sequencing, the runs worker wraps its detached execution context, the audit distinguishes approved judged wrappers from raw readers, the fence witnesses state their timing, exhaustion is witnessed at every transition, and the sibling undo has one dispatch path. Earlier reviews: rev 1 (`after236/selfserve-plan1-review.md`, 16 findings), rev 2 (F1 to F12), rev 3 (R1 to R10), rev 4 (1 to 9), rev 5 (1 to 6), rev 6 (`selfserve-plan1-review6.md`, findings 1 to 5). Rev 7 answers rev 6's five: every claims-off record write runs with `WithRecordSequence`, so `record_seq` exists after a record's first write and an ETag never recurs, and recovery writes a **fence** (a sequencing rewrite) before it terminates an operation on evidence of a changed record; one migration dispatcher classifies the observation at every entry, so a resume can never supersede its own marker; the attach door's claim rollback runs inside the hostname exclusion and never releases a claim a pending operation or a valid record needs; the judge reaches every mutation context (the console's top-level handler wraps every request, browser and API alike; the seed callback wraps its own context; the full-record updater's re-read is judged; `RecordIndex.Find` and `FindRecordByHost` take the judge from the context), and the audit is by AST; the remaining contracts (the running N1 table, a slot-retirement helper, one expectation type, listed-key absence, exhaustion at read, the import witness's state, the producer-state publication tests) are answered. Rev 5 removed the slot cache (every judgement reads the slot fresh, after the record, in bounded rounds) and made a successor lifetime's hold one row write. Rev 6 answers rev 5's six: a retirement persists its **kind** (normal, orphan, cleanup) and each kind has its own steps and recovery table; a migration's marker-CAS conflict is classified by a re-read for both the bound and the unbound form, so an own marker that landed is resumed, never superseded; every `retireInstanceRecord` caller states its expectation (the sibling undo names the lifetime it created; detach deletes what is current; the attach door never asks for a record rollback over a lifetime failure); a judge carried on the context makes `updateRecordSeeding`, and so every `SetRecord*` writer, refuse a stale record, and every box reader (Phase 1's included) and the import journal's readers go through the judge; the provenance mark is dropped (the fold compares pairs between unstamped entries, a pair-less entry is `(0, 0)` and equal pairs fall to the existing rule, exactly §6.4); the remaining interface gaps are answered.
**Goal:** Give every claims-off instance record a durable lifetime (the host incarnation slot) and every served row a serving order `(incarnation, serving_seq)` that boxes publish and edges honour, with crash-safe create, retirement and migration, so a later plan's holding pages and moves can never be out-ordered by a stale publication.
@@ -20,13 +20,13 @@ Rev 8, 2026-10-22. Rev 7's review (`selfserve-plan1-review7.md`, findings 1 to 6
- 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 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.
+- No console write to a bucket outside the deployment bucket. On the box, `ClaimRootHTTP`'s claims-on readers are untouched; root's proof and restart record readers take the judge (Task 6).
- 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; a stored value above `math.MaxInt64` is `ErrLifetimeExhausted` at read (the doors answer 502, reconcile protects the row).
-- **Rollout guarantee (the late legacy delete).** The claims-off console is one process per deployment; an older console's `RetireRecord` deletes a record unconditionally and must never run beside this code. Stated and enforced: (a) `StartLifetimeRecoveryPass`, the migration pass and every termination branch (`refused`, `void`, `superseded`) wait `lifetimeSettle` (60 s) after the console's start, longer than any in-flight S3 request an older console could have issued before it stopped; (b) the recovery passes do nothing while `admit` for the host would refuse; (c) an older console is not reintroduced once any record carries `incarnation` (the same rule as plan 2's `active.json`); `TestNoTerminationBeforeTheSettleWindow` pins (a). Within that guarantee a key never disappears under a live owned slot except through this code's conditional deletes, so a record's `record_seq` is a non-repeating revision.
+- **Cutover (the late legacy delete).** An older console's `RetireRecord` deletes a record unconditionally and must never run beside this code. The prerequisite is **enforced**: `lifetimesOn()` is true only when `control/lifetimes-cutover.json` exists (`{"at": <time>, "by": <operator>}`, written create-only by `carlos system lifetimes cutover`, which the operator runs **after** the old console process has stopped) and `now > cutover.At + lifetimeSettle` (60 s); until then every claims-off door behaves exactly as main (no slot, no allocation, no judging) and the recovery and migration passes do nothing. The assumption this rests on, stated: S3 applies a mutation before it acknowledges it and never after the client's connection closed before the request was fully received, and a fully received request completes within seconds; so once the old process has exited and the settle window has passed, no legacy mutation is outstanding. (b) the recovery passes do nothing for a host while `admit` would refuse; (c) an older console is never reintroduced once the marker exists (the marker is never deleted; the same rule as plan 2's `active.json`). `TestCutoverGatesLifetimes` pins the enforcement: without the marker, or within the window, the doors are main's and the passes idle; after, the protocol runs. Within that guarantee a key never disappears under a live owned slot except through this code's conditional deletes, so a record's `record_seq` is a non-repeating revision.
- **Record sequencing.** Every claims-off record write runs with `provision.WithRecordSequence(ctx)`, which `provision.WithLifetimes(ctx, judge)` (Task 6) applies together with the judge, so one context helper at each entry point (the console's `ServeHTTP`, every pass start, the agent's passes, the runs worker's execution, the seed callback, root's proof reader) enforces both: 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`.
@@ -142,7 +142,7 @@ Door mappings: the create door: `*hostHeldError` → 409 `hostHeldSentence(host,
- `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); `TestCompetingAttachCannotReleaseAnotherAttachsClaim` (A creates the claim and pauses **before** its lifetime create; B's attach blocks on the exclusion until A's attempt ends; A fails pre-allocation and its rollback releases the claim; B then proceeds and mints its own claim and record: no record is ever left without its claim; the pending-allocation and completed-record variants of rev 7 stay; and detach in place of A's rollback blocks the same way); `TestDetachCompletesThroughTheRealHandlers` (browser and API first-route detach: the record goes before the claim, no deadlock).
- - `TestFenceMakesAnOutstandingWriteHarmless` (a record without `record_seq`; an outstanding tombstone PUT and, separately, a marker PUT against its ETag E paused; recovery's classification observes the record at B (changed); the test then restores A, so the bytes and ETag return to E, **before** recovery fences; recovery fences (the PUT is also released during a fence retry in one variant) and terminates; the released PUT fails CAS; with the fence disabled by a test hook the same PUT lands, which is the defect the fence closes); `TestNoTerminationBeforeTheSettleWindow` (a pending operation whose record changed: within 60 s of the console's start recovery leaves it pending; after, it terminates).
+ - `TestFenceMakesAnOutstandingWriteHarmless` (a record without `record_seq`; an outstanding tombstone PUT and, separately, a marker PUT against its ETag E paused; recovery's classification observes the record at B (changed); the test then restores A, so the bytes and ETag return to E, **before** recovery fences; recovery fences and terminates; the released PUT fails CAS; with the fence disabled by a test hook the same PUT lands, which is the defect the fence closes; a second variant releases the PUT **during** a fence retry and splits on who won: the fence won → the late PUT fails CAS; the operation's write won → the next classification sees the tombstone (or the own marker, `ErrRecordMigrating` to the fence) and the operation is completed or recovered from that observation, never terminated from the earlier changed-record classification); `TestCutoverGatesLifetimes` (no marker → the create door writes a plain record as main and the passes idle; a marker younger than 60 s → the same; older → the lifetime protocol; a pending operation whose record changed is terminated only after the window).
- `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; a record whose stored `incarnation` or `move_seq` is above it → unreadable at the doors and protected by reconcile).
- `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).
@@ -156,34 +156,34 @@ 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, 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`
+- 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, expect provision.RetireExpectation)`; its claims-off callers: `apiinstances.go:1684` (the sibling undo) with `{Incarnation: created.Incarnation}` through the locking `retireRecordWithLifetime`; **detach does not go through it on the claims-off path**: `apidomains.go:989` and `domainattach.go:349` call Task 3's `detachFirstRouteLocked`, which calls `retireRecordWithLifetimeLocked(..., retireAny)` inside the exclusion it already holds; the claims-on detach keeps today's `retireInstanceRecord` chain; `domainattach.go:557` (`rollbackAttach`) never retires on the claims-off path; `apiinstances.go:2245` is not reached on claims-off: `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 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` (takes `withHostname`) and `func (s *Server) retireRecordWithLifetimeLocked(...)` (assumes it; detach's and the attach rollback's form) (two expectation fields set → a programming error, 500); `func (s *Server) quarantineCorrupt(ctx context.Context, acct, app, host string, raw []byte, etag string) error`: copies the bytes to `control/quarantine/<account>/<app>/<host>.<unix nanos>.json` (create-only) then `DeleteIfMatch` the key on `etag` (a conflict or absence is success: the body changed or went; the quarantine copy stays); `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.
+- 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` (takes `withHostname`) and `func (s *Server) retireRecordWithLifetimeLocked(...)` (assumes it; detach's form) (two expectation fields set → a programming error, 500); `func (s *Server) quarantineCopy(ctx context.Context, acct, app, host string, raw []byte, etag string) error`: copies the bytes to `control/quarantine/<account>/<app>/<host>.<unix nanos>.json` (create-only) and **does not touch the key**; `func (s *Store) ConvertRetirementKind(opID, kind, recordETag string) error` (the durable change of a normal retirement into a cleanup one, with the body's ETag); `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.
- (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: 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.
+- (d)/(g) present and stale or corrupt: slot `{N, live}` owned by this key → kind **cleanup**: a corrupt body is first `quarantineCopy`-ed; `BeginRetirement({Kind: cleanup, N, RecordETag: obs.ETag, SlotETag: S, SlotIntent})`; steps C1 (retire the slot against S), C2 (`DeleteIfMatch` the body on `RecordETag`; absence is fine; a **conflict re-classifies**: a body now present under the retired slot is stale whatever it is → `DeleteIfMatch` on its current ETag, retried through recovery; never "success"), done. Otherwise (no slot, a retired slot, or another key's) → a corrupt body is `quarantineCopy`-ed, then `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(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 → `quarantineCorrupt` then N2 (the key is absent beneath the live predecessor; an outstanding write against the corrupt body's ETag can no longer land because the key is gone, and a create-only write lands stale under the retired slot); 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.
+- **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 → `quarantineCopy`, then `ConvertRetirementKind(OpID, cleanup, its ETag)` and continue as kind cleanup at C1 (the key is **not** deleted while the slot is live; the slot retires first, then C2 removes the body on its ETag); 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** `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`.
+- **C1** `retireSlotLocked` with the cleanup's S; `slotRetired` → **C2** `DeleteIfMatch` the body on `RecordETag`; absence → `committed`; a conflict → re-classify: any body now present (repaired, replaced, still corrupt) is stale under the retired slot → `DeleteIfMatch` on its current ETag (a further conflict → `pending`, recovery retries); then `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 → `fenceRecord`, `refused`; a present record whose `Incarnation != N` (not a tombstone) → `fenceRecord`, `void`; a corrupt body → `quarantineCorrupt`, N2, `committed`.
+- 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 → `quarantineCopy`, `ConvertRetirementKind(cleanup)`, C1, C2.
- 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`.
+- kind **cleanup**, `pred`: any observation (absent, corrupt, stale, or a valid body at N: the DELETE this operation answers was for this lifetime, and under the retired slot every body is stale) → C1 then C2 on the current body's ETag when present, `committed`.
- [ ] **Step 1: Write the failing tests** through the real doors and the function:
- `TestDeleteDoorRetiresThroughTheTombstone`; `TestSlotRetiresBeforeTheKeyIsAbsent`.
@@ -196,7 +196,7 @@ Inside `withHostname(host)`: `admit(host, "")`; `obs := classifyLocked` (unreada
- `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 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; N1 losing to a deletion then committing without N3); `TestCorruptBodyIsQuarantinedNotFenced` (malformed JSON, not a path mismatch, injected between the observation and the tombstone CAS: the bytes land under `control/quarantine/`, the key is removed, the slot retires, the retirement commits; a later create proceeds); `TestTerminationFencesFirst` (every `refused`/`void` branch that terminates on a **changed record**: `record_seq` present and advanced afterwards; a fence that fails leaves the row pending; the successor-slot branches terminate on slot evidence without a record fence, asserted separately).
+ - `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); `TestCorruptBodyIsQuarantinedSlotFirst` (malformed JSON, not a path mismatch, injected between the observation and the tombstone CAS: the bytes land under `control/quarantine/`, the retirement converts to kind cleanup, the slot retires **before** the key is removed, the retirement commits; crashes after the copy, after the conversion, after C1 and during C2, each with a delayed original create released in the interval: the key is never absent under a live slot, the restored record is stale, and the retirement always ends committed with the slot retired; the sequence-2 ETag-recurrence execution of the review cannot occur because the key never disappears while the slot is live; a quarantine delete losing to a repaired body re-classifies and removes the repaired body under the retired slot); `TestTerminationFencesFirst` (every `refused`/`void` branch that terminates on a **changed record**: `record_seq` present and advanced afterwards; a fence that fails leaves the row pending; the successor-slot branches terminate on slot evidence without a record fence, asserted separately).
- `TestDetachMapsLifetimeErrors`.
- [ ] **Step 2: Run to verify they fail.**
- [ ] **Step 3: Implement.**
@@ -213,7 +213,7 @@ 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})`; 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; a corrupt body → `quarantineCorrupt`, `superseded`; 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.
+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; a corrupt body (slot absent, so no live slot is exposed) → `quarantineCopy`, then `DeleteIfMatch` on its ETag (a conflict → dispatch again), `superseded`; 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)`: `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.
@@ -233,7 +233,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, the seed hook and the proof binding source; every pass context on the box is `provision.WithLifetimes(ctx, boxJudge)`: the reconcile loop, the moves pass, the runs worker and its execution, 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.WithLifetimes(r.Context(), s.judge))`, so browser handlers (`placeAppTarget`'s callers in `fleets.go` and `appsettings.go` included) and the API alike carry the judge and sequencing; every pass start derives its context with `WithLifetimes(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)`), `internal/host/proofbinding.go` (`productionProofBindingSource` gains `judge *provision.LifetimeJudge`, set by its constructor from `!hostwrite.Enabled(getenv)` in **both** the agent and the root process (`runAdoptUnderRootRelease` and the restart paths, the retained-census configuration included); `findRecord` wraps whatever context it is given with `WithLifetimes(ctx, s.judge)` before `FindRecordByHost`/`index.Find`, so `proofObserver.observe`'s `context.Background()` is covered; the scope statement corrected: root's proof and restart record readers are judged like the agent's; only `ClaimRootHTTP`'s claims-on readers stay slot-free), `internal/host/rootinventory_install.go:62` and `rootrecord_claim.go:40` (claims-on callers: their contexts carry no judge), `internal/host/runs.go` (`pickup` line 239: the detached execution context is `WithLifetimes(context.Background(), w.judge)`, so `execute`'s `SetRecordRun` calls are judged and sequenced), `internal/host/seedwiring.go` (`seedPlacedHook(deploy, data, judge)`: the callback does `ctx = provision.WithLifetimes(ctx, judge)` first; `wireSeedPlacement` passes `boxJudge`)
+- Modify: `internal/provision/reconcile.go` (the decode loop at lines 855–941 becomes **one sequence**: parse the key → `cfg.Judge.ObserveWith(ctx, s, acct, app, host, raw, etag, err)` (which decodes, path-validates and judges; a nil judge decodes and path-validates only) → the shape, host-shape, channel and scope checks on the returned observation's record; stale hosts are recorded in `stale map[string]bool`; **the removal loop**, claims-off (`cfg.Judge != nil`): a provisioned row is authorised only by a record **of its own key**: `recs[rt.Host]` present but `RecordKey(rec.Account, rec.App, rec.Host) != RecordKey(rt.Account, rt.App, rt.Host)` → the row is retired as "record gone" (a held row by an op the current record does not carry → `cfg.DeleteIfHold` first), the journal exemption and hand-managed rows untouched, so a lagging box withdraws a predecessor row owned by another app before the successor is minted; 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, the seed hook and the proof binding source; every pass context on the box is `provision.WithLifetimes(ctx, boxJudge)`: the reconcile loop, the moves pass, the runs worker and its execution, 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.WithLifetimes(r.Context(), s.judge))`, so browser handlers (`placeAppTarget`'s callers in `fleets.go` and `appsettings.go` included) and the API alike carry the judge and sequencing; every pass start derives its context with `WithLifetimes(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` (`IndexRecords` keeps **every** candidate key per hostname instead of marking two keys ambiguous; `RecordIndex.Find` and `FindRecordByHost` observe each candidate through `JudgeFrom(ctx)`: exactly one valid → it; none → `os.ErrNotExist`; two or more valid → `os.ErrPermission` (genuine ambiguity); any unreadable candidate → its error; with a nil judge, two candidates → `os.ErrPermission` as today; the production proof source's cached index caches keys, not verdicts), `internal/host/proofbinding.go` (`productionProofBindingSource` gains `judge *provision.LifetimeJudge`, set by its constructor from `!hostwrite.Enabled(getenv)` in **both** the agent and the root process (`runAdoptUnderRootRelease` and the restart paths, the retained-census configuration included); `findRecord` wraps whatever context it is given with `WithLifetimes(ctx, s.judge)` before `FindRecordByHost`/`index.Find`, so `proofObserver.observe`'s `context.Background()` is covered; the scope statement corrected: root's proof and restart record readers are judged like the agent's; only `ClaimRootHTTP`'s claims-on readers stay slot-free), `internal/host/rootinventory_install.go:62` and `rootrecord_claim.go:40` (claims-on callers: their contexts carry no judge), `internal/host/runs.go` (`pickup` line 239: the detached execution context is `WithLifetimes(context.Background(), w.judge)`, so `execute`'s `SetRecordRun` calls are judged and sequenced), `internal/host/seedwiring.go` (`seedPlacedHook(deploy, data, judge)`: the callback does `ctx = provision.WithLifetimes(ctx, judge)` first; `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:**
@@ -245,9 +245,9 @@ Recovery, per pending migration, inside the exclusion with `admit(host, OpID)`:
- `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)`: 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 audit `TestNoUnjudgedRecordReads` parses `internal/console`, `internal/host` and `internal/provision` (non-test) with `go/ast` and `go/types`. **Forbidden callees** (raw readers): `provision.GetRecord`, `GetRecordETag`, `GetRecordRaw`, `ListRecords`, `ListRecordsStrict`, `ListRecordsComplete`, `ListAllRecords`, `ListAllRecordsStrict`, `ListAllRecordsTolerant`, `release.Store.Get` with a `RecordKey(...)` argument, and `DecodeRecord` on bytes read in the same function from the store. **Approved callees** (judged wrappers, allowed anywhere): the `*Judged` listings, `ReadRecordJudged`, `Observe`, `ObserveWith`, `FindRecordByHost`, `(*RecordIndex).Find`. **Permitted enclosing functions** for the forbidden set: the judged readers, the judge itself, `updateRecordSeeding`, `claimRawUpdateRecord`, `WriteRecord`, `TombstoneBody`, `fenceRecord`, `quarantineCorrupt`, and a reviewed, named list of claims-on functions that decode a body **captured in an ETag-bound claim observation** without reading the store again (`claim_*.go` in the console and the `hostops` adapters), each listed by resolved symbol. Reconcile's decode moves into `ObserveWith`. No other exemptions.
+ - 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`. **Forbidden callees** (raw readers): `provision.GetRecord`, `GetRecordETag`, `GetRecordRaw`, `ListRecords`, `ListRecordsStrict`, `ListRecordsComplete`, `ListAllRecords`, `ListAllRecordsStrict`, `ListAllRecordsTolerant`, `release.Store.Get` with a `RecordKey(...)` argument, and `DecodeRecord` on bytes read in the same function from the store. **Approved callees** (judged wrappers, allowed anywhere): the `*Judged` listings, `ReadRecordJudged`, `Observe`, `ObserveWith`, `FindRecordByHost`, `(*RecordIndex).Find`. The forbidden set also includes `hostops.ReadRecord`. **Permitted enclosing functions** for the forbidden set, by resolved symbol: `provision.ReadRecordJudged`, `(*LifetimeJudge).Observe`, `(*LifetimeJudge).ObserveWith`, the five `List*Judged`, `provision.FindRecordByHost`, `(*RecordIndex).Find`, `provision.updateRecordSeeding`, `provision.claimRawUpdateRecord`, `provision.WriteRecord`, `provision.TombstoneBody`, `provision.GetRecordRaw` (the raw primitive the judge uses), `(*console.Server).fenceRecord`, `(*console.Server).quarantineCopy`, and the claims-on functions that decode a body captured in an ETag-bound claim observation or run only under `cfg.HostClaim`: `(*console.ClaimPlacementCoordinator).Resume`, `(*console.Server).movesRecovery`, `(*console.Server).adoptHostRecord`, and the legacy unjudged getters themselves (`GetRecord`, `GetRecordETag`, the unjudged listings) which delegate to `GetRecordRaw`/`getEach`. `hostops` is outside the three audited packages and is claims-on. Reconcile's decode moves into `ObserveWith`. No other exemptions.
-- [ ] **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, pickup **and the real pickup-to-execution path's `SetRecordRun`**, `launchable`, the steering loop's desired state, retention's app records all ignore it; **the production proof binding source, constructed as the agent and as the root process construct it (host claims off, with and without a retained census), invoked with `context.Background()`, yields no proof evidence from 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 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, pickup **and the real pickup-to-execution path's `SetRecordRun`**, `launchable`, the steering loop's desired state, retention's app records all ignore it; **the production proof binding source, constructed as the agent and as the root process construct it (host claims off, with and without a retained census), invoked with `context.Background()`, yields no proof evidence from 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); `TestCrossOwnerRecreationWithdrawsThePredecessorRow` (two apps, two boxes; box B misses A's retirement; C recreates the hostname at incarnation 2; B's next reconcile retires A's row (held and unheld variants) and mints C's; the journal exemption and a hand-managed row are untouched); `TestIndexFindJudgesCandidates` (a stale key beside a valid successor key → the successor; two valid legacy candidates → `ErrPermission`; through the production proof source's cached index); `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.
@@ -290,6 +290,7 @@ Each `+1` sets `rec.MoveSeq++` and `rec.Move.Seq = rec.MoveSeq` (nil-safe) in th
- (iii) **recreation, unheld, no hold asked**: `0 < existing.Incarnation < rec.Incarnation && existing.HoldOp == ""` → `InstallLifetimeIf(host, {existing.Incarnation, existing.ServingSeq}, {rec.Incarnation, rec.MoveSeq})`.
- (iv) **recreation, held by a vanished op**: `existing.Incarnation < rec.Incarnation && existing.HoldOp != "" && (rec.Move == nil || rec.Move.ID != existing.HoldOp)` → `cfg.DeleteIfHold(host, existing.HoldOp)`: `true` → mint the record on this pass (held, if the record asks for a hold; ready otherwise); `false` or an error → log, skip this pass; nil callback → log, skip; dry-run → log only.
- (v) **same incarnation, unheld**: nothing; a held row whose op is the record's own move is left to the move.
+- A row owned by **another record key** than the hostname's current record is not this pass's to install anything on: Task 6's removal retires it before the successor is minted.
- [ ] **Step 1: Write the failing tests**:
- `TestOfflineTransitionsAdvanceMoveSeq` (one subtest per table row); `TestMoveSeqExhaustionAtEveryTransition` (`MoveSeq` at `MaxInt64`: start, start-staged, open, abort and finish each return `ErrLifetimeExhausted` with no write); `TestOfflineRecordDiffersOnlyWhereTheMoveSaysSo` with `move_seq`; `TestOfflineAbort` unchanged.
@@ -334,6 +335,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`, `provision.RetireExpectation` (`retireExpectAbsent`, `retireAny`, `retireExpectETag`, `retireExpectIncarnation`), `Retirement.Kind` (`normal`, `orphan`, `cleanup`), `WithJudge`, `JudgeFrom`, `ErrLifetimeExhausted`, `fenceRecord`, `quarantineCorrupt`, `retireSlotLocked`, `slotDisposition`, `migrationDispatch`, `WithLifetimes`, `WithRecordSequence` (existing), `createRecordWithLifetimeLocked`, `retireRecordWithLifetimeLocked`, `attachFirstRouteLocked`, `rollbackAttachLocked`, `detachFirstRouteLocked`, `lifetimeSettle`, `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`, `quarantineCopy`, `ConvertRetirementKind`, `lifetimes-cutover.json`, `TestCutoverGatesLifetimes`, `retireSlotLocked`, `slotDisposition`, `migrationDispatch`, `WithLifetimes`, `WithRecordSequence` (existing), `createRecordWithLifetimeLocked`, `retireRecordWithLifetimeLocked`, `attachFirstRouteLocked`, `rollbackAttachLocked`, `detachFirstRouteLocked`, `lifetimeSettle`, `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.