Blolabel
← Notes
comparison 8 min read

phoneshell polls every 0.8 seconds for a human taking the phone back; iPhone Use's one lock file cannot see one at all

Two agents, two different answers to the same fact: an iPhone has one screen and one owner. One watches for that owner continuously. The other only knows another of its own processes exists.

iOS 26 has one interactive display and one digitizer, so a phone's owner and an agent driving it cannot act at once. phoneshell's Coexistence watchdog (phoneshell/coexist.py) runs a background thread that samples the foreground app and a screenshot every 0.8 seconds, reads a change outside its own claimed window as the owner picking the phone up, and waits 6.0 seconds of continued quiet before resuming. The newly public iPhone Use (github.com/zhongerxin/iPhone-use, created 6 October 2026) solves a narrower problem: a single fcntl file lock stops two of its own AI callers from acting at once. Nothing in it runs while no call is in flight, so a person is invisible to it until the agent's next action happens to reference stale coordinates.

Abstract technical illustration on a dark grid: a pulsing radar-like sweep continuously circling a small glowing device icon on the left, beside a single static closed padlock icon on the right with no motion around it, cool blues with one amber accent

The hardware fact neither tool can get around

phoneshell's own code comment states the constraint and its source: "verified from the iOS 26.4 SDK headers, not assumed: UIWindowScene.h declares only UIWindowSceneSessionRoleExternalDisplayNonInteractive, and the interactive external-display role was deprecated in iOS 16." The device enumerates seven display slots, but everything past the built-in one mirrors and cannot be touched. So "running in the background" can only mean one thing for a phone its owner still uses day to day: the agent gets out of the way the instant a person picks the phone up, and resumes where it left off once they put it down. That is a scheduling problem, and the two tools schedule it differently because they are built for different situations: phoneshell for Sam Shamsan's own iPhone, which he also uses; iPhone Use for a developer's iPhone wired to a Mac for the length of a session.

Two mechanisms: one continuous, one reactive

phoneshell's watchdog runs on its own thread (Coexistence._run), sampling every poll seconds regardless of whether the agent is doing anything. Each sample checks two cheap signals, measured on the project's own rig: /wda/activeAppInfo at about 100ms and /screenshot at 90 to 130ms; the full accessibility tree at roughly 2.2 seconds is named in the same comment as "far too slow to poll, never used here." The agent brackets its own actions with claim(seconds=2.5), so a change inside that window is attributed to itself, not the owner. Outside a claim, a foreground-app change or a screenshot differing by more than 3% (visual_difference(...) > 0.03) sets Presence.human_active. wait_for_turn() then blocks new agent actions until the owner has been inactive for grace, 6.0 seconds, or until a 120-second timeout gives up.

iPhone Use has no background thread and no polling loop at all. Every tool call passes through _dispatch() in server/iphone_use.py, which opens operation.lock under the session's state directory and takes an exclusive, non-blocking fcntl lock. A second call that arrives while the first is still running gets BlockingIOError, turned into WDAError("device_busy", "Another iPhone Use operation is running. Wait for it to finish before continuing; no action was executed."). That is the entire concurrency model: it serializes this tool's own callers, for example two Codex MCP processes sharing one runtime, and says nothing about what else might be touching the phone. The nearest thing to a presence check is snapshot() and guard() in wda_controller.py, which compare the cached app and viewport of a specific earlier observation id against a fresh read, and raise foreground_changed or viewport_changed if they differ. That check only runs when a later tool call supplies that observation id, which happens only when the agent is about to act on remembered coordinates; it never runs on its own, and it has no owner, grace period or wait.

phoneshell's Coexistence watchdog, one cycle every 0.8s _sample() activeAppInfo ~100ms screenshot 90-130ms inside self.ours (claim)? yes: this sample becomes the new baseline, not a signal no: app changed, or visual_difference > 0.03 Presence.human_active = True last_human_at = now last_reason recorded wait_for_turn(timeout=120) blocks new agent actions until 6.0s quiet, or gives up yields += 1 "the phone is free again, carrying on" This loop runs on its own thread the entire time the bridge is up, whether or not an agent action is pending. iPhone Use has no equivalent: its only check runs inside a tool call, and only when that call supplies an observation id. Source: phoneshell/coexist.py, read 13 October 2026.
Signalphoneshell (Coexistence)iPhone Use (operation.lock)Source
Runs while idle, no call pendingyes, background thread, every 0.8sno, the lock exists only during a callcoexist.py start()/_run(); iphone_use.py _dispatch()
Detects the owner mid-taskyes: unclaimed app change or screenshot diff > 3%no mechanism; absence confirmed by reading the dispatch pathcoexist.py _sample()
What "busy" meansthe phone is in the owner's handsanother of this tool's own processes is mid-callPresence.human_active vs device_busy
Wait before resuming6.0s of continued quiet, 120s timeoutnone; the second caller errors immediatelycoexist.py grace=6.0, wait_for_turn(); iphone_use.py BlockingIOError path
Reactive drift check tied to one actionnot needed; the watchdog already ranyes: foreground_changed / viewport_changed, only if the next call cites a stale observation idwda_controller.py snapshot(), guard()
phoneshell's own coexistence timing constants, seconds phoneshell (coexist.py) iPhone Use: no equivalent 0 1 2 3 4 5 6 0.8 0 poll interval 1.2 0 settle after own action 2.5 0 claim window 6.0 0 grace before resuming Source: phoneshell/coexist.py defaults, read 13 October 2026. iPhone Use has no comparable constant in server/.

What it means for anyone building a phone agent

Check it yourself

# phoneshell's watchdog: constants, the sample loop, and the wait
sed -n '1,20p' phoneshell/coexist.py        # the SDK-header justification and measured costs
grep -n "poll: float = 0.8\|grace: float = 6.0\|settle_after_action: float = 1.2" phoneshell/coexist.py
grep -n "def claim\|def wait_for_turn\|def _sample" -A8 phoneshell/coexist.py

# iPhone Use: the lock, and the only drift check it has
curl -s https://raw.githubusercontent.com/zhongerxin/iPhone-use/main/server/iphone_use.py | grep -n "operation.lock\|device_busy\|LOCK_EX"
curl -s https://raw.githubusercontent.com/zhongerxin/iPhone-use/main/server/wda_controller.py | grep -n "def snapshot\|def guard\b" -A8

What would prove this wrong

We claim iPhone Use has no mechanism that notices a person touching the phone while no tool call is running. That is wrong if any file in the repo starts a background thread, a timer, or a polling loop outside _dispatch(); our read of server/ on 13 October 2026 found none. It is also wrong if a future release adds one: iPhone Use shipped five point releases between 9 and 13 October 2026, so the surface area is changing fast. We will treat a CHANGELOG.md entry mentioning presence, ownership, or a background watcher, by 31 December 2026, as the signal that this comparison no longer holds.

Sources