carlos / platform Public

plan: claim Plan 6 rev 3, answers the rev 2 review (two P1, ten P2)

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
codex agent, operated by paul@keymail.dev pushed by claude agent, operated by paul@keymail.dev 2227733269e6fc25d50227c5b26b0af1c840864b parent c3923c6
1 file changed, +973 −63
  • docs/superpowers/plans/2026-10-05-host-lifecycle-claim-6-box-wiring.md +973 −63

CI passed — run details

diff --git a/docs/superpowers/plans/2026-10-05-host-lifecycle-claim-6-box-wiring.md b/docs/superpowers/plans/2026-10-05-host-lifecycle-claim-6-box-wiring.md
index 7fb07b124..45010a53c 100644
--- a/docs/superpowers/plans/2026-10-05-host-lifecycle-claim-6-box-wiring.md
+++ b/docs/superpowers/plans/2026-10-05-host-lifecycle-claim-6-box-wiring.md
@@ -1,18 +1,18 @@
# Host lifecycle claim, Plan 6: box wiring
-Rev 2: Tasks 6-11 completed against claim rev 6.27 (Paul's rulings) and the moves Phase 1 index.
+Rev 3 answers the rev 2 review (two P1, ten P2).
-**Authoring status:** the requested revision heading is retained, but the completeness requirement is not met. The code below compiles against the pinned base and has focused tests; production composition, the complete root collector/launch journal, registry ownership and several final caller bindings remain unfinished. Enabled production startup refuses. The missing implementation is listed at Tasks 9 and 11; it must not be mistaken for an unresolved ruling or a completed integration.
+**Authoring status:** this revision addresses the twelve rev 2 findings. The earlier completeness requirement remains unmet. The code below compiles against the pinned base and has focused tests; production composition, the complete root collector/launch journal, registry ownership and several final caller bindings remain unfinished. Enabled production startup refuses. The missing implementation is listed at Tasks 9 and 11; it must not be mistaken for an unresolved ruling or a completed integration.
> **For agentic workers:** Tasks 1-5 are built on `feat/hostclaim-box` at `2bd3ea1f`. Do not rerun their branch-creation commands. Execute Tasks 6-11 in order, with the failing test first, on that implementation branch. Keep `CARLOS_HOST_CLAIM` unset. This document is a plan, not authorization to enable the feature or change production. Missing required providers return an error before dependent effects.
**Goal:** give moves its admitted-start and held-timer interfaces first, then implement the ruled admission, retry, stop, retirement and background-writer contracts. No warm admission, local database reuse or clean-close certificate is permitted.
-**Baseline:** `feat/hostclaim-box` **2bd3ea1f**, carrying main **67547f5**: the claim library, generation selector, box identity, guard, park engine and version-pinned door are present. `internal/hostwrite`, `internal/hostbox`, `internal/activator/claimspawn.go` and `internal/schedule/claimtimers.go` were read from that exact commit. The moves index is `origin/design-moves-any-region` **da2832aa**, `docs/superpowers/plans/2026-10-06-moves-phase1-0-index.md`. The claim spec is **rev 6.27**, as committed in this authoring worktree at **989ee349**. A concurrent working-tree edit to rev 6.28 is outside this revision's brief.
+**Baseline:** `feat/hostclaim-box` **2bd3ea1f**, carrying main **67547f5**: the claim library, generation selector, box identity, guard, park engine and version-pinned door are present. `internal/hostwrite`, `internal/hostbox`, `internal/activator/claimspawn.go` and `internal/schedule/claimtimers.go` were read from that exact commit. The moves index is `origin/design-moves-any-region` **d0907747**, `docs/superpowers/plans/2026-10-06-moves-phase1-0-index.md`, including its BLOCKED-ON-CLAIM C1/C2 table. The claim spec is **rev 6.29** in this authoring worktree. The implementation base remains **2bd3ea1f**.
**Architecture:** retain the dependency-free `hostwrite` interface. The host assembles the claim reader, verified roster, local Plan 8 authority, moves input provider and root collector. Libraries receive captured work and typed providers. They do not read the environment or install a global lifecycle controller. `stopevidence` already imports `activator`; root evidence assembly therefore belongs in `host`, not in `activator`.
-**Tasks 1-5:** their retained instructions below are the historical implementation record. The interface extensions are listed in Tasks 6 and 8: the writer adapter enforces cold-only admission, `SelectedTimers` gains `ReplaceTimers`, and `WorkSource.SelectAt` accepts an already chosen installation. The strict spawn signature and returned `*cgroup.Prepared` remain unchanged. Task 8 adds `Prepared.PrepareRoleChild` and `Identity`, and couples role starts to their parent cleanup state. Punctuation is normalized to omit em dashes.
+**Tasks 1-5:** their retained instructions below are the historical implementation record. The interface extensions are listed in Tasks 6 and 8: the writer adapter enforces cold-only admission, `SelectedTimers` gains `ReplaceTimers`, and `WorkSource.SelectAt` accepts an already chosen installation. The strict spawn signature and returned `*cgroup.Prepared` remain unchanged. Task 6 adds a private pre-start hook so the probe window is journalled immediately before execution. Task 8 adds `Prepared.PrepareRoleChild` and `Identity`, and couples role starts to their parent cleanup state. Punctuation is normalized to omit em dashes.
## The ruled contracts
@@ -31,7 +31,7 @@ Section 1.1 amends section 6.6. None of P6-D1 through P6-D8 remains an undecided
P3 requires `internal/boxid` proof before roster enrollment. The roster's private cloud tuple is `{provider, account, region, instance_id}`; a box label, heartbeat or region string is not evidence. P2's automatic loss path belongs to Plan 8 and the region placer: obtain account, region and instance ID from that roster tuple, require positively observed EC2 `terminated` state from EventBridge or DescribeInstances, and never treat API absence as termination. Discharging local chores additionally requires running-time records proving **every** attached EBS volume had `DeleteOnTermination: true`; otherwise use the operator-attested path. The console gains no EC2 permission.
-P4 uses #232 ownership acquisition and backfill through Plan 5's coordinator. Plan 6 does not create another hostname owner. Stateful fleet transfer, record-backed rename/account transfer, and region HA's later `relocating` operation are outside rev 6.27 and must refuse under the flag.
+P4 uses #232 ownership acquisition and backfill through Plan 5's coordinator. Plan 6 does not create another hostname owner. Stateful fleet transfer and record-backed rename/account transfer remain refused under the flag. Claim rev 6.29 includes region HA's `relocating` contract, but its callers and multi-box support belong to the region HA plan. This Plan 6 implementation still refuses `relocating`; it does not implement that newer consumer.
## Shared implementation rules
@@ -982,29 +982,77 @@ Expected: the focused tests pass; the delegated regression passes after two actu
### Task 6: Land the moves interfaces and cold admitted-start sequence first
-**Contract:** retain `Writer{Claims, Data, Bucket, Region, Box, AdmissionEvidence, PublicationEvidence, NewID}` and its exact `Baseline`, `Admit`, `Published` signatures; retain `GenerationPrefix(holder) (string,error)` and `spawnClaimed(ctx, enabled, manager, host, limit, selected, identity, build) (*exec.Cmd,*cgroup.Prepared,error)`. Add `startAdmitted(ctx,e,admitted,startPolicy) error` in `activator`, callable by the moves-owned `ProbeWake` file in that same package. Do not introduce a second conflicting `ProbeWake` API. `startPolicy` has precisely `MaxStarts`, `Window`, `Deadline`, `Probe`.
+**Contract:** retain `Writer{Claims, Data, Bucket, Region, Box, AdmissionEvidence, PublicationEvidence, NewID}` and its exact `Baseline`, `Admit`, `Published` signatures; retain `GenerationPrefix(holder) (string,error)` and `spawnClaimed(ctx, enabled, manager, host, limit, selected, identity, build) (*exec.Cmd,*cgroup.Prepared,error)`. Add `startAdmitted(ctx,e,admitted,startPolicy) error` in `activator`, callable by the moves-owned `ProbeWake` file in that same package. Do not introduce a second conflicting `ProbeWake` API. `startPolicy` has precisely `MaxStarts`, `Window`, `Deadline`, `Probe`, `Prepared`, `Inputs`.
-- [ ] Apply the writer and held-timer patches, then the complete policy, restore, generation-client and activator files below. Run the Task 6 tests before and after applying production files.
+- [ ] Bring forward the moves-owned `InstallManifest`, `MoveInputs` and `ErrAttemptOver` contracts below, then apply the writer and held-timer patches and the complete policy, restore, generation-client and activator files below. Run the Task 6 tests before and after applying production files.
- [ ] K6 is the first enabled `doWake` action: read the claim directly, and if its executor is not this box, terminate the recorded host tree before any registry refresh, lease acquisition, restore or materialization. A failed read refuses new work; it does not invent permission to kill an unrelated successor. Allow ordinary target wake under `landing/committed`, and source wake under `reverting` or `restoring` in `probed`/`opened`. Probe authorization remains operation-bound.
- [ ] The host's `ClaimRuntime.Prerequisites` verifies current row ownership, held inputs, floor, enrollment, quiet horizon, retry eligibility, stop/handover evidence and supported containment **before** reservation/admission. `Admit` composes the serving first-attempt reservation, `Writer.Admit` and session binding. Probe admission is supplied by moves' completed install-manifest/open-once transition, not by serving retry state.
- [ ] `Prepare` returns closures bound to that exact admitted tuple. `Restore` calls `ColdRestore.Install` with **target** `Generation = admitted.Claim.Holder.Generation`; the selected plan's `Generation` is the remote **baseline**, which may differ. The `.gen` marker always records the admitted target. The predecessor retirement callback runs outside `Work.With`, binds its journalled artifact stamp, and calls `park.Local.RetireArtifacts` or the typed journal equivalent. It must retire `.gen` too. No callback may recapture a successor.
+- [ ] C1 uses exactly `startPolicy{MaxStarts int; Window time.Duration; Deadline time.Time; Probe bool; Prepared *moves.InstallManifest; Inputs *provision.MoveInputs}`. `Prepare` receives both pointers unchanged and binds its restore/materialization checks to them. The data-only files below bring forward moves Plan 1's input pin and Plan 2's install manifest so Task 6 compiles against the pinned base. Moves later reuses these definitions and `moves.ErrAttemptOver`; do not redeclare them or substitute empty test stubs.
+- [ ] The cut `Deadline` bounds preparation and restore. A probe's readiness `Window` starts inside the guarded strict launcher, after restore, materialization and cgroup preparation, immediately before the first application start. `ClaimStart.OpenProbe` must durably journal that first timestamp under the exact moves attempt and admission, return the saved timestamp on every retry/restart, and refuse a changed attempt. This provider is required for probes and runs inside the selected `Work.With`; it must not reacquire hostguard. The 30-second window never restarts between process attempts. Expiry before readiness returns `ErrAttemptOver`, including a late successful readiness callback that ignored cancellation; probe failures never mark a serving host failed. Publication keeps the separate cut deadline.
- [ ] Only an independently authorized, completely observed initial empty bootstrap calls `InstallEmpty`. It writes no zero-TXID LTX and does not infer emptiness from `genselect.ErrEmpty`, missing files, read errors or local purge. Seed is app code and needs the same durable reservation and strict role-child launcher as the main process.
- [ ] `Materialize` is moves' `RenderHeld` plus `MaterializeHeld` for operations; it emits the held acknowledgement even when bytes match. Recovery consumes `recovery.inputs`, not stale forward `move.inputs`. The returned `Build` uses the verified executable descriptor and composed environment. A probe supplies `CARLOS_MOVE_PROBE=1` and obtains readiness from its actual process tree, not a cached edge answer.
- [ ] `Replicate` uses the already-held location lease. Call `Held.BindGeneration(prefix, capturedAuthorityCheck)` before `Held.Replicate`, and keep that immutable client for its life. `Publish` validates the durable marker's exact bytes/version and current session, then calls `Writer.Published`. It may not treat readiness or `Select` as a marker receipt. After an ambiguous marker response, exact matching bytes can establish publication; conflicting bytes cannot authorize an unconditional retry.
- [ ] `FailedAdmission` hands any post-admission preparation/restore failure to durable serving-attempt recovery; it must settle or fence that exact spent attempt, never refund it. `Reserve(0)` validates the first reservation already bound by admission. Later process starts reserve against the same serving episode or moves attempt. `Failed` settles the exact reserved attempt; on ambiguous cleanup, `Retain` owns the process, prepared descriptor and replicator until root recovery proves termination. `Healthy` identifies the exact invocation/session/config/release and uses backend readiness. Probe health never clears or writes serving failure state.
- [ ] On success the process owns the `Prepared` handle through its whole life. On failed start or exit, kill/drain, wait for the direct child, then close the original handle. A failed cleanup retains ownership. A readiness failure can retry the process in the same admitted session; a replication/publication failure closes that opening and returns for operation-owned recovery. No close path writes a clean-close certificate.
+`BindGeneration` accepts the canonical trailing slash returned by the real `GenerationPrefix` and stores the cleaned path for the client. The external-package regression calls both real functions. Generation-bound `DeleteLTXFiles` always refuses with `ErrGenerationRetention` because `ltx.FileInfo` carries no version ID; `DeleteAll` also refuses. There is no bare-key retention delete. Superseded LTX versions stay until the park engine retires their generation by exact version under C3, subject to its live-set protections. This can retain more data while a generation remains live; no live-generation pruning authority is invented here.
+
+After `litestream.NewStore`, call `store.SetRetentionEnabled(false)` only on a generation-bound replica. `NewStore` resets the DB setting, so disabling it earlier is ineffective. The setter updates the store, DB and compactor before `Open`; Litestream v0.5.14's snapshot, L0 and compaction retention paths skip remote deletion while keeping local cleanup and replication active. Tests run these real retention methods and verify a subsequent upload. Flag-off replicas keep the existing defaults. An unexpected direct retention call gets the explicit refusal and must not be retried through the underlying client.
+
The exact-version adapter reads only selected `VersionID`s, fully decodes LTX trailers, checks replay coordinates/final TXID, checks a database checksum when the format supplies one, and runs SQLite integrity verification before durable install. Litestream v0.5.14's `HeaderFlagNoChecksum` omits the database checksum: a valid LTX trailer detects corruption but is not authentication. Moves' authenticated manifest and pinned snapshot digest remain required admission evidence. Prepared position zero uses the explicit empty path.
-`SelectedTimers.ReplaceTimers` preserves source registration IDs and all bookkeeping columns. Its selected action is `held-timers`; the policy requires the row's observed `hold_op` to match the same landing operation. Ordinary delivery and convergence retain their refusal on held rows.
+`SelectedTimers.ReplaceTimers` preserves source registration IDs and all bookkeeping columns. Its selected action is `held-timers`; the policy requires the row's observed `hold_op` to match the same landing operation. Ordinary delivery and convergence retain their refusal on held rows. After `ReleaseHold`, ordinary wake actions use C2's phase authorization plus the captured operation/executor and moves input checks; an empty hold no longer blocks an authorized committed/opened wake. Held materializers still require the exact nonempty hold.
```bash
-go test ./internal/hostbox -run 'WakePhase|HeldTimer|WriterCold|ColdRestore|LoadPinned' -count=1
+go test ./internal/hostbox -run 'WakePhase|HeldTimer|PolicyOrdinary|WriterCold|ColdRestore|LoadPinned' -count=1
go test ./internal/lease -run 'TestBindGeneration|TestGenerationClient' -count=1
-go test ./internal/activator -run '^Test(ClaimK6|ClaimReadFailure|StartAdmitted|SameAdmission)' -count=1
+go test ./internal/activator -run '^Test(ClaimK6|ClaimReadFailure|ClaimProbeWindow|StartAdmitted|SameAdmission)' -count=1
go test ./internal/schedule -run 'SelectedHeld|SelectedTimer' -count=1
```
+#### File: `internal/moves/install_contract.go`
+
+```go
+package moves
+
+import (
+ "errors"
+ "time"
+)
+
+var ErrAttemptOver = errors.New("moves: probe attempt window or deadline expired")
+
+// InstallManifest is the moves Phase 1 Plan 2 Task 8 prepared snapshot contract.
+type InstallManifest struct {
+ V int
+ Op, Host string
+ N uint64
+ SnapshotKey, SnapshotVersion string
+ SnapshotCRC64 uint64
+ AnchorKey, AnchorVersion string
+ AnchorCRC64 uint64
+ IntegrityCheck string
+ DBSHA256 string
+ BuiltAt time.Time
+}
+```
+
+#### File: `internal/provision/moveinputs_contract.go`
+
+```go
+package provision
+
+// MoveInputs pins the inputs checked by the moves owner before admission.
+type MoveInputs struct {
+ BundleETags map[string]string `json:"bundle_etags,omitempty"`
+ OverlayETag string `json:"overlay_etag,omitempty"`
+ Release string `json:"release,omitempty"`
+ FilesPlain string `json:"files_plain,omitempty"`
+ EnvReport string `json:"env_report,omitempty"`
+}
+```
+
#### Patch: `internal/hostbox/writer.go`
```diff
@@ -1065,7 +1113,7 @@ type Policy struct {
}
func (p Policy) Check(ctx context.Context, c *hostclaim.Claim, s Selection, id hostwrite.Identity) error {
- if p.Held == nil {
+ if c == nil || p.Held == nil {
return ErrUnsupported
}
hold, err := p.Held(ctx, s.Host)
@@ -1086,15 +1134,26 @@ func (p Policy) Check(ctx context.Context, c *hostclaim.Claim, s Selection, id h
}
return Serving(c, s)
}
- if p.Moves == nil || hold == "" || hold != s.Op || c.Op.ID != s.Op || c.Op.Exec != s.Box {
+ if p.Moves == nil || s.Op == "" || c.Op.ID != s.Op || c.Op.Exec != s.Box {
return ErrChanged
}
switch s.Action {
- case "held-config", "held-release", "held-timers", "restore", "spawn", "seed", "writer", "install":
- return p.Moves(ctx, c, s, id)
+ case "held-config", "held-release", "held-timers":
+ if hold != s.Op {
+ return ErrChanged
+ }
+ case "restore", "spawn", "seed", "writer", "install":
+ if hold != "" {
+ if hold != s.Op {
+ return ErrChanged
+ }
+ } else if err := WakePhase(c, s.Box, false); err != nil {
+ return err
+ }
default:
return ErrChanged
}
+ return p.Moves(ctx, c, s, id)
}
const RequestLimit = 120 * time.Second
@@ -1172,6 +1231,36 @@ func TestWriterColdOnlyEvenWithCertificate(t *testing.T) {
t.Fatal("certificate provider consulted", evidenceCalls)
}
}
+
+func TestPolicyOrdinaryWakeAfterReleaseHold(t *testing.T) {
+ for _, phase := range []struct{ kind, phase string }{{"landing", "committed"}, {"reverting", "probed"}, {"reverting", "opened"}, {"restoring", "probed"}, {"restoring", "opened"}} {
+ c := &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: phase.kind, Phase: phase.phase, ID: "op", Exec: "box"}}
+ p := Policy{Held: func(context.Context, string) (string, error) { return "", nil }, Moves: func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error { return nil }}
+ s := Selection{Host: "x.example", Box: "box", Op: "op"}
+ for _, action := range []string{"restore", "spawn", "seed", "writer", "install"} {
+ s.Action = action
+ if err := p.Check(t.Context(), c, s, hostwrite.Identity{}); err != nil {
+ t.Fatalf("%+v %s: %v", phase, action, err)
+ }
+ }
+ for _, action := range []string{"held-config", "held-release", "held-timers", "config", "timers"} {
+ s.Action = action
+ if p.Check(t.Context(), c, s, hostwrite.Identity{}) == nil {
+ t.Fatalf("unheld %s accepted", action)
+ }
+ }
+ s.Action = "spawn"
+ c.Op.Phase = "probing"
+ if p.Check(t.Context(), c, s, hostwrite.Identity{}) == nil {
+ t.Fatal("ordinary wake in probing")
+ }
+ c.Op.Phase = phase.phase
+ s.Op = "other"
+ if p.Check(t.Context(), c, s, hostwrite.Identity{}) == nil {
+ t.Fatal("stale op")
+ }
+ }
+}
```
#### File: `internal/hostbox/restore.go`
@@ -1814,6 +1903,14 @@ func mustRead(t *testing.T, path string) []byte {
+ poison error
+ replicaClient litestream.ReplicaClient // private local-replica test seam
+ generationPrefix string
+@@ -284,0 +286 @@
++
+@@ -297,0 +300,5 @@
++ if h.generationPrefix != "" {
++ // NewStore resets the DB flag. Set all three store/DB/compactor flags
++ // afterward, before Open starts any monitor or compaction work.
++ store.SetRetentionEnabled(false)
++ }
```
#### File: `internal/lease/claimgeneration.go`
@@ -1835,6 +1932,8 @@ import (
"github.com/superfly/ltx"
)
+var ErrGenerationRetention = errors.New("lease: generation retention requires exact versions through park")
+
var ErrGenerationBinding = errors.New("lease: invalid or changed generation binding")
const generationRequestLimit = 120 * time.Second
@@ -1848,7 +1947,7 @@ func (h *Held) BindGeneration(prefix string, check func(context.Context) error)
base := strings.TrimSuffix(path.Clean(h.leaser.Path), "/")
clean := path.Clean(prefix)
rel, ok := strings.CutPrefix(clean, base+"/")
- if prefix != clean || strings.Contains(prefix, "\\") || base == "." || base == "" || !ok || strings.Contains(rel, "/") || !strings.HasPrefix(rel, "g-") || len(rel) == 2 {
+ if (prefix != clean && prefix != clean+"/") || strings.Contains(prefix, "\\") || base == "." || base == "" || !ok || strings.Contains(rel, "/") || !strings.HasPrefix(rel, "g-") || len(rel) == 2 {
return ErrGenerationBinding
}
h.mu.Lock()
@@ -1953,12 +2052,8 @@ func (c *generationClient) DeleteLTXFiles(ctx context.Context, files []*ltx.File
return ErrGenerationBinding
}
}
- ctx, cancel, err := c.bounded(ctx)
- if err != nil {
- return err
- }
- defer cancel()
- return c.client.DeleteLTXFiles(ctx, files)
+ // FileInfo has no VersionID. Only the park engine may retire exact versions.
+ return ErrGenerationRetention
}
func (c *generationClient) DeleteAll(context.Context) error {
return fmt.Errorf("%w: DeleteAll refused", ErrGenerationBinding)
@@ -1987,8 +2082,10 @@ package lease
import (
"bytes"
"context"
+ "errors"
"io"
"log/slog"
+ "path/filepath"
"testing"
"time"
@@ -2000,6 +2097,7 @@ import (
type generationFake struct {
calls int
ctx context.Context
+ files []*ltx.FileInfo
}
func (f *generationFake) Type() string { return "fake" }
@@ -2008,7 +2106,7 @@ func (f *generationFake) Init(ctx context.Context) error { f.calls++; f.ctx = ct
func (f *generationFake) LTXFiles(ctx context.Context, _ int, _ ltx.TXID, _ bool) (ltx.FileIterator, error) {
f.calls++
f.ctx = ctx
- return ltx.NewFileInfoSliceIterator(nil), nil
+ return ltx.NewFileInfoSliceIterator(f.files), nil
}
func (f *generationFake) OpenLTXFile(ctx context.Context, _ int, _ ltx.TXID, _ ltx.TXID, _ int64, _ int64) (io.ReadCloser, error) {
f.calls++
@@ -2096,6 +2194,130 @@ func TestGenerationClientBoundsAndRetainsStreamContext(t *testing.T) {
t.Fatalf("authority checks=%d", checks)
}
}
+
+// GenerationTestBinding is a test-only bridge for the external package to
+// exercise hostbox.GenerationPrefix without a lease -> hostbox import cycle.
+func GenerationTestBinding(location, prefix string) error {
+ l := lss3.NewLeaser()
+ l.Path = location
+ h := &Held{leaser: l, replicaClient: &generationFake{}}
+ return h.BindGeneration(prefix, func(context.Context) error { return nil })
+}
+
+func TestGenerationClientRetentionRefusedButUploadsContinue(t *testing.T) {
+ f := &generationFake{}
+ c := &generationClient{client: f, check: func(context.Context) error { return nil }}
+ for _, level := range []int{0, 1, litestream.SnapshotLevel} {
+ if err := c.DeleteLTXFiles(t.Context(), []*ltx.FileInfo{{Level: level, MinTXID: 1, MaxTXID: 2}}); !errors.Is(err, ErrGenerationRetention) {
+ t.Fatalf("level %d delete: %v", level, err)
+ }
+ }
+ if f.calls != 0 {
+ t.Fatal("bare-key deletion reached backing client")
+ }
+ if _, err := c.WriteLTXFile(t.Context(), 0, 3, 3, bytes.NewReader(nil)); err != nil {
+ t.Fatal(err)
+ }
+ if f.calls != 1 {
+ t.Fatal("replication stopped after refusal")
+ }
+}
+
+func TestGenerationClientDisablesRemoteRetentionInRealStore(t *testing.T) {
+ for _, bound := range []bool{false, true} {
+ l := lss3.NewLeaser()
+ l.Path = "instances/x.example/inc"
+ h := &Held{leaser: l, replicaClient: &generationFake{}}
+ if bound {
+ if err := h.BindGeneration(l.Path+"/g-one/", func(context.Context) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ }
+ _, db := h.newStore(filepath.Join(t.TempDir(), "db"), time.Second)
+ if db.RetentionEnabled == bound {
+ t.Fatalf("bound=%v retention=%v", bound, db.RetentionEnabled)
+ }
+ }
+}
+
+func TestGenerationClientRealRetentionMonitorsDoNotDelete(t *testing.T) {
+ l := lss3.NewLeaser()
+ l.Path = "instances/x.example/inc"
+ f := &generationFake{files: []*ltx.FileInfo{{Level: 1, MinTXID: 1, MaxTXID: 2, CreatedAt: time.Unix(1, 0)}, {Level: 1, MinTXID: 3, MaxTXID: 4, CreatedAt: time.Unix(2, 0)}}}
+ h := &Held{leaser: l, replicaClient: f}
+ if err := h.BindGeneration(l.Path+"/g-one/", func(context.Context) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ store, db := h.newStore(filepath.Join(t.TempDir(), "db"), time.Second)
+ if store.RetentionEnabled || db.RetentionEnabled {
+ t.Fatal("remote retention enabled")
+ }
+ db.MonitorInterval = 0
+ db.Replica.MonitorEnabled = false
+ if err := db.Open(); err != nil {
+ t.Fatal(err)
+ }
+ defer db.Close(t.Context())
+ // These execute the real DB and compactor retention code. A deletion
+ // attempt would get ErrGenerationRetention and fail the call.
+ if err := db.EnforceRetentionByTXID(t.Context(), 1, 10); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := db.EnforceSnapshotRetention(t.Context(), time.Now()); err != nil {
+ t.Fatal(err)
+ }
+ if err := db.EnforceL0RetentionByTime(t.Context()); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := db.Replica.Client.WriteLTXFile(t.Context(), 0, 5, 5, bytes.NewReader(nil)); err != nil {
+ t.Fatal(err)
+ }
+}
+```
+
+#### File: `internal/lease/claimgeneration_external_test.go`
+
+```go
+package lease_test
+
+import (
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/lease"
+ "testing"
+)
+
+func TestBindGenerationAcceptsRealGenerationPrefix(t *testing.T) {
+ h := hostclaim.Holder{Location: "instances/x.example/inc", Generation: "one", Session: 1}
+ prefix, err := hostbox.GenerationPrefix(h)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := lease.GenerationTestBinding(h.Location, prefix); err != nil {
+ t.Fatalf("real prefix %q: %v", prefix, err)
+ }
+}
+```
+
+#### Patch: `internal/activator/claimspawn.go`
+
+```diff
+--- a/internal/activator/claimspawn.go
++++ b/internal/activator/claimspawn.go
+@@ -11,0 +12,4 @@
++ return spawnClaimedBefore(ctx, enabled, manager, host, limit, selected, identity, build, nil)
++}
++
++func spawnClaimedBefore(ctx context.Context, enabled bool, manager *cgroup.Manager, host string, limit cgroup.Limit, selected hostwrite.Work, identity hostwrite.Identity, build func() *exec.Cmd, before func() error) (*exec.Cmd, *cgroup.Prepared, error) {
+@@ -27,0 +32,8 @@
++ if err := ctx.Err(); err != nil {
++ return err
++ }
++ if before != nil {
++ if err := before(); err != nil {
++ return err
++ }
++ }
```
#### Patch: `internal/activator/activator.go`
@@ -2140,14 +2362,20 @@ import (
"github.com/carlosframework/platform/internal/hostbox"
"github.com/carlosframework/platform/internal/hostclaim"
"github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/moves"
+ "github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/registry"
)
+var ErrAttemptOver = moves.ErrAttemptOver
+
type StartPolicy struct {
MaxStarts int
Window time.Duration
Deadline time.Time
Probe bool
+ Prepared *moves.InstallManifest
+ Inputs *provision.MoveInputs
}
type startPolicy = StartPolicy
@@ -2167,6 +2395,10 @@ type ClaimRuntime struct {
// ClaimStart is one admitted session. Its closures retain the admission tuple.
// Only Restore may purge data. Process retries reuse it and the live replication.
type ClaimStart struct {
+ // OpenProbe journals the first app-start time before execution and returns
+ // that same time on retries, including after controller restart. Its
+ // provider binds the moves attempt and admission, never resets Opened.
+ OpenProbe func(context.Context, time.Time) (time.Time, error)
Work hostwrite.Work
Identity hostwrite.Identity
Manager *cgroup.Manager
@@ -2249,6 +2481,16 @@ func (a *Service) startAdmitted(ctx context.Context, e *actEntry, admitted hostc
if !a.cfg.HostWrites.Enabled || r == nil || r.Read == nil || r.Prepare == nil || e == nil || admitted.Host != e.host || policy.MaxStarts < 1 || policy.MaxStarts > 5 || policy.Window <= 0 || policy.Window > 30*time.Second {
return hostwrite.ErrUnavailable
}
+ budget := &probeWindow{policy: policy, now: time.Now}
+ defer func() { result = budget.result(result) }()
+ if !policy.Deadline.IsZero() {
+ bounded, cancel := context.WithDeadline(ctx, policy.Deadline)
+ defer cancel()
+ ctx = bounded
+ }
+ if err := ctx.Err(); err != nil {
+ return err
+ }
if err := hostbox.WakePhase(admitted.Claim, r.Box, policy.Probe); err != nil {
return err
}
@@ -2263,26 +2505,13 @@ func (a *Service) startAdmitted(ctx context.Context, e *actEntry, admitted hostc
if err != nil {
return err
}
- if !s.valid() || s.Host != e.host {
+ if !s.valid() || s.Host != e.host || (policy.Probe && s.OpenProbe == nil) {
return hostwrite.ErrUnavailable
}
prefix, err := hostbox.GenerationPrefix(*admitted.Claim.Holder)
if err != nil {
return err
}
- if policy.Probe {
- deadline := time.Now().Add(policy.Window)
- if !policy.Deadline.IsZero() && policy.Deadline.Before(deadline) {
- deadline = policy.Deadline
- }
- bounded, cancel := context.WithDeadline(ctx, deadline)
- defer cancel()
- ctx = bounded
- } else if !policy.Deadline.IsZero() {
- bounded, cancel := context.WithDeadline(ctx, policy.Deadline)
- defer cancel()
- ctx = bounded
- }
if err := s.Restore(ctx); err != nil {
return err
}
@@ -2301,7 +2530,7 @@ func (a *Service) startAdmitted(ctx context.Context, e *actEntry, admitted hostc
}()
var last error
for attempt := 0; attempt < policy.MaxStarts; attempt++ {
- if err := ctx.Err(); err != nil {
+ if err := budget.result(ctx.Err()); err != nil {
return err
}
if err := s.Reserve(ctx, attempt); err != nil {
@@ -2310,14 +2539,24 @@ func (a *Service) startAdmitted(ctx context.Context, e *actEntry, admitted hostc
if err := s.Materialize(ctx); err != nil {
return errors.Join(err, s.Failed(ctx, attempt))
}
- cmd, prepared, startErr := spawnClaimed(ctx, true, s.Manager, s.Host, s.Limit, s.Work, s.Identity, s.Build)
+ // This hook runs after strict preparation and immediately before Start.
+ // Restore and materialization consume only the separate cut deadline.
+ beforeStart := func() error { return budget.start(ctx, s.OpenProbe) }
+ cmd, prepared, startErr := spawnClaimedBefore(ctx, true, s.Manager, s.Host, s.Limit, s.Work, s.Identity, s.Build, beforeStart)
+
p := newClaimProcess(cmd, prepared)
if startErr == nil {
- readyCtx, cancelReady := context.WithTimeout(ctx, policy.Window)
+ deadline := time.Now().Add(policy.Window)
+ if policy.Probe {
+ deadline = budget.opened.Add(policy.Window)
+ }
+ readyCtx, cancelReady := context.WithDeadline(ctx, deadline)
startErr = s.Ready(readyCtx, cmd)
cancelReady()
+ startErr = budget.result(startErr)
}
ready := startErr == nil
+ budget.ready = ready
if ready {
rep, startErr = s.Replicate(ctx, prefix)
if startErr == nil && rep == nil {
@@ -2358,13 +2597,23 @@ func (a *Service) startAdmitted(ctx context.Context, e *actEntry, admitted hostc
if ready {
return last
}
+ if errors.Is(startErr, ErrAttemptOver) {
+ return startErr
+ }
if attempt+1 < policy.MaxStarts {
+ waitCtx := ctx
+ cancelWait := func() {}
+ if policy.Probe && !budget.opened.IsZero() {
+ waitCtx, cancelWait = context.WithDeadline(ctx, budget.opened.Add(policy.Window))
+ }
timer := time.NewTimer(time.Second << attempt)
select {
- case <-ctx.Done():
+ case <-waitCtx.Done():
timer.Stop()
- return ctx.Err()
+ cancelWait()
+ return budget.result(waitCtx.Err())
case <-timer.C:
+ cancelWait()
}
}
}
@@ -2436,6 +2685,156 @@ func sameAdmission(a, b hostclaim.Observed) bool {
}
```
+#### File: `internal/activator/claimwindow.go`
+
+```go
+package activator
+
+import (
+ "context"
+ "errors"
+ "time"
+
+ "github.com/carlosframework/platform/internal/hostwrite"
+)
+
+type probeWindow struct {
+ policy startPolicy
+ opened time.Time
+ ready bool
+ now func() time.Time
+}
+
+func (b *probeWindow) result(err error) error {
+ if !b.policy.Probe || b.ready {
+ return err
+ }
+ now := b.now()
+ if (!b.policy.Deadline.IsZero() && !now.Before(b.policy.Deadline)) ||
+ (!b.opened.IsZero() && !now.Before(b.opened.Add(b.policy.Window))) {
+ return errors.Join(ErrAttemptOver, err)
+ }
+ return err
+}
+
+func (b *probeWindow) start(ctx context.Context, journal func(context.Context, time.Time) (time.Time, error)) error {
+ if !b.policy.Probe {
+ return ctx.Err()
+ }
+ if err := b.result(ctx.Err()); err != nil {
+ return err
+ }
+ if journal == nil {
+ return hostwrite.ErrUnavailable
+ }
+ proposed := b.now()
+ opened, err := journal(ctx, proposed)
+ if err != nil {
+ return err
+ }
+ if opened.IsZero() || opened.After(proposed) || (!b.opened.IsZero() && !b.opened.Equal(opened)) {
+ return hostwrite.ErrUnavailable
+ }
+ b.opened = opened
+ return b.result(ctx.Err())
+}
+```
+
+#### File: `internal/activator/claimwindow_test.go`
+
+```go
+package activator
+
+import (
+ "context"
+ "errors"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/moves"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/registry"
+ "os/exec"
+ "testing"
+ "time"
+)
+
+func TestStartAdmittedPreparedInputsAndRestoreOutsideProbeWindow(t *testing.T) {
+ manifest := &moves.InstallManifest{Op: "op", Host: "x.example", SnapshotVersion: "pinned"}
+ inputs := &provision.MoveInputs{Release: "release", OverlayETag: "overlay"}
+ policy := startPolicy{MaxStarts: 5, Window: time.Millisecond, Deadline: time.Now().Add(time.Minute), Probe: true, Prepared: manifest, Inputs: inputs}
+ obs := hostclaim.Observed{Host: "x.example", ETag: "one", Claim: &hostclaim.Claim{Holder: &hostclaim.Holder{Location: "instances/x.example/inc", Session: 1, Generation: "one"}, Op: hostclaim.Op{Kind: "landing", ID: "op", Phase: "probing", Exec: "box"}}}
+ work, _ := (hostwrite.Config{}).Select(t.Context(), obs.Host, "spawn")
+ stop := errors.New("stop before kernel start")
+ restored, materialized, journaled := false, false, false
+ a := &Service{cfg: Config{HostWrites: hostwrite.Config{Enabled: true}, Claims: &ClaimRuntime{Box: "box", Read: func(context.Context, string) (hostclaim.Observed, error) { return obs, nil }, Prepare: func(ctx context.Context, rt registry.Route, got hostclaim.Observed, p startPolicy) (ClaimStart, error) {
+ if p.Prepared != manifest || p.Inputs != inputs {
+ t.Fatal("C1 inputs lost before preparation")
+ }
+ return ClaimStart{Host: obs.Host, Work: work, Identity: hostwrite.Identity{Config: "c", Release: "r"},
+ OpenProbe: func(context.Context, time.Time) (time.Time, error) { journaled = true; return time.Now(), nil },
+ Restore: func(ctx context.Context) error {
+ restored = true
+ deadline, ok := ctx.Deadline()
+ if !ok || !deadline.Equal(policy.Deadline) {
+ t.Fatal("restore consumed probe window")
+ }
+ time.Sleep(5 * policy.Window)
+ return ctx.Err()
+ },
+ Materialize: func(context.Context) error { materialized = true; return stop }, Build: func() *exec.Cmd { return nil }, Ready: func(context.Context, *exec.Cmd) error { return nil },
+ Reserve: func(context.Context, int) error { return nil }, Failed: func(context.Context, int) error { return nil }, Healthy: func(context.Context, int, *exec.Cmd) error { return nil }, Replicate: func(context.Context, string) (Rep, error) { return nil, nil }, Publish: func(context.Context, hostclaim.Observed) (hostclaim.Observed, error) { return obs, nil }, Retain: func(Proc, Rep, error) {},
+ }, nil
+ }}}}
+ err := a.startAdmitted(t.Context(), &actEntry{host: obs.Host}, obs, policy)
+ if !errors.Is(err, stop) || !restored || !materialized || journaled {
+ t.Fatalf("err=%v restore=%v materialize=%v journal=%v", err, restored, materialized, journaled)
+ }
+ policy.Deadline = time.Now().Add(-time.Second)
+ if err := a.startAdmitted(t.Context(), &actEntry{host: obs.Host}, obs, policy); !errors.Is(err, ErrAttemptOver) {
+ t.Fatalf("cut deadline: %v", err)
+ }
+}
+
+func TestClaimProbeWindowJournalsFirstStartAndSurvivesRetry(t *testing.T) {
+ now := time.Unix(1000, 0)
+ var saved time.Time
+ policy := startPolicy{Probe: true, Window: 30 * time.Second, Deadline: now.Add(time.Minute)}
+ journal := func(_ context.Context, proposed time.Time) (time.Time, error) {
+ if saved.IsZero() {
+ saved = proposed
+ }
+ return saved, nil
+ }
+ b := &probeWindow{policy: policy, now: func() time.Time { return now }}
+ // Restore took longer than the window, but has not started the app.
+ now = now.Add(40 * time.Second)
+ if err := b.start(t.Context(), journal); err != nil {
+ t.Fatal(err)
+ }
+ first := saved
+ now = now.Add(time.Second)
+ if err := b.start(t.Context(), journal); err != nil || saved != first {
+ t.Fatal("retry reset", err, saved)
+ }
+ b = &probeWindow{policy: policy, now: func() time.Time { return now }}
+ if err := b.start(t.Context(), journal); err != nil || b.opened != first {
+ t.Fatal("restart reset", err, b.opened)
+ }
+ now = policy.Deadline
+ if !errors.Is(b.result(nil), ErrAttemptOver) {
+ t.Fatal("late readiness accepted after cut")
+ }
+ b.policy.Deadline = time.Time{}
+ now = first.Add(policy.Window)
+ if err := b.result(nil); !errors.Is(err, ErrAttemptOver) {
+ t.Fatalf("late readiness ignored context: %v", err)
+ }
+ if err := b.start(t.Context(), journal); !errors.Is(err, ErrAttemptOver) {
+ t.Fatalf("expired retry: %v", err)
+ }
+}
+```
+
#### File: `internal/activator/claimwake_test.go`
```go
@@ -3376,6 +3775,10 @@ go test ./internal/host -run '^Test(ClaimStop|HostClaimParked)' -count=1
go test ./internal/hostbox -run 'TypedRetirement|TypedParker' -count=1
```
+A failed parent `Prepared.Close` latches `draining` before checking identity or population, just as a failed kill does. Existing and new role children then refuse starts even if the parent later becomes empty. The existing stale-handle test now expects `ErrStrictState` after cleanup begins; it still asserts that replacement-group bytes remain untouched.
+
+A fence retry first supplies a freshly read claim ETag. `BeginFence` validates that ETag and the current session/operation, then compares the pending fence's stable authority fields while ignoring only the request's ETag and proposed fence ID. An unrelated claim CAS therefore resumes the original persisted fence and evidence identity. A different session, phase, operation, controller, enrollment or directory revision cannot reuse it. A stale request still refuses and must be refreshed by the authenticated root caller.
+
#### Patch: `internal/cgroup/strict_linux.go`
```diff
@@ -3415,6 +3818,18 @@ go test ./internal/hostbox -run 'TypedRetirement|TypedParker' -count=1
+ if p.closed || p.attempted || p.empty || p.draining {
@@ -313,0 +335 @@
+ p.draining = true
+@@ -353,0 +376 @@
++ p.draining = true
+```
+
+#### Patch: `internal/cgroup/strict_linux_test.go`
+
+```diff
+--- a/internal/cgroup/strict_linux_test.go
++++ b/internal/cgroup/strict_linux_test.go
+@@ -213 +213 @@
+- if e := p.Start(exec.Command("/not-executed")); !errors.Is(e, ErrStrictRefused) {
++ if e := p.Start(exec.Command("/not-executed")); !errors.Is(e, ErrStrictState) {
```
#### File: `internal/cgroup/role_linux.go`
@@ -3581,7 +3996,7 @@ func TestPrepareRoleChildEmptyAndIdentity(t *testing.T) {
}
func TestRoleChildCannotOutliveParentCleanup(t *testing.T) {
- for _, action := range []string{"kill", "close", "failed-kill"} {
+ for _, action := range []string{"kill", "close", "failed-kill", "failed-close"} {
t.Run(action, func(t *testing.T) {
p := strictFixture(t)
makeRoleFixture(t, p, "seed", "populated 0\n")
@@ -3592,7 +4007,18 @@ func TestRoleChildCannotOutliveParentCleanup(t *testing.T) {
defer child.Close()
starts := 0
child.start = func(*exec.Cmd) error { starts++; return nil }
- if action == "close" {
+ if action == "failed-close" {
+ if err := strictWrite(p.events, "populated 1\n"); err != nil {
+ t.Fatal(err)
+ }
+ if err := p.Close(); !errors.Is(err, ErrStrictRefused) {
+ t.Fatalf("failed close=%v", err)
+ }
+ // Even once the parent's population drops, cleanup remains latched.
+ if err := strictWrite(p.events, "populated 0\n"); err != nil {
+ t.Fatal(err)
+ }
+ } else if action == "close" {
if err := p.Close(); err != nil {
t.Fatal(err)
}
@@ -3941,6 +4367,9 @@ func (t FenceTarget) validate(host string) error {
func (t FenceTarget) sameAuthority(other FenceTarget) bool {
t.FenceID, other.FenceID = "", ""
+ // The fresh request has already revalidated this session and operation.
+ // A bookkeeping CAS must not replace the persisted fence or its evidence.
+ t.ClaimETag, other.ClaimETag = "", ""
return t == other
}
@@ -4334,6 +4763,43 @@ func TestCleanupOldFencePreservesSuccessor(t *testing.T) {
func hostguardCaptured(target FenceTarget) Captured {
return Captured{Incarnation: target.Incarnation, Stamp: target.Stamp}
}
+
+func TestBeginFenceResumesAfterUnrelatedClaimCAS(t *testing.T) {
+ f, target, _ := fenceFixture(t)
+ if _, err := f.g.BeginFence(t.Context(), target); err != nil {
+ t.Fatal(err)
+ }
+ f.obs.ETag = "bookkeeping"
+ if _, err := f.g.BeginFence(t.Context(), target); !errors.Is(err, ErrFenceTarget) {
+ t.Fatalf("stale request: %v", err)
+ }
+ fresh := target
+ fresh.ClaimETag = f.obs.ETag
+ fresh.FenceID = "retry"
+ got, err := f.g.BeginFence(t.Context(), fresh)
+ if err != nil || got != target {
+ t.Fatalf("resume=%+v %v", got, err)
+ }
+ at := time.Unix(1, 0).UTC()
+ if err := f.g.CompleteFence(t.Context(), got, []byte("original receipt"), at, func(context.Context, FenceTarget, []byte, time.Time) error { return nil }); err != nil {
+ t.Fatal(err)
+ }
+ receipt, err := f.g.FenceReceiptFor(t.Context(), target)
+ if err != nil || string(receipt.Bytes) != "original receipt" || receipt.Target != target {
+ t.Fatalf("receipt=%+v %v", receipt, err)
+ }
+ for _, change := range []func(*FenceTarget){func(v *FenceTarget) { v.Session++ }, func(v *FenceTarget) { v.Op = "another" }, func(v *FenceTarget) { v.ControllerID = "another" }} {
+ g, old, _ := fenceFixture(t)
+ if _, err := g.g.BeginFence(t.Context(), old); err != nil {
+ t.Fatal(err)
+ }
+ fresh := old
+ change(&fresh)
+ if _, err := g.g.BeginFence(t.Context(), fresh); err == nil {
+ t.Fatal("changed authority resumed")
+ }
+ }
+}
```
#### File: `internal/host/claimstop.go`
@@ -5793,6 +6259,8 @@ go test ./internal/release -run 'Claim' -count=1
go test ./internal/host -run '^TestClaimWiring' -count=1
```
+With the flag unset, config passes retain per-host error logging, continue with later hosts and run their existing reapers. Enabled config errors still refuse the pass. Timer convergence captures each route's work before either declaration or run-signal reads, then retains it for mutations and reports. Enabled upserts compare the observed registration and return the resulting registration transactionally. Status and run-now updates use that returned registration; deletions use the original observed rows, including exec-to-unit cleanup. Task 9 defines `RegionalLoop` locally and tests its lifecycle with a fake, so its host tests compile before Task 10's concrete worker exists.
+
#### Patch: `internal/host/agent.go`
```diff
@@ -6035,7 +6503,7 @@ patch = r"""--- a/internal/appconfig/sync.go
+ selected[rt.Host] = work
@@ -260 +276,3 @@
- syncHost(rt, m, defaultMissing, priv, envDir, restart, instETag, ack)
-+ if err := syncHostClaimed(ctx, selected[rt.Host], rt, m, defaultMissing, priv, envDir, restart, instETag, ack); err != nil {
++ if err := syncHostClaimed(ctx, selected[rt.Host], rt, m, defaultMissing, priv, envDir, restart, instETag, ack); err != nil && hostWrites.Enabled {
+ return err
+ }
@@ -270 +288,5 @@
@@ -6155,7 +6623,7 @@ patch = r"""--- a/internal/appconfig/syncexec.go
+ selected[rt.Host] = work
@@ -572 +588,3 @@
- syncExecHost(rt, m, Key(k.acct, k.app), priv, dataDir, hostBounce, header, instETag, ack)
-+ if err := syncExecHostClaimed(ctx, selected[rt.Host], rt, m, Key(k.acct, k.app), priv, dataDir, hostBounce, header, instETag, ack); err != nil {
++ if err := syncExecHostClaimed(ctx, selected[rt.Host], rt, m, Key(k.acct, k.app), priv, dataDir, hostBounce, header, instETag, ack); err != nil && hostWrites.Enabled {
+ return err
+ }
@@ -625 +643,5 @@
@@ -6326,6 +6794,7 @@ import (
"github.com/carlosframework/platform/internal/hostwrite"
"github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
)
type pausedConfigWork struct {
@@ -6430,6 +6899,56 @@ func TestSyncRejectsMultipleHostWriteConfigurations(t *testing.T) {
t.Fatalf("multiple configs = %v", err)
}
}
+
+func TestClaimDisabledConfigErrorContinuesHostsAndReaps(t *testing.T) {
+ for _, exec := range []bool{false, true} {
+ t.Run(map[bool]string{false: "unit", true: "exec"}[exec], func(t *testing.T) {
+ reg := testRegistry(t)
+ defer reg.Close()
+ store := release.NewDirStore(t.TempDir())
+ dir := t.TempDir()
+ if err := SetEnv(t.Context(), store, "bab", "app", map[string]string{"VALUE": "new"}); err != nil {
+ t.Fatal(err)
+ }
+ suffix := ".env"
+ backing := registry.BackingUnit
+ if exec {
+ suffix = configEnvSuffix
+ backing = ""
+ }
+ for _, host := range []string{"a.example", "b.example"} {
+ if err := reg.Upsert(registry.Route{Host: host, Kind: "instance", Account: "bab", App: "app", Backing: backing, Status: "ready", Socket: "/unused.sock"}); err != nil {
+ t.Fatal(err)
+ }
+ }
+ // A directory at the temporary-file path produces a deterministic error even as root.
+ if err := os.Mkdir(filepath.Join(dir, "a.example"+suffix+".tmp"), 0700); err != nil {
+ t.Fatal(err)
+ }
+ stale := filepath.Join(dir, "gone.example"+suffix)
+ if err := os.WriteFile(stale, []byte("old"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ calls := map[string]int{}
+ restart := func(host string) error { calls[host]++; return nil }
+ var err error
+ if exec {
+ err = SyncExec(t.Context(), reg, store, nil, dir, restart, "", nil)
+ } else {
+ err = Sync(t.Context(), reg, store, nil, dir, restart, nil)
+ }
+ if err != nil {
+ t.Fatalf("legacy pass aborted: %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(dir, "b.example"+suffix)); err != nil || calls["b.example"] != 1 {
+ t.Fatalf("later host: %v calls=%v", err, calls)
+ }
+ if _, err := os.Stat(stale); !os.IsNotExist(err) {
+ t.Fatalf("reap skipped: %v", err)
+ }
+ })
+ }
+}
```
#### Patch: `internal/release/adopt.go`
@@ -6859,6 +7378,92 @@ func TestPassEnabledMissingHostWriteProviderRefusesBeforeChannelRead(t *testing.
+ return
```
+#### File: `internal/registry/claimtimers.go`
+
+```go
+package registry
+
+import (
+ "database/sql"
+ "errors"
+)
+
+var ErrTimerChanged = errors.New("registry: timer registration changed")
+
+// UpsertTimerRegistration compares the captured registration and returns the
+// resulting registration in the same transaction. Empty means absent at read.
+func (r *Registry) UpsertTimerRegistration(t Timer, expected string) (*Timer, error) {
+ tx, err := r.sql.Begin()
+ if err != nil {
+ return nil, err
+ }
+ defer tx.Rollback()
+ old, err := scanTimer(tx.QueryRow(`SELECT `+timerCols+` FROM timers WHERE host=? AND name=?`, t.Host, t.Name))
+ if errors.Is(err, sql.ErrNoRows) {
+ if expected != "" {
+ return nil, ErrTimerChanged
+ }
+ } else if err != nil {
+ return nil, err
+ } else {
+ if old.Kind != t.Kind {
+ return nil, ErrTimerKind
+ }
+ if expected == "" || old.Registration != expected {
+ return nil, ErrTimerChanged
+ }
+ }
+ const rearm = `(timers.spec != excluded.spec OR timers.path != excluded.path OR excluded.next_at != 0)`
+ row, err := scanTimer(tx.QueryRow(`INSERT INTO timers (`+timerCols+`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,lower(hex(randomblob(16))))
+ON CONFLICT(host, name) DO UPDATE SET
+ spec=excluded.spec, path=excluded.path,
+ next_at=CASE WHEN `+rearm+` THEN excluded.next_at ELSE timers.next_at END,
+ due_at=CASE WHEN `+rearm+` THEN excluded.due_at ELSE timers.due_at END,
+ attempts=CASE WHEN `+rearm+` THEN 0 ELSE timers.attempts END,
+ registration=CASE WHEN `+rearm+` THEN excluded.registration ELSE timers.registration END
+WHERE timers.kind=excluded.kind AND timers.registration=?
+RETURNING `+timerCols,
+ t.Host, t.Name, t.Kind, t.Spec, t.Path, t.NextAt, t.LastAt, t.LastStatus, t.LastMs, t.Attempts, t.CreatedAt, t.DueAt, expected))
+ if errors.Is(err, sql.ErrNoRows) {
+ return nil, ErrTimerChanged
+ }
+ if err != nil {
+ return nil, err
+ }
+ if err := tx.Commit(); err != nil {
+ return nil, err
+ }
+ return &row, nil
+}
+
+func (r *Registry) SetTimerStatusRegistration(host, name, registration, status string) error {
+ if registration == "" {
+ return ErrTimerChanged
+ }
+ res, err := r.sql.Exec(`UPDATE timers SET last_status=? WHERE host=? AND name=? AND registration=?`, status, host, name, registration)
+ return timerRegistrationResult(res, err)
+}
+
+func (r *Registry) FireTimerNowRegistration(host, name, registration string, now int64) error {
+ if registration == "" {
+ return ErrTimerChanged
+ }
+ res, err := r.sql.Exec(`UPDATE timers SET next_at=?, due_at=?, attempts=0, registration=lower(hex(randomblob(16))) WHERE host=? AND name=? AND registration=?`, now, now, host, name, registration)
+ return timerRegistrationResult(res, err)
+}
+
+func timerRegistrationResult(res sql.Result, err error) error {
+ if err != nil {
+ return err
+ }
+ n, err := res.RowsAffected()
+ if err == nil && n == 0 {
+ return ErrTimerChanged
+ }
+ return err
+}
+```
+
#### Patch: `internal/schedule/converge.go`
```diff
@@ -6891,16 +7496,29 @@ func TestPassEnabledMissingHostWriteProviderRefusesBeforeChannelRead(t *testing.
+ } else if n > 0 {
+ log.Printf("schedule: pruned %d timer row(s)", n)
+ }
-@@ -169 +173,7 @@
-- changed, err := c.convergeHost(rt, decl, fireName, now)
+@@ -122,0 +127,11 @@
++ // Capture once before either grouped remote input read. Reuse on every effect.
++ selected := map[string]hostwrite.Work{}
++ var errs []error
++ for _, rt := range rts {
+ work, err := c.HostWrites.Select(ctx, rt.Host, "timers")
+ if err != nil {
+ errs = append(errs, fmt.Errorf("%s: select lifecycle authority: %w", rt.Host, err))
+ continue
+ }
++ selected[rt.Host] = work
++ }
+@@ -167 +181,0 @@
+- var errs []error
+@@ -169 +183,6 @@
+- changed, err := c.convergeHost(rt, decl, fireName, now)
++ work, ok := selected[rt.Host]
++ if !ok {
++ continue
++ }
+ hctx := context.WithValue(ctx, timerWorkKey{}, timerWork{ctx: ctx, work: work})
+ changed, err := c.convergeHost(hctx, rt, decl, fireName, now)
-@@ -175 +185,6 @@
+@@ -175 +194,6 @@
- st, err := StatusFor(c.Reg, rt, c.Box, now, c.Idle)
+ var st Status
+ err := withTimerWork(hctx, func() error {
@@ -6908,7 +7526,7 @@ func TestPassEnabledMissingHostWriteProviderRefusesBeforeChannelRead(t *testing.
+ st, statusErr = StatusFor(c.Reg, rt, c.Box, now, c.Idle)
+ return statusErr
+ })
-@@ -188 +203,26 @@
+@@ -188 +212,26 @@
-func (c *Converger) convergeHost(rt registry.Route, decl Declaration, fireName string, now time.Time) (bool, error) {
+func (c *Converger) convergeHost(ctx context.Context, rt registry.Route, decl Declaration, fireName string, now time.Time) (bool, error) {
+ var before []registry.Timer
@@ -6936,43 +7554,91 @@ func TestPassEnabledMissingHostWriteProviderRefusesBeforeChannelRead(t *testing.
+ }
+ before = kept
+ }
-@@ -194 +234,6 @@
+@@ -194 +243,6 @@
- n, err := c.Reg.DeleteTimersExcept(rt.Host, KindSchedule, map[string]bool{})
+ var n int
+ err := withTimerWork(ctx, func() error {
+ var writeErr error
-+ n, writeErr = c.Reg.DeleteTimersExcept(rt.Host, KindSchedule, map[string]bool{})
++ n, writeErr = c.deleteSchedules(rt.Host, before, map[string]bool{})
+ return writeErr
+ })
-@@ -206,3 +251,6 @@
+@@ -206,4 +260,8 @@
- before, err := c.Reg.TimersFor(rt.Host)
- if err != nil {
- return false, err
+- }
+ if !c.HostWrites.Enabled {
+ var err error
+ before, err = c.Reg.TimersFor(rt.Host)
+ if err != nil {
+ return false, err
+ }
-@@ -241 +289,3 @@
++ }
++ registrations := map[string]string{}
+@@ -241 +299,14 @@
- err = c.Reg.UpsertTimer(registry.Timer{Host: rt.Host, Name: sc.Name, Kind: KindSchedule, Spec: sp.String(), Path: sc.Path, NextAt: next, DueAt: next, CreatedAt: now.Unix()})
++ var installed *registry.Timer
+ err = withTimerWork(ctx, func() error {
-+ return c.Reg.UpsertTimer(registry.Timer{Host: rt.Host, Name: sc.Name, Kind: KindSchedule, Spec: sp.String(), Path: sc.Path, NextAt: next, DueAt: next, CreatedAt: now.Unix()})
++ t := registry.Timer{Host: rt.Host, Name: sc.Name, Kind: KindSchedule, Spec: sp.String(), Path: sc.Path, NextAt: next, DueAt: next, CreatedAt: now.Unix()}
++ if !c.HostWrites.Enabled {
++ return c.Reg.UpsertTimer(t)
++ }
++ expected := ""
++ if prev != nil {
++ expected = prev.Registration
++ }
++ var err error
++ installed, err = c.Reg.UpsertTimerRegistration(t, expected)
++ return err
+ })
-@@ -253 +303 @@
+@@ -250,0 +322,3 @@
++ if installed != nil {
++ registrations[sc.Name] = installed.Registration
++ }
+@@ -253 +327,6 @@
- if err := c.Reg.SetTimerStatus(rt.Host, sc.Name, StatusNeverFires); err != nil {
-+ if err := withTimerWork(ctx, func() error { return c.Reg.SetTimerStatus(rt.Host, sc.Name, StatusNeverFires) }); err != nil {
-@@ -261 +311,6 @@
++ if err := withTimerWork(ctx, func() error {
++ if c.HostWrites.Enabled {
++ return c.Reg.SetTimerStatusRegistration(rt.Host, sc.Name, installed.Registration, StatusNeverFires)
++ }
++ return c.Reg.SetTimerStatus(rt.Host, sc.Name, StatusNeverFires)
++ }); err != nil {
+@@ -261 +340,6 @@
- n, err := c.Reg.DeleteTimersExcept(rt.Host, KindSchedule, keep)
+ var n int
+ err := withTimerWork(ctx, func() error {
+ var writeErr error
-+ n, writeErr = c.Reg.DeleteTimersExcept(rt.Host, KindSchedule, keep)
++ n, writeErr = c.deleteSchedules(rt.Host, before, keep)
+ return writeErr
+ })
-@@ -269 +324 @@
+@@ -269 +353,6 @@
- if err := c.Reg.FireTimerNow(rt.Host, fireName, now.Unix()); err != nil {
-+ if err := withTimerWork(ctx, func() error { return c.Reg.FireTimerNow(rt.Host, fireName, now.Unix()) }); err != nil {
++ if err := withTimerWork(ctx, func() error {
++ if c.HostWrites.Enabled {
++ return c.Reg.FireTimerNowRegistration(rt.Host, fireName, registrations[fireName], now.Unix())
++ }
++ return c.Reg.FireTimerNow(rt.Host, fireName, now.Unix())
++ }); err != nil {
+@@ -389,0 +479,19 @@
++
++// deleteSchedules only removes rows observed by this pass. A same-name
++// replacement keeps its registration and survives the stale deletion.
++func (c *Converger) deleteSchedules(host string, before []registry.Timer, keep map[string]bool) (int, error) {
++ if !c.HostWrites.Enabled {
++ return c.Reg.DeleteTimersExcept(host, KindSchedule, keep)
++ }
++ n := 0
++ for _, row := range before {
++ if row.Kind != KindSchedule || keep[row.Name] {
++ continue
++ }
++ if err := c.Reg.DeleteTimerRegistration(host, row.Name, row.Registration); err != nil {
++ return n, err
++ }
++ n++
++ }
++ return n, nil
++}
```
#### File: `internal/schedule/claimintegration_test.go`
@@ -6989,8 +7655,11 @@ import (
"testing"
"time"
+ "encoding/json"
"github.com/carlosframework/platform/internal/hostwrite"
"github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+ "reflect"
)
type lifecycleWork struct{ valid *atomic.Bool }
@@ -7072,6 +7741,133 @@ func TestEnabledRunnerMissingLifecycleProviderRefusesTimer(t *testing.T) {
t.Fatal("delivery ran without lifecycle provider")
}
}
+
+type rotatingSelector struct {
+ current *atomic.Int64
+ calls int
+}
+type capturedEpoch struct {
+ current *atomic.Int64
+ selected int64
+}
+
+func (s *rotatingSelector) Select(context.Context, string, string) (hostwrite.Work, error) {
+ s.calls++
+ return capturedEpoch{s.current, s.current.Load()}, nil
+}
+func (w capturedEpoch) With(_ context.Context, _ hostwrite.Identity, fn func() error) error {
+ if w.current.Load() != w.selected {
+ return errors.New("installation changed")
+ }
+ return fn()
+}
+
+type changeOnRead struct {
+ release.Store
+ key string
+ current *atomic.Int64
+}
+
+func (s changeOnRead) Get(ctx context.Context, key string) ([]byte, string, error) {
+ b, e, err := s.Store.Get(ctx, key)
+ if key == s.key {
+ s.current.Add(1)
+ }
+ return b, e, err
+}
+
+func TestConvergerCapturesBeforeDeclarationAndRunReads(t *testing.T) {
+ for _, key := range []string{DeclarationKey("bab", "app"), RunKey("bab", "app")} {
+ t.Run(key, func(t *testing.T) {
+ reg, store := convergeFixture(t)
+ decl := []byte(`{"schedules":[{"name":"tick","every":"1h","path":"/tick"}]}`)
+ if _, err := store.Put(t.Context(), DeclarationKey("bab", "app"), decl, ""); err != nil {
+ t.Fatal(err)
+ }
+ epoch := &atomic.Int64{}
+ selector := &rotatingSelector{current: epoch}
+ c := NewConverger(reg, changeOnRead{store, key, epoch}, "box", nil)
+ c.HostWrites = hostwrite.Config{Enabled: true, Selector: selector}
+ if err := c.Pass(t.Context()); err == nil {
+ t.Fatal("old inputs acquired successor authority")
+ }
+ rows, err := reg.TimersFor("a.bab.oncarlos.com")
+ if err != nil || len(rows) != 0 {
+ t.Fatalf("stale rows=%v %v", rows, err)
+ }
+ if selector.calls != 4 {
+ t.Fatalf("selection refreshed: %d", selector.calls)
+ }
+ })
+ }
+}
+
+type replaceAtWork struct {
+ n, at int
+ replace func()
+}
+
+func (w *replaceAtWork) With(_ context.Context, _ hostwrite.Identity, fn func() error) error {
+ w.n++
+ if w.n == w.at {
+ w.replace()
+ }
+ return fn()
+}
+func TestConvergerRegistrationMutationsPreserveReplacement(t *testing.T) {
+ for _, kind := range []string{"delete", "unit-delete", "status", "run", "upsert"} {
+ t.Run(kind, func(t *testing.T) {
+ reg, _ := convergeFixture(t)
+ host := "a.bab.oncarlos.com"
+ original := registry.Timer{Host: host, Name: "tick", Kind: KindSchedule, Spec: "every 1h0m0s", Path: "/tick", NextAt: 100, DueAt: 100}
+ if err := reg.UpsertTimer(original); err != nil {
+ t.Fatal(err)
+ }
+ rt, _ := reg.Get(host)
+ var decl Declaration
+ if err := json.Unmarshal([]byte(`{"schedules":[{"name":"tick","every":"1h","path":"/tick"}]}`), &decl); err != nil {
+ t.Fatal(err)
+ }
+ fire := ""
+ at := 2
+ switch kind {
+ case "delete":
+ decl = Declaration{}
+ case "unit-delete":
+ rt.Backing = registry.BackingUnit
+ decl = Declaration{}
+ case "status":
+ decl = Declaration{}
+ if err := json.Unmarshal([]byte(`{"schedules":[{"name":"tick","cron":"0 0 31 2 *","path":"/tick"}]}`), &decl); err != nil {
+ t.Fatal(err)
+ }
+ at = 3
+ case "run":
+ fire = "tick"
+ at = 4
+ }
+ var replacement *registry.Timer
+ w := &replaceAtWork{at: at, replace: func() {
+ new := original
+ new.Path = "/successor"
+ new.NextAt = 999
+ new.DueAt = 999
+ if err := reg.UpsertTimer(new); err != nil {
+ t.Fatal(err)
+ }
+ replacement, _ = reg.GetTimer(host, "tick")
+ }}
+ ctx := context.WithValue(t.Context(), timerWorkKey{}, timerWork{ctx: t.Context(), work: w})
+ c := NewConverger(reg, nil, "box", nil)
+ c.HostWrites.Enabled = true
+ _, _ = c.convergeHost(ctx, *rt, decl, fire, time.Unix(1000, 0))
+ got, err := reg.GetTimer(host, "tick")
+ if replacement == nil || err != nil || !reflect.DeepEqual(got, replacement) {
+ t.Fatalf("%s replaced row: got=%+v want=%+v err=%v work=%d", kind, got, replacement, err, w.n)
+ }
+ })
+ }
+}
```
#### File: `internal/host/claimwiring.go`
@@ -7092,10 +7888,14 @@ import (
// ClaimProviders contains deployment providers after identity, floor and
// quarantine verification. Build must fail if any of those gates is unknown.
+type RegionalLoop interface {
+ Loop(context.Context, <-chan struct{}, func(error)) error
+}
+
type ClaimProviders struct {
Selector hostwrite.Selector
Wake *activator.ClaimRuntime
- Regional *hostbox.RegionalWorker
+ Regional RegionalLoop
Stop *ClaimStopCoordinator
}
type ClaimFactory func(context.Context) (ClaimProviders, error)
@@ -7229,6 +8029,28 @@ func TestClaimWiringOnMissingProviderRefuses(t *testing.T) {
t.Fatal("missing provider configured")
}
}
+
+type testRegionalLoop struct{ started chan struct{} }
+
+func (r testRegionalLoop) Loop(ctx context.Context, _ <-chan struct{}, _ func(error)) error {
+ close(r.started)
+ <-ctx.Done()
+ return ctx.Err()
+}
+
+var _ RegionalLoop = testRegionalLoop{}
+
+func TestClaimWiringRegionalContractBeforeTask10(t *testing.T) {
+ loop := testRegionalLoop{started: make(chan struct{})}
+ w := &ClaimWiring{Writes: hostwrite.Config{Enabled: true}, Providers: ClaimProviders{Regional: loop}}
+ if err := w.startRegional(t.Context(), nil, nil); err != nil {
+ t.Fatal(err)
+ }
+ <-loop.started
+ if err := w.close(t.Context()); err != nil {
+ t.Fatal(err)
+ }
+}
```
### Task 10: Run regional obligations and serving/free sweeps
@@ -7245,6 +8067,8 @@ func TestClaimWiringOnMissingProviderRefuses(t *testing.T) {
go test ./internal/hostbox ./internal/park -run 'Regional|Aggregate|Sweep|Horizon|Remote' -count=1 -race
```
+A regional sweep may join an existing holderless `parking` operation only when its ID is nonempty, its executor is empty and `intent.park_sweep == "v1"`. The park engine adds the new region's residue under that persisted operation ID, preserving another region's chores. Other holderless operation shapes still refuse new sweep work. The regression uses the real park engine with another region's outstanding obligation.
+
#### File: `internal/hostbox/requeststore.go`
```go
@@ -7728,7 +8552,8 @@ func (w RegionalWorker) Census(ctx context.Context) ([]string, error) {
}
// Run performs one bounded pass. Existing chores run in every claim state;
-// only serving and free claims may create a new sweep. A missing claim is
+// serving/free claims start sweeps; holderless park_sweep:v1 operations admit
+// residue from another region under the persisted operation. A missing claim is
// refused because legacy/backfill disposition is a separate authority.
func (w RegionalWorker) Run(ctx context.Context) error {
hosts, err := w.Census(ctx)
@@ -7854,7 +8679,9 @@ func (w RegionalWorker) runHost(ctx context.Context, host string) error {
if err != nil {
return err
}
- if obs.Absent || obs.Claim == nil || (!obs.Claim.Free() && obs.Claim.Op.Kind != hostclaim.KindServing) {
+ joining := obs.Claim != nil && obs.Claim.Holder == nil && obs.Claim.Op.Kind == hostclaim.KindParking &&
+ obs.Claim.Op.ID != "" && obs.Claim.Op.Exec == "" && obs.Claim.Op.Intent["park_sweep"] == "v1"
+ if obs.Absent || obs.Claim == nil || (!obs.Claim.Free() && obs.Claim.Op.Kind != hostclaim.KindServing && !joining) {
return nil
}
if pending {
@@ -8132,6 +8959,55 @@ func TestRegionalWorkerDoesNotBindEvidenceToLateVersion(t *testing.T) {
t.Fatalf("late residue queued under old evidence: %+v, %v", obs.Claim.Obligations, err)
}
}
+
+func TestRegionalWorkerJoinsHolderlessSweepFromAnotherRegion(t *testing.T) {
+ ctx := t.Context()
+ host := "joining.example"
+ claims := release.NewDirStore(t.TempDir())
+ at := time.Unix(1, 0).UTC()
+ inc := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ uk := hostclaim.Obligation{ID: "uk-chore", Op: "original-op", Kind: "park-replica", Region: "uk", Owner: "region:uk", Retirement: "uk-retire", Park: &hostclaim.ParkScope{Location: "instances/" + host + "/" + inc, Incarnation: inc, EvidenceAt: at, RequestLimit: RequestLimit}}
+ if _, err := hostclaim.Update(ctx, claims, hostclaim.Observed{Host: host, Absent: true}, func(c *hostclaim.Claim) error {
+ c.Op = hostclaim.Op{Kind: hostclaim.KindParking, ID: "original-op", Intent: map[string]string{"park_sweep": "v1"}}
+ c.Obligations = []hostclaim.Obligation{uk}
+ return nil
+ }); err != nil {
+ t.Fatal(err)
+ }
+ bucket := leasetest.NewVersionedBucket()
+ bucket.AddBucket("data")
+ bucket.Put("data", "instances/"+host+"/"+inc+"/old", []byte("residue"))
+ seq := 0
+ seen := false
+ engine := park.Engine{Claims: claims, Store: bucket, Bucket: "data", Region: "ie", Leases: workerLeases{}, Now: func() time.Time { return at.Add(18 * time.Minute) }, NewID: func() string { seq++; return fmt.Sprintf("new%d", seq) }, View: func(_ context.Context, o hostclaim.Observed) (park.View, error) {
+ return park.View{ClaimETag: o.ETag, Region: "ie"}, nil
+ }, Authorize: func(_ context.Context, _ hostclaim.Observed, o hostclaim.Obligation) error {
+ if o.Op != "original-op" {
+ t.Fatalf("replaced operation: %+v", o)
+ }
+ seen = true
+ return nil
+ }}
+ w := RegionalWorker{Regional: Regional{Engine: engine, Owner: func(context.Context, hostclaim.Observed) error { return nil }}, Enrollment: enrollmentFunc(func(context.Context, string, string, time.Duration) error { return nil }), Evidence: evidenceFunc(func(context.Context, hostclaim.Observed, []lease.ObjectVersion) (string, time.Time, error) {
+ return inc, at, nil
+ })}
+ if err := w.Run(ctx); err != nil {
+ t.Fatal(err)
+ }
+ obs, err := hostclaim.Read(ctx, claims, host)
+ if err != nil || !seen || obs.Claim.Op.ID != "original-op" {
+ t.Fatalf("join: seen=%v obs=%+v err=%v", seen, obs, err)
+ }
+ found := false
+ for _, o := range obs.Claim.Obligations {
+ if o.ID == uk.ID {
+ found = true
+ }
+ }
+ if !found {
+ t.Fatal("other region's chore lost")
+ }
+}
```
#### Patch: `internal/park/sweep.go`
@@ -8311,3 +9187,37 @@ The default Go cache was refused as read-only by this sandbox. Authoring checks
**Limits:** the full repository test gate is not verified. Broader host/release and other network-listener suites encountered sandbox `listen: operation not permitted` failures. No live AWS call, production resource mutation, delegated root cgroup experiment or feature activation was performed. Passing fixture-based containment tests does not establish the privileged kernel witness. An independent code review found and rechecked fixes for forged retirement progress, sweep evidence binding, rename-retry fsync, inherited cgroup descriptors and child launches after parent cleanup. This was not the repository-required Opus review; that provider was unavailable.
**Completeness:** this is a tested partial implementation plan, despite the requested revision heading. Complete production provider assembly is absent and `productionClaimFactory` refuses enabled startup. Plan 6 still needs the registry ownership migration and archived conditional retirement transaction, full root instance-control transport/collector and durable launch journal, backend/session composition and the remaining actual caller bindings enumerated in Task 9. Plan 8's verified local controller/deadline/roster providers and moves' held-input/marker providers must be supplied through their named contracts. These missing implementations are not open design decisions. The only unresolved design item identified by the brief remains `park-superseded`, which is refused.
+
+
+## Rev 3 authoring verification
+
+Re-verified the twelve rev 2 findings against a disposable export of **feat/hostclaim-box 2bd3ea1f**, claim **rev 6.29** and the moves index at **d0907747**. Reconstructed all **72** source/test files represented by Tasks 6-10 into a second fresh export and compared every resulting byte with the tested source. Tasks 1-5 remain byte-identical to rev 2. Checked the exact revision heading, task order, fenced blocks, trailing whitespace and absence of em dashes.
+
+Each finding has a regression in the plan:
+
+| Rev 2 finding | Regression |
+|---|---|
+| P1: bare-key LTX deletes | `TestGenerationClientRetentionRefusedButUploadsContinue`, `TestGenerationClientDisablesRemoteRetentionInRealStore`, `TestGenerationClientRealRetentionMonitorsDoNotDelete` |
+| P2: generation-prefix trailing slash | `TestBindGenerationAcceptsRealGenerationPrefix` calls both real functions; the existing canonical/immutable binding tests remain |
+| P2: C1's missing prepared manifest and input pin | `TestStartAdmittedPreparedInputsAndRestoreOutsideProbeWindow` constructs all six policy fields and checks both pointers at `Prepare` |
+| P2: restore consumes readiness time and expiry lacks the required error | The same admitted-start regression plus `TestClaimProbeWindowJournalsFirstStartAndSurvivesRetry` cover slow restore, first-start journalling, process/controller retry, late readiness and cut/window expiry with `ErrAttemptOver` |
+| P2: released hold blocks an ordinary wake | `TestPolicyOrdinaryWakeAfterReleaseHold` covers landing committed, reverting/restoring probed/opened, held-action refusal and stale operation refusal |
+| P2: unrelated claim CAS strands a fence | `TestBeginFenceResumesAfterUnrelatedClaimCAS` preserves the original fence/receipt and refuses stale reads or changed authority |
+| P2: failed parent close permits later child start | `TestRoleChildCannotOutliveParentCleanup/failed-close` keeps the latch after the parent becomes empty |
+| P2: disabled config error aborts later hosts/reaping | `TestClaimDisabledConfigErrorContinuesHostsAndReaps` covers both unit and exec passes |
+| P1: timer convergence captures after remote reads | `TestConvergerCapturesBeforeDeclarationAndRunReads` changes installation during each read separately |
+| P2: stale timer mutations reach replacement registrations | `TestConvergerRegistrationMutationsPreserveReplacement` covers deletion, exec-to-unit deletion, status, run-now and upsert, preserving the replacement row byte-for-byte |
+| P2: Task 9 references Task 10's worker | `TestClaimWiringRegionalContractBeforeTask10`, plus the actual Task 9 host gate with only Tasks 6-9 applied |
+| P2: another region cannot join holderless parking | `TestRegionalWorkerJoinsHolderlessSweepFromAnotherRegion` exercises the real park engine and preserves the original operation ID and other region's chore |
+
+Restoring each rev 2 behavior made its regression fail. Removing C1's fields caused the expected caller compile errors. Restoring Task 9's concrete worker dependency while excluding Task 10 caused `undefined: hostbox.RegionalWorker`; the corrected Task 9 gate passed without Task 10. All twelve fixes were restored afterward, and the final files match the reconstructed document.
+
+The final source passed whole-tree `go build -buildvcs=false ./...`, `go vet ./...`, and `gofmt -l .` with no format output. It also passed whole-tree builds for Darwin/amd64, Windows/amd64 and Linux/arm64. Full race-enabled package tests passed for `internal/hostwrite`, `hostbox`, `hostguard`, `hostclaim`, `park`, `genselect`, `appconfig`, `schedule` and `cgroup`. The cgroup run initially found the old stale-handle assertion expecting `ErrStrictRefused` after cleanup; the plan now expects the earlier `ErrStrictState` latch, and the complete cgroup race suite passes. Focused race-enabled tests passed with:
+
+```bash
+go test ./internal/activator ./internal/host ./internal/lease ./internal/release ./internal/instancectl ./internal/cgroup -run '^Test(Claim|HostClaimParked|SameAdmission|StartAdmitted|BindGeneration|GenerationClient|PassStaleSelection|PassRollbackReuses|PassEnabledMissingHostWrite|AgentClaimConsumers|DisabledAgentClaim|ProcStart|Strict|Role|PrepareRole)' -count=1 -race
+```
+
+The actual Task 6 gate passed with only Task 6 applied, and Task 9's prescribed `go test ./internal/host -run '^TestClaimWiring' -count=1` passed before Task 10. Tests used the default on-disk scratch location. The sandbox again rejected the default Go cache as read-only; subsequent commands used the existing shared `/home/paulca/.cache/tmp/gocache` with `GOPROXY=off`, as in rev 2. No new per-task cache was created.
+
+**Limits:** the full repository test suite and privileged cgroup witness remain unverified; this revision does not convert the fixture tests into a kernel proof. The required Opus review was attempted explicitly through the CLI, but timed out without a verdict. No Opus approval is claimed. No production resources, AWS services or feature flags were changed. Temporary exports and review/test scratch were removed. Only this plan was edited in the repository, with no staging or commit; concurrent files belonging to other work were left alone. The production-composition gaps recorded above remain, and `park-superseded` remains the sole unresolved design item identified by the brief, with unknown kinds refused.