carlos / platform Public

provision: commit conditional on the deadlines, open, source park, close (§7.9)

MoveCommit takes landing/probed to committed, refused past the earlier of
cut_deadline and landing_deadline and over a recorded rollback reason
(ruling H8). MoveOpened and MoveSourceParked stamp a committed move,
MoveClose clears it into LastMove (D11), MoveProbeAttempt counts attempts
on a probing landing. Two new rollback reasons (probe out of attempts,
commit past the deadline) with copy rows MV-N182 and MV-N183, pending.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev dc961449c65be8df337f698dce5a2531ebeef29b parent 96144a1
4 files changed, +319 −1
  • approved_moves_copy_test.go +1 −0
  • docs/superpowers/plans/2026-10-06-moves-phase1-copy.md +6 −1
  • internal/provision/move_test.go +177 −0
  • internal/provision/provision.go +135 −0
diff --git a/approved_moves_copy_test.go b/approved_moves_copy_test.go
index 197d9427c..ef0477bd0 100644
--- a/approved_moves_copy_test.go
+++ b/approved_moves_copy_test.go
@@ -70,6 +70,7 @@ var movesCopyIDs = []string{
"MV-N163", "MV-N164", "MV-N165", "MV-N166", "MV-N167", "MV-N168",
"MV-N169", "MV-N170", "MV-N171", "MV-N172", "MV-N173", "MV-N174",
"MV-N175", "MV-N176", "MV-N177", "MV-N178", "MV-N179", "MV-N180", "MV-N181",
+ "MV-N182", "MV-N183",
}
// copyRow is one row of the record. The File cell is the file the sentence is
diff --git a/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md b/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md
index 57c2347bf..40607c9e1 100644
--- a/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md
+++ b/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md
@@ -231,7 +231,12 @@ Columns: id, file, text, status.
## Plan 3
-(appended by Plan 3 Task 9)
+(Task 2's rows, then the rest appended by Plan 3 Task 9)
+
+| id | file | text | status |
+|---|---|---|---|
+| MV-N182 | internal/provision/provision.go | the probe ran out of attempts | pending |
+| MV-N183 | internal/provision/provision.go | the commit came after the deadline | pending |
## Plan 4
diff --git a/internal/provision/move_test.go b/internal/provision/move_test.go
index 529935a77..5030ab441 100644
--- a/internal/provision/move_test.go
+++ b/internal/provision/move_test.go
@@ -623,3 +623,180 @@ func TestMarkRollbackRecordsASentenceOnALandingRecord(t *testing.T) {
t.Fatal("LandingOrder hands out the package's slice")
}
}
+
+func commitTestRecord(g MoveGuard, dl time.Time) *Record {
+ return &Record{Region: "uk", Move: &MoveState{ID: g.ID, Incarnation: g.Incarnation, From: "ie", To: "uk", State: MoveLanding, LandingPhase: LandProbed, CutDeadline: &dl}}
+}
+
+func TestCommitRefusedPastTheDeadline(t *testing.T) {
+ g := MoveGuard{ID: "mv_abcdefghijklmnopqrstuvwxyz", Incarnation: strings.Repeat("ab", 32)}
+ now := time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC)
+ dl := now.Add(115 * time.Second)
+ cut := now.Add(-10 * time.Second)
+ rec := &Record{Region: "uk", Move: &MoveState{ID: g.ID, Incarnation: g.Incarnation, From: "ie", To: "uk", State: MoveLanding, LandingPhase: LandProbed, CutDeadline: &dl, CutAt: &cut}}
+ if err := MoveCommit(g, dl.Add(time.Second))(rec); !errors.Is(err, ErrCutDeadline) || !errors.Is(err, ErrMoveConflict) || rec.Move.State != MoveLanding {
+ t.Fatalf("late commit: %v %s", err, rec.Move.State)
+ }
+ if err := MoveCommit(g, dl)(rec); err != nil || rec.Move.State != MoveCommitted || rec.Move.CommittedAt == nil {
+ t.Fatalf("commit at the deadline: %v", err)
+ }
+ if err := MoveCommit(g, dl)(rec); !errors.Is(err, ErrMoveConflict) {
+ t.Fatal("second commit")
+ }
+ if err := MoveClose(g, now)(rec); !errors.Is(err, ErrMoveConflict) {
+ t.Fatal("close before open and park")
+ }
+ if err := MoveOpened(g, now)(rec); err != nil || rec.Move.OpenedAt == nil {
+ t.Fatal(err)
+ }
+ if err := MoveClose(g, now)(rec); !errors.Is(err, ErrMoveConflict) || rec.Move == nil {
+ t.Fatalf("close before the source park: %v", err)
+ }
+ if err := MoveSourceParked(g, now)(rec); err != nil {
+ t.Fatal(err)
+ }
+ if err := MoveClose(g, now)(rec); err != nil || rec.Move != nil || rec.Region != "uk" || rec.LastMove == nil || rec.LastMove.ID != g.ID || rec.LastMove.Outcome != "done" {
+ t.Fatalf("close: %v %+v", err, rec)
+ }
+ if rec.LastMove.OfflineS != 10 || !rec.LastMove.At.Equal(now) {
+ t.Fatalf("last move: %+v", rec.LastMove)
+ }
+}
+
+// Ruling H8: a recorded rollback wins over a commit, and the earlier of the
+// cut and landing deadlines bounds it.
+func TestCommitRefusesARecordedRollbackAndTheEarlierDeadline(t *testing.T) {
+ g := cutTestGuard()
+ now := time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC)
+ cutDL, landDL := now.Add(115*time.Second), now.Add(60*time.Second)
+ rec := commitTestRecord(g, cutDL)
+ rec.Move.LandingDeadline = &landDL
+ if err := MoveCommit(g, landDL.Add(time.Second))(rec); !errors.Is(err, ErrCutDeadline) || rec.Move.State != MoveLanding {
+ t.Fatalf("past the landing deadline, inside the cut's: %v", err)
+ }
+ if err := MoveCommit(g, landDL)(rec); err != nil || rec.Move.State != MoveCommitted {
+ t.Fatalf("at the earlier deadline: %v", err)
+ }
+ // The landing deadline alone does not stand in for the cut's.
+ rec = commitTestRecord(g, landDL)
+ rec.Move.LandingDeadline = &cutDL
+ if err := MoveCommit(g, landDL.Add(time.Second))(rec); !errors.Is(err, ErrCutDeadline) {
+ t.Fatalf("past the cut deadline, inside the landing's: %v", err)
+ }
+ // A recorded rollback reason refuses the commit; nothing is written.
+ rec = commitTestRecord(g, cutDL)
+ if err := MoveMarkRollback(g, RollbackLandingDeadline)(rec); err != nil {
+ t.Fatal(err)
+ }
+ if err := MoveCommit(g, now)(rec); !errors.Is(err, ErrMoveConflict) || errors.Is(err, ErrCutDeadline) || rec.Move.State != MoveLanding || rec.Move.CommittedAt != nil {
+ t.Fatalf("commit over a rollback marker: %v %+v", err, rec.Move)
+ }
+ // No cut deadline recorded: the commit cannot be shown to be in time.
+ rec = commitTestRecord(g, cutDL)
+ rec.Move.CutDeadline = nil
+ if err := MoveCommit(g, now)(rec); !errors.Is(err, ErrCutDeadline) {
+ t.Fatalf("no cut deadline: %v", err)
+ }
+ // Not probed yet, a replacement move, and a record with no move.
+ for _, phase := range []string{LandChecking, LandRestored, LandProbing} {
+ rec = commitTestRecord(g, cutDL)
+ rec.Move.LandingPhase = phase
+ if err := MoveCommit(g, now)(rec); !errors.Is(err, ErrMoveConflict) || rec.Move.State != MoveLanding {
+ t.Fatalf("commit from %s: %v", phase, err)
+ }
+ }
+ other := MoveGuard{ID: g.ID, Incarnation: strings.Repeat("cd", 32)}
+ if err := MoveCommit(other, now)(commitTestRecord(g, cutDL)); !errors.Is(err, ErrMoveConflict) {
+ t.Fatalf("another incarnation: %v", err)
+ }
+ if err := MoveCommit(g, now)(&Record{}); !errors.Is(err, ErrMoveConflict) {
+ t.Fatalf("no move: %v", err)
+ }
+}
+
+func TestOpenedParkedAndCloseAreCommittedOnly(t *testing.T) {
+ g := cutTestGuard()
+ now := time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC)
+ later := now.Add(time.Minute)
+ for _, state := range []string{MoveLanding, MoveCutting, MoveReverting, MoveRollingBack} {
+ for name, step := range map[string]func(*Record) error{"opened": MoveOpened(g, now), "parked": MoveSourceParked(g, now), "close": MoveClose(g, now)} {
+ rec := commitTestRecord(g, now)
+ rec.Move.State = state
+ if err := step(rec); !errors.Is(err, ErrMoveConflict) || rec.Move == nil || rec.Move.OpenedAt != nil || rec.Move.SourceParkedAt != nil {
+ t.Fatalf("%s from %s: %v", name, state, err)
+ }
+ }
+ }
+ // A replay keeps the first stamp.
+ rec := commitTestRecord(g, now)
+ rec.Move.State = MoveCommitted
+ if err := MoveOpened(g, now)(rec); err != nil {
+ t.Fatal(err)
+ }
+ if err := MoveOpened(g, later)(rec); err != nil || !rec.Move.OpenedAt.Equal(now) {
+ t.Fatalf("opened replay: %v %v", err, rec.Move.OpenedAt)
+ }
+ if err := MoveSourceParked(g, now)(rec); err != nil {
+ t.Fatal(err)
+ }
+ if err := MoveSourceParked(g, later)(rec); err != nil || !rec.Move.SourceParkedAt.Equal(now) {
+ t.Fatalf("parked replay: %v %v", err, rec.Move.SourceParkedAt)
+ }
+ // Close with no cut_at recorded (a record from before it was stamped):
+ // the outcome is written and the offline time is left at zero.
+ if err := MoveClose(g, later)(rec); err != nil || rec.Move != nil || rec.LastMove == nil || rec.LastMove.OfflineS != 0 {
+ t.Fatalf("close without cut_at: %v %+v", err, rec.LastMove)
+ }
+ // Close refuses a record whose region is not the target's.
+ rec = commitTestRecord(g, now)
+ rec.Move.State, rec.Move.OpenedAt, rec.Move.SourceParkedAt = MoveCommitted, &now, &now
+ rec.Region = "ie"
+ if err := MoveClose(g, later)(rec); !errors.Is(err, ErrMoveConflict) || rec.Move == nil || rec.LastMove != nil {
+ t.Fatalf("close with the region still the source's: %v", err)
+ }
+}
+
+func TestProbeAttemptRecordsTheCountOnAProbingLanding(t *testing.T) {
+ g := cutTestGuard()
+ now := time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC)
+ rec := commitTestRecord(g, now)
+ rec.Move.LandingPhase = LandProbing
+ for _, n := range []int{1, 1, 2, 3} {
+ if err := MoveProbeAttempt(g, n)(rec); err != nil || rec.Move.Landing == nil || rec.Move.Landing.Attempts != n {
+ t.Fatalf("attempt %d: %v %+v", n, err, rec.Move.Landing)
+ }
+ }
+ rec.Move.Landing.InstalledDBSHA256 = "keep"
+ if err := MoveProbeAttempt(g, 4)(rec); err != nil || rec.Move.Landing.InstalledDBSHA256 != "keep" {
+ t.Fatalf("the install digest was lost: %v %+v", err, rec.Move.Landing)
+ }
+ for _, n := range []int{3, 0, -1} {
+ if err := MoveProbeAttempt(g, n)(rec); !errors.Is(err, ErrMoveConflict) || rec.Move.Landing.Attempts != 4 {
+ t.Fatalf("attempt %d after 4: %v %+v", n, err, rec.Move.Landing)
+ }
+ }
+ for _, phase := range []string{LandChecking, LandRestored, LandProbed} {
+ rec := commitTestRecord(g, now)
+ rec.Move.LandingPhase = phase
+ if err := MoveProbeAttempt(g, 1)(rec); !errors.Is(err, ErrMoveConflict) || rec.Move.Landing != nil {
+ t.Fatalf("from %s: %v", phase, err)
+ }
+ }
+ rec = commitTestRecord(g, now)
+ rec.Move.LandingPhase = LandProbing
+ if err := MoveMarkRollback(g, RollbackProbeAttempts)(rec); err != nil || rec.Move.Reason != RollbackProbeAttempts {
+ t.Fatalf("out of attempts: %v %q", err, rec.Move.Reason)
+ }
+ if err := MoveProbeAttempt(g, 1)(rec); !errors.Is(err, ErrMoveConflict) || rec.Move.Landing != nil {
+ t.Fatalf("an attempt over a rollback marker: %v", err)
+ }
+ rec = commitTestRecord(g, now)
+ if err := MoveMarkRollback(g, RollbackCommitDeadline)(rec); err != nil || rec.Move.Reason != RollbackCommitDeadline {
+ t.Fatalf("commit past the deadline: %v %q", err, rec.Move.Reason)
+ }
+ rec = commitTestRecord(g, now)
+ rec.Move.State = MoveCommitted
+ if err := MoveProbeAttempt(g, 1)(rec); !errors.Is(err, ErrMoveConflict) {
+ t.Fatalf("committed: %v", err)
+ }
+}
diff --git a/internal/provision/provision.go b/internal/provision/provision.go
index 1dba6ebb3..71345a85a 100644
--- a/internal/provision/provision.go
+++ b/internal/provision/provision.go
@@ -1566,6 +1566,12 @@ const (
// installation registered them, so nothing retired them). They are not
// the landing's to clear (final review, finding 1).
RollbackFilesOccupied = "the files directory already holds files this move did not write"
+ // RollbackProbeAttempts: the probe used every attempt it was given
+ // without the app answering (§7.7).
+ RollbackProbeAttempts = "the probe ran out of attempts"
+ // RollbackCommitDeadline: the probe passed, but the commit would have
+ // come after the cut or landing deadline (§7.9).
+ RollbackCommitDeadline = "the commit came after the deadline"
)
// MoveMarkRollback records on a landing record why the landing stopped
@@ -1600,6 +1606,10 @@ func MoveMarkRollback(g MoveGuard, reason string) func(*Record) error {
rec.Move.Reason = RollbackInstallMismatch
case RollbackFilesOccupied:
rec.Move.Reason = RollbackFilesOccupied
+ case RollbackProbeAttempts:
+ rec.Move.Reason = RollbackProbeAttempts
+ case RollbackCommitDeadline:
+ rec.Move.Reason = RollbackCommitDeadline
default:
return fmt.Errorf("provision: rollback reason %q has no recorded sentence", reason)
}
@@ -1607,6 +1617,131 @@ func MoveMarkRollback(g MoveGuard, reason string) func(*Record) error {
}
}
+// MoveProbeAttempt records, on a landing in its probing phase, how many
+// probe attempts have begun (§7.7). The count only goes up: the same n again
+// is a replay and changes nothing, a smaller one is a late write from an
+// earlier attempt and is refused. It is refused too once a rollback reason
+// is recorded, because the probe is then over. A record at probing says
+// only that the probe is running and how many attempts it has made; whether
+// the attempt's first start has been journalled is the claim's to say
+// (hostclaim.WakeRetryAttempt), not the record's.
+func MoveProbeAttempt(g MoveGuard, n int) func(*Record) error {
+ return func(rec *Record) error {
+ if err := g.check(rec); err != nil {
+ return err
+ }
+ m := rec.Move
+ if m.State != MoveLanding || m.LandingPhase != LandProbing || m.Reason != "" {
+ return fmt.Errorf("%w: probe attempts are counted while a landing is probing", ErrMoveConflict)
+ }
+ have := 0
+ if m.Landing != nil {
+ have = m.Landing.Attempts
+ }
+ if n < 1 || n < have {
+ return fmt.Errorf("%w: the attempt count does not go down", ErrMoveConflict)
+ }
+ if m.Landing == nil {
+ m.Landing = &MoveLandingJournal{}
+ }
+ m.Landing.Attempts = n
+ return nil
+ }
+}
+
+// ErrCutDeadline is a commit attempted after the deadline that bounds it
+// (§7.4, §7.9): the record CAS is the decision, so the deadline cannot be
+// passed between the record write and the claim write.
+var ErrCutDeadline = fmt.Errorf("%w: the commit deadline has passed", ErrMoveConflict)
+
+// MoveCommit is the commit (§7.9): landing/probed to committed, in one
+// record write. It is refused when a rollback reason is already recorded, so
+// a recorded rollback wins over a commit that raced it, and when now is
+// after the earlier of cut_deadline and landing_deadline (ruling H8: §7.9
+// names the first and §7.8 makes the second a rollback trigger, so the
+// earlier bounds the commit). A record with no cut_deadline cannot be shown
+// to be in time and is refused. now equal to the deadline is in time.
+//
+// A replay finds the move already committed and is an ErrMoveConflict; the
+// caller re-reads the record and accepts committed for its own move (H22).
+func MoveCommit(g MoveGuard, now time.Time) func(*Record) error {
+ return func(rec *Record) error {
+ if err := g.check(rec); err != nil {
+ return err
+ }
+ m := rec.Move
+ if m.State != MoveLanding || m.LandingPhase != LandProbed {
+ return fmt.Errorf("%w: commit follows probed", ErrMoveConflict)
+ }
+ if m.Reason != "" {
+ return fmt.Errorf("%w: a rollback is recorded for this landing", ErrMoveConflict)
+ }
+ if m.CutDeadline == nil || now.After(*m.CutDeadline) || (m.LandingDeadline != nil && now.After(*m.LandingDeadline)) {
+ return ErrCutDeadline
+ }
+ t := now.UTC()
+ m.State, m.CommittedAt = MoveCommitted, &t
+ return nil
+ }
+}
+
+// committedStamp stamps one of a committed move's obligations. The first
+// stamp stands: a replay changes nothing.
+func committedStamp(g MoveGuard, now time.Time, field func(*MoveState) **time.Time) func(*Record) error {
+ return func(rec *Record) error {
+ if err := g.check(rec); err != nil {
+ return err
+ }
+ if rec.Move.State != MoveCommitted {
+ return fmt.Errorf("%w: this follows the commit", ErrMoveConflict)
+ }
+ if p := field(rec.Move); *p == nil {
+ t := now.UTC()
+ *p = &t
+ }
+ return nil
+ }
+}
+
+// MoveOpened stamps opened_at on a committed move: the target is serving
+// (§7.9).
+func MoveOpened(g MoveGuard, now time.Time) func(*Record) error {
+ return committedStamp(g, now, func(m *MoveState) **time.Time { return &m.OpenedAt })
+}
+
+// MoveSourceParked stamps source_parked_at on a committed move: the source
+// has parked what it kept (§7.9).
+func MoveSourceParked(g MoveGuard, now time.Time) func(*Record) error {
+ return committedStamp(g, now, func(m *MoveState) **time.Time { return &m.SourceParkedAt })
+}
+
+// MoveClose is the close (§7.9 step 5, index D11): a committed move with
+// both obligations stamped clears Move and leaves the outcome in LastMove,
+// in the same write. The region is already the target's from the flip; a
+// record whose region is not is refused rather than closed on a lie. The
+// offline time is opened_at minus cut_at, zero when cut_at was never
+// stamped. Plan 5 calls it once no obligation under the move remains.
+func MoveClose(g MoveGuard, now time.Time) func(*Record) error {
+ return func(rec *Record) error {
+ if err := g.check(rec); err != nil {
+ return err
+ }
+ m := rec.Move
+ if m.State != MoveCommitted || m.OpenedAt == nil || m.SourceParkedAt == nil {
+ return fmt.Errorf("%w: close follows the open and the source park", ErrMoveConflict)
+ }
+ if rec.Region != m.To {
+ return fmt.Errorf("%w: the record's region is not the target's", ErrMoveConflict)
+ }
+ out := &MoveOutcome{ID: g.ID, Outcome: "done", At: now.UTC()}
+ if m.CutAt != nil {
+ out.OfflineS = m.OpenedAt.Sub(*m.CutAt).Seconds()
+ }
+ rec.LastMove, rec.Move = out, nil
+ return nil
+ }
+}
+
// InstanceStoreEnv is the environment name an instance's private object
// store hangs off: the record's own Environment, or its Slug when it has
// none. Twin of the create door's inline rule (internal/console/