plan: self-serve moves 2, rev 17 (Astra review 16: an expired request dispatches to the fence first; void and abort obligation in one UPDATE; a late receipt retires the expiry; the observed cut value passed explicitly)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 file changed,
+5
−5
docs/superpowers/plans/2026-10-22-self-serve-moves-2-door-token-job.md+5 −5
CI passed — run details
CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (1 s)
ok amadan.net/carlos/platform.git/launchctx 0.014s
? amadan.net/carlos/platform.git/launchctx/controlname [no test files]
? amadan.net/carlos/platform.git/launchctx/wireid [no test files]
=== vet (22 s)
=== gofmt (1 s)
gofmt -l .: clean
=== rest (179 s)
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 5.659s
ok github.com/carlosframework/platform/cmd/moveprobe 12.289s
ok github.com/carlosframework/platform/cmd/zddprobe 0.979s
ok github.com/carlosframework/platform/internal/activator 61.981s
ok github.com/carlosframework/platform/internal/agentsock 0.030s
ok github.com/carlosframework/platform/internal/alerting 0.016s
ok github.com/carlosframework/platform/internal/analytics 0.108s
ok github.com/carlosframework/platform/internal/appconfig 3.727s
ok github.com/carlosframework/platform/internal/apperr 0.262s
ok github.com/carlosframework/platform/internal/applog 0.318s
ok github.com/carlosframework/platform/internal/awsinv 0.047s
ok github.com/carlosframework/platform/internal/boxchoice 0.015s
ok github.com/carlosframework/platform/internal/boxid 1.208s
? 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.032s
ok github.com/carlosframework/platform/internal/caps 0.042s
ok github.com/carlosframework/platform/internal/certwarm 0.042s
ok github.com/carlosframework/platform/internal/cgroup 0.885s
ok github.com/carlosframework/platform/internal/changelog 0.785s
ok github.com/carlosframework/platform/internal/claimactivation 2.023s
ok github.com/carlosframework/platform/internal/clamd 0.062s
? 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.079s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.019s
ok github.com/carlosframework/platform/internal/dbverify 0.011s
ok github.com/carlosframework/platform/internal/dnsrec 0.011s
ok github.com/carlosframework/platform/internal/docsite 0.021s
ok github.com/carlosframework/platform/internal/domains 3.468s
ok github.com/carlosframework/platform/internal/doorkey 0.011s
ok github.com/carlosframework/platform/internal/economics 0.010s
ok github.com/carlosframework/platform/internal/edge 47.244s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.025s
ok github.com/carlosframework/platform/internal/fleetapi 0.196s
ok github.com/carlosframework/platform/internal/gate 1.685s
ok github.com/carlosframework/platform/internal/genselect 0.062s
ok github.com/carlosframework/platform/internal/geo 0.037s
ok github.com/carlosframework/platform/internal/geoip 0.037s
ok github.com/carlosframework/platform/internal/historyreadback 0.279s
ok github.com/carlosframework/platform/internal/host 142.775s
ok github.com/carlosframework/platform/internal/hostactors 7.238s
ok github.com/carlosframework/platform/internal/hostactors/awsmembership 0.013s
ok github.com/carlosframework/platform/internal/hostbox 4.027s
ok github.com/carlosframework/platform/internal/hostclaim 1.448s
ok github.com/carlosframework/platform/internal/hostenv 0.010s
ok github.com/carlosframework/platform/internal/hostevidence 0.039s
ok github.com/carlosframework/platform/internal/hostexec 6.966s
ok github.com/carlosframework/platform/internal/hostfence 0.186s
ok github.com/carlosframework/platform/internal/hostfiles 10.763s
ok github.com/carlosframework/platform/internal/hostfloor 0.033s
ok github.com/carlosframework/platform/internal/hostguard 1.173s
ok github.com/carlosframework/platform/internal/hostops 2.790s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.007s
ok github.com/carlosframework/platform/internal/hostquarantine 0.008s
ok github.com/carlosframework/platform/internal/hostrelocate 0.998s
ok github.com/carlosframework/platform/internal/hostresolve 0.478s
ok github.com/carlosframework/platform/internal/hostwrite 0.038s
ok github.com/carlosframework/platform/internal/infraview 0.007s
ok github.com/carlosframework/platform/internal/instancectl 5.279s
ok github.com/carlosframework/platform/internal/instanceexec 2.867s
ok github.com/carlosframework/platform/internal/instancefs 0.009s
ok github.com/carlosframework/platform/internal/instancekey 0.012s
ok github.com/carlosframework/platform/internal/instanceupdates 0.016s
ok github.com/carlosframework/platform/internal/layout 2.631s
ok github.com/carlosframework/platform/internal/lbhealth 0.007s
ok github.com/carlosframework/platform/internal/lease 10.227s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.849s
ok github.com/carlosframework/platform/internal/ledger 0.064s
ok github.com/carlosframework/platform/internal/mail 0.437s
ok github.com/carlosframework/platform/internal/maintenance 0.132s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.753s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.254s
ok github.com/carlosframework/platform/internal/metrics 0.028s
ok github.com/carlosframework/platform/internal/moves 25.001s
ok github.com/carlosframework/platform/internal/moves/archive 1.522s
ok github.com/carlosframework/platform/internal/objstore 0.594s
ok github.com/carlosframework/platform/internal/ops 0.379s
ok github.com/carlosframework/platform/internal/park 0.565s
ok github.com/carlosframework/platform/internal/plans 0.027s
ok github.com/carlosframework/platform/internal/privatefile 0.005s
ok github.com/carlosframework/platform/internal/provision 25.625s
ok github.com/carlosframework/platform/internal/publicrelease 0.019s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 6.431s
ok github.com/carlosframework/platform/internal/relay 7.208s
ok github.com/carlosframework/platform/internal/release 28.653s
ok github.com/carlosframework/platform/internal/rollout 4.643s
ok github.com/carlosframework/platform/internal/rolloutwords 0.007s
ok github.com/carlosframework/platform/internal/rootaws 0.012s
ok github.com/carlosframework/platform/internal/rootparticipation 0.069s
ok github.com/carlosframework/platform/internal/rootstate 0.038s
ok github.com/carlosframework/platform/internal/runs 0.507s
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 3.357s
ok github.com/carlosframework/platform/internal/seeds 0.012s
ok github.com/carlosframework/platform/internal/sim/hostlife 19.036s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 4.071s
ok github.com/carlosframework/platform/internal/status 0.124s
ok github.com/carlosframework/platform/internal/steering 0.007s
ok github.com/carlosframework/platform/internal/stopevidence 0.076s
ok github.com/carlosframework/platform/internal/svcrole 0.006s
ok github.com/carlosframework/platform/internal/telemetry 0.005s
ok github.com/carlosframework/platform/internal/termfence 0.010s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.010s
ok github.com/carlosframework/platform/internal/updatewords 0.017s
ok github.com/carlosframework/platform/internal/usage 0.018s
ok github.com/carlosframework/platform/internal/version 0.007s
ok github.com/carlosframework/platform/internal/vet 1.797s
=== internal_console@0 (82 s)
ok github.com/carlosframework/platform/internal/console 80.034s
=== internal_console@1 (41 s)
ok github.com/carlosframework/platform/internal/console 39.063s
=== internal_console@2 (46 s)
ok github.com/carlosframework/platform/internal/console 44.102s
=== internal_console@3 (40 s)
ok github.com/carlosframework/platform/internal/console 38.062s
=== internal_console@4 (41 s)
ok github.com/carlosframework/platform/internal/console 39.192s
=== internal_console@5 (39 s)
ok github.com/carlosframework/platform/internal/console 37.555s
=== internal_console@6 (40 s)
ok github.com/carlosframework/platform/internal/console 38.052s
=== internal_console@7 (46 s)
ok github.com/carlosframework/platform/internal/console 44.701s
=== root@0 (39 s)
ok github.com/carlosframework/platform 36.920s
=== root@1 (47 s)
ok github.com/carlosframework/platform 45.073s
=== root@2 (45 s)
ok github.com/carlosframework/platform 42.794s
=== 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.025s
=== RUN TestT4StorageProbeMinio
2026/10/22 11:21:27 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E0D620B103D0FC, 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.023s
minio contract: PASS
diff --git a/docs/superpowers/plans/2026-10-22-self-serve-moves-2-door-token-job.md b/docs/superpowers/plans/2026-10-22-self-serve-moves-2-door-token-job.md| index 6bbcb3316..4768bad19 100644 |
| --- a/docs/superpowers/plans/2026-10-22-self-serve-moves-2-door-token-job.md |
| +++ b/docs/superpowers/plans/2026-10-22-self-serve-moves-2-door-token-job.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 16, 2026-10-22. Rev 15's review (`selfserve-plan2-review15.md`: declarations closed; N12 three gaps) is answered: request expiry has **one** path (`selfServeApply`'s step (2) is the only place that decides it; the step's branch calls into it and acts only on a won decision, `open-uncertain` included); the decision compares **the cut state the worker saw** (`cut_requested_at = ?`) rather than requiring no cut, so expiry against the current deadline stays possible after a cut; an already-expired request (`pr.Expired`) resumes fence-and-classify directly and never re-decides; issuance resets `request_expired = 0`; the reload after a lost decision dispatches on what it finds (same nonce → retry; consumed or voided → stop; another nonce → `errRequestForeign`). Rev 14's review (`selfserve-plan2-review14.md`: N10b closed; N12 narrowed) is answered: the request-expiry decision is **durable and conditional** (`ExpireSelfServeRequest(id, nonce, sawDeadline)`: one UPDATE predicated on the nonce, the deadline the worker saw, no consumption and no accepted cut), mutually exclusive with `MarkSelfServeCut` (which also requires `request_expired = 0`), and only the winning expiry fences, voids and aborts; `persistedRequest` carries `Deadline`; the abbreviated call sites name their full arguments. Rev 13's review (`selfserve-plan2-review13.md`: N11, N10a closed; N10b narrowed; N12 new) is answered: the maintenance-to-idle consume requires `cut_requested_at = 0` in its predicate, so a cut that won makes a stale consume fail and the worker re-reads into the handoff (N10b); `MarkSelfServeCut` also transfers an outstanding unconfirmed `maintain` request's `request_deadline` to the cut's copy allowance in the same UPDATE, so one deadline policy governs adoption (N12). Rev 12's review (`selfserve-plan2-review12.md`: N9 closed; N10 narrowed; N11 new) is answered: **a request deadline exists only for writes whose expiry action is abort** (the pre-open writes: queue, stage, maintain, precheck, start, seal, flip, open); the post-open and clearing writes (gate 1, gate 2, finish, the abort path's abort and finish, estimate) carry `request_deadline = 0`, which `selfServeApply` never checks, so a page-and-retain phase whose evidence arrives late still clears the move (N11); abandonment rolls the next wait's deadline in its own UPDATE (`AbandonSelfServeRequest(id, nonce, nextDeadline)`) (N10a); a maintenance round's allowance starts at its issue and `ready` has **no step deadline while idle** (`next_maintenance_at` schedules the next round; `step_deadline = 0` between rounds) (N10b). Rev 11's review (`selfserve-plan2-review11.md`: N8 closed; N9 narrowed; N10 new) is answered: `selfServeApply` checks the request's deadline **before every forward-write attempt, replays included**, and an expired request leaves the loop as `errRequestExpired` for the fence-and-classify path, so no retry can open after the deadline inside one invocation (N9); a same-phase consume **rolls the next wait's deadline** in the same UPDATE (`ConsumeSelfServeRequest(id, nonce, nextStep, nextDeadline)`), issuance copies that fresh deadline into `request_deadline`, and the phase-wide limits stay on `staging_started_at` and `window_deadline` (N10). Rev 10's review (`selfserve-plan2-review10.md`: N6 closed; N8 narrowed; N9 new) is answered: **every** writer, the operator doors included, proposes its stamp as `max(now, the observed record's previous stamp + 1 ns)` and the mutation stores it verbatim, so stamps are monotonic under a backward clock and supersession by stamp is sound (N8); every request persists **its own deadline** (`request_deadline`, the issuing phase's step deadline), and an unconfirmed request whose deadline has passed while the record is at or before `offline-landed` is never replayed from **any** row phase, `open-uncertain` included: fence, classify, confirm if the write won, else void and abort (N9). Rev 9's review (`selfserve-plan2-review9.md`: 8, 10, 13, 20, N7 closed) is answered: **a committed confirmation expiry wins over a later cut** (`MarkSelfServeCut` also requires `expiry_pending = 0`; the cut door answers 409 "the move expired. prepare again"), and the persisted expiry carries its kind and deadline so execution revalidates its identity (N6); **an operator's newer staging stamp supersedes the job's outstanding wait**: when the record's stamp for the awaited field has moved past `request_proposed` without the receipt changing, the job abandons its request and issues a fresh bound round, with the crash between the two stated (N8). Rev 8's review (`selfserve-plan2-review8.md`: 21, N4 closed) is answered: reconciliation maps the record to a **phase and substep** (the first substep whose transition the record has not made), so an operator's progress never makes the job repeat a refused transition (item 10); abandonment is only for **leaving** a wait (abort, cancel, operator supersession), never for a page-and-retain expiry, with the phase table stated (item 13); the confirmation-deadline expiry commits only against `cut_requested_at = 0` and the deadline it saw, and the cut's copy expiry is a different decision (N6); the cut handoff persists which continuation it chose as a `restage` substep (`adopted` or `issue`) in the advancing transaction (N7); acceptance and discovery share one participant predicate (item 8); the superseded tag arrangement is deleted (item 20). Rev 7's review (`selfserve-plan2-review7.md`: N1, N2, N3, N5 closed) is answered: a phase with several writes has a durable **substep** (`phase_step`), advanced atomically with the consumption that ends the previous substep, and the phase function dispatches on it (item 10); confirmation is the **receipt alone** (no state predicate), phase progress is reconciliation's business (item 10); issuance refused with no outstanding request is its own result (`errRequestRefused`) (item 10); a confirmed request whose wait will never be satisfied is **abandoned** (consumed without its wait) by abort, expiry and operator supersession, and the finaliser consumes a confirmed clearing request in the transaction that records the outcome (item 13); `maintain` updates the stage identity like `stage` (N4); the cut's `MarkSelfServeCut` retires the ready deadline in the same UPDATE and ready expiry is conditional on no accepted cut (N6); return reports are **immutable per request key** (item 21); an id-less forward keeps `BoxID` membership and drops only the instance comparison (item 8); one untagged bridge file with the complete helper closure (item 20). Rev 6's review (`selfserve-plan2-review6.md`: items 1, 6, 17 closed) is answered: the request lifecycle is a complete state machine (`issued → confirmed → consumed`, or `voided`) with a **resume** path for a confirmed-unconsumed request, a standalone consume for same-phase sequences (start then seal, finish gate 1 then finish, round after round, estimate), issuance that returns the existing request so a caller of another kind or phase stops instead of interpreting it, and no-op-free receipts (a request-bearing mutation always writes its receipt) (item 10); the bound write checks **incarnation and sequence** (`provision.Expect{Incarnation, Seq}`) for the job and the operator doors (N5, item 13); every finalisation requires no outstanding request after a fence (item 13); the expiry branch classifies the fenced observation (N3); the cut is a **handoff**: the door marks `cut_requested_at` and the job turns the outstanding maintenance round into the first cut round (N4); event delivery marks `emitted` inside the transaction before delivering and the terminal event is inserted with the outcome (N1); per-box FIFO is stated (N2); the open intent is prepared before the request is issued so adoption and the persisted proposal agree, return requests carry a full identity ordered deterministically, reports name the request key, and acceptance binds the report to the reverse move's selected target (item 21); `Expect`'s real fields, `Load`'s absence distinction and one id-less rule (item 8); the fixtures move to an untagged `linux` file so today's tests keep compiling, `CopyEvent` carries its completion fields, the saved book is validated before seeding, `DiskFreeBytes` is numeric (item 20). Rev 5's review (`selfserve-plan2-review5.md`: items 2, 3, 4, 5, 7, 9, 11, 12, 14, 15, 16, 18, 19 closed) is answered: every job and operator record write is **predecessor-bound** (`provision.SetRecordMoveAt(ctx, s, acct, app, host, expectSeq, mut)`: one CAS write against the record whose `record_seq` equals the persisted expectation, no fresh-read retry), so a voided request can never land and an operator door never rebuilds a write after a reclaim fence (items 10, 13); every self-serve mutation stamps a **nonce receipt** on the record (`SelfServe.LastRequest`, or `LastMove.Request` for a clearing write), so confirmation is exact for every transition and distinct from an operator's reconciliation (item 10); confirm and advance require an unconsumed, unvoided request, so a stale worker cannot confirm a voided one (item 10); "pending" means **no confirmed request for the phase's write**, so the interval before issuance is compatible with the predecessor state (item 10); an expired uncommitted open is fenced and aborted, never replayed (N1); the stage book is reconciled against each complete successful round so deleted keys leave it (item 6); identity records are loaded by `boxid.Key(BoxID)` and `Verified` is narrowed to what `Enroll` proves (item 8); the gate's exception for an authorised malformed hostname and plan 1's outer 503 are declared (item 1); the view joins row id, move id, self-serve id and incarnation (item 17); the open record adopts an existing complete intent, return discovery and acceptance compare `InstanceID`, incarnation is re-checked at the door and in the guarded write, and `Layout` comes from the source's `stopped` report so every operator move has it (item 21); a maintenance restage in `ready` has its own kind and keeps the confirmation deadline (N2); the gate types, `StagedRequest`, failed diagnostics, the request argument to `confirmed`, and the bridge's compile rule are stated (item 20). Reviews: rev 1 (`after236/selfserve-plan2-review.md`, 20 findings), rev 2 (`selfserve-plan2-review2.md`, 21), rev 3 (`selfserve-plan2-review3.md`, 21 items), rev 4 (`selfserve-plan2-review4.md`, 21 items, 9 closed). Rev 5 is **self-contained** (nothing is "as rev N"; every schema, phase, predicate, sentence and test list is in this document) and answers rev 4's open items: the gate orders authentication, host shape and the capability check before the limiter and the no-bucket rail (item 1); operator moves keep today's admission (nil or stale metrics, no identity record) and only the self-serve path requires fresh identity; `BootID` is dropped (items 4, 8); the evidence reader takes the move and a freshness table is stated (item 5); the staged inventory is a **move-bound book** built from every copy event the mirror reports, including same-ETag and deferred copies, through a new observer hook that leaves ordinary mirroring unchanged (item 6); a participant is identified by `BoxID` **and** `InstanceID` in the resolver, in report validation and at every gate, and abort never requires the target (item 8); every request persists its **nonce, expected sequence and proposed stamp before the write**, confirmation is conditional on the record carrying that exact stamp, replay is idempotent, and consumption and the phase advance are one UPDATE predicated on the nonce (item 10); compatible sets include the predecessor state while a phase's write is pending (item 10); expiry is bound to its phase and cleared on reconciliation into another phase, with the health rule taking precedence (item 12); abandonment is conditional on the fenced observation and persisted before release, and every reclaim fences first, operator reservations included (item 13); own retries are accepted only for a complete expected selection after the whole table validates (item 14); the status view joins rows regardless of the mode (item 17); events carry a per-row sequence and are delivered inside the caller's transaction, the harness is an exported bridge **inside package host** behind the `e2e` tag with the CI step named, the Go declarations are complete, and queue ordering is stated (item 20); the open record is written **before** the open transition with the proposed open time, return requests have unique durable identities, the report path is named, acceptance is bound to the lifetime and the forward's participants, and leftovers are judged by the forward's layout (item 21). |
| +Rev 17, 2026-10-22. Rev 16's review (`selfserve-plan2-review16.md`: gaps 1 and 2 closed; gap 3 and two whole-plan items) is answered: an outstanding request already marked `Expired` is dispatched into fence-and-classify **before** the generic replay/void handling of step (3); the won expiry's void and its abort obligation are **one UPDATE** (`VoidExpiredSelfServeRequest` sets `request_consumed`, `request_voided` and `abort_requested_at` together), so no crash between them loses the abort; a receipt that resolves an expired request successfully **retires the flag** (`ConfirmSelfServeRequest` sets `request_expired = 0`), so a cut after a resolved maintenance expiry is not refused; `ExpireSelfServeRequest` takes `sawCutRequestedAt` and `persistedRequest` carries `CutRequestedAt`, loaded in the same SELECT as the request on every load and reload. Rev 15's review (`selfserve-plan2-review15.md`: declarations closed; N12 three gaps) is answered: request expiry has **one** path (`selfServeApply`'s step (2) is the only place that decides it; the step's branch calls into it and acts only on a won decision, `open-uncertain` included); the decision compares **the cut state the worker saw** (`cut_requested_at = ?`) rather than requiring no cut, so expiry against the current deadline stays possible after a cut; an already-expired request (`pr.Expired`) resumes fence-and-classify directly and never re-decides; issuance resets `request_expired = 0`; the reload after a lost decision dispatches on what it finds (same nonce → retry; consumed or voided → stop; another nonce → `errRequestForeign`). Rev 14's review (`selfserve-plan2-review14.md`: N10b closed; N12 narrowed) is answered: the request-expiry decision is **durable and conditional** (`ExpireSelfServeRequest(id, nonce, sawDeadline)`: one UPDATE predicated on the nonce, the deadline the worker saw, no consumption and no accepted cut), mutually exclusive with `MarkSelfServeCut` (which also requires `request_expired = 0`), and only the winning expiry fences, voids and aborts; `persistedRequest` carries `Deadline`; the abbreviated call sites name their full arguments. Rev 13's review (`selfserve-plan2-review13.md`: N11, N10a closed; N10b narrowed; N12 new) is answered: the maintenance-to-idle consume requires `cut_requested_at = 0` in its predicate, so a cut that won makes a stale consume fail and the worker re-reads into the handoff (N10b); `MarkSelfServeCut` also transfers an outstanding unconfirmed `maintain` request's `request_deadline` to the cut's copy allowance in the same UPDATE, so one deadline policy governs adoption (N12). Rev 12's review (`selfserve-plan2-review12.md`: N9 closed; N10 narrowed; N11 new) is answered: **a request deadline exists only for writes whose expiry action is abort** (the pre-open writes: queue, stage, maintain, precheck, start, seal, flip, open); the post-open and clearing writes (gate 1, gate 2, finish, the abort path's abort and finish, estimate) carry `request_deadline = 0`, which `selfServeApply` never checks, so a page-and-retain phase whose evidence arrives late still clears the move (N11); abandonment rolls the next wait's deadline in its own UPDATE (`AbandonSelfServeRequest(id, nonce, nextDeadline)`) (N10a); a maintenance round's allowance starts at its issue and `ready` has **no step deadline while idle** (`next_maintenance_at` schedules the next round; `step_deadline = 0` between rounds) (N10b). Rev 11's review (`selfserve-plan2-review11.md`: N8 closed; N9 narrowed; N10 new) is answered: `selfServeApply` checks the request's deadline **before every forward-write attempt, replays included**, and an expired request leaves the loop as `errRequestExpired` for the fence-and-classify path, so no retry can open after the deadline inside one invocation (N9); a same-phase consume **rolls the next wait's deadline** in the same UPDATE (`ConsumeSelfServeRequest(id, nonce, nextStep, nextDeadline)`), issuance copies that fresh deadline into `request_deadline`, and the phase-wide limits stay on `staging_started_at` and `window_deadline` (N10). Rev 10's review (`selfserve-plan2-review10.md`: N6 closed; N8 narrowed; N9 new) is answered: **every** writer, the operator doors included, proposes its stamp as `max(now, the observed record's previous stamp + 1 ns)` and the mutation stores it verbatim, so stamps are monotonic under a backward clock and supersession by stamp is sound (N8); every request persists **its own deadline** (`request_deadline`, the issuing phase's step deadline), and an unconfirmed request whose deadline has passed while the record is at or before `offline-landed` is never replayed from **any** row phase, `open-uncertain` included: fence, classify, confirm if the write won, else void and abort (N9). Rev 9's review (`selfserve-plan2-review9.md`: 8, 10, 13, 20, N7 closed) is answered: **a committed confirmation expiry wins over a later cut** (`MarkSelfServeCut` also requires `expiry_pending = 0`; the cut door answers 409 "the move expired. prepare again"), and the persisted expiry carries its kind and deadline so execution revalidates its identity (N6); **an operator's newer staging stamp supersedes the job's outstanding wait**: when the record's stamp for the awaited field has moved past `request_proposed` without the receipt changing, the job abandons its request and issues a fresh bound round, with the crash between the two stated (N8). Rev 8's review (`selfserve-plan2-review8.md`: 21, N4 closed) is answered: reconciliation maps the record to a **phase and substep** (the first substep whose transition the record has not made), so an operator's progress never makes the job repeat a refused transition (item 10); abandonment is only for **leaving** a wait (abort, cancel, operator supersession), never for a page-and-retain expiry, with the phase table stated (item 13); the confirmation-deadline expiry commits only against `cut_requested_at = 0` and the deadline it saw, and the cut's copy expiry is a different decision (N6); the cut handoff persists which continuation it chose as a `restage` substep (`adopted` or `issue`) in the advancing transaction (N7); acceptance and discovery share one participant predicate (item 8); the superseded tag arrangement is deleted (item 20). Rev 7's review (`selfserve-plan2-review7.md`: N1, N2, N3, N5 closed) is answered: a phase with several writes has a durable **substep** (`phase_step`), advanced atomically with the consumption that ends the previous substep, and the phase function dispatches on it (item 10); confirmation is the **receipt alone** (no state predicate), phase progress is reconciliation's business (item 10); issuance refused with no outstanding request is its own result (`errRequestRefused`) (item 10); a confirmed request whose wait will never be satisfied is **abandoned** (consumed without its wait) by abort, expiry and operator supersession, and the finaliser consumes a confirmed clearing request in the transaction that records the outcome (item 13); `maintain` updates the stage identity like `stage` (N4); the cut's `MarkSelfServeCut` retires the ready deadline in the same UPDATE and ready expiry is conditional on no accepted cut (N6); return reports are **immutable per request key** (item 21); an id-less forward keeps `BoxID` membership and drops only the instance comparison (item 8); one untagged bridge file with the complete helper closure (item 20). Rev 6's review (`selfserve-plan2-review6.md`: items 1, 6, 17 closed) is answered: the request lifecycle is a complete state machine (`issued → confirmed → consumed`, or `voided`) with a **resume** path for a confirmed-unconsumed request, a standalone consume for same-phase sequences (start then seal, finish gate 1 then finish, round after round, estimate), issuance that returns the existing request so a caller of another kind or phase stops instead of interpreting it, and no-op-free receipts (a request-bearing mutation always writes its receipt) (item 10); the bound write checks **incarnation and sequence** (`provision.Expect{Incarnation, Seq}`) for the job and the operator doors (N5, item 13); every finalisation requires no outstanding request after a fence (item 13); the expiry branch classifies the fenced observation (N3); the cut is a **handoff**: the door marks `cut_requested_at` and the job turns the outstanding maintenance round into the first cut round (N4); event delivery marks `emitted` inside the transaction before delivering and the terminal event is inserted with the outcome (N1); per-box FIFO is stated (N2); the open intent is prepared before the request is issued so adoption and the persisted proposal agree, return requests carry a full identity ordered deterministically, reports name the request key, and acceptance binds the report to the reverse move's selected target (item 21); `Expect`'s real fields, `Load`'s absence distinction and one id-less rule (item 8); the fixtures move to an untagged `linux` file so today's tests keep compiling, `CopyEvent` carries its completion fields, the saved book is validated before seeding, `DiskFreeBytes` is numeric (item 20). Rev 5's review (`selfserve-plan2-review5.md`: items 2, 3, 4, 5, 7, 9, 11, 12, 14, 15, 16, 18, 19 closed) is answered: every job and operator record write is **predecessor-bound** (`provision.SetRecordMoveAt(ctx, s, acct, app, host, expectSeq, mut)`: one CAS write against the record whose `record_seq` equals the persisted expectation, no fresh-read retry), so a voided request can never land and an operator door never rebuilds a write after a reclaim fence (items 10, 13); every self-serve mutation stamps a **nonce receipt** on the record (`SelfServe.LastRequest`, or `LastMove.Request` for a clearing write), so confirmation is exact for every transition and distinct from an operator's reconciliation (item 10); confirm and advance require an unconsumed, unvoided request, so a stale worker cannot confirm a voided one (item 10); "pending" means **no confirmed request for the phase's write**, so the interval before issuance is compatible with the predecessor state (item 10); an expired uncommitted open is fenced and aborted, never replayed (N1); the stage book is reconciled against each complete successful round so deleted keys leave it (item 6); identity records are loaded by `boxid.Key(BoxID)` and `Verified` is narrowed to what `Enroll` proves (item 8); the gate's exception for an authorised malformed hostname and plan 1's outer 503 are declared (item 1); the view joins row id, move id, self-serve id and incarnation (item 17); the open record adopts an existing complete intent, return discovery and acceptance compare `InstanceID`, incarnation is re-checked at the door and in the guarded write, and `Layout` comes from the source's `stopped` report so every operator move has it (item 21); a maintenance restage in `ready` has its own kind and keeps the confirmation deadline (N2); the gate types, `StagedRequest`, failed diagnostics, the request argument to `confirmed`, and the bridge's compile rule are stated (item 20). Reviews: rev 1 (`after236/selfserve-plan2-review.md`, 20 findings), rev 2 (`selfserve-plan2-review2.md`, 21), rev 3 (`selfserve-plan2-review3.md`, 21 items), rev 4 (`selfserve-plan2-review4.md`, 21 items, 9 closed). Rev 5 is **self-contained** (nothing is "as rev N"; every schema, phase, predicate, sentence and test list is in this document) and answers rev 4's open items: the gate orders authentication, host shape and the capability check before the limiter and the no-bucket rail (item 1); operator moves keep today's admission (nil or stale metrics, no identity record) and only the self-serve path requires fresh identity; `BootID` is dropped (items 4, 8); the evidence reader takes the move and a freshness table is stated (item 5); the staged inventory is a **move-bound book** built from every copy event the mirror reports, including same-ETag and deferred copies, through a new observer hook that leaves ordinary mirroring unchanged (item 6); a participant is identified by `BoxID` **and** `InstanceID` in the resolver, in report validation and at every gate, and abort never requires the target (item 8); every request persists its **nonce, expected sequence and proposed stamp before the write**, confirmation is conditional on the record carrying that exact stamp, replay is idempotent, and consumption and the phase advance are one UPDATE predicated on the nonce (item 10); compatible sets include the predecessor state while a phase's write is pending (item 10); expiry is bound to its phase and cleared on reconciliation into another phase, with the health rule taking precedence (item 12); abandonment is conditional on the fenced observation and persisted before release, and every reclaim fences first, operator reservations included (item 13); own retries are accepted only for a complete expected selection after the whole table validates (item 14); the status view joins rows regardless of the mode (item 17); events carry a per-row sequence and are delivered inside the caller's transaction, the harness is an exported bridge **inside package host** behind the `e2e` tag with the CI step named, the Go declarations are complete, and queue ordering is stated (item 20); the open record is written **before** the open transition with the proposed open time, return requests have unique durable identities, the report path is named, acceptance is bound to the lifetime and the forward's participants, and leftovers are judged by the forward's layout (item 21). |
| Builds on plan 1 (`2026-10-22-self-serve-moves-1-lifetimes-and-order.md`, rev 13): records carry a lifetime, `record_seq` and `move_seq`, rows carry the pair, readers and writers are judged, `fenceRecord(ctx, acct, app, host, op fenceOwner) (fenceResult, Observation, error)` exists, the console runs behind the lifetimes cutover. This plan adds everything a Tito customer's request needs up to a finished move except the holding page, the edge probe mode and cleanup (plans 3 and 4). |
| @@ -103,7 +103,7 @@ Builds on plan 1 (`2026-10-22-self-serve-moves-1-lifetimes-and-order.md`, rev 13 |
| **Interfaces:** |
| - Table `self_serve_moves (id TEXT PRIMARY KEY, account TEXT, app TEXT, host TEXT, incarnation INTEGER, from_region TEXT, to_region TEXT, participants TEXT, display_name TEXT, requested_by TEXT, phase TEXT, phase_started_at INTEGER, step_deadline INTEGER, window_deadline INTEGER, expiry_pending INTEGER, expiry_phase TEXT, expiry_kind TEXT, expiry_deadline INTEGER, staging_started_at INTEGER, request_issued INTEGER, request_kind TEXT, request_phase TEXT, request_nonce TEXT, request_expect_seq INTEGER, request_proposed INTEGER, request_confirmed INTEGER, request_consumed INTEGER, request_voided INTEGER, request_abandoned INTEGER, request_expect_inc INTEGER, request_deadline INTEGER, request_expired INTEGER, next_maintenance_at INTEGER, cut_requested_at INTEGER, phase_step TEXT, stage_request INTEGER, precheck_request INTEGER, rounds INTEGER, estimate_seconds INTEGER, estimate_terms TEXT, reason TEXT, health_attempts INTEGER, health_consecutive_ok INTEGER, health_first_ok_at INTEGER, health_last_probe_at INTEGER, health_last_error TEXT, finish_gate TEXT, notified_seq INTEGER, event_seq INTEGER, outcome TEXT, outcome_reason TEXT, warning TEXT, abort_requested_at INTEGER, source_reopened_at INTEGER, abort_finished_at INTEGER, effects_done INTEGER, release_pending INTEGER, open_record_pending INTEGER, created_at INTEGER, updated_at INTEGER)`; `CREATE UNIQUE INDEX IF NOT EXISTS self_serve_moves_open ON self_serve_moves(account, app, host) WHERE outcome = ''`. |
| -- **The request lifecycle** (one request at a time per row): `IssueSelfServeRequest(id, kind, phase string, pr persistedRequest, now time.Time) (issued bool, existing *persistedRequest, err error)` (`WHERE phase = ? AND outcome = '' AND (request_consumed <> 0 OR request_issued = 0)`: sets `request_issued = now, request_kind, request_phase, request_nonce, request_expect_inc, request_expect_seq, request_proposed, request_deadline = the issuing phase's step deadline **for a request whose expiry action is abort** (queue, stage, maintain, precheck, start, seal, flip, open) and **0 for every other kind** (gate 1, gate 2, finish, abort, the abort path's finish, estimate: their phases page and retain, and their writes must always be allowed to complete), request_confirmed = 0, request_consumed = 0, request_voided = 0, request_expired = 0`; not issued → `existing` is the outstanding request, whatever its kind and phase); `ConsumeSelfServeRequest(id, nonce, nextStep string, nextDeadline time.Time) (bool, error)` (`WHERE request_nonce = ? AND request_confirmed <> 0 AND request_consumed = 0`: `request_consumed = 1, phase_step = nextStep, step_deadline = nextDeadline` in one statement, so **each wait's deadline starts when the previous request is consumed**: a staging round's `max(10 min, 3 × est.Copy)` runs from its own issue, each finishing gate gets its own 10 min, each maintenance round its own allowance; the phase-wide limits (`now - staging_started_at > 30 min`, `WindowDeadline`) are separate columns and unaffected; `AdvanceSelfServe` sets `step_deadline` for the new phase's first wait likewise; a crash between a consume and the next issue finds the fresh deadline already persisted; the standalone consume for a same-phase sequence: the substep advances exactly when the previous request is consumed, so a crash between them cannot lose the position); `AbandonSelfServeRequest(id, nonce string, nextDeadline time.Time) (bool, error)` (`WHERE request_nonce = ? AND request_confirmed <> 0 AND request_consumed = 0`: `request_consumed = 1, request_abandoned = 1, step_deadline = nextDeadline` in one statement, so the replacement wait gets a fresh allowance and a crash between the abandon and the next issue finds it persisted; only abort, an expiry action and operator supersession call it: the request's write committed but its wait will never be satisfied, so the phase gives it up and proceeds); `ConfirmSelfServeRequest(id, nonce string, now time.Time) (bool, error)` (`WHERE request_nonce = ? AND request_consumed = 0 AND request_voided = 0 AND request_confirmed = 0`: `request_confirmed = now`; false → the caller re-reads the row: confirmed already → **resume** (success), voided or consumed → stop; for kinds `stage` **and `maintain`** also `stage_request = request_proposed`, for `precheck` also `precheck_request = request_proposed`); `VoidSelfServeRequest(id, nonce string) error` (`WHERE request_nonce = ? AND request_consumed = 0 AND request_confirmed = 0`: `request_consumed = 1, request_voided = 1`; a confirmed request is never voided; the phase function decides again); `AdvanceSelfServe(id, from, to, reason string, step, window time.Time, nonce string, now time.Time) (bool, error)`: one UPDATE with the predicate `phase = ? AND (abort_requested_at = 0 OR ? IN ('reverting','done','opened','health','finishing')) AND (? = '' OR request_nonce = ?)` that sets the phase, `phase_started_at`, the deadlines, `expiry_pending = 0, expiry_phase = ''` when `to <> expiry_phase`, and, when a nonce is given, additionally requires `request_nonce = ? AND request_confirmed <> 0 AND request_consumed = 0` and sets `request_consumed = 1` (**consumption and the advance are one statement**), and inserts the phase event in the same transaction (`event_seq = event_seq + 1`); a stale worker holding another nonce, or the same nonce after a void, cannot confirm or consume. |
| +- **The request lifecycle** (one request at a time per row): `IssueSelfServeRequest(id, kind, phase string, pr persistedRequest, now time.Time) (issued bool, existing *persistedRequest, err error)` (`WHERE phase = ? AND outcome = '' AND (request_consumed <> 0 OR request_issued = 0)`: sets `request_issued = now, request_kind, request_phase, request_nonce, request_expect_inc, request_expect_seq, request_proposed, request_deadline = the issuing phase's step deadline **for a request whose expiry action is abort** (queue, stage, maintain, precheck, start, seal, flip, open) and **0 for every other kind** (gate 1, gate 2, finish, abort, the abort path's finish, estimate: their phases page and retain, and their writes must always be allowed to complete), request_confirmed = 0, request_consumed = 0, request_voided = 0, request_expired = 0`; not issued → `existing` is the outstanding request, whatever its kind and phase); `ConsumeSelfServeRequest(id, nonce, nextStep string, nextDeadline time.Time) (bool, error)` (`WHERE request_nonce = ? AND request_confirmed <> 0 AND request_consumed = 0`: `request_consumed = 1, phase_step = nextStep, step_deadline = nextDeadline` in one statement, so **each wait's deadline starts when the previous request is consumed**: a staging round's `max(10 min, 3 × est.Copy)` runs from its own issue, each finishing gate gets its own 10 min, each maintenance round its own allowance; the phase-wide limits (`now - staging_started_at > 30 min`, `WindowDeadline`) are separate columns and unaffected; `AdvanceSelfServe` sets `step_deadline` for the new phase's first wait likewise; a crash between a consume and the next issue finds the fresh deadline already persisted; the standalone consume for a same-phase sequence: the substep advances exactly when the previous request is consumed, so a crash between them cannot lose the position); `AbandonSelfServeRequest(id, nonce string, nextDeadline time.Time) (bool, error)` (`WHERE request_nonce = ? AND request_confirmed <> 0 AND request_consumed = 0`: `request_consumed = 1, request_abandoned = 1, step_deadline = nextDeadline` in one statement, so the replacement wait gets a fresh allowance and a crash between the abandon and the next issue finds it persisted; only abort, an expiry action and operator supersession call it: the request's write committed but its wait will never be satisfied, so the phase gives it up and proceeds); `ConfirmSelfServeRequest(id, nonce string, now time.Time) (bool, error)` (`WHERE request_nonce = ? AND request_consumed = 0 AND request_voided = 0 AND request_confirmed = 0`: `request_confirmed = now, request_expired = 0` (a receipt that arrives after a won expiry resolves the request successfully and retires the expiry decision, so the cut's `request_expired = 0` predicate is satisfied again; `TestLateReceiptRetiresTheExpiry`: expiry wins, the delayed PUT lands before the fence, the receipt confirms, the maintenance report is consumed into idle, a cut in that idle interval is accepted); false → the caller re-reads the row: confirmed already → **resume** (success), voided or consumed → stop; for kinds `stage` **and `maintain`** also `stage_request = request_proposed`, for `precheck` also `precheck_request = request_proposed`); `VoidSelfServeRequest(id, nonce string) error` (`WHERE request_nonce = ? AND request_consumed = 0 AND request_confirmed = 0`: `request_consumed = 1, request_voided = 1`; a confirmed request is never voided; the phase function decides again); `AdvanceSelfServe(id, from, to, reason string, step, window time.Time, nonce string, now time.Time) (bool, error)`: one UPDATE with the predicate `phase = ? AND (abort_requested_at = 0 OR ? IN ('reverting','done','opened','health','finishing')) AND (? = '' OR request_nonce = ?)` that sets the phase, `phase_started_at`, the deadlines, `expiry_pending = 0, expiry_phase = ''` when `to <> expiry_phase`, and, when a nonce is given, additionally requires `request_nonce = ? AND request_confirmed <> 0 AND request_consumed = 0` and sets `request_consumed = 1` (**consumption and the advance are one statement**), and inserts the phase event in the same transaction (`event_seq = event_seq + 1`); a stale worker holding another nonce, or the same nonce after a void, cannot confirm or consume. |
| - Other store methods: `AdmitSelfServeMove(m SelfServeMove) (admitted bool, open *SelfServeMove, err error)`; `SelfServeMove(id string) (*SelfServeMove, error)`; `SelfServeMoveForHost(acct, app, host string) (open, terminal *SelfServeMove, err error)` (the open row, and the newest terminal row); `OpenSelfServeMoves() ([]SelfServeMove, error)` (`outcome = '' OR release_pending = 1 OR effects_done = 0 OR open_record_pending = 1`, **ordered by `created_at, id`**: the pass steps rows in that order; **per-box FIFO**: a row in `reserving` may reserve only when no older open row (by `created_at, id`) that shares any participant `BoxID` is still in `requested` or `reserving`; otherwise it waits with `reason` "waiting for an earlier move on this box"; rows with disjoint participants never wait on each other); `MarkSelfServeCut(id string, now time.Time, copyDeadline time.Time) (bool, error)` (`WHERE phase = 'ready' AND abort_requested_at = 0 AND cut_requested_at = 0 AND expiry_pending = 0 AND request_expired = 0`: a confirmation expiry already committed wins, and the cut door re-reads and answers 409 "the move expired. prepare again"; sets `cut_requested_at = now`, **`window_deadline = 0`** (the confirmation deadline is retired by the cut that won), `step_deadline = copyDeadline` (the cut's copy wait) **and, when an unconsumed `maintain` request is outstanding, `request_deadline = copyDeadline`** (the adopted round gets the cut's allowance whether or not its write has committed yet) in the same UPDATE; the cut door's only write); the `ready` expiry rule applies only while `cut_requested_at = 0`; `SetSelfServeEstimate`, `RecordSelfServeHealth(id string, probeAt time.Time, ok bool, errText string) error` (`health_attempts + 1`; `consecutive_ok = CASE ok THEN consecutive_ok + 1 ELSE 0`; `first_ok_at` kept once set), `SetSelfServeFinishGate`, `SetSelfServeWarning`, `SetSelfServeNotifiedSeq(id string, seq uint64) error`, `SetSelfServeExpiry(id, phase string, kind string, sawDeadline time.Time, pending bool) (bool, error)` (`kind` is `window` or `step`; the UPDATE also records `expiry_kind = kind, expiry_deadline = sawDeadline`, and the expiry **action** later re-reads the row and runs only when `expiry_pending`, `expiry_phase`, `expiry_kind` and `expiry_deadline` still match what it is acting on (an expiry whose identity changed is dropped); the UPDATE's predicate is `phase = ? AND (window_deadline = ? for window, step_deadline = ? for step) AND (kind <> 'window' OR cut_requested_at = 0)`, so a worker whose snapshot predates a successful cut cannot record the obsolete confirmation expiry (`TestStaleExpiryLosesToTheCut`: a worker paused across the cut at T-1 resumes at T+1 and its `window` expiry is refused; the copy's `step` expiry after the cut is accepted; **expiry first**: the window expiry commits at T+ε, then the cut door → 409 expired, and the abort runs; a restart between the committed expiry and the cut → the same); `SetSelfServeExpiry` returning false → the worker re-reads the row and decides again`, `RequestSelfServeAbort(id string, phases []string, now time.Time) (bool, error)` (`WHERE phase IN (...) AND abort_requested_at = 0`), `SetSelfServeReopened`, `SetSelfServeAbortFinished`, `FinishSelfServeMove(id, outcome, reason, warning string, now time.Time) error` (`release_pending = 1`, `effects_done = 0`), `SetSelfServeEffectsDone(id string) error`, `ClearSelfServeRelease(id string) error`, `SetSelfServeOpenRecordPending(id string, pending bool) error`. |
| - Table `self_serve_events (id TEXT, seq INTEGER, phase TEXT, account TEXT, app TEXT, host TEXT, at INTEGER, reason TEXT, detail TEXT, emitted INTEGER, PRIMARY KEY (id, seq))`: `InsertSelfServeEventTx(tx *sql.Tx, e SelfServeEvent) error` (allocates `seq` from the row's `event_seq` in the same transaction; used by `AdvanceSelfServe` and by every refusal, fact decision, lane probe, health probe and timing); `UnemittedSelfServeEvents(limit int) ([]SelfServeEvent, error)`; `DeliverSelfServeEvent(ctx context.Context, e SelfServeEvent, deliver func(tx *sql.Tx) error) error` (one transaction: first `UPDATE self_serve_events SET emitted = 1 WHERE id = ? AND seq = ? AND emitted = 0`; zero rows → commit nothing and return (another worker delivered it); else `deliver(tx)` inserts the audit row through `auditActivityTx(tx, ...)`; a failure rolls both back). The terminal event is inserted by `FinishSelfServeMove` in the transaction that sets the outcome, so a crash between finalisation steps never allocates a second terminal event. Table `self_serve_timings (id TEXT, mark TEXT, account TEXT, app TEXT, host TEXT, at INTEGER, bytes INTEGER, PRIMARY KEY (id, mark))`. |
| - Phases: `requested`, `reserving`, `queued`, `reports`, `staging`, `prechecks`, `ready`, `restage`, `barrier`, `stopping`, `sealing`, `landing`, `landed`, `open-uncertain`, `opened`, `health`, `finishing`, `reverting`, `done`. |
| @@ -161,9 +161,9 @@ Builds on plan 1 (`2026-10-22-self-serve-moves-1-lifetimes-and-order.md`, rev 13 |
| - Test: `internal/e2e/selfserve/job_test.go` |
| **Interfaces:** |
| -- `type persistedRequest struct { Kind, Phase, Nonce string; ExpectInc, ExpectSeq uint64; Proposed, Deadline time.Time; Confirmed, Consumed, Voided, Expired bool }`; `func (s *Server) selfServeApply(ctx context.Context, t *leaseToken, row *SelfServeMove, req request, proposed *time.Time, mut func(at time.Time, rc provision.Receipt) func(*provision.Record) error) (*provision.Record, error)` with `type request struct { Kind, Phase string }`: (1) the judged record → `pr.ExpectInc = rec.Incarnation`, `pr.ExpectSeq = rec.RecordSeq`; `pr.Proposed = *proposed` when given (the open intent) else `max(now, latest stamp on the record + 1 ns)`; `pr.Nonce = random`; `IssueSelfServeRequest(id, kind, phase, pr, now)`: not issued and `existing == nil` → `errRequestRefused` (the row left the phase or ended; the step ends); `existing.Kind != req.Kind || existing.Phase != req.Phase` → return `errRequestForeign` (this caller never interprets another request; the step ends and reconciliation decides next tick); `existing.Confirmed && !existing.Consumed` → **resume**: return the record (the caller continues its wait and the notification catch-up); otherwise `pr = *existing` and go to (3); (2) **first, when `pr.Deadline != 0` and `now >= pr.Deadline`**, the expiry is **decided durably**: `ExpireSelfServeRequest(id, pr.Nonce, pr.Deadline)` (`UPDATE ... SET request_expired = 1 WHERE request_nonce = ? AND request_deadline = ? AND cut_requested_at = ? AND request_consumed = 0 AND request_expired = 0`, the third `?` being the `cut_requested_at` the worker read with `pr`, so a cut that landed since makes the decision lose, while after a cut the decision is made against the current deadline as usual); true → return `errRequestExpired` without writing (the step's fence-and-classify path takes over, and only this winner may fence, void and abort); false → **reload the request** and dispatch on what is found: the same nonce, still unconsumed and unexpired → back to (2) with the fresh `pr` (a cut extended the deadline); the same nonce **already expired** (`pr.Expired`, a previous worker won and crashed before fencing) → return `errRequestExpired` **without re-deciding** (the fence-and-classify path resumes; a crash around the fence re-enters here and resumes again); consumed or voided → stop (`errRequestRefused`); another nonce → `errRequestForeign` (`TestStaleRequestDeadlineLosesToTheCut`: a worker paused in its confirmation read across a cut that extends the deadline → its expiry UPDATE matches nothing, it reloads and continues under the new allowance; expiry first → the cut door is refused; **after a cut**, an open request whose extended deadline passes expires normally against the current `cut_requested_at`; **a won expiry followed by a crash before the fence** → the restart finds `pr.Expired` and fences, classifies and aborts without a second decision; crashes around the fence in both orders); otherwise a request with `Deadline == 0` is never expired by this check (`TestLateEvidenceStillClearsTheMove`: a source's `gone` report arriving after `finishing`'s deadline → the finish is written and the reservations release; the same for `reverting`'s facts and for an already-issued uncommitted finish recovering after the deadline); then `provision.SetRecordMoveAt(ctx', s, acct, app, host, provision.Expect{Incarnation: pr.ExpectInc, Seq: pr.ExpectSeq}, mut(pr.Proposed, Receipt{pr.Nonce}))` with the lease-bounded context and the write guard: **one CAS attempt against the record at that incarnation and sequence; no fresh-read retry inside**; (3) **confirmation** from a fresh judged read, whatever (2) returned: `rec` carries the receipt (`SelfServe.LastRequest == pr.Nonce`, or `LastMove.Request == pr.Nonce` for a clearing write) → `ConfirmSelfServeRequest(id, nonce, now)` (**the receipt alone confirms**; a box that has already advanced the record past the written state (seal → sealed, flip → landed) does not change that; the `confirmed` callback is dropped and phase progress is reconciliation's business) (false → re-read: already confirmed → resume; voided → stop); else `rec.Incarnation == pr.ExpectInc && rec.RecordSeq == pr.ExpectSeq` (unchanged) → the write did not land: repeat (2), **which re-checks the deadline first** (idempotent replay of the same bound write; bounded by the lease and the deadline; `TestReplayStopsAtTheDeadlineInsideOneInvocation`: an open request whose PUT fails at T-1 s and whose confirmation read returns at T+1 s is not replayed; the step fences and classifies; the same with the PUT having committed → confirmed); else (the record advanced without the receipt: another writer, an operator) → `VoidSelfServeRequest(id, nonce)` and return `errRequestVoided`; the bound write makes this safe: a PUT transmitted against `ExpectSeq` cannot commit on a record that has moved past it; after a confirmed write: `bumpSerialErr` and, on success, `SetSelfServeNotifiedSeq(id, rec.RecordSeq)`; a failed bump leaves `notified_seq` behind and every later tick bumps again while `rec.RecordSeq != notified_seq`. A crash anywhere resumes at (3) on the next tick from the persisted request. |
| +- `type persistedRequest struct { Kind, Phase, Nonce string; ExpectInc, ExpectSeq uint64; Proposed, Deadline, CutRequestedAt time.Time; Confirmed, Consumed, Voided, Expired bool }` (loaded by one `SELECT` of the row's request columns together with `cut_requested_at`, on the first load and on every reload, so the decision below always compares the cut state read with the request it is deciding about`; `func (s *Server) selfServeApply(ctx context.Context, t *leaseToken, row *SelfServeMove, req request, proposed *time.Time, mut func(at time.Time, rc provision.Receipt) func(*provision.Record) error) (*provision.Record, error)` with `type request struct { Kind, Phase string }`: (1) the judged record → `pr.ExpectInc = rec.Incarnation`, `pr.ExpectSeq = rec.RecordSeq`; `pr.Proposed = *proposed` when given (the open intent) else `max(now, latest stamp on the record + 1 ns)`; `pr.Nonce = random`; `IssueSelfServeRequest(id, kind, phase, pr, now)`: not issued and `existing == nil` → `errRequestRefused` (the row left the phase or ended; the step ends); `existing.Kind != req.Kind || existing.Phase != req.Phase` → return `errRequestForeign` (this caller never interprets another request; the step ends and reconciliation decides next tick); `existing.Confirmed && !existing.Consumed` → **resume**: return the record (the caller continues its wait and the notification catch-up); otherwise `pr = *existing` and go to (3); (2) **first, when `pr.Deadline != 0` and `now >= pr.Deadline`**, the expiry is **decided durably**: `ExpireSelfServeRequest(id, pr.Nonce, pr.Deadline, pr.CutRequestedAt)` (`UPDATE ... SET request_expired = 1 WHERE request_nonce = ? AND request_deadline = ? AND cut_requested_at = ? AND request_consumed = 0 AND request_expired = 0`, the third `?` being `pr.CutRequestedAt`, the cut state read with the request, so a cut that landed since makes the decision lose, while after a cut the decision is made against the current deadline as usual); true → return `errRequestExpired` without writing (the step's fence-and-classify path takes over, and only this winner may fence, void and abort); false → **reload the request** and dispatch on what is found: the same nonce, still unconsumed and unexpired → back to (2) with the fresh `pr` (a cut extended the deadline); the same nonce **already expired** (`pr.Expired`, a previous worker won and crashed before fencing) → return `errRequestExpired` **without re-deciding** (the fence-and-classify path resumes; a crash around the fence re-enters here and resumes again); consumed or voided → stop (`errRequestRefused`); another nonce → `errRequestForeign` (`TestStaleRequestDeadlineLosesToTheCut`: a worker paused in its confirmation read across a cut that extends the deadline → its expiry UPDATE matches nothing, it reloads and continues under the new allowance; expiry first → the cut door is refused; **after a cut**, an open request whose extended deadline passes expires normally against the current `cut_requested_at`; **a won expiry followed by a crash before the fence** → the restart finds `pr.Expired` and fences, classifies and aborts without a second decision; crashes around the fence in both orders); otherwise a request with `Deadline == 0` is never expired by this check (`TestLateEvidenceStillClearsTheMove`: a source's `gone` report arriving after `finishing`'s deadline → the finish is written and the reservations release; the same for `reverting`'s facts and for an already-issued uncommitted finish recovering after the deadline); then `provision.SetRecordMoveAt(ctx', s, acct, app, host, provision.Expect{Incarnation: pr.ExpectInc, Seq: pr.ExpectSeq}, mut(pr.Proposed, Receipt{pr.Nonce}))` with the lease-bounded context and the write guard: **one CAS attempt against the record at that incarnation and sequence; no fresh-read retry inside**; (3) **an already-expired request first**: when `pr.Expired` (a previous worker won the expiry and died before finishing it, or this worker just won it), return `errRequestExpired` at once (the step's fence-and-classify path runs; it never reaches the generic replay or void below); otherwise **confirmation** from a fresh judged read, whatever (2) returned: `rec` carries the receipt (`SelfServe.LastRequest == pr.Nonce`, or `LastMove.Request == pr.Nonce` for a clearing write) → `ConfirmSelfServeRequest(id, nonce, now)` (**the receipt alone confirms**; a box that has already advanced the record past the written state (seal → sealed, flip → landed) does not change that; the `confirmed` callback is dropped and phase progress is reconciliation's business) (false → re-read: already confirmed → resume; voided → stop); else `rec.Incarnation == pr.ExpectInc && rec.RecordSeq == pr.ExpectSeq` (unchanged) → the write did not land: repeat (2), **which re-checks the deadline first** (idempotent replay of the same bound write; bounded by the lease and the deadline; `TestReplayStopsAtTheDeadlineInsideOneInvocation`: an open request whose PUT fails at T-1 s and whose confirmation read returns at T+1 s is not replayed; the step fences and classifies; the same with the PUT having committed → confirmed); else (the record advanced without the receipt: another writer, an operator) → `VoidSelfServeRequest(id, nonce)` and return `errRequestVoided`; the bound write makes this safe: a PUT transmitted against `ExpectSeq` cannot commit on a record that has moved past it; after a confirmed write: `bumpSerialErr` and, on success, `SetSelfServeNotifiedSeq(id, rec.RecordSeq)`; a failed bump leaves `notified_seq` behind and every later tick bumps again while `rec.RecordSeq != notified_seq`. A crash anywhere resumes at (3) on the next tick from the persisted request. |
| - `type provision.Expect struct { Incarnation, Seq uint64 }`; `provision.SetRecordMoveAt(ctx, s, acct, app, host string, expect Expect, mut) (*provision.Record, error)`: reads the judged record; `rec.Incarnation != expect.Incarnation || rec.RecordSeq != expect.Seq` → `ErrCASConflict` without writing; otherwise one `WriteRecord` against the observed ETag; a CAS conflict → `ErrCASConflict` (no retry). `SetRecordMove` (today's, with its retry) is no longer used by any self-serve or operator move writer. |
| -- `selfServeStep`: (1) the row's deadlines: an expired `StepDeadline`, or an expired `WindowDeadline` while `cut_requested_at = 0`, → `SetSelfServeExpiry(id, phase, kind, sawDeadline, true)` (`kind` `window` or `step` by which deadline passed, `sawDeadline` the value the worker read) (no action yet); (2) the judged record; (3) **reconcile forward** by the compatible sets (Task 4): an unconfirmed request → step (3) of `selfServeApply` first, **except** when `selfServeApply`'s step (2) returned `errRequestExpired` for the outstanding request (**the only place expiry is decided**, whatever the row's current phase: `landed`, `open-uncertain` or any other; the step never compares deadlines itself) and the record is at or before `offline-landed`: then the record is observed once; the receipt present → confirmed (the write won before the expiry, the expiry is post-write); absent → `fenceRecord` (plan 1; so the outstanding bound PUT can never land) and **the fenced observation is classified**: the receipt present in it → the write won during the fence: confirm and proceed forward (an open that committed is reconciled as opened, never aborted); absent → `VoidSelfServeRequest` and the expiry action runs (abort), **never a replay** (`TestExpiredOpenIsNeverReplayed`); then the row's phase not in the record's set → `AdvanceSelfServe` to the set's first phase (without the abort predicate for `opened`), which clears a foreign expiry; (4) the expiry action for an `expiry_pending` whose `expiry_phase == phase`: the record at or before `offline-landed` → abort; committed open → the blocked action (page); unreadable → `open-uncertain`; `health` → warning, page, `finishing`; (5) the abort step when requested and the record at or before `offline-landed`; (6) the phase function. Every wait for a report is for `Request`/`StagedRequest == request_proposed` of the **confirmed** request; **supersession by stamp**: each tick the waiting phase also reads the record's stamp for the awaited field (`StagedAt` for `stage`/`maintain`, `PrecheckAt` for `precheck`); when it is **later** than `request_proposed` while the receipt is unchanged, an operator round has superseded the job's (`OfflineRestage`/`OfflinePrecheck` from the operator doors move the stamp without a receipt), so the job `AbandonSelfServeRequest`s its request and, on the next step, issues a fresh bound round (the abandon is one UPDATE; a crash after it finds no outstanding request and issues; a crash before it repeats the comparison); the job never adopts an operator round as its own (`TestOperatorRoundSupersedesTheJobsWait`: a confirmed restage at S1, an operator stage to S2, the stager reports S2 only → the job abandons S1 and issues S3, the cut proceeds; the same in `staging/round` and `ready/round`; a crash between the abandon and the issue); the report is consumed by the `AdvanceSelfServe` that leaves the waiting phase, with the nonce, **or**, inside a phase that writes more than once (`stopping`: start then seal; `finishing`: gate 1 then gate 2; `staging`: round after round; `prechecks`: the precheck then the estimate; `ready`: each maintenance round), by `ConsumeSelfServeRequest(id, nonce)` once its wait is satisfied, immediately before the next request is issued, and **the phase function dispatches on `phase_step`**: `stopping` = `start` → `seal`; `finishing` = `gate1` → `gate2`; `prechecks` = `precheck` → `estimate`; `reverting` = `abort` → `finish`; `staging` and `ready` = `round` (repeated); a phase entered by `AdvanceSelfServe` from the **job's own** progression starts at its first substep (`phase_step` set by the advance); a phase entered by **reconciliation** starts at the substep the record dictates (below); a restart resumes the substep recorded, never the phase's first write (`TestSubstepsResumeAfterEveryCrash`: a crash after consuming start and before issuing seal re-enters at `seal`; a crash with the estimate request outstanding re-enters at `estimate` and resumes it; the same for gate 1 → gate 2 and abort → finish). **Abandonment** is for **leaving a wait**, never for retaining it: when abort is requested, when an expiry whose action is **abort** fires, or when an operator transition supersedes the phase, a confirmed request whose wait is unsatisfied is `AbandonSelfServeRequest`ed before the abort or the reconciliation step issues its own request; an expiry whose action is page-and-retain (`opened`, `finishing`, `reverting`, `open-uncertain`) or warn-and-finish (`health`) abandons nothing and the wait continues (`TestPageOnlyExpiriesKeepTheWait`: the open request confirmed, `opened` missing at the deadline, the page sent, then the target reports opened and the nonce-bearing advance succeeds). By phase: `requested`/`reserving` resolve or fence an issued queue request before finalising or releasing; `queued`/`reports` abandon a superseded wait, a queued abort clears and finalises atomically; `staging`/`prechecks`/`ready` may abandon on cancel or aborting expiry; `restage`/`barrier`/`stopping`/`sealing`/`landing`/pre-open `landed` may abandon on aborting expiry after resolving the uncertain write; `open-uncertain` abandons nothing; a confirmed clearing write is consumed with the terminal outcome (`TestAbandonedRequestsNeverStrandAbort`: a confirmed stage request whose copy never reports, then cancel → the abort request issues; a confirmed precheck whose box died, then the deadline → abort issues). **Clearing requests**: `OfflineFinish`, `OfflineAbortAt` and the abort path's finish are confirmed from `LastMove.Request`; the finaliser's `FinishSelfServeMove` consumes that confirmed request in the same transaction as the outcome, so a crash between confirmation and finalisation resumes at the finaliser, not at a re-issue. |
| +- `selfServeStep`: (1) the row's deadlines: an expired `StepDeadline`, or an expired `WindowDeadline` while `cut_requested_at = 0`, → `SetSelfServeExpiry(id, phase, kind, sawDeadline, true)` (`kind` `window` or `step` by which deadline passed, `sawDeadline` the value the worker read) (no action yet); (2) the judged record; (3) **reconcile forward** by the compatible sets (Task 4): an unconfirmed request → step (3) of `selfServeApply` first, **except** when `selfServeApply`'s step (2) returned `errRequestExpired` for the outstanding request (**the only place expiry is decided**, whatever the row's current phase: `landed`, `open-uncertain` or any other; the step never compares deadlines itself) and the record is at or before `offline-landed`: then the record is observed once; the receipt present → confirmed (the write won before the expiry, the expiry is post-write); absent → `fenceRecord` (plan 1; so the outstanding bound PUT can never land) and **the fenced observation is classified**: the receipt present in it → the write won during the fence: confirm and proceed forward (an open that committed is reconciled as opened, never aborted); absent → `VoidExpiredSelfServeRequest(id, nonce, now)` (**one UPDATE**: `request_consumed = 1, request_voided = 1, abort_requested_at = now` where `abort_requested_at = 0`, so the won expiry's abort obligation is durable in the same statement as the void; a crash before it re-enters at `pr.Expired` and fences again, a crash after it finds `abort_requested_at` set and runs the abort step), and the abort runs, **never a replay** (`TestExpiredOpenIsNeverReplayed`); then the row's phase not in the record's set → `AdvanceSelfServe` to the set's first phase (without the abort predicate for `opened`), which clears a foreign expiry; (4) the expiry action for an `expiry_pending` whose `expiry_phase == phase`: the record at or before `offline-landed` → abort; committed open → the blocked action (page); unreadable → `open-uncertain`; `health` → warning, page, `finishing`; (5) the abort step when requested and the record at or before `offline-landed`; (6) the phase function. Every wait for a report is for `Request`/`StagedRequest == request_proposed` of the **confirmed** request; **supersession by stamp**: each tick the waiting phase also reads the record's stamp for the awaited field (`StagedAt` for `stage`/`maintain`, `PrecheckAt` for `precheck`); when it is **later** than `request_proposed` while the receipt is unchanged, an operator round has superseded the job's (`OfflineRestage`/`OfflinePrecheck` from the operator doors move the stamp without a receipt), so the job `AbandonSelfServeRequest`s its request and, on the next step, issues a fresh bound round (the abandon is one UPDATE; a crash after it finds no outstanding request and issues; a crash before it repeats the comparison); the job never adopts an operator round as its own (`TestOperatorRoundSupersedesTheJobsWait`: a confirmed restage at S1, an operator stage to S2, the stager reports S2 only → the job abandons S1 and issues S3, the cut proceeds; the same in `staging/round` and `ready/round`; a crash between the abandon and the issue); the report is consumed by the `AdvanceSelfServe` that leaves the waiting phase, with the nonce, **or**, inside a phase that writes more than once (`stopping`: start then seal; `finishing`: gate 1 then gate 2; `staging`: round after round; `prechecks`: the precheck then the estimate; `ready`: each maintenance round), by `ConsumeSelfServeRequest(id, nonce)` once its wait is satisfied, immediately before the next request is issued, and **the phase function dispatches on `phase_step`**: `stopping` = `start` → `seal`; `finishing` = `gate1` → `gate2`; `prechecks` = `precheck` → `estimate`; `reverting` = `abort` → `finish`; `staging` and `ready` = `round` (repeated); a phase entered by `AdvanceSelfServe` from the **job's own** progression starts at its first substep (`phase_step` set by the advance); a phase entered by **reconciliation** starts at the substep the record dictates (below); a restart resumes the substep recorded, never the phase's first write (`TestSubstepsResumeAfterEveryCrash`: a crash after consuming start and before issuing seal re-enters at `seal`; a crash with the estimate request outstanding re-enters at `estimate` and resumes it; the same for gate 1 → gate 2 and abort → finish). **Abandonment** is for **leaving a wait**, never for retaining it: when abort is requested, when an expiry whose action is **abort** fires, or when an operator transition supersedes the phase, a confirmed request whose wait is unsatisfied is `AbandonSelfServeRequest`ed before the abort or the reconciliation step issues its own request; an expiry whose action is page-and-retain (`opened`, `finishing`, `reverting`, `open-uncertain`) or warn-and-finish (`health`) abandons nothing and the wait continues (`TestPageOnlyExpiriesKeepTheWait`: the open request confirmed, `opened` missing at the deadline, the page sent, then the target reports opened and the nonce-bearing advance succeeds). By phase: `requested`/`reserving` resolve or fence an issued queue request before finalising or releasing; `queued`/`reports` abandon a superseded wait, a queued abort clears and finalises atomically; `staging`/`prechecks`/`ready` may abandon on cancel or aborting expiry; `restage`/`barrier`/`stopping`/`sealing`/`landing`/pre-open `landed` may abandon on aborting expiry after resolving the uncertain write; `open-uncertain` abandons nothing; a confirmed clearing write is consumed with the terminal outcome (`TestAbandonedRequestsNeverStrandAbort`: a confirmed stage request whose copy never reports, then cancel → the abort request issues; a confirmed precheck whose box died, then the deadline → abort issues). **Clearing requests**: `OfflineFinish`, `OfflineAbortAt` and the abort path's finish are confirmed from `LastMove.Request`; the finaliser's `FinishSelfServeMove` consumes that confirmed request in the same transaction as the outcome, so a crash between confirmation and finalisation resumes at the finaliser, not at a re-issue. |
| - **Reconciliation and recovery table** (the record is the truth): |
| | row phase | record | action | |
| @@ -241,5 +241,5 @@ Builds on plan 1 (`2026-10-22-self-serve-moves-1-lifetimes-and-order.md`, rev 13 |
| - Spec coverage: §5 (T1), §6.1 (T5), §6.2 (T7), §6.3 (T9, T5, T6), §7 admission and recovery (T7's table), §7.1 (T3), §7.2 and the three facts with the sealer's durable lift (T6, T8), §7.3 (T6, T7, T8's barrier), §7.4 without the probe mode (T8), §7.5 the no-return boundary and the operator return (T2's open record, T8), §7.7 (T5), §8 pages, log and member stream (T8, T9), §9 (T3's rollout). Deferred: §6.4 holding page and the probe headers (plan 3); §7.6 cleanup (plan 4), whose status contract the reclaim rule names (`RetirementsComplete`, a terminal state, `RootRequest == ""`, `ReleasePending == false`, `Metrics.BoxID`). |
| - Dependencies for plan 3: `SelfServeMove{DisplayName, OfflineAt, EstimateSeconds}`, `Participants.Target.BoxID`, `moveCfg.BoxID`, `probeTarget`; for plan 4: the status `Retirements` contract and the open record. |
| -- Names used across tasks: `selfServeGate`, `instanceRails(w, r, acct, noun, c)`, `moveCapable`, `Principal.Moves/TokenID`, `selfServeMode`, `selfServeAdmits`, `recordEligibility`, `offlineRecordChecks`, `offlineRegionChecks`, `offlineSelection`, `OfflineQueued`, `OfflineQueue`, `OfflineQueuedToStaging`, `OfflinePrecheck`, `OfflineSetEstimate`, `OfflineAbortAt`, `provision.SelfServeMove`, `Participant{BoxID, Label, InstanceID, Verified}`, `Participants`, `moves.OpenRecord{..., Layout}`, `OpenRecordKey`, `moves.ParticipantRole`, `moves.ParticipantStillPresent`, `moves.OfflineReservation`, `reserveBoxes`, `releaseBoxes`, `selfServeTick`, `selfServeStep`, `selfServeApply`, `request`, `persistedRequest`, `provision.Receipt`, `provision.Expect`, `ConsumeSelfServeRequest(id, nonce, nextStep, nextDeadline)`, `errRequestExpired`, `AbandonSelfServeRequest(id, nonce, nextDeadline)`, `next_maintenance_at`, `phase_step` (`adopted`/`issue` in `restage`), `participantMatches`, `SetSelfServeExpiry(id, phase, kind, sawDeadline, pending)`, `expiry_kind`/`expiry_deadline`, `ExpireSelfServeRequest`/`request_expired`, supersession by stamp, `errRequestRefused`, `MarkSelfServeCut`/`cut_requested_at`, `errRequestForeign`, `OfflineReport.RequestKey`, `DiskFreeBytes`, `OfflineReservation.ExpectIncarnation`, `SelfServeMove.LastRequest`, `LastMove.Request`, `provision.SetRecordMoveAt`, `OfflineReservation.ExpectSeq`, `moves.FailedDetail`, `OfflineReport.Layout`, `errRequestVoided`, `selfServeFinalise`, `leaseToken`, `provision.WithWriteGuard`, `IssueSelfServeRequest`, `ConfirmSelfServeRequest`, `VoidSelfServeRequest`, `request_voided`, `AdvanceSelfServe(..., nonce)`, `SetSelfServeExpiry`, `SetSelfServeNotifiedSeq`, `SetSelfServeOpenRecordPending`, `InsertSelfServeEventTx`, `DeliverSelfServeEvent`, `selfServeMoveView`, `selfServeAbortView`, `selfServeLast`, `moves.Fact`, `PrefixFacts`, `PrefixObject`, `StagedObject{Size, ETag, VersionID, Deferred}`, `LaneReport`, `TargetReadyReport`, `SourcePrecheck{..., Layout}`, `StrayEntry`, `StraysFact`, `ReturnLeftovers`, `ValidateOfflineReportFor`, `readOfflineEvidence(ctx, host, id, m)`, `readReturnReport`, `lease.CopyEvent`, `lease.MirrorPrefixKeepObserved`, `stageBook`, `status.HostFacts`, `BoxMetrics.Hosts/Lanes/BoxID/InstanceID`, `measureFiles`, `targetReadyDecision`, `sourcePrecheckDecision`, `probeTarget`, `estimateTerms`, `MarkSelfServeTiming`, `SelfServeEvent`, `host.E2E`/`NewE2E`, `console.TestHooks`. |
| +- Names used across tasks: `selfServeGate`, `instanceRails(w, r, acct, noun, c)`, `moveCapable`, `Principal.Moves/TokenID`, `selfServeMode`, `selfServeAdmits`, `recordEligibility`, `offlineRecordChecks`, `offlineRegionChecks`, `offlineSelection`, `OfflineQueued`, `OfflineQueue`, `OfflineQueuedToStaging`, `OfflinePrecheck`, `OfflineSetEstimate`, `OfflineAbortAt`, `provision.SelfServeMove`, `Participant{BoxID, Label, InstanceID, Verified}`, `Participants`, `moves.OpenRecord{..., Layout}`, `OpenRecordKey`, `moves.ParticipantRole`, `moves.ParticipantStillPresent`, `moves.OfflineReservation`, `reserveBoxes`, `releaseBoxes`, `selfServeTick`, `selfServeStep`, `selfServeApply`, `request`, `persistedRequest`, `provision.Receipt`, `provision.Expect`, `ConsumeSelfServeRequest(id, nonce, nextStep, nextDeadline)`, `errRequestExpired`, `AbandonSelfServeRequest(id, nonce, nextDeadline)`, `next_maintenance_at`, `phase_step` (`adopted`/`issue` in `restage`), `participantMatches`, `SetSelfServeExpiry(id, phase, kind, sawDeadline, pending)`, `expiry_kind`/`expiry_deadline`, `ExpireSelfServeRequest(id, nonce, deadline, sawCutRequestedAt)`/`request_expired`, `VoidExpiredSelfServeRequest`, `persistedRequest.CutRequestedAt`, supersession by stamp, `errRequestRefused`, `MarkSelfServeCut`/`cut_requested_at`, `errRequestForeign`, `OfflineReport.RequestKey`, `DiskFreeBytes`, `OfflineReservation.ExpectIncarnation`, `SelfServeMove.LastRequest`, `LastMove.Request`, `provision.SetRecordMoveAt`, `OfflineReservation.ExpectSeq`, `moves.FailedDetail`, `OfflineReport.Layout`, `errRequestVoided`, `selfServeFinalise`, `leaseToken`, `provision.WithWriteGuard`, `IssueSelfServeRequest`, `ConfirmSelfServeRequest`, `VoidSelfServeRequest`, `request_voided`, `AdvanceSelfServe(..., nonce)`, `SetSelfServeExpiry`, `SetSelfServeNotifiedSeq`, `SetSelfServeOpenRecordPending`, `InsertSelfServeEventTx`, `DeliverSelfServeEvent`, `selfServeMoveView`, `selfServeAbortView`, `selfServeLast`, `moves.Fact`, `PrefixFacts`, `PrefixObject`, `StagedObject{Size, ETag, VersionID, Deferred}`, `LaneReport`, `TargetReadyReport`, `SourcePrecheck{..., Layout}`, `StrayEntry`, `StraysFact`, `ReturnLeftovers`, `ValidateOfflineReportFor`, `readOfflineEvidence(ctx, host, id, m)`, `readReturnReport`, `lease.CopyEvent`, `lease.MirrorPrefixKeepObserved`, `stageBook`, `status.HostFacts`, `BoxMetrics.Hosts/Lanes/BoxID/InstanceID`, `measureFiles`, `targetReadyDecision`, `sourcePrecheckDecision`, `probeTarget`, `estimateTerms`, `MarkSelfServeTiming`, `SelfServeEvent`, `host.E2E`/`NewE2E`, `console.TestHooks`. |
| - Review Focus 1 to 5 are pinned by T7, T3, T8, T5, T1. |