feat(android): one persistent automation helper owning snapshot + viewport + canonical injection#1281
Conversation
|
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
…the persistent snapshot helper (#1275) One Android automation helper now owns snapshot capture, gesture viewport resolution, and canonical one-/two-pointer plan injection. A live persistent helper session executes gesture/viewport commands over its socket protocol; without a session the same APK runs one-shot via am instrument. The separate one-shot multitouch helper APK is deleted (atomic replacement, no fallback). Touch scheduling/injection is extracted into focused Java classes (TouchPlan, TouchPlanInjector, PointerEventSchedule, GestureViewportReader) instead of growing SnapshotInstrumentation. ADR 0013 amended.
702ccbe to
0b033f1
Compare
|
Review findings at
The architecture direction, no-double-injection policy, 23 green checks, and live emulator measurements otherwise look good. |
…e-shot viewport retry A structured ok=false viewport response leaves the session process alive, and Android permits only one instrumentation owner of UiAutomation - running the one-shot fallback against a still-live helper contends with it and masks the original structured failure. Stop the session first; regression pins that the one-shot retry only executes once the session is gone.
…; split session tests; document helper API v2 (PR #1281 review) Addresses findings 2 and 3 from PR #1281 review (finding 1, viewport session-stop ordering, was already fixed in 5961b92). - Extract SnapshotInstrumentation.java's one-shot/session touch dispatch into TouchCommandHandler.java (viewport/gesture population, UiAutomation- parameterized) and SessionResponseWriter.java (session response encoding), with shared PROTOCOL/HELPER_API_VERSION/OUTPUT_FORMAT constants moved to a tiny HelperProtocol.java. SnapshotInstrumentation.java shrinks from 908 to 803 lines; wire format (header keys/values, error shapes) is unchanged. - Split touch-helper.test.ts (~720 lines) into touch-helper.test.ts (normalize/parse/one-shot gesture+viewport+result envelope) and touch-helper-session.test.ts (persistent-session transport + fake-session harness), moving shared device/plan/install-probe fixtures used by both files into touch-helper.fixtures.ts. - Update android/snapshot-helper/README.md to document helper API v2: the one-shot viewport/gesture modes, the android-touch-plan-v1 payload shape, and the persistent session's socket command/response contract.
|
All three findings addressed, CI green (23/23):
Post-refactor live smoke on emulator-5556 (rebuilt APK, throwaway state dir): 🤖 Addressed by Claude Code |
|
Re-review of current head
All current CI checks are green; the remaining gaps are lifecycle/resource correctness not exercised by those checks. |
…e viewport windows; align ADR 0002 (PR #1281 re-review) - prepareAndroidTouchHelper now mirrors the snapshot path: when ensureAndroidSnapshotHelper replaces the APK (install.installed), the persistent session started against the previous binary is stopped before any touch command, so gestures run one-shot against the fresh install instead of a dead/stale session socket. Regression: 'an APK replacement stops the stale session and the gesture runs one-shot' drives a live fake session through an outdated-install probe (new outdatedVersionAdb fixture) and asserts the session socket receives no gesture, the one-shot instrumentation path executes, and the session is gone. - GestureViewportReader.read no longer leaks AccessibilityWindowInfo: a single pass copies the active/focused and first-application bounds into locals, every window is recycled in a finally, and the existing precedence (active/focused app bounds, root-in-active-window, fallback app bounds, IllegalStateException) is applied afterwards, unchanged. - ADR 0002's touch-synthesis paragraph is amended (2026-07, issue #1275) to the shared-helper model, consistent with ADR 0013: a live persistent helper session executes touch commands directly, one-shot otherwise; the old stop-before-gesture requirement is kept as historical context.
|
All three re-review findings addressed in 9c13215, CI green (23/23): P1 — session invalidation after APK replacement. P2 — AccessibilityWindowInfo recycling. P3 — ADR alignment. ADR 0002's touch-synthesis paragraph is amended (2026-07, issue #1275, consistent with ADR 0013) to the shared-helper model: one bundled helper owns snapshot capture, gesture viewport resolution, and planned-touch injection; a live persistent session executes touch commands directly over its socket, one-shot otherwise. The old stop-before-gesture requirement remains as historical context. Post-fix live smoke on emulator-5556 with the rebuilt APK: first snapshot triggered a real replacement ( 🤖 Addressed by Claude Code |
|
Re-review finding on current head
Please resolve touch/viewport from Two small docs corrections can ride the same update: ADR 0002 should explicitly exempt non-idempotent gesture failures from one-shot retry, and the helper README should describe a persistent process with one short-lived socket connection per request (the server closes each accepted connection). |
…e snapshots do (PR #1281 re-review) - prepareAndroidTouchHelper now uses the same artifact precedence as snapshot capture: the scoped adbProvider's snapshotHelperArtifact when present, otherwise the bundled resolver (whose strict unavailable error is preserved). The provider artifact drives both the install decision and the instrumentationRunner used for one-shot commands, so an ADB-backed provider that supplies a helper artifact but no native touch override runs snapshots and gestures against the same single helper (issue #1275). Regression: 'a provider-supplied snapshotHelperArtifact overrides the bundled artifact for touch' pins the provider packageName on the install probe, the provider apkPath on the install call, the provider instrumentationRunner on the am instrument args, and that the bundled resolver is never invoked. - ADR 0002 now states explicitly that one-shot retry applies only to idempotent reads (viewport) after the failed session is stopped; non-idempotent gesture failures surface directly. - Helper README session transport corrected: a persistent process serving one short-lived socket connection per request (the server closes each accepted connection), not a single long-lived connection.
|
Addressed in ff6031b, CI green (23/23): Provider artifact precedence. Doc riders. ADR 0002 now states explicitly that one-shot retry after a failed session command applies only to idempotent reads (viewport, and only after the failed session is stopped); gesture injection is not idempotent — events may already be partially injected — so it surfaces directly. Helper README session transport corrected: a persistent process serving one short-lived socket connection per request (the server closes each accepted connection), not a single long-lived connection. Live emulator smoke re-run on the same head: 🤖 Addressed by Claude Code |
|
Current-head re-review (
Please apply the same identity guard before touch/viewport session reuse and add a regression with: live session from A + provider artifact B reported current + no install; assert A is not used and B's runner handles the command. |
…atched sessions (PR #1281 re-review) Persistent helper sessions are keyed by device, so touch reuse must also prove the live session runs the helper binary the command selected. The session record now stores its helper identity (packageName, runner, helperVersion, helperVersionCode — the same values that feed the snapshot session identity), and runAndroidSnapshotHelperSessionTouchCommand takes the requesting helper identity: on mismatch (packageName/runner always; version/versionCode when both sides define them) it stops the session and returns undefined, so the touch command runs one-shot against the selected artifact — gestures never start sessions; the next snapshot restarts one with the right artifact. Matching identity reuses the session as before. Snapshot capture identity and behavior are unchanged. Regression: 'a provider artifact that mismatches the live session helper stops it and runs one-shot' — a live fake session from the bundled fixture artifact, then a gesture through an ADB provider supplying an already-current artifact with a distinct packageName/runner (no install): the old session socket receives zero gesture commands, the session is stopped, the one-shot am instrument args end with the provider runner, and helperTransport is 'instrumentation'.
|
Addressed in 3d0aad9, CI green (23/23; the iOS Smoke Tests run needed one rerun for an unrelated Helper-identity guard on touch session reuse. The persistent session record now stores its helper binary identity ( Regression: Live emulator smoke re-run proves matching-identity reuse intact: 🤖 Addressed by Claude Code |
|
Current-head re-review ( The persistent-session identity stores package, runner, version, and versionCode, but not the artifact SHA. This is a supported case: the install path already replaces same-version helpers when APK bytes differ. Include the artifact checksum in the live-session identity (and requested touch identity), invalidate stale cached decisions as needed, and add the missing regression: A and B share package/runner/version/versionCode but have different SHA; after B owns the live session, selecting A must not reuse B. |
…stale install memo entries (PR #1281 re-review) Same-version binary replacement changes only the APK sha, so identity guards keyed on package/runner/version/versionCode could not detect a crossover between two artifacts that differ only in bytes: - The artifact sha256 now joins the helper identity end-to-end: AndroidSnapshotHelperCaptureOptions gains helperSha256 (snapshot.ts passes artifact.manifest.sha256 alongside version/versionCode), the session record stores it, createSessionIdentity includes it (making snapshot session reuse sha-aware, consistent with the install path's existing sha check), and the touch identity guard compares it via the same both-defined rule. - ensureAndroidSnapshotHelper's install memo now evicts every other cached decision for the same device+package when it records an install/current decision, so installing B invalidates A's stale 'current' memo and a later command selecting A re-inspects the device instead of skipping the sha check. Regressions: 'a same-version artifact with a different sha stops the live session and runs one-shot' (touch-helper-session.test.ts — B owns the live session, a gesture selecting same-version different-sha A sends zero commands to B's socket, stops it, and completes one-shot) and 'installing a same-version different-sha helper evicts the stale install memo' (snapshot-helper.test.ts — A:current cached, B installed, selecting A re-inspects and reinstalls instead of serving the stale memo). Both verified to fail without their fix.
|
Addressed in 4ec5915, CI green (23/23, no reruns needed): sha256 joins the helper identity end-to-end. Install-memo eviction. Regressions, both stash-verified to fail without their fix:
Live emulator smoke: 🤖 Addressed by Claude Code |
|
Reviewed exact head Code review is clean. The APK SHA now participates end-to-end in persistent helper identity (snapshot session identity and touch-session reuse), and recording a confirmed/current install decision evicts competing memo entries for the same device + package. That closes the same-version, different-bytes A → B → A crossover. Both new regressions are revert-sensitive: without SHA identity the gesture reaches the wrong live session, and without memo eviction the final A selection incorrectly returns the stale cached decision. One residual merge-readiness requirement remains under the repository device-evidence policy: the current live evidence proves matching-SHA helper reuse, but it does not exercise a real emulator crossover where A and B share package/runner/version/versionCode and differ only in APK bytes. Please capture that A → B → A run before merge, proving each selected artifact is installed/re-inspected and no command crosses into the other live helper session. The coordinator will handle |
Closes #1275.
What
One persistent Android automation helper now owns snapshot capture, gesture viewport resolution, and canonical one-/two-pointer plan injection. The separate one-shot multitouch helper APK is deleted — atomic replacement, no legacy fallback, no second production path.
android/snapshot-helper): touch scheduling/injection is extracted into focused classes instead of growingSnapshotInstrumentation—TouchPlan(payload parse/validate),TouchPlanInjector(pacedinjectInputEventwith cancel-on-failure),PointerEventSchedule(moved verbatim from the multitouch helper, including the per-stepwaitForDispatchsemantics),GestureViewportReader.SnapshotInstrumentationgains only dispatch: one-shot-e mode snapshot|viewport|gesture, andviewport <id>/gesture <id> <payloadBase64>commands on the existing session socket protocol.helperApiVersionbumps to 2; the response protocol literal, APK/package/runner identity, and artifact names are unchanged (no orphan installs, no release-pipeline churn).src/platforms/android/touch-helper.tsreplacesmultitouch-helper.ts/multitouch-helper-install.ts. A live snapshot-helper session executes touch commands directly over its socket; without one, the same APK runs one-shot. Gestures never start or stop the session — the forcedstopAndroidSnapshotHelperSessionForDevice()before every local gesture (the instrumentation churn measured in refactor: replace Maestro compat with typed direct engine #1217 validation) is gone. Install rides the sharedensureAndroidSnapshotHelper(sha-256 verified, version-coded).ok=falsepropagates and leaves the session alive; a transport-level session failure stops the session and propagates without a one-shot retry for gestures (events may already be partially injected — double-injection is worse than a loud error). Viewport reads are idempotent and do retry one-shot after a session error.backend: "android-helper"withhelperTransport: "persistent-session" | "instrumentation"(previouslyandroid-multitouch-helper).scripts/check-affectedandroid-helpers rule now also coversandroid/ime-helper/(was previously untracked by any build-ownership rule).Prototype gate (issue requirement) — measured on a live emulator
Identical scripted sequence (
open → snapshot → 5 scroll passes w/ snapshot each → short pan → 2 snapshots → 5 s pan → pinch/rotate/two-finger pan/transform → close) against the repo test app (gesture-canary screen), branch vsorigin/mainbaseline, same emulator (Pixel 9 Pro XL API 37). The multitouch APK was uninstalled from the device before the branch run.1. Touch request latency (command wall time):
2. Touch → two stable snapshots without process restart: on main the first snapshot after a gesture reports
helperSessionReused: false(forced session restart — the churn); on branch both reporthelperSessionReused: true. Post-gesture snapshots: ~1.5 s (main) → ~0.8 s (branch).3. Five consecutive scrollable passes: all snapshots non-empty and correct on both variants.
4. Long one-pointer pan (5 s): green on both. One gate sample showed helper-elapsed 8.3 s on the session transport; three follow-up repeats measured 5.4–6.0 s, matching one-shot (6.0 s) and main (5.8 s) — one-off emulator noise, not a transport property.
5. Canary parity (plans and app-observable effects unchanged):
injectedEventsidentical per gesture kind on main and branch (pan 21, 5 s pan 315, pinch 23, rotate 23, two-finger pan 48, transform 54 — the planner is untouched and the injector was ported verbatim including upstream'swaitForDispatch/cancel-timing changes). App canaries flip identically (pan/pinch/rotate changed yes), final transform state equal within recognizer frame noise (scale 0.65 both; rotate 60° vs 58°).The one-shot transport was additionally live-verified with sessions disabled (
AGENT_DEVICE_ANDROID_SNAPSHOT_HELPER_SESSION=0): gestures reporthelperTransport: "instrumentation",helperApiVersion: 2, canaries flip — with the multitouch APK absent.Verification
pnpm typecheck,pnpm lint,pnpm check:tooling,pnpm check:fallowclean; unit suite 462 files / 4092 tests green (one known CPU-contention flake inruntime-hints.test.ts, passes in isolation).touch-helper.test.tspins: one-shot arg/payload shape (android-touch-plan-v1), timeout budgetmax(45 s, durationMs + 15 s), result envelope, session gesture with noam instrumentcall and session left alive,ok=falsekeeps the session, malformed/stale session response stops the session without one-shot gesture retry, viewport one-shot fallback after session error.PointerEventScheduleTest(upstreamwaitForDispatchexpectations) as part ofpackage:android-snapshot-helper.Notes for review
android-snapshot-helpername/identity; renaming the artifact family to "automation helper" was deliberately deferred to avoid release-pipeline and installed-package churn in the same change.touch-executor.tsbefore any helper involvement.