Skip to content

sync: a game being played is held back from syncing until the session ends - #117

Merged
Omar-VF merged 6 commits into
Liquid-co:dev-publicfrom
ChakraFusion:pr/play-hold
Oct 28, 2026
Merged

Omar-VF merged 6 commits into
Liquid-co:dev-publicfrom
ChakraFusion:pr/play-hold

Conversation

@ChakraFusion

Copy link
Copy Markdown

The next standalone part of the play-sessions draft (#34), rebuilt on dev-public without save versions.

The problem. A game autosaves every few minutes, and each autosave was synced as it came. One afternoon, a device took eleven versions of one game in 26 minutes from the device it was being played on, each with its own "synced from another device" snapshot. The playing device snapshotted every autosave too. The gameplay guard only waits while save files are locked, and many games don't keep them locked. And a device took other devices' versions of a game running on it, writing a save under the game.

The change (syncengine/playing.go, daemon/sessions.go). While a game is being played on a device (the daemon's play sessions):

  • it isn't synced either way there: SyncGame declines quietly (ErrPlayingHere, which counts as ErrHeld);
  • another device asking for it, for its manifest or for deletions, is answered "playing". That device says so once per session instead of failing. The answer contains SettlingMessage, so an older build takes it as "busy, ask again";
  • the watcher snapshots a change to it at most once per 30 minutes, so a crash still leaves a recent copy.

When the session ends, the existing "After playing" snapshot is taken and the save goes to the other devices in one sync.

A session must end when the game does (second commit), since it now holds the save. A process named like the game stayed behind for fifteen minutes after one closed, with under 1 MB and no processor time; a killed one left two that kept their memory and used none. On Windows, processes are now measured (memory and processor time). One holding under 32 MB that used no processor time since the last poll, or one that used none for a minute whatever it holds, is not the game. A game being played uses some all the time; a paused one still draws its menu. A process that can't be measured still counts.

Tests:

  • syncengine: TestPlaying_NotSyncedWhilePlayedHere, TestPlaying_PeerPlayingIsWaitedFor
  • daemon: checkpoint timing, TestSessionEndsWhenOnlyAStubIsLeft (fails without the idle filter), TestActiveProcesses_HoldingMemoryDoingNothing
  • sessions: TestIdle

Left for a follow-up (P2): finding a running game wherever it is installed, and protected games whose path can't be read.

Checks: the full unit and e2e suites pass locally on today's dev-public.

🤖 Generated with Claude Code

ChakraFusion and others added 2 commits October 20, 2026 21:28
… ends

A game autosaves every few minutes, and each autosave was synced as it
came: one afternoon a device took eleven versions of a game in 26 minutes
from the device it was being played on, each with its own snapshot, and
the playing device snapshotted every autosave too. And a device took
other devices' versions of a game running on it, writing a save under the
game.

While a game is being played on a device (its play sessions):
- it is not synced either way there (ErrPlayingHere, quiet as ErrHeld);
- another device asking for it is answered "playing", for its manifest
  and for deletions, and says so once per session instead of failing; the
  answer contains SettlingMessage, so an older build takes it as busy;
- the watcher snapshots a change to it at most once per 30 minutes.
When the session ends, the existing "After playing" snapshot is taken
and the save goes to the other devices in one sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a session is open the game's save is now held back from syncing,
so a session that outlives the game holds the save with it. A process
named like the game stayed behind for fifteen minutes after one closed,
with under 1 MB and no processor time; a killed one left two that kept
their memory and used no processor time at all.

Processes are measured on Windows (memory, processor time; sessions.Proc).
One holding under 32 MB that used no processor time since the last poll
(sessions.Idle), or one that used none for a minute whatever it holds, is
not the game (daemon.activeProcesses). A game being played uses some all
the time; a paused one still draws its menu. One that cannot be measured
still counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 21, 2026

Copy link
Copy Markdown

@ChakraFusion is attempting to deploy a commit to the sivadaboi's projects Team on Vercel.

A member of the Team first needs to authorize it.

@Omar-VF

Omar-VF commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

Thanks, this reads well, and it fits with the Deck plugin's play sessions (#109). One question on idleFor: a game minimised in the background can throttle to almost nothing, and some suspend outright. If one used no processor time for a minute, its session would end while it's still running, and the save would be synced under it, which is the case this PR is there to stop. Would a longer quiet period (a few minutes) still catch the stubs you saw, which lingered for fifteen?

ChakraFusion and others added 3 commits October 26, 2026 08:51
Asked in review: a game minimised in the background can throttle to
almost nothing, or be suspended, and Windows trims its memory as well.
Either rule - under 32 MB and no processor time since the last poll, or
no processor time for a minute - could end its session while it was still
running, and the save would be synced under it. There is one rule now: a
process that has used no processor time for five minutes is not the game,
whatever memory it holds. That still lets go of what a closed game left
behind well before the quarter of an hour one lingered. Memory is no
longer read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A process left behind that still ticks over now and then - a crash
reporter, a launcher's helper - counts as running however long it
lingers, and while a session is open the game's save is held back from
syncing. So a session whose save has not changed for an hour is ended:
kept and synced as when the game closes. The process still there does not
start it again; the save changing again does, and so does the game being
started anew.

TestQuietSession_EndsAfterAnHourWithoutASave fails without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hour without a save ended any session, and so two it should not:
- a game saved only by hand, at save points, can go hours without a save
  while it is played, and its save would then be synced under it;
- a session begun from outside - the Deck plugin from Steam's launch,
  `opensave wrap` - ends when that says so, and knows.

Now a session begun from outside is never ended this way, and one seen
from running processes is ended only if, besides not saving for an hour,
the game's processes used under 1% of one core in it (some 36 seconds).
A game being played keeps drawing and uses far more; a crash reporter or
launcher helper left behind uses less. Measured in hour-long windows from
the session's start or the save's last change. A process that cannot be
measured is never taken as quiet. sessions.CPUOf sums a game's processes,
matched as Running matches them; Tracker.Marked says a session was begun
from outside.

Tests: a leftover that ticks over is let go, a game played for three hours
without saving keeps its session, and a session begun from outside is not
ended; the last two fail under the hour alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ChakraFusion

Copy link
Copy Markdown
Author

Good point, and it went further than idleFor. The other rule, under 32 MB with no processor time since the last poll, could also end a minimised game's session: Windows trims a minimised program's working set. Done in e49b0b1: there is one rule now. A process that has used no processor time for five minutes is not the game, whatever memory it holds, and memory is no longer read. That still lets go of the stubs I saw (they lingered about fifteen minutes).

This makes it the intended behaviour: a game that has used no processor time for five minutes is taken as not being played, its session ends and the save syncs, so nothing a game leaves behind can hold the save back for longer. A game in the background that still ticks over keeps its session.

One gap was left: a leftover process that keeps using a little processor time (a crash reporter, a launcher helper) would keep the session, and the hold on the save, open indefinitely. Closed in af64c5b and 9d4ecb1: a session found from running processes is taken as over when, for an hour, its save hasn't changed and the game's processes used under 1% of one core (about 36 seconds). It's then kept and synced as when the game closes. The process still there doesn't start it again; the save changing again does. Two cases are deliberately left alone:

What's left is a game paused and minimised for over an hour, throttled under 1% and not saving. Its session ends and its save is kept and synced. If another device changes the save meanwhile, the user is asked when they come back and save.

Tests:

  • TestActiveProcesses_QuietForLongIsNotTheGame: a quiet game still counts at 70 seconds and at 4 minutes, and not after 5. It fails at both under the old one-minute rule.
  • TestQuietSession_ALeftoverThatTicksOverIsLetGo: a leftover that ticks over is let go after the hour.
  • TestQuietSession_AGamePlayedWithoutSavingKeepsItsSession and TestQuietSession_ASessionBegunFromOutsideIsNotEnded: a game played three hours without saving keeps its session, and so does a Deck session. Both fail under the hour alone.

🤖 Generated with Claude Code

# Conflicts:
#	internal/p2p/syncengine/engine.go
@Omar-VF

Omar-VF commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator

Thanks, the new rules read well: five minutes without processor time, the hour-long backstop only for nearly idle sessions found from running programs, and outside sessions left alone. We'd like to merge it. It now conflicts with #114 (just merged) in SyncGame's status switch: both add a result to the case "conflict", "error", … line. Keeping both ("unwritable" and "peer_playing") is all it needs. Please rebase.

@ChakraFusion

Copy link
Copy Markdown
Author

Thanks! Done in d2161e4: today's dev-public is merged in, and the line keeps both, case "conflict", "error", "unwritable", "peer_playing":. GitHub shows it mergeable. I merged rather than rebased to keep the review history; happy to rebase instead if you prefer.

@Omar-VF
Omar-VF merged commit f8005a0 into Liquid-co:dev-public Oct 28, 2026
10 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants