# Connection and session states

> connection_state values ready, blocked, no-window, not-running; ensure_mirroring gates; blocked interstitial markers; and why reconnect is a user physical action.

- Repository: ShawnPana/phone-harness
- GitHub: https://github.com/ShawnPana/phone-harness
- Human docs: https://grok-wiki.com/public/docs/shawnpana-phone-harness-bf80173a2a2e
- Complete Markdown: https://grok-wiki.com/public/docs/shawnpana-phone-harness-bf80173a2a2e/llms-full.txt

## Source Files

- `src/phone_harness/helpers.py`
- `src/phone_harness/mirror.py`
- `SKILL.md`
- `install.md`
- `src/phone_harness/admin.py`

---

---
title: "Connection and session states"
description: "connection_state values ready, blocked, no-window, not-running; ensure_mirroring gates; blocked interstitial markers; and why reconnect is a user physical action."
---

Session readiness for phone-harness is decided in `connection_state()` and gated by `ensure_mirroring()` in `src/phone_harness/helpers.py`. Both helpers are pre-imported into every `phone-harness` stdin script. The harness discovers the **iPhone Mirroring** app (`com.apple.ScreenContinuity`) and its on-screen window; it never launches the app, never taps **Connect** / **Continue**, and never polls for a reconnect.

## Why the harness does not reconnect

Connecting or resuming iPhone Mirroring is a **physical user action**: open the app, approve pairing/prompts, and — when the interstitial says **iPhone in Use** — **lock the iPhone**. Automation cannot complete those steps. Tapping Connect while the phone is unlocked does nothing useful; loop-polling burns time without progress.

<Warning>
When `ensure_mirroring()` raises, **stop and relay the error text to the user**. Do not tap through the interstitial. Retry only after the user confirms they connected or locked the phone.
</Warning>

## `connection_state()`

Cheap readiness probe. Use it before work, or when a capture looks like a connect screen instead of an app UI.

```python
connection_state()  # -> "ready" | "blocked" | "no-window" | "not-running"
```

### Decision order

| Step | Check | Result |
|------|--------|--------|
| 1 | `mirror.running_app()` is `None` | `"not-running"` |
| 2 | `mirror.find_window()` is `None` | `"no-window"` |
| 3 | Capture the window, OCR all text, match blocked markers | `"blocked"` if any marker hits |
| 4 | Otherwise | `"ready"` |

Steps 3–4 require a successful window capture and Vision OCR. Permissions that break capture or OCR also break distinguishing `"blocked"` from `"ready"`.

### State values

| Value | Meaning | Typical user fix |
|-------|---------|------------------|
| `ready` | App running, phone window present, no blocked interstitial text | Proceed |
| `blocked` | Window present, but OCR shows a connect / paused interstitial | Open Mirroring; if **iPhone in Use**, lock the phone |
| `no-window` | App process exists, no qualifying phone window | Connect the phone in the app |
| `not-running` | Bundle `com.apple.ScreenContinuity` not running | Open **iPhone Mirroring** |

```mermaid
stateDiagram-v2
  [*] --> not_running: running_app() is None
  not_running --> no_window: app starts, no phone window
  no_window --> blocked: window exists, interstitial OCR
  no_window --> ready: window exists, no markers
  blocked --> ready: user connects / locks phone
  ready --> blocked: user unlocks phone ("iPhone in Use")
  ready --> no_window: window disappears
  ready --> not_running: app quits
```

## Blocked interstitial markers

When a window exists, `connection_state()` captures it, runs OCR, joins every box’s `text` (lowercased), and substring-matches against `_BLOCKED_MARKERS`:

| Marker (lowercase) | Role |
|--------------------|------|
| `iphone in use` | Session paused because the physical phone is unlocked |
| `lock your iphone` | Explicit lock instruction on the interstitial |
| `mirroring ended` | Session ended interstitial |
| `to connect` | Connect-flow copy |

Any match → `"blocked"`. Matching is case-insensitive substring on the concatenated OCR string, not exact full-screen equality.

## `ensure_mirroring()`

Hard gate used before tasks that need a live session.

```python
ensure_mirroring()  # -> window bounds dict when ready; else raises RuntimeError
```

| `connection_state()` | Behavior |
|----------------------|----------|
| `ready` | Calls `mirror.activate()`, returns `mirror.find_window()` |
| `not-running` | `RuntimeError` — open iPhone Mirroring and connect |
| `no-window` | `RuntimeError` — connect the phone in the app |
| `blocked` | `RuntimeError` — connect; if **iPhone in Use**, lock the iPhone; harness will not tap Connect |

On success the return value is the window bounds dict from `find_window()`: `{x, y, w, h, id}` in global screen points.

<Note>
`ensure_mirroring()` does **not** launch the app, tap Connect/Continue, or wait/poll for the user. Doc comments in helpers are the contract; treat SKILL phrasing that says it “launches” as outdated relative to this implementation.
</Note>

### Example gate

```bash
phone-harness <<'PY'
print(connection_state())
win = ensure_mirroring()
print(win)
print(screen_info())
PY
```

Expected when ready: `connection_state()` prints `ready`, `ensure_mirroring()` returns bounds, `screen_info()` includes `window`, `frontmost`, and `img_px`.

## Window presence vs session readiness

| Helper | Module | What it guarantees |
|--------|--------|--------------------|
| `find_window()` | backend (`mirror` / re-export) | On-screen layer-0 window owned by **iPhone Mirroring** with width ≥ 100, or `None` |
| `ensure_window(timeout=5.0)` | backend | Same bounds, or raises if no window after optional activate/retry (classic) or immediate raise (background) |
| `connection_state()` | helpers | App + window + interstitial OCR classification |
| `ensure_mirroring()` | helpers | Full ready session, or user-facing `RuntimeError` |

The mirroring window can exist while the stream shows a paused/connect interstitial. `ensure_window()` only proves geometry; `connection_state()` / `ensure_mirroring()` prove the session is usable.

### Backend differences for `activate` / `ensure_window`

| Behavior | Classic `mirror` backend | Background backend (default when load succeeds) |
|----------|--------------------------|--------------------------------------------------|
| `activate()` | Bring app frontmost; does **not** launch | No-op (never steals focus) |
| `ensure_window()` | May activate and poll up to `timeout` for a window | Immediate raise if no window |
| `ensure_mirroring()` on ready | Calls `activate()` (focuses classic; no-op background) | Same call path |

App identity constants (classic backend): `APP_NAME = "iPhone Mirroring"`, `BUNDLE_ID = "com.apple.ScreenContinuity"`, `APP_PATH = "/System/Applications/iPhone Mirroring.app"`.

## Agent recovery protocol

<Steps>
  <Step title="Probe">
    Call `connection_state()` (or catch `ensure_mirroring()`).
  </Step>
  <Step title="Stop on non-ready">
    Do not tap **Connect** / **Continue**. Do not loop-poll waiting for the session.
  </Step>
  <Step title="Relay exact failure">
    Surface the `RuntimeError` text from `ensure_mirroring()` (or map the state to the same instructions).
  </Step>
  <Step title="Wait for user confirmation">
    User opens Mirroring, connects, and locks the phone if required.
  </Step>
  <Step title="Retry once after confirm">
    Call `ensure_mirroring()` again only after the user says they finished the physical steps.
  </Step>
</Steps>

## Session lifecycle notes

- **One phone, one session** — product limit; unlocking the physical phone pauses mirroring (`"iPhone in Use"` / blocked).
- **Pairing** is a first-time manual step on the physical device (see install path and doctor “window not found”).
- **`--doctor`** checks installed → running → window found → capture → OCR. It does not call `connection_state()` by name, but the same ladder underpins “window found” vs blank capture failures.

## Failure modes

| Symptom | Likely state / cause | Recovery |
|---------|----------------------|----------|
| `ensure_mirroring` not-running error | App closed | User opens **iPhone Mirroring** |
| `ensure_mirroring` no-window error | No phone window | User connects phone in the app |
| `ensure_mirroring` blocked / “iPhone in Use” error | Interstitial OCR match | User locks phone and reconnects; agent does not tap Connect |
| Window found but work hits connect UI | Agent skipped `ensure_mirroring` or race after unlock | Call `connection_state()` / re-gate |
| Capture blank / tiny | Screen Recording not effective yet | Restart terminal after grant; re-run doctor |
| Doctor: window not found | Unpaired, out of range, or connect screen without usable window | Open app manually once; complete pairing |

## Related pages

<CardGroup cols={2}>
  <Card title="Quickstart" href="/quickstart">
    First script: check state, print `screen_info()`, confirm a live window.
  </Card>
  <Card title="Doctor diagnostics" href="/doctor-diagnostics">
    Ordered ladder for permissions, app running, window, capture, and OCR.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Blank capture, window missing, blocked **iPhone in Use**, silent input.
  </Card>
  <Card title="Input backends" href="/input-backends">
    How `activate()` differs between background and classic mirror backends.
  </Card>
  <Card title="Helpers API" href="/helpers-api">
    Full signatures for session, capture, OCR, and input helpers.
  </Card>
  <Card title="Consent and limits" href="/consent-and-limits">
    One session, real phone constraints, and when not to automate.
  </Card>
</CardGroup>
