Summary
On Android, replay of native list UIs produces systematic false divergences (measured FDR 1.0
across two independent Settings flows) because the annotator selects the non-unique framework
resource-id android:id/title as the primary identity for list-row text. iOS is unaffected (FDR 0),
because its AX cells expose unique per-cell identifiers.
This was found during a controlled false-divergence experiment (wave-3 E1) on main @ 6efe54451
(daemon v0.19.3). The app is unchanged between record and replay — every divergence here is false by
construction.
Root cause (from the divergence targetBinding)
A recorded Android get/press on a list row annotates like:
get "text" "id=\"android:id/title\" || role=\"textview\" label=\"Network & internet\" || label=\"Network & internet\""
# target-v1: { "id":"android:id/title", "role":"textview", "label":"Network & internet",
# "sibling":1, "viewportOrder":1, "ancestry":[linearlayout, recyclerview, framelayout, ...] }
android:id/title is a shared framework resource-id present on every titled row — on the Settings
root it matches 11 elements (matchCount: 11).
- The recorded ancestry is label-less generic container roles (linearlayout / recyclerview /
framelayout), so it contributes no disambiguation — leaving only sibling/viewportOrder position.
- On replay the position→element mapping shifts (a conditional row appears, or the shared-id set
reorders), so the id-path resolves the wrong element. The identity verifier then catches the label
mismatch and refuses:
targetBinding: {
classification: "identity-mismatch",
matchCount: 11,
recorded: { id: "android:id/title", role: "textview", label: "Network & internet" },
observed: { id: "android:id/title", role: "textview", label: "Apps" },
mismatches: [ "label: recorded=Network & internet observed=Apps" ]
}
This is a safe failure, not a silent mis-bind
The verifier did the right thing — it prevented acting on the wrong element ("Apps") and failed loudly.
The cost is a false divergence, not a wrong action. So the fix belongs upstream in Android identity
selection, not in loosening the verifier.
Proof it's the shared id (not Android capture)
The same flow with label="..."-only selectors (no android:id/title) replays 3/3 clean. Android
capture and the label resolver are fine; the defect is choosing a non-unique framework id as primary.
Reproduction
- Build a daemon from
main. On an Android emulator, record a native list flow, e.g. Settings root:
open com.android.settings --relaunch --save-script=f.ad, then get text 'label="Network & internet"',
get text 'label="Connected devices"', close --save-script.
replay f.ad → REPLAY_DIVERGENCE / identity-mismatch at the first get; the recorded selector
leads with id="android:id/title".
- Hand-author the same flow with
label="..."-only gets → replays clean, repeatedly.
Measured (wave-3 E1, 20 replays/flow, unchanged app)
| Flow |
Platform |
FDR |
| Settings → General → About |
iOS native |
0.00 (20/20 clean) |
| test-app Form fill |
iOS RN |
0.00 (20/20 clean) |
| Settings/Battery (default annotation) |
Android native |
1.00 (20/20 diverge) |
Settings root, label=-only selectors |
Android native |
0.00 (3/3 clean) |
Recommendation
At annotation time on Android, do not use a non-unique resource-id as the primary identity selector:
- Treat framework/shared ids (namespace
android:id/*, and any id whose match-count > 1 at capture) as
non-selective — demote below the visible label, or omit them from the selector chain.
- Equivalently, the resolver could treat a clause that matches
N > 1 and requires positional
disambiguation as low-confidence and fall through to a more selective later clause (the label clause
here resolves uniquely and correctly).
Either change makes Android native-list replay resolve by the (unique) label and eliminates this class of
false divergence — the single highest-leverage replay-robustness fix for Android.
Secondary (same experiment, lower priority)
A viewport-bottom element (y≈2864 of 2992) intermittently fell outside the captured tree, causing
wait/action-failure divergences — a capture-completeness question worth a separate look.
Found via wave-3 E1 false-divergence experiment. Evidence artifacts (divergence JSONs with targetBinding,
the clean label=-only fixture, per-flow summary.json) available on request.
Summary
On Android,
replayof native list UIs produces systematic false divergences (measured FDR 1.0across two independent Settings flows) because the annotator selects the non-unique framework
resource-id
android:id/titleas the primary identity for list-row text. iOS is unaffected (FDR 0),because its AX cells expose unique per-cell identifiers.
This was found during a controlled false-divergence experiment (wave-3 E1) on
main@6efe54451(daemon v0.19.3). The app is unchanged between record and replay — every divergence here is false by
construction.
Root cause (from the divergence
targetBinding)A recorded Android
get/press on a list row annotates like:android:id/titleis a shared framework resource-id present on every titled row — on the Settingsroot it matches 11 elements (
matchCount: 11).framelayout), so it contributes no disambiguation — leaving only
sibling/viewportOrderposition.reorders), so the id-path resolves the wrong element. The identity verifier then catches the label
mismatch and refuses:
This is a safe failure, not a silent mis-bind
The verifier did the right thing — it prevented acting on the wrong element ("Apps") and failed loudly.
The cost is a false divergence, not a wrong action. So the fix belongs upstream in Android identity
selection, not in loosening the verifier.
Proof it's the shared id (not Android capture)
The same flow with
label="..."-only selectors (noandroid:id/title) replays 3/3 clean. Androidcapture and the label resolver are fine; the defect is choosing a non-unique framework id as primary.
Reproduction
main. On an Android emulator, record a native list flow, e.g. Settings root:open com.android.settings --relaunch --save-script=f.ad, thenget text 'label="Network & internet"',get text 'label="Connected devices"',close --save-script.replay f.ad→REPLAY_DIVERGENCE/identity-mismatchat the firstget; the recorded selectorleads with
id="android:id/title".label="..."-onlygets → replays clean, repeatedly.Measured (wave-3 E1, 20 replays/flow, unchanged app)
label=-only selectorsRecommendation
At annotation time on Android, do not use a non-unique resource-id as the primary identity selector:
android:id/*, and any id whose match-count > 1 at capture) asnon-selective — demote below the visible
label, or omit them from the selector chain.N > 1and requires positionaldisambiguation as low-confidence and fall through to a more selective later clause (the
labelclausehere resolves uniquely and correctly).
Either change makes Android native-list replay resolve by the (unique) label and eliminates this class of
false divergence — the single highest-leverage replay-robustness fix for Android.
Secondary (same experiment, lower priority)
A viewport-bottom element (y≈2864 of 2992) intermittently fell outside the captured tree, causing
wait/action-failuredivergences — a capture-completeness question worth a separate look.Found via wave-3 E1 false-divergence experiment. Evidence artifacts (divergence JSONs with
targetBinding,the clean
label=-only fixture, per-flowsummary.json) available on request.