Blolabel
← Notes
foundational 6 min read

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.

Abstract illustration of stacked translucent layers passing a green-lit signal upward while the bottom layer stays dark

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

callreportswhat the phone didmeasured costthe read that catches itsource
any gesture to a locked or sleeping phoneHTTP 200, null valuenothing; screenshots either side byte-identicalnot reported/wda/locked before every gestureFINDINGS s.1
/wda/apps/activate, /wda/apps/launchOKapp stayed in the background54 s agent loop on one steppoll /wda/activeAppInfo until the bundle matchesFINDINGS s.1
/wda/homescreenOKoften nothing; agent stranded in the app switchernot reportedscreen check: four or more Icon elements, no overlayFINDINGS s.1
devicectl device process launch, runner offloadedexit 0, "Launched application"bundle not on the phone40 s wait, 20 min diagnosisdevicectl device info apps firstFINDINGS s.13
devicectl device process launch, runner wedgedexit 0, supervisor logs wda-launch: OKold process kept the field, no HTTP20 min loop after run 190info processes, then process terminate --pidFINDINGS s.29
writes during an input wedgeOK on dismiss, tap, home, drag0.0% of pixels changed; reads still correct23 turns, $0.41two-second drag probe, measure pixel changeFINDINGS s.14
TCP connect to port 8100connection acceptedusbmux forward up, runner absentnot reportedrequire an HTTP answer from /statusFINDINGS s.22
type_text judged by pixel diff"screen changed"keyboard animated in; keystrokes discardedempty reminder savedread the tree back for the typed textFINDINGS 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.

Every success signal is local to its own layer agent (model + tools) phoneshell on the Mac, macOS 26.5 iproxy + usbmux, port 8100, USB cable WebDriverAgent v16.12.4 runner (XCTest) iOS 26.6: HID events, SpringBoard, apps the screen: the only layer that knows devicectl launch: exit 0 TCP accept on 8100 HTTP 200 on a locked phone activate / launch: OK 110,348 events dropped, writes OK witness reads, no session: activeAppInfo 43 ms, screenshot 72 ms source at depth 20: 159 to 210 ms
Success signals on the left are each true of their own layer. The read-back path on the right costs tens to hundreds of milliseconds and is the only evidence the phone moved.

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.

Launching over a wedged runner reports success forever run 190, 189 min runner alive, no HTTP devicectl process launch no-op on a running app exit 0, wda-launch: OK port 8100 still silent repeated for 20 minutes what worked, in order 1 stop thesupervisor 2 info processesfind runner pid 3 processterminate --pid 4 kill orphanediproxy on 8100 5 restart bridgeready, no reboot Skipping step 4 fails on bind with "iproxy exited immediately", whose advice is to check the cable. Killing the bridge does not kill iproxy, which keeps port 8100.
The false loop, and the five steps that recovered the runner in seconds on 9 September 2026 without a reboot or anyone entering the passcode by hand.

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.

Time lost to one false success, log scale 10 s100 s1,000 s seconds of wall clock launch wait, runner offloaded agent loop, activate said OK diagnosis, devicectl exit 0 supervisor loop, runner wedged 40 s54 s ~1,200 s~1,200 s
The two twenty-minute losses are the ones where automated recovery trusted the signal. FINDINGS reports them as "twenty minutes", so both bars are approximate.

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

  1. 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.
  2. Appium, WebDriverAgent v16.12.4, released 6 September 2026; the version pinned in phoneshell/device.py.
  3. Appium, WebDriverAgent releases: v16.12.5 (7 September 2026) through v16.12.8 (11 September 2026), not re-measured here.
  4. Blolabel, phoneshell/device.py and phoneshell/prep.py: runner launch, termination, recycling and the input probe.
  5. 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.