carlos / platform Public

provision: StuckReturnUnknownStamp, a return's leftover it cannot account for (Task 3 fix round 1)

A dirty return's install that finds the source's inventory listing paths
under a stamp that is neither S nor one the claim shows superseded on
this box stops for an operator at local, before anything moves.
MoveMarkStuck takes it for a dirty rollback at local only; MoveRetry
returns there. Copy row MV-N309 (pending); phase1Conflict speaks it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev af8f1f2cf12cb066853f57e7b5a026a37856d7eb parent 616325b
6 files changed, +24 −9
  • approved_moves_copy_test.go +1 −1
  • docs/superpowers/plans/2026-10-06-moves-phase1-copy.md +1 −0
  • internal/console/apiinstancemove.go +4 −3
  • internal/console/apiinstancemove_test.go +1 −1
  • internal/provision/move_test.go +5 −1
  • internal/provision/provision.go +12 −3
diff --git a/approved_moves_copy_test.go b/approved_moves_copy_test.go
index 828b8c2c9..ca561578e 100644
--- a/approved_moves_copy_test.go
+++ b/approved_moves_copy_test.go
@@ -72,7 +72,7 @@ var movesCopyIDs = []string{
"MV-N175", "MV-N176", "MV-N177", "MV-N178", "MV-N179", "MV-N180", "MV-N181",
"MV-N182", "MV-N183", "MV-N184", "MV-N185", "MV-N186",
"MV-N300", "MV-N301", "MV-N302", "MV-N303", "MV-N304", "MV-N305", "MV-N306",
- "MV-N307", "MV-N308",
+ "MV-N307", "MV-N308", "MV-N309",
}
// 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 fa2369356..d6559f8b5 100644
--- a/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md
+++ b/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md
@@ -256,6 +256,7 @@ Columns: id, file, text, status.
| MV-N306 | internal/provision/provision.go | the target holds nothing the move still needs. there is nothing to declare lost | pending |
| MV-N307 | internal/provision/provision.go | the files directory holds files the return did not write | pending |
| MV-N308 | internal/provision/provision.go | the returned database does not match the build | pending |
+| MV-N309 | internal/provision/provision.go | the source holds files of an installation the return cannot account for | pending |
## Plan 5
diff --git a/internal/console/apiinstancemove.go b/internal/console/apiinstancemove.go
index 8b4935397..3d933d7ca 100644
--- a/internal/console/apiinstancemove.go
+++ b/internal/console/apiinstancemove.go
@@ -889,7 +889,7 @@ func legacyAbortOf(seen provision.MoveState) func(*provision.Record) error {
// phase1Conflict maps a provision transition refusal to its recorded
// sentence (copy record MV-N12 to MV-N16, MV-N38 to MV-N46, and Plan 4's
-// MV-N300 to MV-N308: the rollback, retry and declare-lost refusals and the
+// MV-N300 to MV-N309: the rollback, retry and declare-lost refusals and the
// rollback and stuck reasons): never the
// "provision: move state conflict:" prefix, never an em dash. A refusal
// this table does not know answers a fixed sentence, so a new provision
@@ -916,14 +916,15 @@ func phase1Conflict(err error) string {
msg == "the target holds nothing the move still needs. there is nothing to declare lost",
// The reasons Plan 4 records (ruling J9): provision.RollbackDeclaredLost,
// StuckBaselineMismatch, StuckRecoveryProbeAttempts, StuckRevertProbeBudget,
- // and the shared installer's StuckReturnFilesOccupied and
- // StuckReturnInstallMismatch (Task 3).
+ // and the shared installer's StuckReturnFilesOccupied,
+ // StuckReturnInstallMismatch and StuckReturnUnknownStamp (Task 3).
msg == "the target box was declared lost",
msg == "the source no longer matches what it recorded when it stopped",
msg == "the recovery probe ran out of attempts",
msg == "the source did not answer within 15 minutes of moving back",
msg == "the files directory holds files the return did not write",
msg == "the returned database does not match the build",
+ msg == "the source holds files of an installation the return cannot account for",
strings.HasPrefix(msg, "the record's move is ") && strings.Contains(msg, " for incarnation "),
strings.HasPrefix(msg, "a move to ") && strings.Contains(msg, " is already "),
strings.HasPrefix(msg, "the instance is already in "),
diff --git a/internal/console/apiinstancemove_test.go b/internal/console/apiinstancemove_test.go
index 2a796f07c..feb92b2f4 100644
--- a/internal/console/apiinstancemove_test.go
+++ b/internal/console/apiinstancemove_test.go
@@ -1443,7 +1443,7 @@ func TestPhase1ConflictPassesPlan4Refusals(t *testing.T) {
// Ruling J9 (fix round 1): the four reasons Plan 4 records are spoken as
// their recorded sentences too, not the fixed one.
for _, reason := range []string{provision.RollbackDeclaredLost, provision.StuckBaselineMismatch, provision.StuckRecoveryProbeAttempts, provision.StuckRevertProbeBudget,
- provision.StuckReturnFilesOccupied, provision.StuckReturnInstallMismatch} {
+ provision.StuckReturnFilesOccupied, provision.StuckReturnInstallMismatch, provision.StuckReturnUnknownStamp} {
err := fmt.Errorf("%w: %s", provision.ErrMoveConflict, reason)
if got := phase1Conflict(err); got != reason {
t.Fatalf("phase1Conflict(%v) = %q, want %q", err, got, reason)
diff --git a/internal/provision/move_test.go b/internal/provision/move_test.go
index 3190b682c..dc6fbdd2d 100644
--- a/internal/provision/move_test.go
+++ b/internal/provision/move_test.go
@@ -1262,6 +1262,7 @@ func TestStuckReturnInstallReasons(t *testing.T) {
for _, c := range []struct{ phase, reason string }{
{RbSourceLocal, StuckReturnFilesOccupied},
{RbSourceConfigured, StuckReturnInstallMismatch},
+ {RbSourceLocal, StuckReturnUnknownStamp},
} {
rec := at(c.phase, true)
if err := MoveMarkStuck(g, c.reason)(rec); err != nil {
@@ -1289,6 +1290,9 @@ func TestStuckReturnInstallReasons(t *testing.T) {
{RbSourceParking, StuckReturnFilesOccupied, true},
{RbSourceRestored, StuckReturnInstallMismatch, true},
{RbSourceVerified, StuckReturnFilesOccupied, false},
+ {RbSourceLocal, StuckReturnUnknownStamp, false},
+ {RbSourceConfigured, StuckReturnUnknownStamp, true},
+ {RbSourceParking, StuckReturnUnknownStamp, true},
} {
rec := at(c.phase, c.dirty)
if err := MoveMarkStuck(g, c.reason)(rec); !errors.Is(err, ErrMoveConflict) || rec.Move.State != MoveRollingBack {
@@ -1297,7 +1301,7 @@ func TestStuckReturnInstallReasons(t *testing.T) {
}
// A revert installs nothing, so neither reason is a revert's.
rev := &Record{Region: "ie", Move: &MoveState{ID: g.ID, Incarnation: g.Incarnation, From: "ie", To: "uk", State: MoveReverting, CutPhase: CutStaged, Reason: RevertCutDeadline}}
- for _, reason := range []string{StuckReturnFilesOccupied, StuckReturnInstallMismatch} {
+ for _, reason := range []string{StuckReturnFilesOccupied, StuckReturnInstallMismatch, StuckReturnUnknownStamp} {
if err := MoveMarkStuck(g, reason)(rev); !errors.Is(err, ErrMoveConflict) || rev.Move.State != MoveReverting {
t.Fatalf("a revert with %q: %v", reason, err)
}
diff --git a/internal/provision/provision.go b/internal/provision/provision.go
index ef808c876..19f083b42 100644
--- a/internal/provision/provision.go
+++ b/internal/provision/provision.go
@@ -1804,6 +1804,12 @@ const (
// lane does not verify. A rollback whose source is at configured, where
// the database is installed.
StuckReturnInstallMismatch = "the returned database does not match the build"
+ // StuckReturnUnknownStamp: a dirty return's install found the source's
+ // inventory listing paths under a stamp that is neither the source's own
+ // S nor one the claim shows superseded on this box (live, unknown, or
+ // another holder's). Nothing is moved. A rollback whose source is at
+ // local.
+ StuckReturnUnknownStamp = "the source holds files of an installation the return cannot account for"
)
// The refusals of this section that reach an operator through a door
@@ -2004,8 +2010,9 @@ func MoveRollbackSourcePhase(g MoveGuard, from, to string, stamp func(*MoveRollb
// verified (a clean return's verification, INV-4), or a revert (its
// verify);
// - StuckRecoveryProbeAttempts: a rollback whose source is at probing;
-// - StuckReturnFilesOccupied: a dirty rollback whose source is at local
-// (the shared installer, Plan 4 Task 3);
+// - StuckReturnFilesOccupied and StuckReturnUnknownStamp: a dirty
+// rollback whose source is at local (the shared installer, Plan 4
+// Task 3);
// - StuckReturnInstallMismatch: a dirty rollback whose source is at
// configured;
// - StuckRevertProbeBudget: a revert.
@@ -2033,6 +2040,8 @@ func MoveMarkStuck(g MoveGuard, reason string) func(*Record) error {
why = StuckReturnFilesOccupied
case StuckReturnInstallMismatch:
why = StuckReturnInstallMismatch
+ case StuckReturnUnknownStamp:
+ why = StuckReturnUnknownStamp
default:
return fmt.Errorf("provision: stuck reason %q has no recorded sentence", reason)
}
@@ -2048,7 +2057,7 @@ func MoveMarkStuck(g MoveGuard, reason string) func(*Record) error {
ok = rb.SourcePhase == RbSourceRestoring || rb.SourcePhase == RbSourceVerified
case StuckRecoveryProbeAttempts:
ok = rb.SourcePhase == RbSourceProbing
- case StuckReturnFilesOccupied:
+ case StuckReturnFilesOccupied, StuckReturnUnknownStamp:
ok = rb.Dirty && rb.SourcePhase == RbSourceLocal
case StuckReturnInstallMismatch:
ok = rb.Dirty && rb.SourcePhase == RbSourceConfigured