Creating a WebDriverAgent session terminates the last session's app, so our verification step moved the phone it was measuring
Session creation is a write, not a handshake. WebDriverAgent publishes most of its read path as withoutSession routes for exactly this reason, and an observation loop built from them costs 325 milliseconds and disturbs nothing.
In WebDriverAgent, POST /session begins by killing the session that was already
active, and killing a session terminates the application that session was bound to. The phone then
shows SpringBoard. So a client that creates a session in the middle of a task closes whatever the
agent just opened, and a verification step that needs a session destroys the state it was about to
measure. Ours did: every task in one benchmark run failed with "foreground is springboard", and
the phone had been in the right state until the moment we looked at it.
The three statements that do it
The path is short enough to read. In WebDriverAgentLib/Commands/FBSessionCommands.m,
handleCreateSession: opens with [FBSession killActiveSessionAndWaitForTeardown]
before it looks at a single capability. In WebDriverAgentLib/Routing/FBSession.m,
-kill ends by terminating the tested application when four conditions hold: there is a
tested application, FBConfiguration.sharedInstance.shouldTerminateApp is set, the app
is running, and it is not the same as the system application.
shouldTerminateApp is set to YES in resetSessionSettings,
which is to say it is the default.
Nothing here is a bug. A WebDriver session owns an application under test, and tearing it down means putting that application away; the specification's session model assumes one client driving one app. It stops matching reality when the thing being driven is a phone, the state under test is the phone's state rather than one app's, and more than one process wants to look.
The observations below were measured on a physical iPhone 17 Pro Max running iOS 26.6 from
macOS 26.5, against WebDriverAgent v16.12.4 (released 6 September 2026), and are recorded in
FINDINGS.md section 6 of the phoneshell repository, published 15 September 2026. The
code references are to WebDriverAgent's current main branch, which still registers the routes the
same way as of v16.12.8 on 11 September 2026.
Which calls need a session, and what they cost
The useful detail, and the one that makes a fix possible, is that WebDriverAgent already
publishes most of its read path twice: once under a session and once as
.withoutSession. Twenty-two route registrations in
FBCustomCommands.m alone carry that flag.
| call | session | measured cost | what it is for | source |
|---|---|---|---|---|
GET /source | not required | 159 to 210 ms at depth 20 | the accessibility tree | FBDebugCommands.m |
GET /screenshot | not required | 72 ms | pixels | FBScreenshotCommands.m |
GET /wda/activeAppInfo | not required | 43 ms | which app is in front | FBCustomCommands.m |
GET /wda/locked | not required | not reported | is the phone awake | FBCustomCommands.m |
GET /alert/text, POST /alert/accept, /alert/dismiss | not required | 1,360 ms on a heavy screen | system dialogs | FBAlertViewCommands.m |
POST /wda/homescreen | not required | not reported | go home | FBCustomCommands.m |
POST /wda/apps/launchUnattached | not required | not reported | open an app without owning it | FBCustomCommands.m |
GET /window/size | not required | not reported | screen geometry | FBElementCommands.m |
POST /session | creates one, kills the last | not reported | attach to an app | FBSessionCommands.m |
POST /session/:id/actions | required | 500 ms tap, 19,000 ms drag | W3C gestures | FBElementCommands.m |
POST /wda/dragfromtoforduration | required | 1,400 ms | native drag | FBElementCommands.m |
POST /session/:id/wda/apps/activate | required | returns OK with the app still in the background | bring an app forward | FBSessionCommands.m |
POST /wda/pickerwheel/:uuid/select | required | 550 ms per tap | picker wheels | FBElementCommands.m |
Reading is cheap and safe. Writing is neither. An observation loop built only from the first group never creates a session, so looking at the phone cannot disturb it, and the three calls that make up a full observation add to 325 ms.
Three bugs, one cause
Each of these was a real failure on this rig, and all three are the same sentence applied in a different place.
- Recovery that closed apps. Stale-session handling pressed home and then re-attached. Re-attaching created a session, which closed whatever was open, every few seconds.
- Verification that destroyed its own measurement. The benchmark's check
called
set_settings, which needs a session. Creating it navigated the phone away before the check read anything, so every task in the run failed with "foreground is springboard". The scores were consistent, confident and entirely about our checking code. - Two clients evicting each other. WebDriverAgent holds one active session. When the phone-side app and the agent both create one, each eviction tears down the other's app under test, continuously.
What this changes if you build a rig or grade a run
Separate the channel that measures from the channel that acts, and prefer the sessionless routes
for everything that only looks. That is not a workaround; it is what the .withoutSession
registrations are for, and WebDriverAgent has them for the tree, the screenshot, the foreground app,
the lock state, the window size, the alert and the home button.
Two further consequences are worth stating for anyone running a benchmark rather than a test suite. First, a check that mutates is not a check, and the mutation here is invisible because every call returns success. Second, one WebDriverAgent holds one session, so a rig that also runs a companion app on the same phone needs a rule about who may create one, or the two will keep evicting each other and both will report healthy while doing it.
The judgement, marked as one: the WebDriver session model is a poor fit for device-level agents, and the sessionless read path is the part of WebDriverAgent that admits it. A protocol designed for this job would separate "attach to an app" from "look at the device" explicitly, which is close to what the MCP accessibility-tree proposal (arXiv:2608.24898, 13 July 2026) is arguing for on the assistive side.
Check it yourself
Two terminals, one phone, about a minute. Open an app, confirm it is in front, create a session that owns it, then create another one and watch the first app disappear.
git clone https://github.com/Blomega/phoneshell && cd phoneshell
bin/phoneshell up
# no session anywhere in this line
curl -s localhost:8100/wda/activeAppInfo | python3 -m json.tool # bundleId of what is in front
# session one: attach to Settings, which brings it forward
S1=$(curl -s -X POST localhost:8100/session -H 'content-type: application/json' \
-d '{"capabilities":{"alwaysMatch":{"bundleId":"com.apple.Preferences"}}}' \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["sessionId"])')
curl -s localhost:8100/wda/activeAppInfo # com.apple.Preferences
# session two, from any other client. Session one's app is terminated by the teardown
curl -s -X POST localhost:8100/session -H 'content-type: application/json' \
-d '{"capabilities":{"alwaysMatch":{}}}' > /dev/null
curl -s localhost:8100/wda/activeAppInfo # com.apple.springboard
To see the other half, leave the phone on any screen and hammer the read path:
/source, /screenshot, /wda/activeAppInfo in a loop for a
minute. Nothing moves. The code is
handleCreateSession: in FBSessionCommands.m and -kill in
FBSession.m, both in appium/WebDriverAgent,
and the client side is phoneshell/device.py plus the observation loop in
phoneshell/actions.py.
What would prove this wrong
- Run the sequence above on WebDriverAgent v16.12.x and find the first app still in the
foreground after the second
POST /session. That would mean the termination is conditional on something we have not identified, and our reading of-killis wrong. We predict the home screen, on any iOS 26 device, through 31 December 2026. - Set
shouldTerminateAppto false in the session settings and the teardown should stop closing the app. We have not tested that path and would treat a report either way as new information rather than confirmation. - The narrow claim we are least sure of: our note records that a session created with no bundle id also sends the phone home. The code path we can point at is the teardown of the previous session, not the creation of the empty one, so if you see the home screen with no prior session in existence, the cause is something else and we would want to know.
Sources
- Appium WebDriverAgent, WebDriverAgentLib/Commands/FBSessionCommands.m, handleCreateSession: and its first statement. https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Commands/FBSessionCommands.m
- Appium WebDriverAgent, WebDriverAgentLib/Routing/FBSession.m, the kill teardown and its four termination conditions. https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Routing/FBSession.m
- Appium WebDriverAgent, WebDriverAgentLib/Commands/FBCustomCommands.m, twenty-two withoutSession route registrations. https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Commands/FBCustomCommands.m
- W3C, WebDriver Level 2, the session model. https://www.w3.org/TR/webdriver2/
- phoneshell, FINDINGS.md sections 2, 6 and 18, published 15 September 2026. https://github.com/Blomega/phoneshell/blob/main/FINDINGS.md
- Ramineni et al., MCP-Driven Accessibility Tree Standardization for AI-Powered Screen Reader Agents, arXiv:2608.24898, 13 July 2026. https://arxiv.org/abs/2608.24898