fix(daemon,kernel): device-selection safety — identity conflicts and ambiguity fail instead of retargeting - #1880
Conversation
… device identity `--session-lock strip` resolved every conflict by deleting the offending selector, including --udid/--serial/--device. The command then ran against the BOUND session's device rather than the one the caller named, and the error hint that produced this state actively recommended strip. A wrong-device tap that reports success is worse than any loud failure, so: - a conflict on a device IDENTITY selector now fails under both reject and strip; strip keeps resolving platform/scope selectors (--platform, --target, --ios-simulator-device-set, --android-device-allowlist), which is what it exists for; - the structured error carries both sides (requestedDevice, boundDevice) so a caller can choose a recovery without parsing prose; - the fresh-session 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. The existing bound-session hint already stated both and is unchanged. Admission-layer only: this is device/session resolution policy, evaluated before any interaction dispatch path is selected, so no ADR 0011 guarantee cells move. One table pins the crossing: fresh vs existing session, matching/conflicting/absent identity, reject vs strip, binding vs inventory command, Android serial / Android udid / Apple udid, with the exact error details and hint asserted. 7 of its 13 rows are red against the previous policy.
`--udid` enters Apple resolution unconditionally, so `--platform android --udid emulator-5580` answered "No Apple device with UDID emulator-5580" — an answer about a platform the request had explicitly excluded, which reads as a missing device rather than a mistyped flag. Both directions now fail as INVALID_ARGS naming the correct flag (--serial for Android/HarmonyOS, --udid for Apple). Requests that name no platform keep the existing DEVICE_NOT_FOUND behavior, since nothing contradicts the selector there.
…icking one
resolveDevice answers with 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 (or alphabetically for Apple) — a successful response describing a device the
caller never selected. That is worse than any loud failure, and reads are not safer than writes: a
snapshot of the wrong emulator is a wrong answer that looks right.
Ambiguity is now a refusal at the resolver boundary, not a command-kind allowlist:
- established preference tiers are preserved (virtual over physical, the Apple kind/target rank,
then booted over offline); only what survives them equally is ambiguous, since the comparator's
remaining tie-breaks are name and discovery order, which encode nothing about intent;
- one booted emulator beside offline candidates still resolves, as do explicit --device/--udid/
--serial and any command inside an existing session, whose identity is already fixed;
- multi-device commands ("devices") never enter singular resolution;
- the error reuses the declared device-candidate details domain (AMBIGUOUS_MATCH + "devices"), so
CLI and MCP already render the bounded list, with a hint naming the platform-appropriate selector.
|
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
|
Reviewed exact head |
"Use --serial X to select a android device by serial" — the platform name is interpolated, so no article fits every value. Names the flag's platform family instead.
|
Both items closed: the iOS lane finished green, and here is the live two-device evidence. CI: 30/30 on the exact head, iOS lane included. Live setup: two throwaway emulators — 1. Session-lock identity conflict, 2. The same request with Structured details carry both sides, so a caller can pick a recovery without parsing prose: { "code": "INVALID_ARGS",
"requestedDevice": { "serial": "emulator-5584" },
"boundDevice": { "platform": "android", "name": "Pixel 7 lockA", "id": "emulator-5582" } }3. Ambiguous singular resolution — three booted devices, no selector, no session: All three render in the plain CLI output (not only 4. Platform-specific flag validation: 5. Positive control — an explicit selector still resolves normally: One fix came out of the live run: the hint read "to select a android device by serial" — the platform name is interpolated, so no article fits every value. It now names the flag's platform family ( Sessions closed, both emulators killed, both throwaway AVDs deleted; the Pixel 9 Pro XL was only ever read from by the refusals above. |
|
Re-reviewed exact head |
|
Both CI items are resolved, and the second one answered itself on main. Linux Smoke: re-run, green. iOS Smoke: the first re-run got past Checkout and then failed inside the test — but on a text-entry assertion, not device selection: The decisive evidence is the merge commit itself: this change is now on main as On my earlier claim: "30/30 green" was accurate for For the record, the flake signature is worth watching: |
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.
stripresolved every conflict by deleting the offending selector — including--udid/--serial/--device, after which the command ran against the bound device. It now fails under bothrejectandstrip;stripkeeps 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 mentionsstripfor an identity conflict, since following that advice is what produced the wrong-device run.2. Singular resolution refuses to guess.
resolveDevicereturns 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 bystatevsobservability, 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:
--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-5580answeredNo Apple device with UDID emulator-5580— about a platform the request had explicitly excluded. Both directions now fail asINVALID_ARGSnaming the right flag. No flag unification:--udid/--serial/--deviceencode 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.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).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.Breaking changes
Two, both CHANGELOG'd:
--session-lock stripno 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.mdfor 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.