Eight calls in the iPhone automation stack that report success for work that never happened
None of these is exotic. A tap on a locked phone, an app launch, a runner relaunch, a TCP connect, a keystroke. Each returns the success signal its caller checks, and each needs a separate read of the phone to prove anything happened.
On a physical iPhone 17 Pro Max running iOS 26.6, driven from macOS 26.5 through WebDriverAgent v16.12.4, eight separate calls returned HTTP 200 or exit code 0 on 8 and 9 September 2026 while the phone did nothing. Three of them cost real time: a 54-second agent loop on one step, a supervisor that relaunched a dead runner for twenty minutes, and a task that burned 23 turns and $0.41 against an input stack that was discarding every event.
What was measured, on which versions
Every entry below was measured on the phoneshell rig and written into
FINDINGS.md in the commits of 8 and 9 September 2026. The runner is Appium's
WebDriverAgent at tag v16.12.4, released 6 September 2026 and pinned in
phoneshell/device.py as "the version every measurement in FINDINGS.md was taken
against". Four releases have shipped since, v16.12.5 on 7 September through v16.12.8 on
11 September. We have not re-measured on them, so treat each row as true of v16.12.4 until
someone does.
These are physical-device observations. iOSWorld (arXiv:2606.09764, 8 June 2026), the largest iOS agent benchmark published this year, runs its 133 tasks in the iOS simulator. We did not test a simulator, so we make no claim either way about whether an offloaded runner, a locked screen or an input wedge occurs there.
The eight calls
| call | reports | what the phone did | measured cost | the read that catches it | source |
|---|---|---|---|---|---|
| any gesture to a locked or sleeping phone | HTTP 200, null value | nothing; screenshots either side byte-identical | not reported | /wda/locked before every gesture | FINDINGS s.1 |
/wda/apps/activate, /wda/apps/launch | OK | app stayed in the background | 54 s agent loop on one step | poll /wda/activeAppInfo until the bundle matches | FINDINGS s.1 |
/wda/homescreen | OK | often nothing; agent stranded in the app switcher | not reported | screen check: four or more Icon elements, no overlay | FINDINGS s.1 |
devicectl device process launch, runner offloaded | exit 0, "Launched application" | bundle not on the phone | 40 s wait, 20 min diagnosis | devicectl device info apps first | FINDINGS s.13 |
devicectl device process launch, runner wedged | exit 0, supervisor logs wda-launch: OK | old process kept the field, no HTTP | 20 min loop after run 190 | info processes, then process terminate --pid | FINDINGS s.29 |
| writes during an input wedge | OK on dismiss, tap, home, drag | 0.0% of pixels changed; reads still correct | 23 turns, $0.41 | two-second drag probe, measure pixel change | FINDINGS s.14 |
| TCP connect to port 8100 | connection accepted | usbmux forward up, runner absent | not reported | require an HTTP answer from /status | FINDINGS s.22 |
type_text judged by pixel diff | "screen changed" | keyboard animated in; keystrokes discarded | empty reminder saved | read the tree back for the typed text | FINDINGS s.16 |
Why every layer can say yes
Each layer reports on its own contract, and none of those contracts is "the phone changed". An
HTTP 200 from WebDriverAgent means XCTest accepted the request. Exit 0 from
devicectl means the launch request was delivered. A TCP accept on 8100 means
iproxy is listening on the Mac. The only layer that knows whether the screen moved
is the screen.
The wedged runner is the clearest case because the recovery loop itself runs on a false
signal. A 360-cell sweep died at run 190, after 189 minutes of continuous automation. The runner
process was alive on the phone and answered no HTTP. The supervisor relaunched it, got exit 0,
logged wda-launch: OK, and did it again for twenty minutes, because a launch against
an already-running app is a no-op that reports success. The fix takes seconds and no reboot.
The cost of trusting each signal is uneven. Two of the false successes cost under a minute of wall clock, 40 s and 54 s; the two that fooled automated recovery cost about twenty minutes each, roughly 1,200 seconds, because nothing downstream disagreed.
What this changes if you build on a real iPhone
A write is not done until a read agrees. The reads are cheap next to a model turn:
/wda/activeAppInfo at 43 ms, /screenshot at 72 ms and a full
/source at depth 20 in 159 to 210 ms, against 4.1 to
6.8 seconds per agent turn on the same rig, of which the phone accounts for about one second.
Skipping verification saves under 5% of a turn and risks the 54-second loop.
Keep reads session-free. WebDriverAgent registers /source,
/screenshot, /wda/activeAppInfo and /wda/locked with
.withoutSession. Creating a session switches the app under test and sends the phone
home, which once made our own verification step navigate away from the state it was measuring.
Probe input, not health. A phone in the HID wedge answers /status,
/source and /screenshot correctly. Our doctor reported it healthy
until an input check was added that drags on the home screen and measures pixel
change. It costs about two seconds, and prepare_for_run now refuses to start a
benchmark when it fails.
Recover by killing, not relaunching. recycle_runner now calls
terminate_runner before launching, and PortForward.start clears a stale
iproxy off 8100, killing only iproxy. Judgement: a reboot is the wrong
default. After a cold boot iOS withholds developer services until a person types the passcode on
the phone itself, and WebDriverAgent cannot start to type it, which ends any unattended run.
Check it yourself
You need a Mac, an iPhone with Developer Mode and Settings > Developer > Enable UI Automation on, and the repo at github.com/Blomega/phoneshell.
bin/phoneshell setup && bin/phoneshell up # builds WebDriverAgent v16.12.4, starts the bridge
bin/phoneshell doctor # ends with the input check that drags and measures
# 1. A gesture to a locked phone: lock it with the side button, then
curl -s http://127.0.0.1:8100/wda/locked # {"value":true}
# send any tap through phoneshell shell and compare screenshots before and after
# 2. Exit 0 for an app that is not there: offload any app in
# Settings > General > iPhone Storage > app > Offload App, then
xcrun devicectl device info apps --device <udid> | grep <bundle> # absent
xcrun devicectl device process launch --device <udid> <bundle>; echo $?
# 3. TCP accept is not a runner: with the bridge up, terminate the runner, then
nc -z 127.0.0.1 8100 && echo "port open"
curl -s -m 3 http://127.0.0.1:8100/status || echo "no runner behind it"
# 4. The recovery for a wedged runner
xcrun devicectl device info processes --device <udid> | grep -i WebDriverAgent
xcrun devicectl device process terminate --device <udid> --pid <pid>
The code for each witness: launch_wda, terminate_runner,
recycle_runner and input_check in phoneshell/device.py;
check_input_reaches_screen and prepare_for_run in
phoneshell/prep.py. The device-log line for the input wedge was
process:AutomationModeUI ... dropped:110348 dropStatus:-536870168, visible in
Console.app with the phone selected. Items 1 and 3 reproduce on demand. The offload and the two
wedges were observed, not induced, so we cannot give a command that forces them.
What would prove this wrong
The claim is that these signals are structurally local: each layer reports on its own contract
and cannot see the screen. Two dated predictions follow. By 31 March 2027, the then-current
WebDriverAgent release will still return HTTP 200 for a tap sent to a locked iPhone, and
the then-current Xcode's devicectl device process launch will still exit 0 when
the target app is already running. A release that returns an error for either falsifies that
row, and we will update the table rather than leave it standing.
The limit: one phone, one iOS build, one WebDriverAgent tag, two days. Rows marked "not reported" have no measured cost, and the two wedges were each seen in one long session.
Sources
- Blolabel, FINDINGS.md, sections 1, 2, 13, 14, 16, 22, 24 and 29, measured on an iPhone 17 Pro Max on iOS 26.6 from macOS 26.5, committed 8 and 9 September 2026.
- Appium, WebDriverAgent v16.12.4, released 6 September 2026; the version pinned in
phoneshell/device.py. - Appium, WebDriverAgent releases: v16.12.5 (7 September 2026) through v16.12.8 (11 September 2026), not re-measured here.
- Blolabel, phoneshell/device.py and phoneshell/prep.py: runner launch, termination, recycling and the input probe.
- Lawrence Keunho Jang et al., iOSWorld: A Benchmark for Personally Intelligent Phone Agents, arXiv:2606.09764, submitted 8 June 2026. Runs in the iOS simulator.