Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add optional backend disposal and private, session-owned IDB companions. Release owned resources on stdio close, stdin EOF, SIGINT, and SIGTERM.
- Recover iOS sessions from a lost private companion or an invalidated simulator HID connection: reconnect once and resend only when the command was not delivered. Add optional `DeviceBackend.reconnect()` and export `IdbUnavailableError` (`IDB_HID_UNAVAILABLE`, `IDB_COMPANION_UNAVAILABLE`).
- Start private companions with `--terminate-offline true` and a one-hour idle shutdown, and remove private companion directories left by dead processes.

### Changed

- `LazyDeviceBackend.selectDevice()` now returns a promise and disposes the previous backend before switching. Programmatic callers must await it.

## [0.6.0]

### Added
Expand Down
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,42 @@ The server selects a backend in this order:

When multiple devices are connected and no `DEVICE_ID` is set, the server enters an "awaiting selection" state. Any tool call returns the list of available devices. Use `device_list_devices` to enumerate them and `device_select_device` to choose one.

### Connection lifecycle

IDB backends start a private `idb_companion` child with a unique Unix socket and route their commands through `IDB_COMPANION`. They do not register, reuse, or disconnect the global UDID connection. This isolates teardown across simulators and across backend instances targeting the same simulator.

Programmatic callers should release their backend after use:

```typescript
const backend = await createBackend(deviceId, 'ios');
try {
await backend.snapshot();
} finally {
await backend.dispose?.();
}
```

`dispose()` is idempotent and stops only that backend's companion and active recording, then removes its socket directory. It does not close apps or shut down simulators. Existing shared companions and other sessions remain running. Other backends can omit this optional lifecycle hook. The stdio server disposes its backend on transport close, SIGINT, and SIGTERM; an uncatchable process kill cannot run disposal.

`await lazyBackend.selectDevice(deviceId)` releases the previous backend before switching. Device selection now returns a promise; callers must await it. Disposing an unused lazy backend does not connect to a device.

#### Recovery and lifetime

| Condition | What the IDB backend does |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The private companion exits after it started (crash, external kill, idle shutdown, simulator shutdown) | The next call starts a fresh private companion, then runs the command. |
| A command fails with a lost companion (`Failed to connect to companion`) or an invalid simulator HID connection (`dtuhidd ... invalidated / interrupted / did not answer`) | The backend replaces its private companion and sends the command **once** more. The companion discards HID events sent on an invalid connection, and an unreachable companion received nothing, so the resend cannot repeat a tap. |
| The channel fails after the request may have been sent (`StatusCode.UNAVAILABLE`), or idb prints `retry: unsafe` | Read-only commands are resent once. Input commands (`tap`, `swipe`, `text`, `key`, ...) are not resent; the error says the action may have run. |
| The retry also fails | The call rejects with `IdbUnavailableError` (exported). `err.code` is `IDB_HID_UNAVAILABLE` (HID connection still invalid; usually the simulator rebooted during the session) or `IDB_COMPANION_UNAVAILABLE` (companion cannot be reached or started). The message ends with a remediation. |

`backend.reconnect?.()` replaces the backend's own connection on demand (iOS: its private companion). It is optional on `DeviceBackend`; the lazy backend forwards it. It never reboots or shuts down a device.

Each private companion starts with `--terminate-offline true` (it exits when the simulator shuts down) and `--idle-shutdown-time 3600` (it exits after one hour without requests). An orphan left by a killed host process therefore exits on its own. The backend reconnects transparently when the companion is gone.

Each private directory (`/tmp/device-mcp-idb-*`) holds an `owner.json` with the host and companion pids. Before the first companion starts, each process removes stale directories: the owner process is gone **and** the socket does not accept a connection. It stops a leftover companion only when that process's command line names the directory's socket. It never touches `/tmp/idb/state`, shared companions, or directories of a live owner.

Resolving an explicit simulator ID uses `simctl`, not `idb describe`, so device detection does not start a shared companion.

### Device Discovery

- **iOS simulators** are discovered via `xcrun simctl list devices booted --json` — no IDB needed for discovery
Expand Down
4 changes: 4 additions & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ device_select_device # Pick the one you want to use

If only one device is connected, it's auto-selected. If `DEVICE_ID` is set, this step is skipped.

Each iOS backend owns a private IDB companion. Device selection releases the previous connection. Closing stdio (including stdin EOF), SIGINT, or SIGTERM releases that server's companion and recording without shutting down the simulator or disconnecting other sessions. Programmatic callers must `await backend.dispose?.()` when finished and `await lazyBackend.selectDevice(deviceId)` when switching. Use explicit device IDs for independent sessions; app state is still shared if two sessions target the same simulator.

iOS recovers by itself from a dead private companion or an invalid simulator HID connection (for example after the simulator rebooted): it starts a fresh companion and sends the command once more. If that also fails, the tool error starts with `IDB_HID_UNAVAILABLE` or `IDB_COMPANION_UNAVAILABLE` and ends with a remediation. Do not repeat the action in a loop: check the screen with `device_snapshot`, then recreate the session (or reboot the simulator if the error says so). If the error says the action may have run, check the screen before you repeat it.

## Core Loop

```
Expand Down
Loading
Loading