carlos / platform Public

moves: a legacy source's clean return restores its origin (Task 3 fix 2, legacy regression)

The claim journals a legacy source's src_gen as the origin "" while
genselect names it hostclaim.RootGeneration, so the source baseline and
its pin's recording guard refused every legacy clean return.
selectedGeneration normalises the origin in both. A legacy-source clean
return admits from the root generation, restarts from it before its first
root, and pins it by exact version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 60278d6b857f5372a43e00e28c2dbe2dc0b940eb parent 9c3e2bf
2 files changed, +61 −6
  • internal/host/movesrecoveryowners.go +12 −2
  • internal/host/movesrecoveryowners_test.go +49 −4
diff --git a/internal/host/movesrecoveryowners.go b/internal/host/movesrecoveryowners.go
index 34b0daa3d..efacab1f1 100644
--- a/internal/host/movesrecoveryowners.go
+++ b/internal/host/movesrecoveryowners.go
@@ -852,7 +852,7 @@ func (r movesRecoveryOwners) sourceBaseline(ctx context.Context, host string, v
if err != nil {
return nil, err
}
- if plan.Generation != v.srcGen {
+ if plan.Generation != selectedGeneration(v.srcGen) {
return nil, fmt.Errorf("%w: the source baseline is not src_gen", hostbox.ErrChanged)
}
recorded, ok, err := r.recordedSourceBaseline(host, v)
@@ -885,7 +885,7 @@ func (r movesRecoveryOwners) recordedSourceBaseline(host string, v recoveryProbe
// admission's evidence, before the admission's CAS and the restore; once
// recorded it is never rewritten.
func (r movesRecoveryOwners) recordSourceBaseline(host string, v recoveryProbe, plan *genselect.Plan) error {
- if v.kind != hostclaim.KindRestoring || v.dirty || plan == nil || plan.Generation != v.srcGen {
+ if v.kind != hostclaim.KindRestoring || v.dirty || plan == nil || plan.Generation != selectedGeneration(v.srcGen) {
return nil
}
m := r.m
@@ -907,3 +907,13 @@ func (r movesRecoveryOwners) recordSourceBaseline(host string, v recoveryProbe,
}
return err
}
+
+// selectedGeneration is generation g as a selection names it: the claim
+// writes the origin as "" (a legacy source's src_gen) and genselect as
+// hostclaim.RootGeneration.
+func selectedGeneration(g string) string {
+ if g == "" {
+ return hostclaim.RootGeneration
+ }
+ return g
+}
diff --git a/internal/host/movesrecoveryowners_test.go b/internal/host/movesrecoveryowners_test.go
index 766d87775..80bfc69ac 100644
--- a/internal/host/movesrecoveryowners_test.go
+++ b/internal/host/movesrecoveryowners_test.go
@@ -33,6 +33,9 @@ const (
rcDirty = "dirty"
rcClean = "clean"
rcRevert = "revert"
+ // rcLegacy is a clean return of a legacy source: src_gen is the origin
+ // (""), its files at the location's root and no generation.json.
+ rcLegacy = "legacy"
// rcS is the source installation's stamp S the flip (or the revert)
// journalled.
rcS = 15
@@ -82,9 +85,12 @@ func newRCFixture(t *testing.T, kind string) *rcFixture {
at := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
// The source's own generation in its region, and the marker naming it.
- srcKey := "instances/" + poHost + "/g-src/0009/" + ltx.FormatFilename(1, 30)
- f.bucket.Put("data", srcKey, f.ltxHeader(1, 30))
- f.bucket.Put("data", "instances/"+poHost+"/generation.json", mustJSON(t, genselect.Marker{Op: "src", Incarnation: f.inc, Host: poHost, Box: "ie-box", InstalledAt: at}))
+ if kind == rcLegacy {
+ f.bucket.Put("data", "instances/"+poHost+"/0009/"+ltx.FormatFilename(1, 30), f.ltxHeader(1, 30))
+ } else {
+ f.bucket.Put("data", "instances/"+poHost+"/g-src/0009/"+ltx.FormatFilename(1, 30), f.ltxHeader(1, 30))
+ f.bucket.Put("data", "instances/"+poHost+"/generation.json", mustJSON(t, genselect.Marker{Op: "src", Incarnation: f.inc, Host: poHost, Box: "ie-box", InstalledAt: at}))
+ }
c := f.recoveryClaim()
f.rec.Region = "ie"
@@ -137,7 +143,11 @@ func newRCFixture(t *testing.T, kind string) *rcFixture {
if err := os.Remove(filepath.Join(f.files, archive.MarkerName)); err != nil {
t.Fatal(err)
}
- if err := hostbox.WriteGenerationMarker(f.route.DBPath+".gen", "src"); err != nil {
+ if kind == rcLegacy {
+ if err := os.Remove(f.route.DBPath + ".gen"); err != nil {
+ t.Fatal(err)
+ }
+ } else if err := hostbox.WriteGenerationMarker(f.route.DBPath+".gen", "src"); err != nil {
t.Fatal(err)
}
}
@@ -165,6 +175,12 @@ func (f *rcFixture) recoveryClaim() *hostclaim.Claim {
switch f.kind {
case rcClean:
h.Generation = "src"
+ case rcLegacy:
+ // The source never named a generation: its sessions predate
+ // admission records, and the landing's admission came from the origin.
+ h.Generation, h.Admissions = "", h.Admissions[1:]
+ h.Admissions[0].Predecessor = ""
+ journal[hostclaim.JournalSrcGen] = ""
case rcRevert:
h.Generation, h.Session, h.WriterBox, h.Admissions = "src", 15, "ie-box", h.Admissions[:1]
op.Kind = hostclaim.KindReverting
@@ -411,6 +427,35 @@ func TestCleanReturnProbeIsCoveredCleanReturn(t *testing.T) {
t.Fatalf("the restart after the crash: %v", err)
}
})
+ // Fix round 2: a legacy source's src_gen is the origin, "" in the claim
+ // and hostclaim.RootGeneration in a selection; the source baseline and
+ // its pin compare the two as one.
+ t.Run("a legacy source's clean return", func(t *testing.T) {
+ f := newRCFixture(t, rcLegacy)
+ ctx := t.Context()
+ o := f.read()
+ in := f.selectAdmission(o)
+ if a := in.Admission; a.NewGeneration != "fresh1" || a.Coverage != "clean-return" || a.ObservedGeneration != "" || a.RemoteMaxTXID != 30 ||
+ in.Baseline.Generation != hostclaim.RootGeneration {
+ t.Fatalf("admission %+v baseline %+v", a, in.Baseline)
+ }
+ admitted := f.admitThroughWriter(o, in)
+ again, err := f.owners.Select(ctx, f.route, admitted, f.policy())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if again.Baseline.Generation != hostclaim.RootGeneration || again.Admission.RemoteMaxTXID != 30 || again.Admission.Coverage != "" {
+ t.Fatalf("restart %+v baseline %+v", again.Admission, again.Baseline)
+ }
+ if err := f.owners.AdmissionEvidence(ctx, admitted, again.Admission, again.Baseline); err != nil {
+ t.Fatal(err)
+ }
+ // The origin's pin was journalled at the first admission.
+ f.bucket.Put("data", "instances/"+poHost+"/0009/"+ltx.FormatFilename(1, 30), f.ltxHeader(1, 30))
+ if _, err := f.owners.Select(ctx, f.route, admitted, f.policy()); !errors.Is(err, hostbox.ErrChanged) {
+ t.Fatalf("another version of the origin: %v", err)
+ }
+ })
// A declared or fenced return (Q6) asks no coverage: the claim journals
// the fence and the record carries it as its loss.
t.Run("a fenced return asks no coverage", func(t *testing.T) {