plan: self-serve moves 1, rev 11 (Astra plan review 10: findings 1 to 4; an admission gate activation waits on, a latched on state, scope-aware removal, one worker lifecycle, the CLI)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 file changed,
+22
−20
docs/superpowers/plans/2026-10-22-self-serve-moves-1-lifetimes-and-order.md+22 −20
CI cancelled — superseded by a newer push — run details
CI log
=== gate === .amadan/test-gate.sh === launchctx (2 s) ok amadan.net/carlos/platform.git/launchctx 0.011s ? amadan.net/carlos/platform.git/launchctx/controlname [no test files] ? amadan.net/carlos/platform.git/launchctx/wireid [no test files] === vet (6 s) === gofmt (1 s) gofmt -l .: clean === rest (147 s) ? github.com/carlosframework/platform/cmd/changelog [no test files] ok github.com/carlosframework/platform/cmd/console 3.804s ok github.com/carlosframework/platform/cmd/moveprobe 11.598s ok github.com/carlosframework/platform/cmd/zddprobe 0.704s ok github.com/carlosframework/platform/internal/activator 59.003s ok github.com/carlosframework/platform/internal/agentsock 0.018s ok github.com/carlosframework/platform/internal/alerting 0.006s ok github.com/carlosframework/platform/internal/analytics 0.019s ok github.com/carlosframework/platform/internal/appconfig 2.966s ok github.com/carlosframework/platform/internal/apperr 0.073s ok github.com/carlosframework/platform/internal/applog 0.134s ok github.com/carlosframework/platform/internal/awsinv 0.006s ok github.com/carlosframework/platform/internal/boxchoice 0.006s ok github.com/carlosframework/platform/internal/boxid 0.721s ? 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.012s ok github.com/carlosframework/platform/internal/certwarm 0.002s ok github.com/carlosframework/platform/internal/cgroup 0.704s ok github.com/carlosframework/platform/internal/changelog 0.360s ok github.com/carlosframework/platform/internal/claimactivation 0.906s ok github.com/carlosframework/platform/internal/clamd 0.054s ? github.com/carlosframework/platform/internal/cliflag [no test files] ? github.com/carlosframework/platform/internal/controlname [no test files] ? github.com/carlosframework/platform/internal/controlsock [no test files] ok github.com/carlosframework/platform/internal/dbdoor 6.203s ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.009s ok github.com/carlosframework/platform/internal/dbverify 0.005s ok github.com/carlosframework/platform/internal/dnsrec 0.002s ok github.com/carlosframework/platform/internal/docsite 0.006s ok github.com/carlosframework/platform/internal/domains 2.748s ok github.com/carlosframework/platform/internal/doorkey 0.007s ok github.com/carlosframework/platform/internal/economics 0.004s ok github.com/carlosframework/platform/internal/edge 45.338s ? 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.134s ok github.com/carlosframework/platform/internal/gate 1.821s ok github.com/carlosframework/platform/internal/genselect 0.016s ok github.com/carlosframework/platform/internal/geo 0.021s ok github.com/carlosframework/platform/internal/geoip 0.030s ok github.com/carlosframework/platform/internal/historyreadback 0.156s ok github.com/carlosframework/platform/internal/host 136.376s ok github.com/carlosframework/platform/internal/hostactors 6.663s ok github.com/carlosframework/platform/internal/hostactors/awsmembership 0.008s ok github.com/carlosframework/platform/internal/hostbox 3.285s ok github.com/carlosframework/platform/internal/hostclaim 0.939s ok github.com/carlosframework/platform/internal/hostenv 0.019s ok github.com/carlosframework/platform/internal/hostevidence 0.007s ok github.com/carlosframework/platform/internal/hostexec 5.553s ok github.com/carlosframework/platform/internal/hostfence 0.048s ok github.com/carlosframework/platform/internal/hostfiles 10.288s ok github.com/carlosframework/platform/internal/hostfloor 0.030s ok github.com/carlosframework/platform/internal/hostguard 1.039s ok github.com/carlosframework/platform/internal/hostops 2.175s ? 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.005s ok github.com/carlosframework/platform/internal/hostrelocate 0.715s ok github.com/carlosframework/platform/internal/hostresolve 0.198s ok github.com/carlosframework/platform/internal/hostwrite 0.003s ok github.com/carlosframework/platform/internal/infraview 0.004s ok github.com/carlosframework/platform/internal/instancectl 5.241s ok github.com/carlosframework/platform/internal/instanceexec 2.786s ok github.com/carlosframework/platform/internal/instancefs 0.021s ok github.com/carlosframework/platform/internal/instancekey 0.003s ok github.com/carlosframework/platform/internal/instanceupdates 0.018s ok github.com/carlosframework/platform/internal/layout 1.910s ok github.com/carlosframework/platform/internal/lbhealth 0.003s ok github.com/carlosframework/platform/internal/lease 10.114s ok github.com/carlosframework/platform/internal/lease/leasetest 0.678s ok github.com/carlosframework/platform/internal/ledger 0.024s ok github.com/carlosframework/platform/internal/mail 0.288s ok github.com/carlosframework/platform/internal/maintenance 0.101s ok github.com/carlosframework/platform/internal/maintenanceexec 5.571s ok github.com/carlosframework/platform/internal/maintenanceguard 0.104s ok github.com/carlosframework/platform/internal/metrics 0.008s ok github.com/carlosframework/platform/internal/moves 21.441s ok github.com/carlosframework/platform/internal/moves/archive 1.033s ok github.com/carlosframework/platform/internal/objstore 0.306s ok github.com/carlosframework/platform/internal/ops 0.221s ok github.com/carlosframework/platform/internal/park 0.359s ok github.com/carlosframework/platform/internal/plans 0.006s ok github.com/carlosframework/platform/internal/privatefile 0.005s ok github.com/carlosframework/platform/internal/provision 14.058s 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.673s ok github.com/carlosframework/platform/internal/relay 7.245s ok github.com/carlosframework/platform/internal/release 25.439s ok github.com/carlosframework/platform/internal/rollout 3.837s ok github.com/carlosframework/platform/internal/rolloutwords 0.010s ok github.com/carlosframework/platform/internal/rootaws 0.009s ok github.com/carlosframework/platform/internal/rootparticipation 0.062s ok github.com/carlosframework/platform/internal/rootstate 0.029s ok github.com/carlosframework/platform/internal/runs 0.620s ok github.com/carlosframework/platform/internal/s3compat 0.007s ok github.com/carlosframework/platform/internal/safefile 0.005s ok github.com/carlosframework/platform/internal/schedule 2.707s ok github.com/carlosframework/platform/internal/seeds 0.019s ok github.com/carlosframework/platform/internal/sim/hostlife 10.619s ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.593s ok github.com/carlosframework/platform/internal/status 0.067s ok github.com/carlosframework/platform/internal/steering 0.006s ok github.com/carlosframework/platform/internal/stopevidence 0.227s ok github.com/carlosframework/platform/internal/svcrole 0.002s ok github.com/carlosframework/platform/internal/telemetry 0.005s ok github.com/carlosframework/platform/internal/termfence 0.008s ok github.com/carlosframework/platform/internal/tunnelclient 0.004s ok github.com/carlosframework/platform/internal/unixdial 0.003s ok github.com/carlosframework/platform/internal/updatewords 0.005s ok github.com/carlosframework/platform/internal/usage 0.006s ok github.com/carlosframework/platform/internal/version 0.002s ok github.com/carlosframework/platform/internal/vet 1.646s === internal_console@0 (76 s) ok github.com/carlosframework/platform/internal/console 73.948s === internal_console@1 (36 s) ok github.com/carlosframework/platform/internal/console 34.763s === internal_console@2 (41 s) ok github.com/carlosframework/platform/internal/console 39.244s === internal_console@3 (36 s) ok github.com/carlosframework/platform/internal/console 34.691s === internal_console@4 (37 s) ok github.com/carlosframework/platform/internal/console 35.000s === internal_console@5 (35 s) ok github.com/carlosframework/platform/internal/console 32.935s === internal_console@6 (36 s) ok github.com/carlosframework/platform/internal/console 34.118s === internal_console@7 (43 s) ok github.com/carlosframework/platform/internal/console 41.055s === root@0 (32 s) ok github.com/carlosframework/platform 30.264s === root@1 (44 s) ok github.com/carlosframework/platform 42.230s === root@2 (41 s) ok github.com/carlosframework/platform 39.584s === cross === --- shard 0 of 5 --- cross-compiling darwin/arm64 amadan: job cancelled — a newer push superseded it --- 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
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 19508067c..826a65d5c 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 10, 2026-10-22. Rev 9's review (`selfserve-plan1-review9.md`, findings 1 to 5) is answered: the cutover is a **mode** with a drain: from the marker's time until the window ends the console refuses every claims-off record mutation (503), so no legacy write can be admitted into the window, and the gate, the judge's installation, the recovery pass and the marker's failure modes are one contract with an implementation task for the operator door; the removal loop decides from the row's **own** record (observed by its own key), so a valid legacy owner in a hostname collision is kept as today and only an established absent or stale own record retires the row, with the keep view bypassed on that evidence and replacement stated as two passes; `fenceRecord` returns a classification so an own tombstone or marker that won resumes the operation instead of terminating it; the audit's allowlist names the two remaining claims-on callers of `hostops.ReadRecord` and is pinned both ways; and the remaining contracts are settled (the detach sentence, cleanup recovery through C2, corrupt index candidates, the root restart readers' judge). Rev 8's review (`selfserve-plan1-review8.md`, findings 1 to 5) is answered: the rollout guarantee becomes an **enforced cutover** (an operator marker written after the old console has stopped, plus the settle window, with the storage assumption stated) rather than a timer alone; quarantine copies the corrupt bytes and **never deletes under a live slot**: the retirement converts durably to the cleanup kind and retires the slot first, and a conditional-delete conflict re-classifies instead of counting as absence; a provisioned row owned by another record key than the hostname's current record is retired as "record gone" so a cross-owner recreation cannot leave a lagging box serving the predecessor; `RecordIndex.Find` judges its candidate keys before deciding ambiguity; and the remaining contradictions are resolved (the root scope sentence, one detach chain, the locked retirement's callers, reconcile's one sequence, the audit's enumerated list, the fence witness split). Rev 7's review (`selfserve-plan1-review7.md`, findings 1 to 6) is answered: a stated rollout guarantee closes the late legacy delete (one console per deployment, a settle window before any termination, old consoles never reintroduced over lifetimes records); the first-route attach, its rollback and detach run **inside** the hostname exclusion from the claim write to the record write, through locked variants of the create and retirement helpers; root's proof and restart record readers take the box's judge (the scope statement is corrected: only `ClaimRootHTTP`'s claims-on readers are slot-free); a corrupt body is quarantined (its bytes kept under `control/quarantine/`, the key removed conditionally) so the retirement or migration proceeds as for absence instead of promising a fence that cannot parse it; `provision.WithLifetimes` is the one context helper that carries the judge and enables sequencing, the runs worker wraps its detached execution context, the audit distinguishes approved judged wrappers from raw readers, the fence witnesses state their timing, exhaustion is witnessed at every transition, and the sibling undo has one dispatch path. Earlier reviews: rev 1 (`after236/selfserve-plan1-review.md`, 16 findings), rev 2 (F1 to F12), rev 3 (R1 to R10), rev 4 (1 to 9), rev 5 (1 to 6), rev 6 (`selfserve-plan1-review6.md`, findings 1 to 5). Rev 7 answers rev 6's five: every claims-off record write runs with `WithRecordSequence`, so `record_seq` exists after a record's first write and an ETag never recurs, and recovery writes a **fence** (a sequencing rewrite) before it terminates an operation on evidence of a changed record; one migration dispatcher classifies the observation at every entry, so a resume can never supersede its own marker; the attach door's claim rollback runs inside the hostname exclusion and never releases a claim a pending operation or a valid record needs; the judge reaches every mutation context (the console's top-level handler wraps every request, browser and API alike; the seed callback wraps its own context; the full-record updater's re-read is judged; `RecordIndex.Find` and `FindRecordByHost` take the judge from the context), and the audit is by AST; the remaining contracts (the running N1 table, a slot-retirement helper, one expectation type, listed-key absence, exhaustion at read, the import witness's state, the producer-state publication tests) are answered. Rev 5 removed the slot cache (every judgement reads the slot fresh, after the record, in bounded rounds) and made a successor lifetime's hold one row write. Rev 6 answers rev 5's six: a retirement persists its **kind** (normal, orphan, cleanup) and each kind has its own steps and recovery table; a migration's marker-CAS conflict is classified by a re-read for both the bound and the unbound form, so an own marker that landed is resumed, never superseded; every `retireInstanceRecord` caller states its expectation (the sibling undo names the lifetime it created; detach deletes what is current; the attach door never asks for a record rollback over a lifetime failure); a judge carried on the context makes `updateRecordSeeding`, and so every `SetRecord*` writer, refuse a stale record, and every box reader (Phase 1's included) and the import journal's readers go through the judge; the provenance mark is dropped (the fold compares pairs between unstamped entries, a pair-less entry is `(0, 0)` and equal pairs fall to the existing rule, exactly §6.4); the remaining interface gaps are answered. |
| +Rev 11, 2026-10-22. Rev 10's review (`selfserve-plan1-review10.md`, findings 1 to 4) is answered: the cutover gains an **admission gate**: every claims-off unit of record work (a request's handler, a detached continuation, a worker iteration) is admitted under a counted gate while the state is `before`; the cutover door closes admission synchronously, refuses new work with 503, **waits for the admitted count to reach zero**, then starts the storage settle, then activates; so no unit admitted before the marker ever runs beside the lifetime protocol, and a unit never changes mode mid-flight. `on` is **latched**: once observed it is never left; at startup the state is `unknown` until the first successful marker read and every claims-off record door (reads and mutations) answers 503 until then, so a marker-read failure never restores legacy interpretation. The removal rule separates lifetime validity from placement eligibility: a valid own record that is **in this pass's scope** protects the legacy collision; a valid own record **outside** the scope follows today's removal rules (`KeepParticipant`, the app-trash rule), so offline finishing removes the source row as today; absence or staleness authorises the new path. One worker lifecycle (start once, loop, check the state each iteration), one migration interval, one context contract (`recordCtx`), the CLI command, the retry error, the three root constructions and the copy inventory are stated. Rev 10, 2026-10-22. Rev 9's review (`selfserve-plan1-review9.md`, findings 1 to 5) is answered: the cutover is a **mode** with a drain: from the marker's time until the window ends the console refuses every claims-off record mutation (503), so no legacy write can be admitted into the window, and the gate, the judge's installation, the recovery pass and the marker's failure modes are one contract with an implementation task for the operator door; the removal loop decides from the row's **own** record (observed by its own key), so a valid legacy owner in a hostname collision is kept as today and only an established absent or stale own record retires the row, with the keep view bypassed on that evidence and replacement stated as two passes; `fenceRecord` returns a classification so an own tombstone or marker that won resumes the operation instead of terminating it; the audit's allowlist names the two remaining claims-on callers of `hostops.ReadRecord` and is pinned both ways; and the remaining contracts are settled (the detach sentence, cleanup recovery through C2, corrupt index candidates, the root restart readers' judge). Rev 8's review (`selfserve-plan1-review8.md`, findings 1 to 5) is answered: the rollout guarantee becomes an **enforced cutover** (an operator marker written after the old console has stopped, plus the settle window, with the storage assumption stated) rather than a timer alone; quarantine copies the corrupt bytes and **never deletes under a live slot**: the retirement converts durably to the cleanup kind and retires the slot first, and a conditional-delete conflict re-classifies instead of counting as absence; a provisioned row owned by another record key than the hostname's current record is retired as "record gone" so a cross-owner recreation cannot leave a lagging box serving the predecessor; `RecordIndex.Find` judges its candidate keys before deciding ambiguity; and the remaining contradictions are resolved (the root scope sentence, one detach chain, the locked retirement's callers, reconcile's one sequence, the audit's enumerated list, the fence witness split). Rev 7's review (`selfserve-plan1-review7.md`, findings 1 to 6) is answered: a stated rollout guarantee closes the late legacy delete (one console per deployment, a settle window before any termination, old consoles never reintroduced over lifetimes records); the first-route attach, its rollback and detach run **inside** the hostname exclusion from the claim write to the record write, through locked variants of the create and retirement helpers; root's proof and restart record readers take the box's judge (the scope statement is corrected: only `ClaimRootHTTP`'s claims-on readers are slot-free); a corrupt body is quarantined (its bytes kept under `control/quarantine/`, the key removed conditionally) so the retirement or migration proceeds as for absence instead of promising a fence that cannot parse it; `provision.WithLifetimes` is the one context helper that carries the judge and enables sequencing, the runs worker wraps its detached execution context, the audit distinguishes approved judged wrappers from raw readers, the fence witnesses state their timing, exhaustion is witnessed at every transition, and the sibling undo has one dispatch path. Earlier reviews: rev 1 (`after236/selfserve-plan1-review.md`, 16 findings), rev 2 (F1 to F12), rev 3 (R1 to R10), rev 4 (1 to 9), rev 5 (1 to 6), rev 6 (`selfserve-plan1-review6.md`, findings 1 to 5). Rev 7 answers rev 6's five: every claims-off record write runs with `WithRecordSequence`, so `record_seq` exists after a record's first write and an ETag never recurs, and recovery writes a **fence** (a sequencing rewrite) before it terminates an operation on evidence of a changed record; one migration dispatcher classifies the observation at every entry, so a resume can never supersede its own marker; the attach door's claim rollback runs inside the hostname exclusion and never releases a claim a pending operation or a valid record needs; the judge reaches every mutation context (the console's top-level handler wraps every request, browser and API alike; the seed callback wraps its own context; the full-record updater's re-read is judged; `RecordIndex.Find` and `FindRecordByHost` take the judge from the context), and the audit is by AST; the remaining contracts (the running N1 table, a slot-retirement helper, one expectation type, listed-key absence, exhaustion at read, the import witness's state, the producer-state publication tests) are answered. Rev 5 removed the slot cache (every judgement reads the slot fresh, after the record, in bounded rounds) and made a successor lifetime's hold one row write. Rev 6 answers rev 5's six: a retirement persists its **kind** (normal, orphan, cleanup) and each kind has its own steps and recovery table; a migration's marker-CAS conflict is classified by a re-read for both the bound and the unbound form, so an own marker that landed is resumed, never superseded; every `retireInstanceRecord` caller states its expectation (the sibling undo names the lifetime it created; detach deletes what is current; the attach door never asks for a record rollback over a lifetime failure); a judge carried on the context makes `updateRecordSeeding`, and so every `SetRecord*` writer, refuse a stale record, and every box reader (Phase 1's included) and the import journal's readers go through the judge; the provenance mark is dropped (the fold compares pairs between unstamped entries, a pair-less entry is `(0, 0)` and equal pairs fall to the existing rule, exactly §6.4); the remaining interface gaps are answered. |
| **Goal:** Give every claims-off instance record a durable lifetime (the host incarnation slot) and every served row a serving order `(incarnation, serving_seq)` that boxes publish and edges honour, with crash-safe create, retirement and migration, so a later plan's holding pages and moves can never be out-ordered by a stale publication. |
| @@ -23,10 +23,10 @@ Rev 10, 2026-10-22. Rev 9's review (`selfserve-plan1-review9.md`, findings 1 to |
| - 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`. |
| +- Copy: the new sentences are copy rows pending Paul's review: "the instance changed. read it again and try the delete once more"; "another change to this instance is still settling. try again in a minute"; "the console is switching on instance lifetimes. try again in a minute" (draining); "the console is starting. try again in a minute" (the marker not yet read); "this console runs host claims; lifetimes are not for it"; "the cutover marker exists but cannot be read. repair control/lifetimes-cutover.json by hand"; the CLI's output lines "instance lifetimes: switching on; N unit(s) of earlier work still finishing" and "instance lifetimes: on since <time>". 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). |
| -- **Cutover (the late legacy delete).** An older console's `RetireRecord` deletes a record unconditionally and must never run beside this code; nor may this console admit a legacy mutation that could land after lifetimes start. The cutover is a **mode with a drain** (Task 0): the marker `control/lifetimes-cutover.json` (`{"at": <time>, "by": <operator>}`) is written create-only by the operator door `POST /api/cli/system/lifetimes/cutover` (`carlos system lifetimes cutover`), run **after** the old console process has stopped. The console reads the marker at start and every 10 s. Three states: **before** (no marker): every claims-off door behaves exactly as main, no slot, no judge, the passes idle; **draining** (`now <= At + lifetimeSettle`, 60 s): every claims-off record **mutation** (create, delete, detach, first-route attach, seed and import begin, placement and every `SetRecord*` door) answers 503 "the console is switching on instance lifetimes. try again in a minute" and reads behave as main, so nothing is admitted that could land after the window; **on** (`now > At + lifetimeSettle`): the lifetime protocol, the judge on every context, the passes running. A marker that cannot be read, or does not decode, or has a zero or future `at`: the console treats the state as **draining** (fail closed: mutations refused) and pages once; the state is recomputed from the marker on every evaluation, never cached across a failure. The storage assumption, 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; with the old process gone and this console admitting no mutation during the window, no legacy mutation is outstanding when lifetimes turn on. (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 cutover is a rollout prerequisite and an addition to the design, stated beside the uncached slot read. Within it 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; nor may this console let a legacy mutation admitted before the cutover land after lifetimes start. The cutover is an **admission gate plus a marker** (Task 0). The marker `control/lifetimes-cutover.json` (`{"at": <time>, "by": <operator>}`) is written create-only by the operator door `POST /api/cli/system/lifetimes/cutover` (`carlos system lifetimes cutover`), run **after** the old console process has stopped (one console process per deployment; a unit restart stops the old process before the new one starts). **Admission:** every claims-off unit of record work on the console (an API or browser request that may mutate or read a record; a detached continuation such as the import upload's; one iteration of a worker: the seed sweep, the import pass, the runs pass, the placement pass, the recovery and migration passes) begins with `unit, ok := s.lifetimesAdmit(ctx)` and ends with `unit.done()`; the unit runs in the **mode it was admitted in** for its whole life (a detached continuation inherits its request's unit and ends it when the continuation ends). **States:** `unknown` (startup, the marker not yet read successfully): every claims-off record door, read or mutation, answers 503 "the console is starting. try again in a minute" and workers idle; `before` (the marker read and absent): units are admitted without a judge and behave exactly as main; `draining` (the marker exists, or the door has just written it): no new unit is admitted (mutations and reads that would mutate answer 503 "the console is switching on instance lifetimes. try again in a minute"; plain reads are admitted **judged**, since a read never lands a write); activation waits until the admitted-before count is zero **and** `lifetimeSettle` (60 s) has passed since the later of the marker's `at` and this process's own start; `on`: units are admitted with the judge (`provision.WithLifetimes`), the passes run. `on` is **latched** in the process: once reached, no later marker read, failure or decode error leaves it; a process that starts and reads a marker older than `lifetimeSettle` enters `on` directly (no earlier process is alive to drain). A marker that cannot be read or does not decode: in `unknown` the state stays `unknown` (503s) and pages once per hour; in `before`/`draining` the state becomes `draining` (no new units); in `on` nothing changes. The storage assumption, 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; with the old process gone, every admitted-before unit of this process finished, and the settle passed, no legacy mutation is outstanding when lifetimes turn on. (b) the recovery and migration passes do no work unless admitted in `on`; (c) an older console is never reintroduced once the marker exists. The cutover is a rollout prerequisite and an addition to the design, stated beside the uncached slot read. Within it 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`. |
| @@ -42,24 +42,26 @@ Rev 10, 2026-10-22. Rev 9's review (`selfserve-plan1-review9.md`, findings 1 to |
| --- |
| -### Task 0: The cutover door and the lifetimes gate |
| +### Task 0: The cutover door, the admission gate and the lifetimes state |
| **Files:** |
| - Create: `internal/console/lifetimes_cutover.go` |
| -- Modify: `internal/console/authcli.go` (register `POST /api/cli/system/lifetimes/cutover` with the zero role set, operator-only through `moveOperator`'s check), `internal/console/server.go` (`NewServer`: `s.judge = provision.NewLifetimeJudge()` when `!cfg.HostClaim`; `s.cutover` state; `ServeHTTP` installs the judge on the request context **only** when `s.lifetimesOn()`; every claims-off mutation door checks `s.lifetimesDraining()` first), `cmd/console/main.go` (the marker loop) |
| -- Test: `internal/console/lifetimes_cutover_test.go` |
| +- Modify: `internal/console/authcli.go` (register `POST /api/cli/system/lifetimes/cutover` with the zero role set, operator-only through `moveOperator`'s check), `internal/console/server.go` (`NewServer`: `s.judge = provision.NewLifetimeJudge()` when `!cfg.HostClaim`; `s.cutover *cutoverGate`; `ServeHTTP` calls `s.lifetimesAdmit` for every claims-off record route and `recordCtx` installs the judge iff the unit's mode is `on`), every worker's loop (`StartSeedSweep`, the import pass, the runs pass, the placement pass, Task 3's recovery pass, Task 5's migration pass: one iteration = one unit), the import upload's detached continuation (inherits the request's unit), `cmd/console/main.go` (`StartLifetimesCutoverWatch` registered as a worker), `system.go` (`systemVerb` accepts `lifetimes`; `carlos system lifetimes cutover`), `internal/cli/api.go` or the client file the other `system` verbs use (`SystemLifetimesCutover(ctx) (cutoverStatus, error)`), `docs/site/cli.md` (the command, its output and the operator sequence: stop the old console, start the new, run the command, read its output until "on") |
| +- Test: `internal/console/lifetimes_cutover_test.go`, `system_test.go` (the dispatch), the client's test file |
| **Interfaces:** |
| -- `type cutoverMarker struct { At time.Time \`json:"at"\`; By string \`json:"by"\` }`; `const lifetimesCutoverKey = "control/lifetimes-cutover.json"`; `const lifetimeSettle = 60 * time.Second`. |
| -- `func (s *Server) handleAPILifetimesCutover(w, r)`: an operator's own CLI token (else 404, as the move family); `cfg.HostClaim` → 409 "this console runs host claims; lifetimes are not for it"; writes the marker create-only with `At: now`, `By: addr`; `ErrCASConflict` → 200 with the existing marker (idempotent); the audit line "switched instance lifetimes on (cli)". |
| -- `func (s *Server) cutoverState(now time.Time) cutoverState` (`before`, `draining`, `on`; unreadable or malformed → `draining` and one page per hour); `func (s *Server) lifetimesOn() bool` (`!cfg.HostClaim && cfg.Releases != nil && cutoverState(now) == on`); `func (s *Server) lifetimesDraining() bool`; the marker is read at start and every 10 s by `StartLifetimesCutoverWatch` (registered as a worker); `StartLifetimeRecoveryPass` and the migration pass loop every 60 s and do work only when `lifetimesOn()`. |
| -- Every claims-off mutation door (the create, delete, detach, attach, seed begin, import begin, placement, `SetRecord*` doors) begins with `if s.lifetimesDraining() { 503 "the console is switching on instance lifetimes. try again in a minute" }`. |
| - |
| -- [ ] **Step 1: Write the failing tests**: `TestCutoverDoorIsOperatorOnlyAndIdempotent`; `TestCutoverStates` (no marker → before; a marker 30 s old → draining; 61 s → on; unreadable or malformed → draining and a page; a marker appearing while running is seen within 10 s); `TestDrainingRefusesEveryClaimsOffMutation` (one subtest per door → 503, nothing written; reads unchanged); `TestNoLegacyMutationCrossesTheBoundary` (a legacy detach admitted at `At - 1 s` and paused inside the store: the drain window outlasts its completion before any lifetime write; a detach arriving at `At + 1 s` is refused; the passes idle until `on`); `TestJudgeInstalledOnlyWhenOn`. |
| -- [ ] **Step 2: Run to verify they fail**: `go test ./internal/console -run 'Cutover|Draining|NoLegacyMutation|JudgeInstalled' -count=1`. |
| +- `type cutoverMarker struct { At time.Time \`json:"at"\`; By string \`json:"by"\` }`; `const lifetimesCutoverKey = "control/lifetimes-cutover.json"`; `const lifetimeSettle = 60 * time.Second`; `type cutoverState int` (`cutoverUnknown`, `cutoverBefore`, `cutoverDraining`, `cutoverOn`). |
| +- `type cutoverGate struct { mu sync.Mutex; state cutoverState; at time.Time; started time.Time; admitted int; drained chan struct{}; pageAt time.Time }`: `func (g *cutoverGate) admit(now time.Time, mutating bool) (unit *admittedUnit, ok bool, refusal string)` (`unknown` → false, the starting sentence; `before` → a unit with `mode: before`, `admitted++`; `draining` → `mutating` → false, the draining sentence; a plain read → a unit with `mode: on` (judged; reads never write); `on` → a unit with `mode: on`); `func (u *admittedUnit) done()` (`admitted--` for a `before` unit; the last one closes `drained`); `func (g *cutoverGate) observe(m *cutoverMarker, readErr error, now time.Time)` (the watcher's and the door's input: the transitions of the Global Constraint; `on` is never left; `before`→`draining` closes admission synchronously); `func (g *cutoverGate) activateWhenDrained(now time.Time) bool` (`draining` and `admitted == 0` and `now >= max(at, started) + lifetimeSettle` → `on`); `func (g *cutoverGate) status() cutoverStatus` (`{State string; At time.Time; Pending int}`). |
| +- `func (s *Server) lifetimesAdmit(ctx context.Context, mutating bool) (*admittedUnit, bool, string)`; `func (s *Server) recordCtx(ctx context.Context, u *admittedUnit) context.Context` (`u.mode == on` → `provision.WithLifetimes(ctx, s.judge)`, else `ctx` unchanged); `func (s *Server) lifetimesOn() bool` (`!cfg.HostClaim && cfg.Releases != nil && s.cutover.state == cutoverOn`). Every claims-off record door: `u, ok, why := s.lifetimesAdmit(ctx, mutating); if !ok { 503 why }; defer u.done(); ctx = s.recordCtx(ctx, u)`. A detached continuation receives `u` and calls `u.done()` when it ends instead of the handler. Every worker iteration: admit (non-mutating workers as reads; the passes that write as mutating), skip the iteration when refused, `done()` at its end. |
| +- `func (s *Server) handleAPILifetimesCutover(w, r)`: an operator's own CLI token (else 404, as the move family); `cfg.HostClaim` → 409 "this console runs host claims; lifetimes are not for it"; writes the marker create-only with `At: now`, `By: addr`, then `s.cutover.observe(&m, nil, now)` **synchronously** (admission closes before the response); `ErrCASConflict` → read the existing marker: readable → `observe` it and answer with the status; unreadable or malformed → 502 "the cutover marker exists but cannot be read. repair control/lifetimes-cutover.json by hand" (never a success); the audit line "switched instance lifetimes on (cli)"; the response is `cutoverStatus` (200). The CLI prints "instance lifetimes: switching on; N unit(s) of earlier work still finishing" while `Pending > 0` or the settle has not passed, and "instance lifetimes: on since <time>" when `on`; `carlos system lifetimes cutover` polls the door every 5 s until `on` (the door is idempotent) or `--no-wait`. |
| +- `StartLifetimesCutoverWatch`: reads the marker at start and every 10 s and calls `observe`, then `activateWhenDrained`; the door's synchronous `observe` is what makes refusal immediate at `At`; the watcher covers a restart and a marker written by hand (documented: a hand-written marker closes admission within 10 s). |
| +- **Worker lifecycle (one rule):** every worker starts once at process start and loops for the life of the process; each iteration admits, does its work only when admitted in the mode it requires (`on` for the recovery and migration passes), and ends its unit. No worker returns early because the state was `before` or `unknown` at start. |
| + |
| +- [ ] **Step 1: Write the failing tests**: `TestCutoverDoorIsOperatorOnlyAndIdempotent` (second call → the status; an unreadable existing marker → 502, not a success); `TestCutoverStates` (unknown at start → 503 on every door incl. GET; a read marker absent → before; a marker written → draining at once; `on` after drain and settle; `on` is kept across a later read failure and a corrupt marker; a restart into a marker older than 60 s → on at once; a restart into a marker 10 s old → on at 60 s after the process start); `TestDrainingRefusesEveryClaimsOffMutation` (one subtest per door → 503, nothing written; GETs answer, judged); `TestActivationWaitsForAdmittedWork` (a real creation handler paused before its fresh record-write context, and a detached import continuation paused mid-body, both admitted before the marker; the marker written; 60 s pass; the state is still draining with `Pending == 2`; each finishes → its legacy write lands under `before` rules; the count reaches zero → `on` after the settle; a creation arriving at `At + 1 s` → 503); `TestNoUnitChangesModeMidFlight` (a unit admitted in `before` observes no judge after activation would otherwise have happened; one admitted in `on` has it); `TestWorkersLoopAcrossTheCutover` (the recovery and migration passes started before the marker do nothing, then run after activation without a restart; the seed sweep's iteration admitted before the marker finishes under legacy rules and the next iteration is judged); `TestMarkerFailureAfterActivationKeepsJudgedReads` (a retired slot and an obsolete body; the marker read fails; GET and listings refuse the body); `TestCutoverCLI` (the dispatch, the client call, the two output lines, `--no-wait`). |
| +- [ ] **Step 2: Run to verify they fail**: `go test ./internal/console -run 'Cutover|Draining|Activation|MidFlight|WorkersLoop|MarkerFailure' -count=1`, `go test . -run 'SystemLifetimes' -count=1`. |
| - [ ] **Step 3: Implement.** |
| - [ ] **Step 4: Run to verify they pass.** |
| -- [ ] **Step 5: Commit** `console: the lifetimes cutover door and the three-state gate with a drain`. |
| +- [ ] **Step 5: Commit** `console: the lifetimes cutover door, an admission gate that activation waits on, a latched on state and the CLI command`. |
| ### Task 1: Record fields, the slot type, the tombstone form and the lifetime verdict |
| @@ -132,7 +134,7 @@ The verdict, exactly, with `owned := slot != nil && slot.Account == rec.Account |
| - `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)` (takes `withHostname`) and `func (s *Server) createRecordWithLifetimeLocked(ctx context.Context, rec provision.Record) (*provision.Record, error)` (assumes it): the one protocol every claims-off creation path runs; the create, seed and import doors call the locking form; the attach door calls the locked form inside its own exclusion (below). 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, op fenceOwner) (res fenceResult, obs provision.Observation, err error)` with `type fenceOwner struct { Tombstone bool; MarkerID string }` (what this operation's own outstanding write would look like) and `fenceResult` one of `fenced` (a body that is neither this operation's tombstone nor its marker was rewritten with its sequence introduced or advanced: the caller terminates on `obs`), `ownWriteWon` (the re-read found this operation's tombstone, or a record marked with `MarkerID`: the caller does **not** terminate; it dispatches again from `obs` and the operation resumes, retirement at N2, migration through its dispatcher), `absent` (nothing to fence: the slot is the fence for an absent key), or an error after 3 CAS retries (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 acts on its result. |
| - - `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). |
| + - `func (s *Server) recoverLifetimes(ctx context.Context) error`, `func (s *Server) StartLifetimeRecoveryPass(ctx context.Context)` (Task 0's worker lifecycle: started once, loops every 60 s for the life of the process, each iteration admitted as a mutating unit and doing work only in `on`). |
| The protocol, inside `withHostname(host)`: |
| 1. `admit(host, "")`. |
| @@ -230,7 +232,7 @@ Inside `withHostname(host)`: `admit(host, "")`; `obs := classifyLocked` (unreada |
| - Test: `internal/provision/record_sequence_test.go`, `internal/console/lifetimes_migrate_test.go` |
| **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). |
| +- 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`; Task 0's worker lifecycle: started once, loops every 60 s, each iteration admitted as a mutating unit and doing work only in `on`; 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 (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. |
| @@ -252,7 +254,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 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 by **its own record**: when `recs[rt.Host]` is absent or its key differs from `RecordKey(rt.Account, rt.App, rt.Host)`, the pass observes the row's own key (`cfg.Judge.Observe`): a present valid record → the row is kept (a legacy collision between two valid owners stays as today); established absent or stale → "record gone" (a held row by an op its own record does not carry → `cfg.DeleteIfHold` first), and this evidence also bypasses `KeepParticipant` (the keep view keys by hostname and cannot vouch for a row whose own record is gone); unreadable → protected; the journal exemption and hand-managed rows untouched. Replacement by a cross-owner successor takes **two passes**: this pass retires the predecessor row, the next mints the successor (the minting loop runs before removal and must not mint over a row it does not own); the witness asserts convergence within two passes; `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 valid and none corrupt or unreadable → `os.ErrNotExist`; two or more valid → `os.ErrPermission` (genuine ambiguity); a corrupt or path-mismatched candidate → `ErrRecordCorrupt` (never absence; today's schema and shape validation still applies to the chosen record); an 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; **`rootRestartRequestSource`** (its `target` and `creation` readers) gets the same `judge` field from the same constructor argument and wraps its contexts the same way; 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`): lifetime validity and placement eligibility are **separate questions**. When `recs[rt.Host]` is absent or its key differs from `RecordKey(rt.Account, rt.App, rt.Host)`, the pass observes the row's own key (`cfg.Judge.Observe`): a present valid record **whose placement is in this pass's scope** (the same region and pool the pass lists; the record would have been in `recs` but for the hostname collision) → the row is kept (a legacy collision between two valid owners stays as today); a present valid record **outside the scope** (another region after a flip or a placement change, another pool) → **today's removal rules** apply unchanged (`KeepParticipant` keeps it while a move's view says so; the app-trash rule; otherwise removed), so an offline move's source row is kept while `offlineView.keep` says so and removed at finishing exactly as today; established absent or stale → "record gone" (a held row by an op its own record does not carry → `cfg.DeleteIfHold` first), and this evidence also bypasses `KeepParticipant` (the keep view keys by hostname and cannot vouch for a row whose own record is gone); unreadable → protected; the journal exemption and hand-managed rows untouched. Replacement by a cross-owner successor takes **two passes**: this pass retires the predecessor row, the next mints the successor (the minting loop runs before removal and must not mint over a row it does not own); the witness asserts convergence within two passes; `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(s.recordCtx(r.Context(), u))` after Task 0's `lifetimesAdmit` (the judge only for a unit admitted in `on`), 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 valid and none corrupt or unreadable → `os.ErrNotExist`; two or more valid → `os.ErrPermission` (genuine ambiguity); a corrupt or path-mismatched candidate → `ErrRecordCorrupt` (never absence; today's schema and shape validation still applies to the chosen record); an 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; **`rootRestartRequestSource`** (its `target` and `creation` readers) takes a `judge` in **every production construction**: `productionRestartRequestPass` (both callers in `agent.go`), `rootHistoryCreation` and `productionRootUpdatesHeld`, each given the box's judge by its caller and wrapping its contexts with `WithLifetimes`; `TestRootRestartReadersAreJudged` constructs through those three production functions and asserts a stale record yields no restart target, no history creation and no held update; 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:** |
| @@ -266,7 +268,7 @@ Recovery, per pending migration, inside the exclusion with `admit(host, OpID)`: |
| - `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`. 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`, `(*console.Server).refreshClaimRetireRecord`, `host.relocationRecord` (the `hostops.ReadRecord` callers today, pinned: the audit test lists them and fails on any new `hostops.ReadRecord` caller, and a fixture claims-off call is shown to fail), 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); `TestCrossOwnerRecreationWithdrawsThePredecessorRow` (two apps, two boxes; box B misses A's retirement; C recreates the hostname at incarnation 2; B retires A's row on its next pass (held and unheld variants; A's record physically absent, and present-but-stale; `KeepParticipant` returning true) and mints C's on the following pass; two valid legacy owners for one hostname with the row belonging to the non-selected key → the row is kept as today; 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 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 retires A's row on its next pass (held and unheld variants; A's record physically absent, and present-but-stale; `KeepParticipant` returning true) and mints C's on the following pass; two valid legacy owners for one hostname with the row belonging to the non-selected key → the row is kept as today; the journal exemption and a hand-managed row are untouched); `TestOutOfScopeValidRecordFollowsTodaysRemoval` (a valid record moved to another region, and one moved to another pool: the row is removed as today; with `KeepParticipant` true it is kept; the app-trash rule still removes); `TestOfflineFinishingRemovesTheSourceRow` (through `reconcileSerializedWith`: the source row kept during the move, removed once the record reaches `offline-finishing` and `offlineView.keep` stops; `offlineSourceGone` reports gone; the full `TestOfflineMoveE2E` finishing assertions intact); `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. |
| @@ -353,7 +355,7 @@ These are §11's witnesses plan 1 owns. Every witness that needs a holding publi |
| ## Self-review notes |
| -- §6.4 coverage: slot, owner key and tombstone form (T1, T4), create and recovery (T3), every claims-off removal with an explicit expectation and its recovery (T4), migration, its fence and recovery (T5), the observation protocol in every claims-off consumer (T1, T6), `move_seq`, the setters that install a lifetime with its hold and the existing-row rules (T7), publication 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`/`fenceResult`/`fenceOwner`, `quarantineCopy`, `ConvertRetirementKind`, `cutoverMarker`, `cutoverState`, `lifetimesDraining`, `StartLifetimesCutoverWatch`, `handleAPILifetimesCutover`, `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`. |
| +- §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 stated additions to §6.4's text are the uncached slot read and the cutover (an operator step with an admission gate, a rollout prerequisite); 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`/`fenceResult`/`fenceOwner`, `quarantineCopy`, `ConvertRetirementKind`, `cutoverMarker`, `cutoverState` (`cutoverUnknown/Before/Draining/On`), `cutoverGate`, `admittedUnit`, `lifetimesAdmit`, `recordCtx`, `activateWhenDrained`, `cutoverStatus`, `StartLifetimesCutoverWatch`, `handleAPILifetimesCutover`, `SystemLifetimesCutover`, `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. |