Device ownership¶
A device that a session holds takes control calls only from that session.
Acquiring a device with getAndroid, getApple or startDevice creates the
session and returns its sessionUuid; the session holds the device until it is
released (idle timeout, lost heartbeat, or an explicit release).
daemon/releaseSession returns alreadyReleased:true and known:true when
this daemon remembers an earlier release. An unrecognized session returns
alreadyReleased:false and known:false; it still succeeds so releases remain
idempotent after a daemon restart. Set requireKnown:true to refuse unrecognized
sessions with daemon_session_not_found. When restoration is incomplete,
cleanup is pending or failed (failure takes precedence); inspect the restore
reports before relying on the device being fully restored. Completed restoration
omits cleanup.
Passing the session¶
- MCP: a connection that acquired a device is bound to it, so its calls route
to the session without repeating it. A different connection passes
sessionUuidin the call arguments; while the acquiring connection is still connected, that works for reads only. - CLI: each
--clicall is its own connection. Use thesessionUuidfrom the acquisition result on every later call:auto-mobile --cli --session-uuid <uuid> tapOn --selector '{"text":"Submit"}'. A second--cli getAndroidfor the same device reuses the anonymous session. Free the device with--daemon release-session.--daemon heartbeaton a one-shot CLI session is a successful no-op, so keeper processes are unnecessary. - Default session: only
setActiveDeviceor an acquisition changes the session that untargeted calls run on. An explicitsessionUuidtargets that one call only. A call passing onlyplatformruns on the default session when the platform matches.
If you hold two sessions and the default one ends, a control call that passes the
surviving session’s sessionUuid or deviceId runs on that session. A control
call with only platform, an unmatched deviceId, or no selector is refused
with no_active_device_session. Call setActiveDevice or acquire a new
session.
What is refused¶
A control call on a held device is refused with device_owned_by_other_session
when it carries no sessionUuid (a plain --cli tapOn, for example), names a
different session than the holder, or names the holder’s session from another
connection while the owner is still connected. Naming a session’s UUID never
takes it over. Once the owner has disconnected (a finished --cli call, for
example) another connection may name the session. A device that no session holds
takes calls from anyone.
Other refusals you may meet, and what to do:
| Code | Meaning and next step |
|---|---|
ambiguous_session_selector |
Several of your sessions match; pass sessionUuid or deviceId. |
ambiguous_device_target |
An unnamed call matched several devices; reads pass deviceId, control calls acquire a session and pass sessionUuid. |
device_outside_bound_session |
The call names a deviceId other than the session’s device. |
plan_execution_in_progress (retryable) |
A plan is running; retry when it finishes. Reads stay allowed. |
session_ownership_lost, no_active_device_session |
The session is gone for good (retryable: false, nextAction: "acquire_new_session"). Acquire a new session. |
session_released_during_creation |
The session was released before it finished creating. Acquire a new one. |
daemon_session_suspect (retryable) |
The owner’s heartbeat is late; retry now. |
session_held_by_recovery (retryable) |
Device recovery is in flight; retry after retryAfterMs. |
discovery_incomplete (retryable) |
adb or simctl listing failed, so AutoMobile will not boot or adopt a device blind. Retry. |
registry_schema_unsupported (not retryable) |
A strict ownership read found unknown migrations; dbPath, unknownMigrations and recorded writerVersions identify the registry and builds. Upgrade the daemons sharing it. |
device_owned_by_other_daemon (retryable) |
Another AutoMobile daemon holds the device. Stop it there, wait, or choose another device. |
device_assigned_to_binding |
A broker binding holds the device; use another device. |
A daemon warning about unknown registry migrations means a newer release has updated
shared ownership data. Allocation claim files alone decide ownership, and this daemon
skips the shared claims table until restart. This files_only mode does not enforce
managed bindings: this daemon can allocate a device held only by a managed binding.
The warning names writer versions when recorded. Upgrade the daemons sharing managed
devices to a release that includes these migrations, then restart older daemons.
To check versions, run auto-mobile --daemon capabilities --json. In session-broker
mode, an already-open registry adds bindingRegistry: schemaVersion is the latest
recorded migration, knownSchemaVersion is the latest migration this daemon knows,
supported says whether all recorded migrations are known, and writerVersion
identifies the build that recorded the latest migration. A null version means it is
unknown; the block is omitted when no registry is open or a read fails unexpectedly.
The refusal’s writerVersions maps migration names to the builds that applied them;
older migrations without a recorded writer are left out. Use these versions to
choose an upgrade that understands every recorded migration.
Choosing a device when a call is ambiguous¶
If your MCP client declares the elicitation capability (form mode) and connects
over stdio, an ambiguous_device_target refusal first becomes a question: the
client shows the candidate devices and the call continues on the one you pick.
If you decline or cancel, do not answer within about 20 seconds, or your client
does not declare elicitation, you get the ambiguous_device_target refusal
above, unchanged. Unattended agents are never held open. Pass deviceId on the
call to skip the question.
If you decline or cancel, the same connection is not asked again for the same set
of devices, so you are not prompted on every call; a different set of devices, a
new connection, or an accepted answer asks again. Set AUTOMOBILE_ELICITATION=off
to never ask (CI, benchmarks): the call returns the refusal immediately.
Answering the question from code or a headless run¶
What happens to the question depends on how you run your client:
| Client setup | What the question does |
|---|---|
| Claude Code, interactive | Shows a dialog listing the devices; your pick continues the call. |
Claude Code -p (headless) with no hook |
Answered with cancel within milliseconds, so you get the ambiguous_device_target refusal. Nothing waits. |
Claude Agent SDK (TypeScript) with onElicitation |
Your callback decides. |
| Claude Agent SDK (TypeScript) without a handler | Declined automatically, so you get the refusal. |
| Claude Agent SDK (Python) or the CLI | There is no callback; only an Elicitation hook can answer. Without one the question is declined or cancelled. |
The Agent SDK runs Claude Code underneath, so an external AutoMobile server always sees Claude Code’s behavior, not a separate client.
TypeScript onElicitation. The callback receives the request and returns an elicitation result:
{
"serverName": "automobile",
"message": "tapOn matched 2 devices. Which device should it use?",
"mode": "form",
"requestedSchema": {
"type": "object",
"properties": {
"deviceId": {
"type": "string",
"title": "Device",
"description": "The device this call should run on.",
"oneOf": [
{ "const": "emulator-5554", "title": "emulator-5554 (android)" },
{ "const": "emulator-5556", "title": "emulator-5556 (android)" }
]
}
},
"required": ["deviceId"]
}
}
Answer with { "action": "accept", "content": { "deviceId": "emulator-5554" } }, or
{ "action": "decline" } to get the refusal back. A deviceId that was not offered is treated as
invalid and also returns the refusal.
Elicitation hook (CLI and both SDKs). A command hook in your settings receives:
{
"hook_event_name": "Elicitation",
"mcp_server_name": "automobile",
"message": "tapOn matched 2 devices. Which device should it use?",
"mode": "form",
"requested_schema": { "type": "object", "properties": { "deviceId": { "type": "string" } } }
}
and answers on stdout:
{
"hookSpecificOutput": {
"hookEventName": "Elicitation",
"action": "accept",
"content": { "deviceId": "emulator-5554" }
}
}
A hook that always accepts picks a device for you without anyone looking, which can run a control
call on the wrong device. Have the hook choose by a rule you trust (for example the one device that
is booted), or decline; and pass deviceId on the call whenever you know it.
When a refusal carries retryAfterMs, wait that long before retrying.
Refusals do not count as session use and do not extend the session’s idle window.
Reads and control¶
Read-only calls never need a session and never count as activity, even for the holder. Anything that changes visible UI or starts a device-side process is control.
- Reads include
observe,snapshotOf,hitTest,identifyInteractions,listApps,getDeviceState,getNetworkGraph,getPreference,listDataStores,getDataStore,getAppPermissions,getNotificationPolicy,getDeepLinksandgetNavigationGraph. - Mixed tools are classified per call:
keyboarddetect/list calls,clipboardget,mediaSessionget,displayConfigwith no set field,accessibilitywith no toggle,prototypestatus/inspect and a single read-onlysqlQuerystatement are reads. Their other forms,systemTray,videoRecordinganddeviceSnapshotare control. - A sessionless read without
deviceIduses an already-running device and never boots one. If none is running it answers that there is nothing to read: start one withstartDeviceorprovisionDevice, or pass adeviceId. - A sessionless read that passes
deviceIdmay boot that stopped device (an Android AVD name or an available iOS simulator UDID) and never creates one. Boot capacity limits still apply. - Ownership is a guard between cooperating clients, not a security boundary against other local processes.
Cold iOS observe freshness¶
With more simultaneous cold iOS observes than the configured launch concurrency,
a queued launch or one already running can outlast the read budget. observe
keeps that budget and reports the pending state with ok: true, freshness.isFresh: false, and
freshness.unavailableReason: "runner_launch_queued", without a hierarchy.
The detail includes the queue position when queued, or says the runner is still
starting in the background. Any captured screenshot is kept. The start continues
for up to 60 seconds from the moment its launch leaves the queue (waiting in the
queue is bounded separately, at 5 minutes), and a retry joins that same start. While it runs it holds the
device’s readiness lock, so a concurrent startDevice or session upgrade for that
simulator waits within its own deadline. Retry the observe shortly.
A warm retry can complete in about 1.4 seconds; timing depends on the device and host.
A rollback of an Android AVD that provisionDevice created and never attempted to boot
is not blocked by an unrelated unnamed sibling emulator while the request holds the
lifecycle reservation. If rollback is refused before changing the device, the response
keeps the original failure’s retryability and reports the retained device in lifecycle;
follow recovery.nextAction, which permits reacquiring an intact retained device when
the original failure is retryable.
When a session is released¶
A session is released by an explicit release, a lost owner heartbeat, the idle window, the owning connection closing, or the device being killed or lost. Tune the windows with the variables in Environment variables.
- Lost heartbeat. A session whose owner stops heartbeating is released after roughly ten seconds. Calls from other callers do not keep a dead owner alive.
- Idle. A session is released after 2 minutes (default) with no control call. Heartbeats and reads do not extend the window; a control call still running holds the release until it finishes.
- Terminal releases. A released session UUID is never revived. A later call
naming it is refused with
session_ownership_lost,retryable: falseandnextAction: "acquire_new_session". After an idle release the refusal also carriesidle: true. A call still running when its session is released is cut with the same refusal. - Daemon restart. A daemon restart is not terminal: the owner reclaims the
session under the same UUID. While the daemon reclaims the binding, calls may
receive the retryable
binding_ownership_unconfirmed; waitretryAfterMs. If ownership cannot be confirmed in about a minute the session is released and calls getsession_ownership_lost. - Device restart. When a device restarts, its session can resume under the
same UUID for 180 seconds; other sessions cannot take that device meanwhile
(a start naming it is refused with a retry hint). If the window passes without
a resume the session ends with
session_ownership_lost. - Device killed or lost. An explicit kill ends the session with
session_ownership_lost; while it runs, calls getsession_terminal_release_in_progress(retryable: false). Acquire again under a new session. With session continuity enabled, a connection loss can instead returndevice_lostwhile the session is retained. ItsdeviceIdnames the lost serial;replacementDeviceId, when present, names the live replacement.retry.sameSession: trueandrequiresNewSession: falsemean retry using the samesessionUuid.recovery.status: "pending"means recovery is still settling, even if a replacement serial is already known; wait and retry."recovered"means recovery completed, andrecovery.attemptscounts recorded attempts (zero when only live-session evidence is available). An exhausted or unattempted recovery retains its reported status; followsessionStateandretryto decide whether to reacquire. Without a retained session or incident, no retry guidance is inferred. Verify continuity using the binding metadata and liveness notification. The generation counter is valid only within one daemon lifetime and restarts at zero after a daemon restart.session_device_reboundis sent atinfolevel, so a client that raised its log level aboveinfowill not see it; readsession-infoinstead. - Plan labels. A multi-device plan runs its extra labels on derived
<base>:<label>sessions that live only as long as the plan run and end with the base session. The next plan run mints them again; do not create<base>:<name>sessions yourself.
Release reasons¶
| Reason | Terminal? | What the client sees |
|---|---|---|
explicit-release |
Yes | session_ownership_lost; acquire a new session. |
cleanup-expired (legacy: lazy-expiry) |
Yes (idle) | session_ownership_lost refusals and session-not-found answers (heartbeat, daemon/sessionInfo) carry idle: true. Acquire a new session. |
cli-idle-timeout |
Yes (idle) | session_ownership_lost refusals and session-not-found answers (heartbeat, daemon/sessionInfo) carry idle: true for a one-shot CLI session. |
heartbeat-timeout, missing-first-heartbeat |
Yes | session_ownership_lost, with the heartbeat age and limit in the message. |
rehydration-owner-timeout |
Yes | session_ownership_lost: a rehydrated session’s owner never reclaimed it. |
owner-disconnected |
Yes | session_ownership_lost: the owning connection closed and no other client owns the session. |
binding-ownership-lost |
Yes | session_ownership_lost: another execution took the broker binding and its device; acquire a new session. |
managed-slot-ownership-lost |
Yes | Persisted legacy spelling of binding-ownership-lost; existing rows remain terminal. |
device-killed (also a sessionless stop of the device a handed-off session was waiting for) |
Yes | session_terminal_release_in_progress while the kill runs, then session_ownership_lost. |
device-disconnected:<device> |
Yes | session_ownership_lost: the device was lost. |
identity-recovery-<reason> |
Yes | session_ownership_lost with recoveryReason and deviceId (and ownerPid when another daemon holds the device): restart recovery could not prove the device’s identity (#11414). |
session-creation-cancelled |
Yes | session_ownership_lost: the creation was cancelled before the session was handed out. |
daemon-shutdown, daemon-restart |
No | The restarted daemon rehydrates the session; its owner reclaims it under the same UUID. |
restart-recovery-timeout |
Yes | session_ownership_lost, without idle: the device-restart recovery window ended without a resume. Acquire a new session. |
device-restart:<device> |
No | The owner can resume the same UUID within the fixed 180 s recovery window. If it lapses unresumed, the session ends terminally as restart-recovery-timeout; an earlier idle deadline ends it as expired with idle: true. |
superseded |
No | Nothing: a newer incarnation of the same UUID holds the device. |
plan-auto-release |
No | The plan freed a device it acquired; caller-held sessions stay held. Naming the same session UUID in a later plan re-issues the session; another client may take the device in between. It is not rehydrated after a daemon restart. |
session-creation-timeout, allocation-rollback, device-disconnected-during-session-create:<device> |
No | The session was never handed out; the UUID may be created again. |
expired |
No | A non-terminal row past its idle deadline: a control call naming it is refused with session_ownership_lost, reason expired and idle: true; heartbeat and daemon/sessionInfo answer daemon_session_not_found, releaseReason: "expired" and idle: true. |
Stopping a held device: killDevice and deleteDevice¶
Only the holder may stop a device it holds. A booted device another session
holds is refused with device_owned_by_other_session unless the call passes
force: true, which stops the device and releases the holder. From the CLI:
auto-mobile --cli killDevice --device '{...}' --force true. Use it only when
the device really should be stopped out from under its holder.
- Naming the holder’s
sessionUuidfrom a connection that does not own that session is refused, with or withoutforce. To force-stop the device, leavesessionUuidout. forcenever overrides another session’s device-restart reservation (the refusal is retryable withretryAfterMs) or a broker binding’s device.- A device another live AutoMobile daemon holds is refused with
device_owned_by_other_daemon: stop it through that daemon, wait for it to release the device, or passforce: true. - For a wedged Android emulator,
forcealso skips the AVD-name comparison. killDevicestops an Apple Watch simulator too: passplatform: "watchos"and its UDID. A UDID that is not a watch simulator is refused.deleteDevicedeletes an Apple Watch simulator withtarget.platform: "watchos"and the watch’s UDID astarget.stableId. A running watch is stopped first, which also releases any session on it. The call succeeds only once a fresh watch listing no longer shows the watch. A UDID that no watch simulator has reportsalready_absentand deletes nothing, even when it names another simulator. Atarget.stableNamethat differs from the watch’s name fails withidentity_conflict, a failedsimctl deletewithwatch_delete_failed, and running out oftimeoutMswithteardown_deadline_exceeded.- A refused
deleteDevicecarries the same typed fields as a refusedkillDevice.
Apple TV simulators¶
Call listDevices with platform: "tvos" to list booted Apple TV simulators, or
listDeviceImages with platform: "tvos" to list configured ones. Read
automobile:devices/images/tvos for configured Apple TVs, installed tvOS runtimes,
Apple TV models and their compatible runtimes. Check inventory completeness before
treating an empty result as no devices; retry a failed listing.
General device and image listings continue to cover Android and iOS.
provisionDevice with device.platform: "tvos" takes an exact name, optional
deviceId (UDID), and spec: { runtime, deviceType } identifiers from that
catalog. It adopts an exact match or creates a simulator; a name clash, missing
UDID or incompatible runtime/model is refused. A missing tvOS runtime must be
installed with xcodebuild -downloadPlatform tvOS; the tool never downloads it.
With boot (the default), it boots and binds a session at readiness booted;
a simulator it created is deleted again if boot or binding fails; if deleting it
again fails, the error says so and tells you to delete it with deleteDevice. boot: false
creates or adopts without a session. resources, enableTools and
readiness: "automation" are unsupported.
deleteDevice takes target: { platform: "tvos", isVirtual: true, stableId: "<UDID>" },
mode: "destroy" and verifyAbsence: true. It stops a running simulator and
releases its session before deletion, then confirms absence with a fresh listing.
An absent UDID returns already_absent; a mismatched target.stableName fails
with identity_conflict, and an unsuccessful deletion with tv_delete_failed.
startDevice with platform: "tvos" boots an existing simulator, starts its automation
runner and binds a session. Set AUTOMOBILE_TVOS_RUNNER_XCTESTRUN to a prebuilt
simulator runner xctestrun; otherwise a source checkout discovers or builds it.
If startup fails, the response explains why and the booted session supports screenshot-only
observation. Releasing the session, killing or deleting the simulator stops its runner.
With a connected runner, observe returns the hierarchy and the existing focusedElement;
use project: "full" to include the full hierarchy. Navigate focus with pressButton
using up, down, left, right, select, menu, home or playPause.
Use tapOn with an exact text or elementId selector and optional zero-based index to
move focus to a visible element and select it. Only the tap action is supported. A missing
match, absent focus, stalled focus or the 30-move limit produces an error; observe and
use pressButton to establish focus or move closer before retrying.
Touch gestures and long press are unsupported. Text input supports only type (append)
into the keyboard opened with pressButton select; selector, key and clear operations
are unsupported. Without a runner, observation
returns a screenshot and hierarchyUnavailable; call startDevice again to prepare automation.
Apple Watch simulators¶
A watch is reached only by naming it: platform: "watchos", or the
sessionUuid of a watch session. A call without a platform never lands on a
watch, even after one was selected.
setActiveDeviceselects a watch only withplatform: "watchos"; a bare watchdeviceIdis not found. With asessionUuidit binds the watch to that session, otherwise it makes the watch the connection’s active watch, which only calls namingplatform: "watchos"use.installApp,uninstallApp,listApps,launchAppandterminateAppwork on a watch.installApptakes a.appbundle built for the watchOS simulator;.ipa,.apkand the Android-only options are refused by name.uninstallApprefuseskeepData: true, because uninstalling always removes the app’s data.launchAppon a watch acceptscoldBoot,clearAppData(wipes the app’s Documents, Library and tmp before launching) andlaunchArguments, and returns the app’spid.intent,activityName,userIdandprototype: trueare refused. An app that is not installed fails withApp is not installed.provisionDevicewithdevice.platform: "watchos"takes the watch’s exactname, optionaldeviceId, andspec.runtimeandspec.deviceTypeidentifiers from the host’s watchOS runtimes and Apple Watch device types. It adopts a watch that matches all of them, or creates one; a near match, an unknown identifier or adeviceIdthat names no watch is refused. Withboot(the default) it boots the watch and returns asessionId; a watch it created is deleted again if that fails.resources,enableToolsandreadiness: "automation"are refused;readinessis always"none".startDevicewithplatform: "watchos"boots the watch, starts its automation runner, then binds a session. If the session ends before runner startup completes, the call fails; retrystartDevice. A second start reuses a connected runner. If the runner cannot start, the call still returns a booted session and explains why inmessage. SetAUTOMOBILE_WATCH_RUNNER_XCTESTRUNto a prebuilt watch runner artifact if automatic discovery or building is unavailable, then retry.- With its runner connected,
observereturns the watch hierarchy and screenshot;tapOn,tapAt, directionalswipeOnandpressButtonwith home are available. Without a runner, observation returns a fresh screenshot (screenshotPath,screenshotExpiresAt) andscreenSize, with ahierarchyUnavailablenote. Hierarchy fields are absent, not empty; use the screenshot and retrystartDevicebefore acting. Apple TV observation uses its connected runner for hierarchy and focus. - A session request (
--session-config/AUTOMOBILE_SESSION_CONFIG) asks for a watch withdevice: { "schemaVersion": 1, "platform": "watchos", "spec": { "runtime": "...", "deviceType": "..." } }: the exact watchOS runtime and Apple Watch device type identifiers, as forprovisionDevice. An unknown device type fails withunsupported_hardwareand an unknown or unavailable runtime withmissing_installed_runtime, both naming the available ones; a host that is not a Mac fails withunsupported_hardware, and an unreadable simulator catalog with retryablecatalog_unavailable. A ready result carries the watch’sprofileandreadiness: { "mode": "none", "status": "device_ready" }: the watch is booted, with no automation runner. A watch counts against the same device limit as every other device. On release the host’s iOS reuse mode decides what happens to the watch:eraseerases and reboots it for the next request with the same runtime and device type,keep-bootederases it (or destroys it when its fallback isdestroy), andminimalor no reuse deletes it. - Unsupported device tools refuse a watch with
<tool> does not support watchOS yet.killDevicestops a watch named withplatform: "watchos"and releases its runner. Ending the owning session or stopping the daemon also stops the runner.
A device that drops off¶
An Android device that adb still lists as offline stays held for up to 60
seconds before it counts as lost, and still appears in listDevices with its
session and connection state. A physical USB device that vanishes from
adb devices entirely is released at once.
A phone reachable over both USB and Wi-Fi is one device: a call may name it by
either endpoint, or an emulator by its localhost:<port> address, and is judged
exactly as if it named the canonical id. If a transport cannot be identified and
a held device could be its alias, a control call is refused with
device_owned_by_other_session; address the device by its USB serial or
emulator-NNNN id. If the adb devices listing itself fails, a control call
naming a wireless address AutoMobile has not seen is refused with the retryable
discovery_incomplete.
Transient Android transport failures¶
An adb transport failure on a session-owned device returns
device_transport_unavailable with retryable: true, retryAfterMs, deviceId
and dispatched.
dispatched: false:adbrefused the command before dispatch. AutoMobile already retried briefly; retrying is safe.dispatched: "unknown": the transport dropped after dispatch, so the command may have run. Observe before reissuing a non-idempotent action, especially anopenLinkwith a single-use sign-in link.
Retryable acquisition refusals¶
An acquisition AutoMobile cannot grant yet is refused with a typed, retryable code; wait and retry:
device_cleanup_in_progress: the device is still being released.retryAfterMssays how long cleanup can take. The CLI and MCP proxy poll this for you.device_owned_by_other_daemon: another daemon holds the device.device_shutting_down: the device is stopping.device_offline: an exact Android device stayedofflineorunauthorizedafter a short wait.session_rebinding: a kill raced the session’s own rebind.device_unhealthy: a health marker refuses the device.deviceslists each unhealthy candidate with itsdeviceId,reasonandrecoveringstate. Whileretryableandrecoveringare true, wait (honouringretryAfterMswhen present). When recovery ends with the marker kept, the refusal is non-retryable: replace the device withkillDevicethenstartDevice. Reads stay allowed on marked devices.orphan_recorder_active: a leftover screen recorder could not be confirmed stopped. Retry, or stop it by recording id.setActiveDeviceon a device another request is still preparing is refused with a retryabledevice_owned_by_other_sessionandretryAfterMs: 2000.
A kill naming a device the session has already left is refused with
session_no_longer_owns_device (retryable: false); check who holds the device
before killing it.
Device status in listings¶
listDevices and the device resources report one status per device. A device
that cannot be lent out is assigned, not idle: that covers a device with a
session, one that is shutting down or still becoming ready, one held by a broker
binding or another daemon, and an idle device with an unhealthy marker. A device
in error stays error. idle + assigned + error equals the total.
Boot capacity¶
Cold boots are admitted against host capacity. Each platform has its own limit on
booted devices: for Android, the smaller of half the host RAM divided by the
measured per-device memory and half the CPU cores; for iOS, the smaller of
60% of host RAM divided by the measured per-simulator memory footprint (about 2.4 GiB;
2.5 GiB when unmeasured) and a settled-CPU budget (about 0.1 core per simulator, half the
cores), at least 1. Every booted emulator or
simulator counts, including ones started outside AutoMobile. A boot over the
count limit is not queued: it fails at once with the retryable capacity_exhausted,
which carries retryAfterMs, limit, booted, platform and, when relevant,
externalDevices (running devices AutoMobile did not start) and heldDevices
(iOS simulators held by AutoMobile sessions or bindings; release the session
first rather than shutting them down). Adopting an already-running device never
fails this way. listDevices reports capacity per platform as
{ limit, booted, inFlight }. If the booted count cannot be read, the boot is
refused with discovery_incomplete. See
environment variables for the opt-out
and per-platform overrides.
Which read tools ready an unheld device¶
A sessionless read that names a running device nobody holds can set up CtrlProxy (Android) or the runner (iOS simulator) so the read can answer. Yes means the read does that setup. A device held or reserved by another session or daemon is never readied: reads use what is already installed, and control is refused.
| Tool | Android | iOS simulator |
|---|---|---|
accessibility |
Yes | Yes |
captureAudio |
No: booted-only verification | No: booted-only verification |
clipboard |
Yes | Yes |
compareScreenshot |
Yes | Yes |
displayConfig |
Yes | Yes |
getAppPermissions |
Yes | Yes |
getDataStore |
Yes | Yes |
getDeepLinks |
Yes | Yes |
getDeviceState |
Yes | Yes |
getLogs |
Yes | Yes |
getNavigationGraph |
Yes | Yes |
getNetworkGraph |
Yes | Yes |
getNotificationPolicy |
Yes | Yes |
getPreference |
Yes | Yes |
hitTest |
Yes | Yes |
identifyInteractions |
Yes | Yes |
inspectPackageSigning |
No: booted-only verification | No: booted-only verification |
keyboard |
Yes | Yes |
listApps |
No: booted-only verification | No: booted-only verification |
listDataStores |
Yes | Yes |
mediaSession |
No: booted-only verification | No: booted-only verification |
observe |
Own path: may start the service (see below) | Own path: may start the service (see below) |
prototype |
Yes | Yes |
snapshotOf |
Yes | Yes |
sqlQuery |
Yes | Yes |
statusBar |
Yes | Yes |
Streams need an observer registration, not a session¶
subscribe, request_observation, video and WebRTC viewing need a lightweight
observer registration, not a session. Tools need nothing, and streams need only
that registration. A viewer keeps streaming when ownership changes.
Recordings¶
- A recording stops and finalizes when its session is released, including an idle release, within about 120 seconds.
- Recording artifacts stay fetchable by id.
- Hiding a desktop or IDE window during a recording does not release the device.
- A video start from the connection holding a device belongs to that session,
even when the call omits
sessionUuid. - Starting a device gesture recording on an unheld device is refused with
no_active_device_session; acquire a session on that device first. - An active recording with no owner keeps running when a session acquires its device: the acquiring holder adopts it. Until then it can be stopped sessionless; afterwards only the holder can stop it.
Display and system settings between owners¶
Display and system settings a session changes are not restored when it releases the device. They are reset when a different session next acquires the device:
- Before a session first changes a setting, AutoMobile records its current value as the device default. Changes made with no session holding the device are not recorded.
- Covered settings: font scale (content size category on the iOS Simulator),
display density, night mode, the increase contrast, reduce motion, bold text
and Android display accessibility preferences (
displayConfig), the 24-hour format and calendar system (changeLocalization), and the iOS Simulator device-wide locale. Android locale changes are not covered. - A different session acquiring the device resets each recorded setting whose value differs from its default. The same session acquiring it again resets nothing. A failed reset is logged and retried at the next acquisition by another session.
The runtime screen size, navigation mode and display-cutout emulation that
displayConfig sets on Android are restored when the session that changed them
releases the device or rebinds to another one. displayConfig reset clears a
leftover size override even after a daemon restart.
Appearance configuration is per session.
Related¶
- Environment variables: session release and idle windows.
- FAQ.