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.
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.
| Signal | phoneshell (Coexistence) | iPhone Use (operation.lock) | Source |
|---|---|---|---|
| Runs while idle, no call pending | yes, background thread, every 0.8s | no, the lock exists only during a call | coexist.py start()/_run(); iphone_use.py _dispatch() |
| Detects the owner mid-task | yes: unclaimed app change or screenshot diff > 3% | no mechanism; absence confirmed by reading the dispatch path | coexist.py _sample() |
| What "busy" means | the phone is in the owner's hands | another of this tool's own processes is mid-call | Presence.human_active vs device_busy |
| Wait before resuming | 6.0s of continued quiet, 120s timeout | none; the second caller errors immediately | coexist.py grace=6.0, wait_for_turn(); iphone_use.py BlockingIOError path |
| Reactive drift check tied to one action | not needed; the watchdog already ran | yes: foreground_changed / viewport_changed, only if the next call cites a stale observation id | wda_controller.py snapshot(), guard() |
What it means for anyone building a phone agent
- Whether a tool needs a continuous presence watchdog depends on who else has physical access to the device, not on how capable the agent is. A phone wired to a Mac for one development session has no owner competing for the screen; a daily-driver iPhone always does.
- A lock file solves a real problem, two of your own processes racing, but it is not a substitute for presence detection and was not built to be one. Calling
device_busya safety mechanism against a human picking up the phone would be a category error; it only ever sees another instance of itself. - phoneshell's reason string (
last_reason, e.g. "the foreground app changed to Messages") is cheap because the two signals it polls are cheap: about 100ms and 90 to 130ms. A watchdog built on the accessibility tree (~2.2s per read, per the same file's own comment) could not run at 0.8-second intervals without competing with the agent for the same channel. - iPhone Use's
foreground_changed/viewport_changedchecks still catch a specific failure mode well: a coordinate computed from an old screenshot being used after the phone rotated or an app switched. That is a different problem than noticing a person is standing there, and solving it does not require solving the other one.
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
- Blolabel, phoneshell:
phoneshell/coexist.py, read 13 October 2026. - zhongerxin, iPhone Use, created 6 October 2026, MIT license:
server/iphone_use.py,server/wda_controller.py, read 13 October 2026. - Apple, iOS 26.4 SDK,
UIWindowScene.h, cited via phoneshell/coexist.py's own docstring, read 13 October 2026.