Add startTimerOnSessionStart: start a user's turn when playback begins, not at claim - #47
Closed
cruhl wants to merge 1 commit into
Closed
Add startTimerOnSessionStart: start a user's turn when playback begins, not at claim#47cruhl wants to merge 1 commit into
cruhl wants to merge 1 commit into
Conversation
cruhl
marked this pull request as ready for review
July 19, 2026 22:32
…ted signal By default a user's turn is counted from claim(), so any client-side loading after the claim (asset download, world build, model warm-up) is spent out of the user's play time — the countdown can run low, or the time_warning can fire, while the user is still on a loading screen. The claim itself can't be deferred: it's what creates the session the client needs in order to connect and load. Add an opt-in startTimerOnSessionStart server option (env RQ_START_TIMER_ON_SESSION_START, default false) and a session_started client message (ReactorQueueClient.sessionStarted()). When enabled, claim still creates the session but the member gets a short loading deadline (admissionGraceMs) so an abandoned claim frees its slot; the full sessionDurationMs countdown starts only when the client reports playback. A timerStarted member flag gates the time_warning during loading and makes a duplicate session_started a no-op so a client cannot extend its turn. Default off preserves today's behavior exactly. Co-authored-by: Cursor <cursoragent@cursor.com>
Dere-Wah
force-pushed
the
feat/defer-budget-until-start
branch
from
July 20, 2026 00:55
b18291c to
be13319
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A queued user's turn is
sessionDurationMslong, and today that countdown starts atclaim()— the moment the queue creates their Reactor session. For a simple demo that's fine, but some apps do meaningful work after claiming and before anything plays: downloading assets, generating or building a world, warming up a model. All of that loading happens on the clock, so the user loses part of their turn to a loading screen. In the worst case the server sendsexpired (timeout), or the "time's almost up" warning fires, seconds after the stream finally goes live.The countdown is the only thing mis-timed here. Reserving a slot at admission and creating the session at claim are both correct — in fact the claim must stay where it is, because it's what provisions the session and WebRTC connection the client needs in order to connect and start loading in the first place. So rather than move the claim, this lets the countdown start later, on an explicit signal from the client that playback has actually begun.
What this changes
A new opt-in server option,
startTimerOnSessionStart(envRQ_START_TIMER_ON_SESSION_START, defaultfalse), and a matching client message,session_started, exposed asReactorQueueClient.sessionStarted().With the option off (the default) nothing changes: the full countdown starts at claim exactly as before. With it on, the claim path in
server.tsstill creates the session, but instead of the fullsessionDurationMsit seats the member with a short deadline ofadmissionGraceMs. That short deadline is a safety cap — a client that claims and then never starts playing (a stall, a tab closed mid-load) still expires and frees its slot for the next person, rather than holding a GPU for the whole turn. A newtimerStartedflag on the member records that the real countdown hasn't begun.When the client's playback actually starts, the app calls
sessionStarted(). The newonMessagecase setsexpiresAt = now + sessionDurationMs, flipstimerStartedtrue, and reschedules the alarm. That same flag does two more jobs: thetime_warningis gated on it, so no warning fires while the user is still loading, and the handler ignores asession_startedonce the timer is already running, so a client can't replay it to extend its own turn.The README gains a "Not counting load time against a turn" section walking through the behavior and the vanilla/React call sites, plus a row in the server config table.
API surface
Server config:
Client — call it when playback is really live (e.g. first video frame):
React reaches the client through the existing escape-hatch hook (no new action added to
useReactorQueue):sessionStarted()is safe to call more than once (only the first call after a claim starts the countdown) and is a no-op against a server that doesn't have the option enabled.Verification
pnpm typecheck: clean.pnpm test: 131 passed (5 new) — the countdown holds at the short loading deadline untilsession_started; a claimed member that never signals expires at that deadline (timeout); a duplicatesession_starteddoesn't extend the deadline; default-off still starts the full countdown at claim; the config resolves from both config object and env.pnpm build(tsup) andpnpm format:check: clean.Naming note
One thing worth a look in review: while loading, the member's deadline reuses
admissionGraceMs(the claim-grace window). If a consumer's load can legitimately run longer than that grace window, they'd expire mid-load unless they raiseadmissionGraceMs. A dedicated load-timeout option might be cleaner than sharing the value, but I've left it shared for now to keep the surface small — happy to split it if you'd prefer.