Skip to content

A branch switch is undone by the device's own next sync while the other device is on the old branch #85

Closed

Description

@ChakraFusion

What happens. On A you start a new run: a new branch, switched to, and the game writes its new save. B is online and still on the old branch. A's next sync with B sees the branches differ and switches A back to B's branch ("branch mismatch on …: local "new-game-plus" vs remote "main" — switching local"). B is never asked to follow. Whichever device syncs first follows the other, so the device where you made the switch is usually the one that gives it up: the first save the game writes on the new branch makes its watcher sync, and the once-a-minute sync comes at the latest.

Nothing is lost: the new run's save is in the automatic snapshot taken before the switch back ("Auto backup before switching to branch "main""). But it's no longer the active save, and B never gets it.

It showed in CI as the first failure of TestBranchFollow_FetchesTheOtherDevicesSaveNotDeletesIt (run #394, on #80, unrelated to that PR). The test keeps A's change-triggered syncs off so that B syncs first and follows, but A's once-a-minute sync landed in that gap on a slow runner and switched A back.

Reproduction (deterministic, engine level):

func TestBranchSwitch_SurvivesThisDevicesNextSync(t *testing.T) {
	env := setupEngine(t)
	write(t, env.localDir, "slot1.sav", "main-save")
	write(t, env.remoteDir, "slot1.sav", "main-save")
	env.engine.SyncWithPeer(ctx, "game1", env.peer)                       // in sync on main
	env.engine.Snapshots.CreateBranch("game1", "new-game-plus", false)
	env.engine.Snapshots.SwitchBranch("game1", "new-game-plus")
	write(t, env.localDir, "slot1.sav", "ngplus-save")
	res, _ := env.engine.SyncWithPeer(ctx, "game1", env.peer)             // peer still on main
	// res.Status == "in_sync"; this device is on "main" again, slot1 == "main-save";
	// no pull was triggered on the peer
}

Why. SyncWithPeer aligns branches before anything else: if the peer's active branch differs, this device switches to it. It can't tell a peer that moved to a new branch (which this device should follow) from a peer that hasn't caught up with this device's own switch (which should follow this device instead).

Proposed fix. When you switch branches on a device, it notes the branch it left, per game. While a peer is still on that branch, this device doesn't follow it back; it asks the peer to pull, so the peer follows the switch, as TestBranchFollow expects. The note is cleared once each peer is on the new branch, or when you switch again. A peer that moves to a branch this device didn't just leave is followed as today.

Related: #81 is the same kind of problem from another path: a choice made on one device ("Keep mine" there, a branch switch here) is undone by that device's own next sync, because the other device hasn't acted on it yet. Both fixes need a small per-game note of the choice until the other device has caught up. If you'd like both to survive a restart (my suggestion on #81), one table can hold both, so they'd share migration 0041.

I have the reproduction as a test and can open the PR, and fix TestBranchFollow's assumption with it, once you've had a look.

Activity

  1. Omar-VF commented on Oct 17, 2026

    @Omar-VF
    Collaborator

    Answered on #81: yes to the fix for both, with the note persisted (migration 0041) and the clean-up conditions listed there.

  2. Omar-VF commented on Oct 19, 2026

    @Omar-VF
    Collaborator

    Fixed by #90, now merged into dev-public. It will ship in the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions