carlos / platform Public

hostclaim: naming a converted move's same-region target takes the move's own target off the barrier in the same CAS

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 151261fe92584e6428733b21c7f0b5abdce844b6 parent f9f254a
2 files changed, +33 −1
  • internal/hostclaim/relocation.go +8 −1
  • internal/hostclaim/relocation_test.go +25 −0
diff --git a/internal/hostclaim/relocation.go b/internal/hostclaim/relocation.go
index c425d6350..5238422c9 100644
--- a/internal/hostclaim/relocation.go
+++ b/internal/hostclaim/relocation.go
@@ -742,7 +742,11 @@ func JournalPin(opID string, p RecoveryPin) func(*Claim) error {
// NameTarget names the target of an operation that has none: a move
// converted to relocating before its flip (claim §4, moves §7.10). The
-// named box acknowledges (D6) before ToInstalling hands it op.exec.
+// named box acknowledges (D6) before ToInstalling hands it op.exec. The
+// move's own target (move_target_box, in the region the move was going
+// to) leaves the barrier in the same CAS: readiness binds to the box named
+// here (moves §7.10), and a failover's participant is its target alone
+// (§5.8). It never held op.exec on the converted operation.
func NameTarget(opID string, t Target) func(*Claim) error {
return func(c *Claim) error {
if err := requireRelocation(c, opID, PhasePinning); err != nil {
@@ -751,6 +755,9 @@ func NameTarget(opID string, t Target) func(*Claim) error {
if c.Op.Intent["target_box"] != "" {
return fmt.Errorf("%w: the operation already has a target", ErrRelocation)
}
+ if moved := c.Op.Intent["move_target_box"]; moved != "" {
+ removeParticipant(c, moved)
+ }
return replaceTargetSlot(c, t)
}
}
diff --git a/internal/hostclaim/relocation_test.go b/internal/hostclaim/relocation_test.go
index 8a1a41055..98bf47e02 100644
--- a/internal/hostclaim/relocation_test.go
+++ b/internal/hostclaim/relocation_test.go
@@ -1135,3 +1135,28 @@ func TestRelocationConvertedMovePinsOnlyAfterFenceSourceAndTheHold(t *testing.T)
t.Fatalf("pin not journaled: %+v", o.Claim.Op.Journal)
}
}
+
+// Task 5 fix round 1: a converted move's own target (in the region the move
+// was going to) leaves the barrier when NameTarget names the same-region
+// target, in the same CAS. Otherwise a move target that never acknowledged,
+// or died, holds the relocation in pinning for ever: nothing in this
+// region can consume its fence (moves §7.10, §5.8's failover participants).
+func TestRelocationNameTargetDropsTheConvertedMovesTarget(t *testing.T) {
+ ctx := context.Background()
+ s, o, ref, a := c7Fixture(t)
+ o, err := ConvertMovingToRelocating(ctx, s, o, "move-1", ref, a, &c7Guard{})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !slices.Equal(o.Claim.Op.Participants, []string{"bri-knitting"}) || o.Claim.Op.Intent["move_target_box"] != "bri-knitting" {
+ t.Fatalf("not the converted shape this test is about: %+v", o.Claim.Op)
+ }
+ gen := o.Claim.Gen
+ o = step(t, s, o, NameTarget("move-1", Target{Box: "cai-knitting", Enrollment: strings.Repeat("e", 32)}))
+ if !slices.Equal(o.Claim.Op.Participants, []string{"cai-knitting"}) || o.Claim.Op.Intent["target_box"] != "cai-knitting" || o.Claim.Gen != gen+1 {
+ t.Fatalf("the move's target is still on the barrier: %+v", o.Claim.Op)
+ }
+ if o.Claim.Op.Intent["move_target_box"] != "bri-knitting" || o.Claim.Op.Journal["replacements"] != "" {
+ t.Fatalf("the move's history or the replacement count changed: %+v", o.Claim.Op)
+ }
+}