Session Lifecycle
The nine honest phases, scale-to-zero waking UX, session expiry, idle handling, and DVR recordings
Sessions are honest by default. The SDK never hides what a GPU session is doing behind one opaque spinner: waking, waiting for capacity, expiring, and idling are all first-class, typed states with drop-in UI. This page is the end-to-end lifecycle toolkit.
The nine phases
idle → queued | unavailable | provisioning → connecting → live → ended | expired
↘ error| Phase | Meaning |
|---|---|
idle | Created, not yet admitted or connected |
queued | Waiting in the admission queue (phase.queue = { position, depth }) |
unavailable | No runtime exists to serve this function — non-terminal; recovers when one appears |
provisioning | Admitted; compute is being allocated/warmed |
connecting | Transport is establishing the media session |
live | First media track has arrived |
error | Admission or transport failed (phase.error.kind names why) |
ended | Intentionally disconnected / closed |
expired | Reached its declared maximum length — terminal and honest, not an error |
Three helpers cover most phase UI: describeSessionPhase(p) returns honest default copy ("Starting model…", a queue position, "Runtime unavailable — …"); isWakingPhase(p) names the pre-live waiting set; whenLive(session) / session.whenLive() is the "is media ready?" gate that rejects with a typed SessionFailedError (kind + full SessionStatus) instead of an opaque failure. UrunSessionStatus renders the phase line; UrunSessionGate gates children on live.
Scale-to-zero waking
Apps scale to zero, so a first session may wake a runtime. The SDK makes that wait honest and short:
useUrunPrewake({ function: 'generate' })— the intent signal. While a page that will start a session is mounted, it pings the advisory prewake endpoint (on mount, then every 60s) so capacity is already warming when the user clicks start. Never throws, never affects the session flow.useSessionWake(session)— a live{ waking, state, reason, seconds }view of the pre-live wait, counted fromphase.wakingSinceas one continuous clock acrossqueued/unavailable/provisioning(never resetting per sub-phase).UrunSessionWaking— the drop-in "waking (Ns)" element; renders nothing when not waking.
import { useUrunPrewake, UrunSessionWaking } from '@urun-sh/react'
function GeneratePage({ session }) {
useUrunPrewake({ function: 'generate' }) // warm while the page shows intent
return <UrunSessionWaking session={session} />
}While waiting, phase.runtime reports why: unavailable (no runtime registered), starting (cold-starting/warming), or busy (all runtimes serving other sessions), plus a frontend-safe reason.
Session clock and expiry
A function can declare a maximum session length; the platform then ends the session honestly at its deadline with the terminal phase expired — distinct from error because nothing failed.
session.endsAt/useSessionEndsAt(session)— the deadline as aDate, ornullwhen no maximum was declared.UrunSessionClock— an mm:ss countdown to the deadline; renders nothing without one (compose it into any header unconditionally), flagsdata-urgentin the last minute.UrunSessionEnded— the terminal ended/expired surface, with anonNewSessionhook to start fresh.
Idle warning
Attached-but-inert sessions time out (platform default; apps configure or opt out server-side). The SDK auto-stamps pointer/key/visibility activity into the session's awareness; before the kick, the server raises a warning in the control doc:
useSessionIdle(session)— the raised warning ({ warning, deadlineEpochS, idleSinceEpochS }), ornullin the steady state.UrunIdleWarning— the "Are you still there?" overlay with a countdown; its button confirms presence viasession.touch().session.touch()— record app-specific activity explicitly (throttled internally). Vanilla (non-React) apps can wire DOM activity withcreateActivityTrackerfrom@urun-sh/core.
import { UrunSessionClock, UrunSessionEnded, UrunIdleWarning } from '@urun-sh/react'
function Shell({ session, children }) {
return (
<>
<header><UrunSessionClock session={session} /></header>
{children}
<UrunIdleWarning session={session} />
<UrunSessionEnded session={session} onNewSession={() => location.reload()} />
</>
)
}Recordings: the DVR
Session streams are recorded by default — every session is replayable, and stream.seek(t) addresses the recording while seek('live') snaps back to the live edge. Retention is explicit:
- Recordings start in the short-lived
tempbucket;retain(id, '7d')keeps one for a duration,pin(id)moves it to the durable bucket,permanent(id)locks it,delete(id)removes it. - Mutations are async intents: a recording reports
pending_pin/pending_delete/ … until the reconciler settles it — surface that (isPendingRecording(r)) instead of assuming completion.
const recordings = await session.recordings.list() // this session's recordings
await session.recordings.retain(recordings[0].id, '7d')Replay after end. Recording playback outlives the session: attaching to an ended session whose recording exists (Session.attach(id)) answers a replay session — the platform replays the recording through the normal session stream surface (no GPU runtime is woken), so the same <Video> / track rendering works unchanged. Drive playback with stream('video').seek(0). Video streams replay today; an ended session with no recording stays an honest dead end rather than a faked replay.
const replay = await Session.attach(endedSessionId, options) // replay session for an ended id
await replay.stream('video').seek(0) // play the recording from the startsession.recordings requires functionsUrl on your AppOptions (a clear error tells you if it's missing); the standalone RecordingsClient from @urun-sh/core covers non-session (org-scoped) access. Files the runtime produces (e.g. rendered outputs) surface separately as artifacts: session.artifacts.list() and getDownloadUrl(artifactId) return short-lived object-store URLs (RuntimeArtifactsClient is the standalone form).
Related: