Runtime
Sessions
A session holds one device for one holder until it is released or expires, and every call that touches a device names its session. This page follows a session from create to release: the spec, installs, waiting, staying alive and ending.
A session from start to end
import { createClient } from '@alivelabs/troupe-client'
const troupe = createClient({ baseUrl, token })
let session = await troupe.call('sessions.create', {
spec: { platform: 'ios-simulator', deviceType: 'iPhone 18 Pro', os: '27.0' },
install: [{ path: '/Users/me/shop/build/Shop.app' }],
})
while (session.status === 'starting') {
const waited = await troupe.call('sessions.wait', { sessionId: session.sessionId })
session = waited.session
}
if (session.status !== 'ready') throw new Error(`The session is ${session.status}`)
await troupe.call('apps.launch', { sessionId: session.sessionId, bundleId: 'com.example.shop' })
// ... drive the device ...
await troupe.call('sessions.release', { sessionId: session.sessionId })The client SDK shows where baseUrl and token come from. The same procedures
are MCP tools and REST routes; the test framework calls them for you.
A session's status moves forward only:
| Status | Meaning |
|---|---|
queued | Waiting for a device. Troupe Cloud only: a local runtime refuses instead |
starting | The device is booting, its driver is starting, or apps are being installed |
ready | The device takes calls |
ended | Released, expired, or the runtime shut down |
failed | A requested install failed because of the app |
errored | Troupe or the machine failed: boot, driver, environment, memory |
canceled | Released before it was ready |
The last four are final. A final session has a reason, and an errored or failed one has the error.
Create
sessions.create returns at once. The session is ready when
a warm device matches the spec and nothing is to be installed. Otherwise it is starting.
| Field | What it does |
|---|---|
spec | The device to get. Required |
install | Up to five apps to install while the session starts |
key | A name of your own for the session, for attaching again |
holder | { kind, label }: who holds the session. The label shows in Troupe Studio |
idleTimeoutMs, maxDurationMs | Replace the holder's default timeouts. Up to 24 hours on a local runtime |
fresh | true boots a new device even when a warm one matches |
A local runtime never queues. With no free device slot or memory, the call fails with
CAPACITY_UNAVAILABLE, which carries retryAfterMs: wait that long and call again.
The device spec
| Field | Default | What it is |
|---|---|---|
platform | Required | ios-simulator, android-emulator or catalyst |
deviceType | The platform's default | A device type by id or exact name: 'iPhone 18 Pro', 'pixel_9', 'iphone-17' |
os | 'latest' | iOS version '27.0', Android API level '36'. 'latest' is the newest installed that the device type runs |
variant | 'google_apis' | Android only: the system image's tag |
arch | The host's | 'arm64' or 'x86_64' |
locale | 'en-US' | A BCP 47 tag, applied at boot |
timeZone | The host's | An IANA name |
orientation | 'portrait' | The orientation at start |
appearance | 'light' | 'light' or 'dark' at start |
The session's resolved field records what the spec came to: the device type's name, the exact OS version
and its build. Each platform page lists its device types:
iOS,
Android and
Catalyst.
A spec the platform cannot satisfy is DEVICE_SPEC_INVALID. A missing simulator runtime or system image is
an ENVIRONMENT_* error whose data.fix is the command that installs it.
Installs
install takes app sources. The runtime detects the format from the extension.
| Source | Use |
|---|---|
{ path } | An absolute path on the runtime's machine. Only the runtime owner may use it |
{ url, sha256 } | An https URL, or http to loopback, with the file's SHA-256. At most 4 GiB |
{ artifactId } | A Troupe Cloud build artifact. A local runtime refuses it |
Installs run while the session is starting, so they never count against a test's timeout or the first
action. An install the app causes to fail ends the session failed with the reason app-install-failed.
The session's installed field lists each app with its bundleId.
To install later, call apps.install.
Wait
sessions.wait answers when the session is ready or final, or
when timeoutMs passes (30 seconds by default). A timeout is a normal answer with done: false: call
again. A session that has not started within 10 minutes ends errored with the reason start-timeout.
Until ready, device calls answer SESSION_NOT_READY.
The live stream opens earlier, once the device is booted and its screen is
captured, while the driver still starts. Wait for that moment with until: 'stream' on a server that
advertises the feature:
if (await troupe.supports('sessions.streamBeforeReady')) {
await troupe.call('sessions.wait', { sessionId, until: 'stream' })
}Stay alive
A session ends by itself when its holder goes quiet. How depends on the holder's kind.
| Kind | Who | Stays alive by | Idle timeout | Longest life |
|---|---|---|---|---|
test-worker | troupe test workers | A heartbeat every 10 seconds; it ends 30 seconds after the last | none | none locally |
client | Troupe Studio, the React components | Activity | 10 minutes | 60 minutes |
mcp | Every session an MCP tool creates | Activity | 10 minutes | 60 minutes |
api | Other callers. The default | Activity | 10 minutes | 60 minutes |
Activity is any call by the holder that names the session, and input sent on the stream by the driver.
Watching the stream, reading events and asking for a watch ticket are not activity.
sessions.heartbeat counts as activity and returns the two
deadlines, which the session also carries as expiresAt and maxEndsAt.
Only the holder may change a session. A call from another principal that would act on the device, keep the
session alive or release it fails with SESSION_BUSY. On a local runtime the CLI, troupe test and the MCP
bridge all act as the runtime's owner, so this separates paired browser pages from each other and from you.
The owner may release any session.
Attach by key
Pass a key to find a session again. sessions.create with the key of a live session of the same caller
and holder kind returns that session and resets its timeout, instead of starting a device. A worker that
restarts gets its warm device back this way. The same key with a different spec is CONFLICT.
Release
sessions.release ends the session: canceled before it was
ready, ended with the reason released after.
- By default the device goes back to the warm pool: its apps are terminated and it stays booted for a few minutes for the next session with the same spec.
- With
healthy: falsethe device is shut down and not used again. Use it when the device misbehaved.
A session that takes a warm device finds the apps and the data the last session left on it. Create the
session with fresh: true when it must start on a new device. In tests, the appIsolation option decides
what each test finds: see App isolation.
Releasing a session that has ended returns it unchanged.
Why a session ended
| Status | reason |
|---|---|
ended | released, idle-timeout, heartbeat-timeout, max-duration, runtime-shutdown |
canceled | released |
failed | app-install-failed |
errored | boot-failed, driver-failed, environment, start-timeout, device-lost, host-pressure |
Troupe Cloud adds queue-timeout and host-lost. A call on a session that has ended fails with
SESSION_ENDED, which carries the status and the reason.
An app crash does not end a session. The call that hit it fails with APP_CRASHED, and
logs.crashes lists the reports.
Follow sessions
sessions.events is an event stream of what happens to one
session, or to all of them when you leave sessionId out.
for await (const event of troupe.follow('sessions.events', { sessionId })) {
if (event.type === 'status') console.log(event.status, event.reason)
if (event.type === 'streamReady') console.log('the stream can be opened')
}A local runtime sends status, appState, inputState and streamReady events. It keeps the last 1000
events of a session, and follow resumes from the last one it saw after a dropped connection. A gap event
says that events were missed.
Next
- The client SDK: calls, errors and event streams
- The live stream: show and drive the session's device
sessionsreference: every field and error