Skip to content

Add startTimerOnSessionStart: start a user's turn when playback begins, not at claim - #47

Closed
cruhl wants to merge 1 commit into
mainfrom
feat/defer-budget-until-start
Closed

Add startTimerOnSessionStart: start a user's turn when playback begins, not at claim#47
cruhl wants to merge 1 commit into
mainfrom
feat/defer-budget-until-start

Conversation

@cruhl

@cruhl cruhl commented Jul 19, 2026

Copy link
Copy Markdown

Why

A queued user's turn is sessionDurationMs long, and today that countdown starts at claim() — 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 sends expired (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 (env RQ_START_TIMER_ON_SESSION_START, default false), and a matching client message, session_started, exposed as ReactorQueueClient.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.ts still creates the session, but instead of the full sessionDurationMs it seats the member with a short deadline of admissionGraceMs. 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 new timerStarted flag on the member records that the real countdown hasn't begun.

When the client's playback actually starts, the app calls sessionStarted(). The new onMessage case sets expiresAt = now + sessionDurationMs, flips timerStarted true, and reschedules the alarm. That same flag does two more jobs: the time_warning is gated on it, so no warning fires while the user is still loading, and the handler ignores a session_started once 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:

createReactorQueueServer({
  model: "helios",
  sessionDurationMs: 120_000,
  startTimerOnSessionStart: true, // countdown starts on session_started, not claim
});

Client — call it when playback is really live (e.g. first video frame):

queue.claim();
const { sessionId, connectionId } = queue.getState();
await reactor.connect(queue.getJwt, { sessionId: sessionId!, connectionId: connectionId! });
// …once the first frame renders / playback is live:
queue.sessionStarted();

React reaches the client through the existing escape-hatch hook (no new action added to useReactorQueue):

import { useReactorQueueClient } from "@reactor-team/queue/react";

const client = useReactorQueueClient();
// in your "playback started" handler:
client.sessionStarted();

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 until session_started; a claimed member that never signals expires at that deadline (timeout); a duplicate session_started doesn'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) and pnpm 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 raise admissionGraceMs. 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.

@cruhl
cruhl marked this pull request as ready for review July 19, 2026 22:32
@cruhl
cruhl requested a review from Dere-Wah July 19, 2026 22:35
…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
Dere-Wah force-pushed the feat/defer-budget-until-start branch from b18291c to be13319 Compare July 20, 2026 00:55
@Dere-Wah Dere-Wah changed the title Add deferBudgetUntilStart: start the session budget on an explicit session_started signal Add startTimerOnSessionStart: start a user's turn when playback begins, not at claim Jul 20, 2026
@Dere-Wah Dere-Wah closed this Jul 20, 2026
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