carlos / platform Public

host: the moves pass acts only on its own walk of the bucket; the record view only says whether there is move work (#336)

Final review finding 1. The snapshot no longer feeds the moves pass's
listing: a pass walks the bucket (ListAllRecordsStrict over live, as on main)
whenever the snapshot has a record with a move or one it cannot decode, the
last walk found one, or the worker holds move state of its own (wake fence,
strict set, hold or offline keep view unbuilt or naming a host, archive
verdicts, landing leases, op journals or page outbox entries on disk), and
otherwise skips the walk. The epoch guard is gone with the snapshot listing.
The offline keep view is always read from the bucket. Regression: an abort
whose bump failed, a live fallback pass, then a recovered snapshot holding
the old abort; the pass no longer deletes the new move's staging.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 0c3e3ebc0b5eb1ef5bc14c1b372333b658829cc6 parent c1d2937
5 files changed, +259 −78
  • docs/superpowers/specs/2026-10-21-s3-change-index-design.md +15 −5
  • internal/host/agent.go +4 −2
  • internal/host/changeindex.go +3 −2
  • internal/host/changeindex_test.go +136 −49
  • internal/host/movespass.go +101 −20
diff --git a/docs/superpowers/specs/2026-10-21-s3-change-index-design.md b/docs/superpowers/specs/2026-10-21-s3-change-index-design.md
index 7246dd8db..498180179 100644
--- a/docs/superpowers/specs/2026-10-21-s3-change-index-design.md
+++ b/docs/superpowers/specs/2026-10-21-s3-change-index-design.md
@@ -267,11 +267,21 @@ contents reflect). `Refresh(ctx)`:
`Refresh` and then takes the current snapshot, so a pass's snapshot is
never older than the previous pass's.
- **Moves.** Today the moves pass walks inside `w.pass`
- (`internal/host/movespass.go:1061,1079`). It keeps that shape: inside
- `w.pass` it calls `Refresh` and takes the current snapshot, and it
- records the epoch it consumed and refuses a snapshot with a lower epoch
- (a guard; it cannot happen while publication is monotonic). It does not
- take a copy handed over from the Reconcile leg.
+ (`internal/host/movespass.go:1061,1079`). It keeps that shape, and acts
+ only on live data: inside `w.pass` it calls `Refresh` and takes the
+ current snapshot, but uses it only to decide whether there is any move
+ work at all. When the snapshot has a record with a move (or one it cannot
+ decode), the last walk found one, or the worker holds any move state of
+ its own (a wake fence, the strict set, the hold or offline keep view
+ unbuilt or naming a host, archive verdicts, a landing lease, an op
+ journal or page outbox entry on disk), the pass walks the bucket and runs
+ exactly as before the index; otherwise it skips the walk. A snapshot can
+ be older than a walk a pass made while the view could not vouch (a bump
+ that failed), and move steps have side effects in both directions, so
+ they never act on it (final review R1, finding 1). The offline keep view
+ is always read from the bucket. A move started by a write whose bump
+ failed waits for the backstop rebuild. The pass never takes a copy
+ handed over from the Reconcile leg.
- **Lock order:** `reconcileMu` → `RecordView.mu`, and `w.pass` →
`reconcileMu` → `RecordView.mu`. `RecordView.mu` is a leaf: nothing is
acquired while holding it. Move operations that take `reconcileMu`
diff --git a/internal/host/agent.go b/internal/host/agent.go
index ba17bd6dc..0c1357ab9 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -1627,11 +1627,13 @@ func reconcileSerializedWith(ctx context.Context, s release.Store, reg *registry
// Offline move holds: the hold from the record alone, the keep from the
// view, which is read here first if no listing has been read yet or the
// last one failed, so keep never fails closed on a box whose listing
- // reads. Nothing on a box that cannot take part, and nothing where
+ // reads. That listing reads the bucket, never the record view: what a
+ // move keeps is decided on live data (S3 change-index final review R1,
+ // finding 1). Nothing on a box that cannot take part, and nothing where
// Phase 1's hooks are set.
if w != nil && w.cfg.OfflineCapable && w.cfg.Offline != nil {
if !w.cfg.Offline.built() || w.cfg.Offline.failed() {
- refreshOfflineView(ctx, rs, w.cfg.Offline)
+ refreshOfflineView(ctx, s, w.cfg.Offline)
}
cfg = withOfflineHolds(cfg, offlineHold(w.cfg), w.cfg.Offline)
}
diff --git a/internal/host/changeindex.go b/internal/host/changeindex.go
index 8f6cf6b89..266663b5e 100644
--- a/internal/host/changeindex.go
+++ b/internal/host/changeindex.go
@@ -118,8 +118,9 @@ func noteViewUse(ok bool, why string) {
// and the snapshot is Complete. Otherwise ok is false and s is live, so the
// caller walks the bucket as before the index (spec §3.4).
//
-// Only record-family walkers take it (Reconcile, ReconcileSidecars, the
-// offline keep view, the moves pass's listing, steering). Its ListDirs of
+// Only record-family walkers take it (Reconcile, ReconcileSidecars,
+// steering), and the moves pass, which asks it only whether there is any
+// move work and acts on its own walk of the bucket (movesIdle). Its ListDirs of
// apps/<a>/ names only apps with record-family state, so app enumerators
// (hostops.Apps, purge and activation sweeps, mail, economics) must never
// get it.
diff --git a/internal/host/changeindex_test.go b/internal/host/changeindex_test.go
index a4321f052..8070a1da7 100644
--- a/internal/host/changeindex_test.go
+++ b/internal/host/changeindex_test.go
@@ -266,12 +266,23 @@ func TestReconcileUsesViewWhenArmed(t *testing.T) {
t.Fatal(err)
}
reg := restartTestRegistry(t)
- // A capable worker whose keep view has read nothing yet, so the pass
- // lists records for it too (refreshOfflineView).
+ // A capable worker whose keep view has read nothing yet: the first pass
+ // lists records for it (refreshOfflineView), from the bucket, never the
+ // view (final review R1, finding 1: everything a move acts on is live).
mw := newMovesWorker(moveCfg{OfflineCapable: true})
mw.cfg.Offline = newOfflineView(mw.cfg, nil)
cfg := ciReconcileConfig(t)
w.live.reset()
+ if err := reconcileSerializedWith(context.Background(), w.live, reg, cfg, mw); err != nil {
+ t.Fatal(err)
+ }
+ if !mw.cfg.Offline.built() || mw.cfg.Offline.failed() {
+ t.Fatal("the offline keep view was not read")
+ }
+ if n := w.live.count("List", "accounts/"); n != 1 {
+ t.Fatalf("the unbuilt offline keep view listed accounts/ %d time(s), want once, from the bucket: %v", n, w.live.got())
+ }
+ w.live.reset()
for range 2 {
if err := reconcileSerializedWith(context.Background(), w.live, reg, cfg, mw); err != nil {
t.Fatal(err)
@@ -283,9 +294,6 @@ func TestReconcileUsesViewWhenArmed(t *testing.T) {
if n := recordFamilyLists(w.live); n != 0 {
t.Fatalf("two armed passes listed the record families %d time(s): %v", n, w.live.got())
}
- if !mw.cfg.Offline.built() || mw.cfg.Offline.failed() {
- t.Fatal("the offline keep view was not read from the view")
- }
for _, h := range []string{"shop.example.com", "cafe.example.com"} {
if rt, err := reg.Get(h); err != nil || rt == nil {
t.Fatalf("no route for %s from the view's records: %v", h, err)
@@ -421,13 +429,13 @@ func TestStalledIndexGetKeepsReconcileWalking(t *testing.T) {
// ---- moves -----------------------------------------------------------------
-// The moves pass takes its own snapshot inside w.pass, never the one the
-// reconcile leg of its pass set read (spec §4.3, codex 1): pass A is paused
-// after its reconcile; pass B runs whole; a write lands; A's moves leg then
-// consumes the newest epoch, never A's own older one.
-func TestMovesPassTakesSnapshotUnderPass(t *testing.T) {
+// The moves pass decides whether it has work from its own refresh of the
+// view inside w.pass, never from the snapshot the reconcile leg of its pass
+// set read (spec §4.3, codex 1): pass A is paused after its reconcile, a
+// move is requested, and A's moves leg sees it and walks the bucket.
+func TestMovesPassDecidesFromItsOwnRefresh(t *testing.T) {
w := newCIWorld(t)
- v := armView(t, w.live)
+ armView(t, w.live)
mw := newMovesWorker(moveCfg{})
armedMovesWorker.Store(mw)
t.Cleanup(func() { armedMovesWorker.Store(nil) })
@@ -435,72 +443,151 @@ func TestMovesPassTakesSnapshotUnderPass(t *testing.T) {
rcfg := ciReconcileConfig(t)
legs := &passLegs{store: w.live, reg: reg, optDir: t.TempDir(), dd: t.TempDir(),
cfg: func() provision.ReconcileConfig { return rcfg }}
+ legs.full(context.Background()) // walks once: nothing to move
+ if !mw.walkedIdle {
+ t.Fatal("fixture: a walk with no move left the worker busy")
+ }
- paused, resume := make(chan uint64), make(chan struct{})
- // Only the first pass (A) pauses; a sync.Once would block pass B on A.
+ paused, resume := make(chan struct{}), make(chan struct{})
var first atomic.Bool
passAfterReconcileHook = func() {
if first.CompareAndSwap(false, true) {
- paused <- v.Snapshot().Epoch
+ paused <- struct{}{}
<-resume
}
}
t.Cleanup(func() { passAfterReconcileHook = nil })
-
done := make(chan struct{})
go func() { defer close(done); legs.full(context.Background()) }() // pass A
- epochA := <-paused
-
- w.record(t, "shop", "two.shop.example.com")
- legs.full(context.Background()) // pass B, whole
- epochB := mw.lastEpoch
- if epochB <= epochA {
- t.Fatalf("pass B's moves consumed epoch %d, not past A's reconcile at %d", epochB, epochA)
+ <-paused
+ if _, err := provision.SetRecordMove(context.Background(), w.live, w.acct, "shop", "shop.example.com", provision.MoveRequest("uk", w.now)); err != nil {
+ t.Fatal(err)
}
- w.record(t, "cafe", "two.cafe.example.com")
+ w.live.reset()
close(resume)
<-done
- if got := mw.lastEpoch; got <= epochB {
- t.Fatalf("pass A's moves consumed epoch %d after B consumed %d: it did not refresh under w.pass", got, epochB)
+ if n := w.live.count("List", "accounts/"); n == 0 {
+ t.Fatalf("pass A's moves leg did not walk for a move requested after its reconcile: %v", w.live.got())
}
- if got, want := mw.lastEpoch, v.Snapshot().Epoch; got != want {
- t.Fatalf("pass A's moves consumed epoch %d, the newest is %d", got, want)
+ if mw.walkedIdle {
+ t.Fatal("a walk that found a move left the worker idle")
}
}
-// A snapshot older than one the worker already consumed is refused: the
-// pass logs it and takes no step (here, the offline keep view stays unread).
-func TestMovesPassRefusesOlderEpoch(t *testing.T) {
+// With nothing to move, the armed moves pass costs no walk: the record view
+// vouches that no record carries a move, and the worker holds no move state.
+// Anything the worker holds (here, a cut's wake fence) makes it walk.
+func TestMovesPassSkipsTheWalkOnlyWithNoMoveWork(t *testing.T) {
w := newCIWorld(t)
- v := armView(t, w.live)
- if err := v.Rebuild(context.Background()); err != nil {
+ armView(t, w.live)
+ mw := newMovesWorker(moveCfg{})
+ if err := movesPass(context.Background(), w.live, mw); err != nil {
t.Fatal(err)
}
- logs := captureLog(t)
- mw := newMovesWorker(moveCfg{})
- mw.cfg.Offline = newOfflineView(mw.cfg, nil)
- newest := v.Snapshot().Epoch
- mw.lastEpoch = newest + 5 // as if a newer snapshot had been consumed
+ w.live.reset()
+ if err := movesPass(context.Background(), w.live, mw); err != nil {
+ t.Fatal(err)
+ }
+ if n := recordFamilyLists(w.live); n != 0 {
+ t.Fatalf("an idle armed moves pass listed the record families: %v", w.live.got())
+ }
+ mw.fenced.Store("shop.example.com", uint64(1))
+ w.live.reset()
if err := movesPass(context.Background(), w.live, mw); err != nil {
t.Fatal(err)
}
- if mw.cfg.Offline.built() {
- t.Fatal("a pass over an older snapshot read its records")
+ if n := w.live.count("List", "accounts/"); n == 0 {
+ t.Fatalf("a worker holding a wake fence skipped its walk: %v", w.live.got())
+ }
+}
+
+// Final review R1 finding 1 (amadan #336): the moves pass acts on the
+// bucket, never on a snapshot older than what a pass already saw there.
+// An abort's completion misses its bump, so the view still holds the move
+// aborting; an unrelated scope then fails to re-read and a pass walks the
+// bucket instead (the live fallback); a new move of the same host is
+// requested (its bump missed too) and staging fills; the unrelated scope
+// recovers, so the view is Complete again with the old aborting state.
+// Acting on that snapshot would delete the new move's staging. The pass
+// walks the bucket, sees the new move preparing, and leaves staging alone.
+func TestMovesPassNeverActsOnAnOlderSnapshot(t *testing.T) {
+ h := newMoveHarness(t)
+ root := t.TempDir()
+ indexed := release.NewDirStore(root)
+ indexed.EnableChangeIndex()
+ live := &ciCountingStore{DirStore: indexed}
+ bypass := release.NewDirStore(root) // writes whose bump failed
+ ctx := context.Background()
+ edges, _, err := h.s.Get(ctx, "control/edges/uk-box.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := live.Put(ctx, "control/edges/uk-box.json", edges, ""); err != nil {
+ t.Fatal(err)
}
- if mw.lastEpoch != newest+5 {
- t.Fatalf("lastEpoch went from %d to %d", newest+5, mw.lastEpoch)
+ h.s = live
+ host := "a.go.example.com"
+ h.record(t, host, provision.MoveRequest("uk", time.Now()), provision.MoveAbort())
+ other := "b.go.example.com"
+ if err := provision.CreateRecord(ctx, live, provision.Record{Schema: 1, Host: other, Slug: "b", App: "other", Account: "bab",
+ Region: "ie", Channel: "stable", CreatedAt: time.Now().UTC()}); err != nil {
+ t.Fatal(err)
}
- want := "agent: moves pass: snapshot epoch"
- if !strings.Contains(logs(), want) || !strings.Contains(logs(), "older than") {
- t.Fatalf("no refusal line in %q", logs())
+ v := armView(t, live)
+ if err := v.Rebuild(ctx); err != nil {
+ t.Fatal(err)
}
- // The same worker at the current epoch runs.
- mw.lastEpoch = 0
- if err := movesPass(context.Background(), w.live, mw); err != nil {
+ if recs, err := provision.ListAllRecords(ctx, v.Snapshot().Store(live)); err != nil || len(recs) != 2 {
+ t.Fatalf("fixture: the view holds %d record(s), %v; want both", len(recs), err)
+ }
+ // The abort completes, and its bump fails.
+ if _, err := provision.SetRecordMove(ctx, bypass, "bab", "app", host, provision.MoveAbortComplete()); err != nil {
+ t.Fatal(err)
+ }
+ // An unrelated scope moves and will not re-read: the view cannot vouch,
+ // and the pass walks the bucket, where the abort is complete.
+ third := provision.Record{Schema: 1, Host: "c.go.example.com", Slug: "c", App: "other", Account: "bab",
+ Region: "ie", Channel: "stable", CreatedAt: time.Now().UTC()}
+ if err := provision.CreateRecord(ctx, live, third); err != nil {
t.Fatal(err)
}
- if !mw.cfg.Offline.built() || mw.lastEpoch != v.Snapshot().Epoch {
- t.Fatalf("the pass at the current epoch did not run (built %v, epoch %d)", mw.cfg.Offline.built(), mw.lastEpoch)
+ live.setFailList(provision.RecordsPrefix("bab", "other"))
+ if _, _, ok := viewStore(ctx, live); ok {
+ t.Fatal("fixture: the view vouched with a scope failing to re-read")
+ }
+ wk := h.worker()
+ if err := movesPass(ctx, live, wk); err != nil {
+ t.Fatal(err)
+ }
+ // The host is moved again, the bump failing again, and staging fills.
+ if _, err := provision.SetRecordMove(ctx, bypass, "bab", "app", host, provision.MoveRequest("uk", time.Now())); err != nil {
+ t.Fatal(err)
+ }
+ h.f.put("data-ie", "instances/"+host+"/generations/g/snapshot", "e1")
+ h.f.put("data-uk", "moves/"+host+"/generations/g/snapshot", "e1")
+ // The unrelated scope recovers: Complete again, with the old abort.
+ live.setFailList("")
+ if _, _, ok := viewStore(ctx, live); !ok {
+ t.Fatal("fixture: the view did not recover")
+ }
+ snapRec, err := provision.GetRecord(ctx, v.Snapshot().Store(live), "bab", "app", host)
+ if err != nil || snapRec.Move == nil || snapRec.Move.State != provision.MoveAborting {
+ t.Fatalf("fixture: the recovered snapshot should hold the old abort: %+v %v", snapRec, err)
+ }
+
+ if err := movesPass(ctx, live, wk); err != nil {
+ t.Fatal(err)
+ }
+ for _, op := range h.f.ops {
+ if strings.HasPrefix(op, "delete data-uk/moves/"+host+"/") {
+ t.Fatalf("the pass acted on the snapshot's old abort and deleted the new move's staging: %v", h.f.ops)
+ }
+ }
+ if staged := h.f.bodies("data-uk", "moves/"+host+"/"); len(staged) != 1 {
+ t.Fatalf("the new move's staging after the pass: %v", staged)
+ }
+ if got := h.read(t, host); got.Move == nil || got.Move.State == provision.MoveAborting {
+ t.Fatalf("the record after the pass: %+v", got.Move)
}
}
diff --git a/internal/host/movespass.go b/internal/host/movespass.go
index b7d03b0e9..48557337f 100644
--- a/internal/host/movespass.go
+++ b/internal/host/movespass.go
@@ -10,6 +10,7 @@ import (
"log"
"os"
"path/filepath"
+ "slices"
"strconv"
"strings"
"sync"
@@ -110,10 +111,11 @@ type movesWorker struct {
// lost names the hosts whose claim lists this box in Lost, as this pass
// read them (lostHere); rebuilt every pass under pass.
lost map[string]bool
- // lastEpoch is the record view snapshot epoch the last pass consumed
- // (S3 change-index design §4.3); guarded by pass. A pass refuses an
- // older one: a guard, since publication is monotonic.
- lastEpoch uint64
+ // walkedIdle says the last pass walked the bucket, found no record with
+ // a move (and none it could not decode), and left this worker holding
+ // no move state of its own (localMoveState). Only then may a pass skip
+ // its walk on the record view's word (movesIdle); guarded by pass.
+ walkedIdle bool
}
// staging is the memoized staging writer for a target bucket region.
@@ -1064,21 +1066,15 @@ func moveReadyDeltaFromEnv(getenv func(string) string) (delta int64, ok bool) {
func movesPass(ctx context.Context, s release.Store, w *movesWorker) error {
w.pass.Lock()
defer w.pass.Unlock()
- // With the change index armed, the record listing below reads the record
- // view, refreshed here inside w.pass, never a snapshot handed over from
- // a reconcile leg (S3 change-index design §4.3). Only the listing does:
- // every step after it reads and writes s, so a step's own re-read and
- // CAS see the bucket as they always have. Lock order: w.pass ->
- // RecordView.mu, which is a leaf.
- records := s
- if vs, epoch, ok := viewStore(ctx, s); ok {
- if epoch < w.lastEpoch {
- log.Printf("agent: moves pass: snapshot epoch %d older than %d consumed", epoch, w.lastEpoch)
- return nil
- }
- w.lastEpoch = epoch
- records = vs
- }
+ // With the change index armed, the record view (refreshed here inside
+ // w.pass; lock order w.pass -> RecordView.mu, a leaf) decides only
+ // whether this pass has any move work at all. Every move step has side
+ // effects in both directions, and a snapshot can be older than a walk a
+ // pass made while the view could not vouch (a bump that failed), so a
+ // pass with work acts only on its own walk of the bucket, below, as
+ // before the index (S3 change-index design §4.3, final review R1
+ // finding 1). A pass with none skips the walk.
+ idle := w.movesIdle(ctx, s)
cfg := w.cfg
if cfg.claimRecords {
ctx = provision.WithRecordSequence(ctx)
@@ -1093,9 +1089,13 @@ func movesPass(ctx context.Context, s release.Store, w *movesWorker) error {
w.flushPageOutbox(ctx)
w.flushLogOutbox(ctx, s)
}
+ if idle {
+ return nil
+ }
// Strict returns exactly ListAllRecords' records and errors, plus the
// hosts whose record did not decode, which the offline keep view keeps.
- recs, unreadable, err := provision.ListAllRecordsStrict(ctx, records)
+ w.walkedIdle = false
+ recs, unreadable, err := provision.ListAllRecordsStrict(ctx, s)
if err != nil {
return fmt.Errorf("moves: list records: %w", err)
}
@@ -1293,9 +1293,90 @@ func movesPass(ctx context.Context, s release.Store, w *movesWorker) error {
w.retirePageTombstones(recs)
}
w.liftFences(recs)
+ w.walkedIdle = len(unreadable) == 0 && !slices.ContainsFunc(recs, func(r provision.Record) bool { return r.Move != nil }) &&
+ !w.localMoveState()
return nil
}
+// movesIdle reports whether this pass may skip its walk: the change index is
+// armed and the record view can vouch, the last pass walked and found
+// nothing (walkedIdle), the view's snapshot has no record with a move and
+// none it cannot decode, and this worker holds no move state of its own. Any
+// doubt is work: the pass walks the bucket. A move started by a write whose
+// bump failed waits for the backstop rebuild (or for any pass that walks).
+func (w *movesWorker) movesIdle(ctx context.Context, s release.Store) bool {
+ vs, _, ok := viewStore(ctx, s)
+ if !ok || !w.walkedIdle {
+ return false
+ }
+ recs, unreadable, err := provision.ListAllRecordsStrict(ctx, vs)
+ if err != nil || len(unreadable) > 0 || slices.ContainsFunc(recs, func(r provision.Record) bool { return r.Move != nil }) {
+ return false
+ }
+ return !w.localMoveState()
+}
+
+// localMoveState reports whether this worker holds anything a moves pass
+// acts on or cleans up from its record walk, whatever the records say: a
+// cut's wake fence, a strict spawn set, the hold or offline keep view
+// unbuilt or naming a host, T5's archive verdicts, a landing's lease, or,
+// on disk, an op journal or a page outbox entry. Under pass.
+func (w *movesWorker) localMoveState() bool {
+ fenced := false
+ w.fenced.Range(func(any, any) bool { fenced = true; return false })
+ if fenced {
+ return true
+ }
+ if m := w.strict.Load(); m != nil && len(*m) > 0 {
+ return true
+ }
+ if w.holds != nil {
+ if snap := w.holds.snap.Load(); snap == nil || len(snap.claims) > 0 || len(snap.unknown) > 0 {
+ return true
+ }
+ }
+ if v := w.cfg.Offline; v != nil {
+ if snap := v.snap.Load(); snap == nil || snap.failed || len(snap.moves) > 0 || len(snap.unreadable) > 0 {
+ return true
+ }
+ }
+ w.mu.Lock()
+ held := len(w.archives) > 0 || len(w.landLeases) > 0
+ w.mu.Unlock()
+ if held {
+ return true
+ }
+ return movesDirHolds(w.cfg.DataDir)
+}
+
+// movesDirHolds reports whether <dataDir>/.moves holds anything below its
+// top level: an op journal (.moves/<host>/<op>/, parked journals leave it)
+// or a page outbox entry (.moves/.page-outbox/...). An unreadable directory
+// holds something, as far as skipping a walk goes.
+func movesDirHolds(dataDir string) bool {
+ if dataDir == "" {
+ return false
+ }
+ root := filepath.Join(dataDir, ".moves")
+ tops, err := os.ReadDir(root)
+ if errors.Is(err, os.ErrNotExist) {
+ return false
+ }
+ if err != nil {
+ return true
+ }
+ for _, top := range tops {
+ if !top.IsDir() {
+ continue
+ }
+ inner, err := os.ReadDir(filepath.Join(root, top.Name()))
+ if err != nil || len(inner) > 0 {
+ return true
+ }
+ }
+ return false
+}
+
// legacyMoveSentence is MV-N7, logged (never shown to a user) when the
// switch is on and a record carries a move with no id.
const legacyMoveSentence = "this move was started by an older console and has no id. abort it and request it again"