fix(daemon,kernel): device-selection safety — identity conflicts and ambiguity fail instead of retargeting
Summary
The device-selection safety cluster: three changes that make "successful response from the wrong device" impossible, rather than only unlikely. All three are admission/resolution policy — evaluated before any interaction dispatch path is selected — so no ADR 0011 guarantee cells move.
The motivating incident is concrete. Driving two attached emulators, I passed --udid emulator-5580, hit a bound-session lock, followed the error's own advice (--session-lock strip), and read three successful snapshots off the other emulator before noticing. Every step reported success.
1. A session-lock identity conflict fails instead of changing device identity. strip resolved every conflict by deleting the offending selector — including --udid/--serial/--device, after which the command ran against the bound device. It now fails under both reject and strip; strip keeps resolving what it exists for (--platform, --target, --ios-simulator-device-set, --android-device-allowlist). The error carries both identities structurally (requestedDevice, boundDevice), and the hint offers the two real recoveries — close the bound session if the requested device is intended, drop the selector if the bound device is — and never mentions strip for an identity conflict, since following that advice is what produced the wrong-device run.
2. Singular resolution refuses to guess. resolveDevice returns exactly one device, so every caller of it needs one concrete device; when the request carried no identity and several candidates were equally preferred, it returned the first by discovery order (alphabetically for Apple). This is now a refusal at the resolver boundary rather than a command-kind allowlist: correctness must not vary by state vs observability, and read-only operations are not safer — a snapshot of the wrong emulator is a wrong answer that looks right.
Established preference tiers are preserved, so the ambiguity is only what survives them equally:
- virtual over physical, the Apple kind/target rank, then booted over offline;
- one booted emulator beside offline candidates still resolves;
- explicit
--device/--udid/--serialstill resolves, and commands inside an existing session are unambiguous by construction; devicesand other multi-device commands never enter singular resolution.
The refusal reuses the declared device-candidate details domain (AMBIGUOUS_MATCH + devices), so the CLI and MCP renderers already print the bounded list, with a hint naming the platform-appropriate selector.
3. Platform-specific device flags are validated before resolution. --platform android --udid emulator-5580 answered No Apple device with UDID emulator-5580 — about a platform the request had explicitly excluded. Both directions now fail as INVALID_ARGS naming the right flag. No flag unification: --udid/--serial/--device encode real platform concepts and are public API.
Validation
pnpm check:affected --rungreen on the pushed head. CI: the 30/30 green run was the previous head (51670b1); on the current head (93abb6e) iOS Smoke failed at the Checkout step and Linux Smoke was cancelled — infrastructure outcomes with no test executed, both re-run rather than claimed as passing.- Live, two throwaway emulators beside an unrelated third device (full transcript in the PR comments): a session bound to
Pixel 7 lockArefuses a request namingPixel 7 lockBunder bothrejectand--session-lock strip— the latter is the command that previously returned a successful snapshot of the bound device — withrequestedDevice/boundDevicein the structured details; three booted devices with no selector refuse with all three listed in the plain CLI output;--platform android --udid …fails naming--serial; and an explicit--serialstill resolves normally (positive control). - Non-vacuity: the session-lock identity table has 13 rows crossing fresh vs existing session, matching/conflicting/absent identity,
rejectvsstrip, binding vs inventory command, and Androidserial/ Androidudid/ Appleudid, asserting the exact structured details and hint — 7 rows are red against the previous policy. Both selector-validation assertions and the ambiguity refusal are likewise proven red againstmain. - Four existing tests pinned the old strip-an-identity behavior and one pinned the arbitrary cross-platform pick; each was rewritten to its scope-only or refusal form rather than deleted, which is the record of exactly what shipped behavior changed.
Breaking changes
Two, both CHANGELOG'd: --session-lock strip no longer discards a device identity (it fails), and ambiguous singular resolution fails instead of picking. The second can surface on machines with several booted devices or several offline AVDs and no selector — the error lists the candidates with a runnable selector, so recovery is one paste.
Docs: website/docs/docs/commands.md for both behaviors; --session-lock's CLI help now states that strip covers scope selectors only. Skills unchanged — this belongs in CLI help/metadata and runtime errors.