mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
v0.20.0
1125 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e4139b6802 | ci: deepen node 22 packaged smoke (#1125) | ||
|
|
318e813cc8 | test: split interaction runtime tests (#1113) | ||
|
|
915f5ba374 | build: support packaged CLI on Node 22.12 (#1116) | ||
|
|
83d54614d8 |
fix: bound iOS capture stalls and make runner recovery session-preserving (#1105) (#1107)
* fix: bound iOS capture stalls and make runner recovery session-preserving (#1105) Runner (Swift): - Coalesce duplicate transport sends of one commandId onto the in-flight execution instead of enqueueing them again behind it (capture pileup). - Fail fast with RUNNER_BUSY while watchdog-abandoned main-thread work is draining; escalate to RUNNER_WEDGED past 120s so the daemon recycles. - Carry the capture-plan deadline into the query-sweep and private-AX ladder tiers so chained recovery cannot stack past the watchdog. - Penalize the tree backend after a slow (>5s) or abandoned capture and lead subsequent regular plans with private-AX for that bundle (sticky, 120s), stamped recovered/budget so the deferral stays observable. Daemon (TS): - Per-request runner recycle budget: at most one invalidate+reboot per request, then fail fast with an actionable, session-preserving hint. - RUNNER_WEDGED joins the runner-fatal invalidation reasons. - Interaction commands (click/fill/longpress/press/type/get/is) preserve the daemon on request timeout like snapshot/wait/find: resetting it destroyed every healthy app session the daemon owned. * fix: suppress AX-broken-screen snapshot issues so the runner survives capture XCTest records 'Failed to get matching snapshot: kAXErrorIllegalArgument' issues for every XCUIApplication query on AX-broken screens; after a few of them the test case tears down the moment the in-flight command completes, killing the long-lived runner after every capture of the screen (the restart loop behind #1105). The capture plan already classifies and recovers from AX failures, so this issue class is noise: swallow exactly it in record(_:); everything else still records and still drives XCTEST_RECORDED_FAILURE. * feat: time-slice the XCTest tree capture on a worker thread The tree snapshot XPC is a single blocking call whose duration moves with live content (4s to minutes on Bluesky profile screens); no in-process budget could bound it on the main thread. Run it on a worker bounded to an 8s slice: on timeout the plan penalizes the tree backend, skips the XCTest-backed tiers while the abandoned XPC drains (they would block behind it inside testmanagerd), and recovers through the private AX backend, which does not use testmanagerd. * tune: lower the tree-backend penalty threshold to 3s The Bluesky profile tree grind measures ~4.5s before kAXErrorIllegalArgument, just under the old 5s threshold, so every capture re-paid the doomed grind (9s each). At 3s the second capture onward defers to private AX (2.4s snapshot, 4.9s press on the live repro). * fix: harden the AX-issue suppression per review - Require the kAXError token: 'Failed to get matching snapshot: Timed out while evaluating UI query.' is a genuinely-hung-query signal and must keep recording (and keep driving XCTEST_RECORDED_FAILURE). Sibling AX server codes (kAXErrorCannotComplete, ...) are deliberately included: any AX-server rejection inside a matching-snapshot fetch is the same capture-plan noise. - State honestly that the override is suite-global and why (tap-triggered queries record the same noise; command outcomes stay honest via their own error paths). - Lock-guarded suppressed-issue counter following the file's existing abandoned-work counter pattern, logged with each suppression. - Unit-test the pure classifier (record(_:) itself is not invoked: the must-record variants would record real failures in the test run). |
||
|
|
a0556583c3 |
docs: tighten agent operating guide (#1104)
* docs: tighten agent operating guide * docs: update agent guide cli paths |
||
|
|
0159975f2e |
test: split the args.test.ts aggregation along source topology (#1102)
* test: split the args.test.ts aggregation along source topology AGENTS.md file-size tripwires now apply to tests with no exemption, and test files are expected to mirror source topology 1:1. args.test.ts was a 2,503-line aggregation in src/utils/__tests__ while the code it exercises lives in src/cli/parser. Split it into six focused files with every test moved verbatim (142 tests before and after): - src/cli/parser/__tests__/args-parse-interaction.test.ts (29 tests): parseArgs shapes for press/click/swipe/gesture/type/record/screenshot and friends - src/cli/parser/__tests__/args-parse-session.test.ts (41 tests): parseArgs shapes for session/daemon/device flags, passthrough, install/metro/connect/proxy/auth and friends - src/cli/parser/__tests__/args-validation.test.ts (17 tests): strict/ compat modes, rejections, deterministic errors - src/cli/parser/__tests__/cli-help-topics.test.ts (15 tests): global usage and help topics - src/cli/parser/__tests__/cli-help-command-usage.test.ts (35 tests): per-command usage copy - src/utils/__tests__/command-schema-guards.test.ts (5 tests): schema/ catalog/capability guards and the cli.ts dispatch-literal walk (the oxc-parser helpers live here) AGENTS.md testing-matrix and help-source pointers updated to the new paths, including the stale src/utils/cli-help.ts and cli-flags.ts locations (both live under src/cli/parser/). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FqeW8sA2ZnvnftdvpCqFMS * docs: fix cli parser paths in agent guide --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
5a362ec752 |
build: drop tsc entirely and fix sandbox-hostile tests (#1100)
Follow-ups from the bundler/CI speed work, re-validated against latest main. The typescript package is gone from the toolchain: - pnpm typecheck stays on tsgo; the typecheck:tsc escape hatch is removed along with the typescript devDependency. - args.test.ts extracted cli.ts dispatch literals through the TypeScript compiler API - the only remaining consumer. It now walks the same AST via oxc-parser (matching the OXC lint/format/build stack); both implementations extract an identical 14-literal set from cli.ts, verified side by side before the swap. The substitution-free template case ts.isStringLiteralLike covered is preserved. - dts bundling is unaffected: the tsdown build uses the tsgo backend and builds green with no typescript package installed. Test fixes for containerized agent environments: - The missing-binary doctor-guidance web provider test pins Node 24 via the file's existing withNodeRuntimeVersion helper, so it asserts the setup hint instead of inheriting the host Node and failing on Node 22 (the supported engines floor). - The clean-xcuitest cleanup-failure smoke test skips as root: chmod 0o500 cannot force a removal failure when the process bypasses directory permissions. AGENTS.md toolchain notes updated to match. Claude-Session: https://claude.ai/code/session_01FqeW8sA2ZnvnftdvpCqFMS Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
86736a5f87 |
feat: versioned snapshot refs with MCP auto-pinning (#1096)
* feat: versioned snapshot refs with MCP auto-pinning Refs are positional indexes into the latest stored session tree; #1093's coarse snapshotRefsStale marker warns honestly but cannot say WHICH tree a ref came from. Give the session a monotonically increasing snapshotGeneration, advanced wherever the stored tree is replaced: the setSessionSnapshot choke point and the snapshot/diff command path that bypasses it. Token economy (non-negotiable): the snapshot tree output is unchanged — plain e12 refs on every node. Ref-issuing responses (snapshot command, find ref outputs) carry the generation ONCE as the additive refsGeneration field. Ref-consuming commands (press/click/fill/longpress/ get/wait) accept both forms: plain @e12 keeps today's behavior including the coarse #1093 warning; pinned @e12~s3 is clean when the generation matches the stored tree, gets a precise warning naming both generations when it does not, and a malformed suffix is INVALID_ARGS with a grammar hint. Warn-only this release — tightening comes later per the compat ladder. The MCP layer auto-pins at zero token cost: it sees snapshot/find responses before the model does, remembers the last refsGeneration per session name, and rewrites plain @ref tool arguments to the pinned form before forwarding. The model never sees or types suffixes; with no remembered generation, refs pass through unpinned (never guess). Replay parsing and script writing strip and IGNORE pins — generations are meaningless outside the session that minted them. Refs #1076 * docs: CONTEXT.md vocabulary for ref generation pins Moved from #1097 per the review sequencing note: the term lands with the behavior it describes. * docs: teach the ref pin syntax in CLI help MCP agents get pins transparently (auto-pinning), but CLI-driving agents only ever met the coarse warning — refsGeneration arrived in snapshot responses with nothing explaining it, making pins an undiscoverable feature on the primary agent surface. One help line in the agent loop guidance closes that; warnings stay short (they fire repeatedly, teaching belongs in once-read surfaces). * fix: per-ref MCP pin provenance and seeded generations Review findings on the first cut: 1. The MCP layer kept ONE refsGeneration per session, so after snapshot(s12) -> find(s13) a plain @e37 from the pre-find snapshot got pinned ~s13 and read as current — recreating the find-blessing hole at the pinning layer. Replace it with per-ref provenance: Map<pinScope, Map<refBody, generation>>, scoped by state dir + session name (stateDir is a per-call MCP config field, so one server process can face multiple daemons). Merge-only updates: refs present in a ref-issuing response (snapshot nodes, digest refs, the find ref) move to its generation; absent refs KEEP their older pins — an old pin on a replaced tree is what makes the daemon warn. Never-issued refs pass through unpinned; an issuing response without refsGeneration clears the scope; memory bounded to the ~1000 most recently issued pins. 2. Generations were per-lifetime counters from 1, so a reopened session's ~s1 collided silently with the previous lifetime's. Seed the first bump at a random 6-digit base (crypto randomInt): cross-lifetime collisions become ~1e-6 — probabilistic (seeded), not identity-based, documented on the field. Pin format unchanged; within-lifetime comparisons stay exact. Tests: the MCP blessing scenario (pre-find ref stays pinned to ITS generation), the daemon half in the provider scenario (find must not bless a pre-find pin), reopen/reseed at unit + handler level, state-dir scope isolation, digest-ref merging; generation fixtures made seed-agnostic (relative bumps, echo the observed seed). Refs #1076 |
||
|
|
2557670193 |
test: slow-test ratchet and speed rules from measured experiments (#1099)
* test: slow-test ratchet, budget-derived emulator poll, speed guidance from experiments Measured (2026-07-04, full unit suite: 340 files / 3,210 tests / 48s wall): wall clock was bounded by the slowest FILE (44.6s android monolith at ~7x file-level parallelism), and the slowest tests were sleeping through real production budgets (10.8s proving 'times out' by waiting the constant out, 8s emulator polls at 1Hz, real retry backoff). Two config experiments rejected with data: --no-isolate exploded the suite to 205s (module state thrashes across files sharing workers) and --pool=threads changed nothing. - scripts/vitest-slow-test-reporter.ts: the slow-test ratchet. Unit budget 2.5s / integration 15s; failure at 2x budget (the band between reports without failing so host-load variance cannot make the gate cry wolf); 36 pinned offenders, exact keys, ratchet-only pin (tracking #1098). - waitForAndroidEmulatorByAvdName: poll cadence derives from the caller's budget (min 1s, floor 50ms, ~timeout/20) — devices.test.ts 25.6s -> 2.8s (9x) in isolation, and short-budget production calls stop sampling at 1Hz against small budgets. - vitest.config: slowTestThreshold 500 for local visibility; reporter wired; isolation/pool decisions documented with the measurements. - docs/agents/testing.md 'Speed rules' + AGENTS.md testing bullet: the three conversion patterns in preference order (budget-derived cadence, budget-wiring assertion, fake clocks), the no-seam constraint, and the file-granularity Amdahl argument that makes the monolith test split a wall-clock fix, not just navigation. * fix: fallow findings on the slow-test gate — import edge, factory reporter, unit tests The string-path reporter wiring read as a dead file (fallow cannot see vitest's reporter loading); the config now imports the factory, making the edge real and type-checked. The class shape tripped the unused-class-members rule (framework callbacks are invisible to reference analysis) — converted to a factory returning the Reporter object, with the classification and rendering logic extracted as pure exported functions. Those functions now carry their own unit tests (budget bands, integration budgets, pin matching, warn-vs-fail rendering), which also grounds the CRAP estimate in real references. Canary re-verified: unpinned 5.2s sleeper fails the run with exit 1; clean runs exit 0. |
||
|
|
cccd34fb27 |
docs: refocus AGENTS.md on principles and enforcement gates (#1097)
* docs: refocus AGENTS.md on principles and gates; index ADRs; extend CONTEXT.md vocabulary AGENTS.md: replace the routing/command-family prose maps (already drifting from the code) with pointers to the self-describing, parity-tested registries; add the two sections agents actually cannot rediscover cheaply — Principles (one line per incident-backed lesson) and Enforcement gates (the classify-don't-suppress index); extend the module-size guidance from raw LOC caps to answer-one-question files, 1:1 test topology mirroring (removing the integration-aggregation exemption that produced 3,400-line test files), sibling fixture modules, claim collocation, and boundary-only barrels; record the dev-loop staleness triple (dist/daemon/adopted-runner), the tsgo typecheck, the Gatekeeper first-node-exec stall, the DEVICE_IN_USE signature, and the contention-flake protocol; append the two gate steps to the new-flag checklist. CONTEXT.md: vocabulary for the ADR 0011 domain (dispatch path, guarantee cell, owned waiver, parity table, coverage manifest, delegation-on-error, ref generation pin) and an architecture paragraph positioning ADR 0011 as ADR 0008's interaction-semantics counterpart. docs/adr: flip 0011 to Accepted (implemented through Layer 3) and add a read-this-when index that names the registries as the living source of truth over ADR prose. * docs: defer versioned-ref references to the implementing PR Review sequencing note on #1097: these lines described #1096 behavior not yet on main. They move to #1096's branch so docs land with the implementation and the two PRs merge in any order. |
||
|
|
a6c254939b |
docs: record that the native-ref path is web-only in production (#1095)
tapTarget/fillTarget are wired solely to the web provider's clickRef (interaction-runtime.ts); apple/android backends never define them. Found while designing the #1088 retirement experiment, which this dissolves: the hypothesized iOS runner round trip does not exist. |
||
|
|
6ee483c33f |
feat: warn when @refs outlive the session snapshot they came from (#1093)
Refs are positional indexes into the latest stored session tree; any selector-based command's resolution capture silently reshuffles them (#1076). Track an honest marker on the daemon session (snapshotRefsStale): set wherever the stored snapshot is replaced by a response that does not hand the new refs to the client, cleared only where the client demonstrably receives them (snapshot responses, find ref outputs). Commands consuming @ref arguments while the marker is set (press/click/longpress/fill/get/wait) attach a warning and still execute — the geometric guards keep catching detectable drift. Closes #1076 |
||
|
|
ad754ac0a4 |
feat: direct iOS selector falls back to tree resolution on semantic failures (ADR 0011) (#1091)
* feat: direct iOS selector falls back to tree resolution on semantic failures (ADR 0011) Runner ELEMENT_NOT_FOUND/AMBIGUOUS_MATCH on the direct iOS selector dispatch now delegate to the tree-based runtime path, which supplies runtime disambiguation, occlusion refusal, non-hittable promotion/annotation, and rich selector diagnostics/hints. Maestro replay dispatches (allowNonHittableCoordinateFallback) keep the runner-native error shapes: the new delegateSemanticFailures option is threaded from the dispatch flag, and the query path (allowElementNotFound at selector-runtime) keeps its exact semantics. Registry: direct-ios-selector errorTaxonomy/nonHittable/occlusion flip from gap waivers to delegated cells; the pinned gap list shrinks 6 -> 3. Success-path parity cells (disambiguation, responseIdentity) intentionally remain gaps per ADR 0011. Refs #1081 * test: contract coverage for the three newly-delegated direct-iOS cells The coverage gate demanded scenarios the moment the rebase flipped errorTaxonomy/nonHittable/occlusion to delegated — as designed. Three transcript scenarios prove the delegation end to end: runner ELEMENT_NOT_FOUND falls back to runtime no-match diagnostics, to the covered refusal, and to an annotated coordinate tap (the transcript asserts the fallback tap is coordinate-keyed). |
||
|
|
d167eaf0f3 |
ci: drop duplicate unit-test run, cache Size base dist, typecheck with tsgo (#1094)
Three measured dev-loop/CI cuts, no signal loss: - typecheck now runs tsgo (already trusted for declaration emit by the tsdown build): 21.7s -> 5.3s locally, and check:tooling drops to ~18s total. tsc stays available as typecheck:tsc; verified tsgo fails on type errors and respects noUnusedLocals. - remove the Unit Tests CI job: Coverage runs the same unit + provider-integration suites under coverage thresholds, so the job reran ~64s of tests every PR for no extra signal. - Size workflow: skip docs-only paths (same paths-ignore as CI) and cache the base commit's dist keyed on base SHA, since dist is fully determined by that commit. Startup medians are still measured fresh on the same runner so the base/PR startup comparison stays same-machine; the cache is saved immediately after the base measurement so the PR build never poisons it. Claude-Session: https://claude.ai/code/session_01FqeW8sA2ZnvnftdvpCqFMS Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
f3e07ff236 |
test: interaction contract suite with registry-driven coverage gate (ADR 0011 Layer 3) (#1092)
* fix: preserve the runner's non-hittable fallback marker through direct selector press
The direct iOS selector press handler spread successText('Tapped <selector>')
after the runner payload, clobbering the 'tapped via non-hittable coordinate
fallback' message that directIosSelectorFallbackDetails keys on — so
maestroNonHittableCoordinateFallbackUsed could never be true end-to-end (the
existing unit test passes because it mocks dispatchCommand above this layer).
Found by the ADR 0011 Layer-3 maestro-fallback contract scenario.
Share the marker string as MAESTRO_NON_HITTABLE_FALLBACK_MESSAGE and keep it
as the success message when the runner reports fallback usage.
* test: interaction contract suite with registry-driven coverage gate (ADR 0011 Layer 3)
test/integration/interaction-contract/ holds one scenario file per dispatch
path, each with a sibling .coverage.ts manifest declaring which guarantee
matrix cells it proves (scenario strings double as the vitest test titles).
index.ts aggregates the manifests statically, and a new Layer-3 gate in
src/contracts/__tests__/interaction-contract-coverage.test.ts fails when any
enforced (runtime/runner/delegated) cell lacks a scenario or when a scenario
claims a waived/inapplicable cell — coverage of the matrix is by
construction in both directions.
Path forcing is natural (no test-only env switch needed): selector/ref
targets take the runtime path, a tapTarget backend takes the native-ref fast
path, x/y takes the coordinate path, and simple-selector clicks on an iOS
provider transcript take the direct runner path (the transcript itself
proves which path ran via assertComplete).
Fixtures are the permanent Bluesky shapes: closed drawer, drawer + visible
twin, edge-grazing container, covered button, non-hittable cell.
Refs #1081
|
||
|
|
03b666bb49 |
feat: actionable hint for adb timeouts (#1090)
A timed-out adb invocation has no stderr for the failure classifier to match, so it fell through to the generic retry hint — retrying a wedged adb server times out identically. Classify exec-layer timeouts on the structured timeoutMs detail (the signal createTimeoutError sets) as a 'timeout' family with a kill-server/start-server hint, winning over untrustworthy partial stderr and losing to site hints as before. Doctor's android inventory sweep already routes adb failures through attachAdbFailureHint via listAndroidDeviceEntries, so the device-android check now surfaces the wedged-server hint; a regression test locks that path. Closes #1079 |
||
|
|
e031a5cf97 |
chore: delete dead tap-by-text runner arm and settledAfterMs; hint on tiny stable trees (#1089)
- Remove the runner .tap text arm: the daemon only ever sends selectorKey/selectorValue or x/y taps (src/platforms/apple/interactions.ts), and daemon+runner ship in lockstep. findElement(app:text:) stays for the findText probe. - Drop settledAfterMs from the wait stable result: it always equaled waitedMs and never shipped in a release (v0.18.3 predates #1059). - Add a loading hint when wait stable settles on a tree with fewer than 5 nodes (#1078): stability on a nearly-empty tree is a weak readiness signal. Refs #1078 |
||
|
|
cd0cd16a9e |
build: migrate the library build from rslib to tsdown (Rolldown) (#1087)
* build: migrate the library build from rslib to tsdown (Rolldown) Replace the Rspack-based rslib build with tsdown, the Rolldown-based library bundler from the Vite toolchain family, so bundling, testing (Vitest/Vite), linting (oxlint), and formatting (oxfmt) all run on the same OXC/Rolldown stack. Outcome vs the rslib baseline (size-report): - build time: ~53s -> ~2s - JS raw +16.2 kB (+1.1%), JS gzip +2.7 kB (+0.6%) - the residual gap is OXC vs SWC minifier tightness, not chunking - npm tarball -3.0 kB - CLI --version startup ~3 ms faster; --help within the +/-5 ms measurement noise of interleaved A/B runs Chunk-merging experiments (single shared group, entries-aware groups, small-module groups) all regressed either total size or --help startup (a merged shared chunk adds +140 ms), so the default Rolldown split graph is kept. Custom codeSplitting groups also currently trip a rolldown-plugin-dts bug that re-emits type-only imports as runtime imports. Declarations still bundle per entry via tsgo; dist layout, entry names, and the internal/ worker/daemon entry resolution contract are unchanged. @microsoft/api-extractor was only consumed by rslib dts bundling and is removed together with @rslib/core. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FqeW8sA2ZnvnftdvpCqFMS * ci: only cache the pnpm store when setup installs dependencies The layering-guard job uses setup-node-pnpm with install-deps: false, so it never creates a pnpm store. setup-node's post-job cache save then fails with a path validation error whenever the lockfile hash misses the cache - which any lockfile-changing PR does. Gate the cache on install-deps so no-install jobs skip pnpm store caching entirely. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FqeW8sA2ZnvnftdvpCqFMS --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
415f599c3b |
feat: golden tap-point parity table for TS and the iOS runner (ADR 0011) (#1086)
ADR 0011 Layer 2: the offscreen tap-point rule now has exactly one home per side of the wire, proven equivalent by a shared golden fixture table. - contracts/fixtures/tap-point-policy.json: 12 cases including this week's real bug shapes (closed drawer fully left, 0.07pt edge-grazing container) plus edge-inclusive and empty-frame fail-open semantics. - Swift: pure TapPointPolicy.isAllowed(elementFrame:windowFrame:) extracted; the ELEMENT_OFFSCREEN guard (onScreenWindowFrame stays the frame getter) and hasTappableFrame (Maestro fallback) both delegate the decision to it. Gated XCTest asserts every table case (runs in the existing AGENT_DEVICE_RUNNER_UNIT_TESTS surface CI already compiles). - TS: isTapPointInsideViewport extracted from isNodeVisibleOnScreen (no duplicated math); vitest parity test asserts the same table. - Registry: parityTable wired on direct-ios-selector/offscreen and maestro-non-hittable-fallback/offscreen. |
||
|
|
248e08afd4 |
feat: warn on out-of-viewport coordinate taps (ADR 0011) (#1085)
* feat: warn on out-of-viewport coordinate taps (ADR 0011) Implements the coordinate/offscreen guarantee: when a point tap target falls outside the last-known viewport, attach a warning to the result instead of silently forwarding the tap. This is a WARNING not an error, maintaining the escape-hatch semantics of coordinate taps. The warning is generated at the runtime layer in resolvePointInteractionTarget using the session's last snapshot to infer viewport bounds via resolveViewportRect. No snapshot is captured (by design), so the implementation reuses the session's existing snapshot state. When no snapshot is available, no warning is emitted. Changes: - Add `warning?: string` field to PressCommandResult and LongPressCommandResult types (FillCommandResult already had it for --verify evidence) - Implement tryResolveOutOfBoundsPointWarning in resolution.ts and export it for registry verification - Update interaction-guarantees.ts to mark coordinate/offscreen as runtime-enforced - Remove coordinate/offscreen from the pinned gap list in the test registry - Add tests: out-of-bounds point with snapshot → warning; in-bounds → no warning; no snapshot → no warning - Carry the warning field through daemon handlers for press/click/longpress Closes #1081 (coordinate/offscreen gap). * refactor: drop suppressed re-export; registry points at the enforcement carrier The registry via now references resolveInteractionTarget (the exported carrier, same precedent as the verifyEvidence cell) instead of forcing a suppressed unused-export of the internal helper. |
||
|
|
b25ef7b024 |
refactor: declare command timeout policy on descriptors (ADR 0011) (#1084)
The wait timeout bug (#1075) happened because request-envelope budgets and on-timeout daemon policy lived in two hand-maintained lists in the daemon client: isExplicitTimeoutCommand (daemon-client.ts) and DAEMON_PRESERVING_TIMEOUT_COMMANDS / shouldResetDaemonAfterRequestTimeout (daemon-client-timeout.ts). A command could fall through both without anyone noticing. Both lists are deleted. Each command descriptor (ADR 0008 registry) now declares a required timeoutPolicy: timeoutPolicy: { budget: { source: 'none' | 'flag' | 'positional-parser'; parser? }; envelopeMs: number | 'unbounded'; onTimeout: 'preserve-daemon' | 'reset-daemon'; } The client derives the request envelope and the on-timeout daemon policy from the declaration; the +30s margin / never-shrink rule for positional budgets is preserved generically. Envelope constants move from src/daemon/request-timeouts.ts to src/core/command-descriptor/timeout-policy.ts next to the policies they parameterize. A completeness gate (timeout-policy.test.ts) asserts every public command declares a policy and pins the deviating sets (preserve-daemon = snapshot/ wait/find; flag budget = prepare/replay/snapshot; positional = wait; envelopes = prepare 240s, install-like 180s, test unbounded) as bounded diffable lists. The pre-existing oracle tests in src/utils/__tests__/daemon-client.test.ts pass byte-for-byte unchanged, proving the migration is behaviorally exact. |
||
|
|
f7dd42234e |
refactor: single construction site for interaction responses (ADR 0011 Layer 2) (#1083)
Every press/click/fill/longpress dispatch branch now builds its result/ responseData payloads through buildInteractionResponseData (interaction-touch-response.ts), so identity extras (ref/refLabel/ selectorChain/targetHittable/hint/evidence) are composed in exactly one place. Hand-rolled per-branch assembly is the class of bug that dropped fill @ref evidence (#1064 review); a guard test now fails any touch handler that assembles a responseData literal outside the builder. Wire shapes are byte-identical: the fill @ref backendResult+identity shape is preserved behind refBackendWireShape, and the daemon + integration suites pass unchanged. Registry: all six responseConstruction cells flip from gap waivers to the shared runtime cell; the pinned gap list shrinks accordingly. Refs #1081, ADR 0011. |
||
|
|
b9fb4db6df |
feat: preflight native-ref fast path with shared interaction guards (ADR 0011) (#1082)
click @ref / fill @ref dispatch straight to backend.tapTarget/fillTarget, and a backend fast path can silently "succeed" — delegation-on-error never triggers there. The ref came from the stored session snapshot, so the node is already in hand: preflightNativeRefInteraction runs the SAME shared guards the runtime path uses against that node before the backend call. - occlusion: isSnapshotNodeInteractionBlocked via assertInteractionNotBlocked (same covered-element error shape as the runtime path) - offscreen: isNodeVisibleOnScreen via assertVisibleRefTarget (same offscreen_ref error shape) - nonHittable: annotation only — targetHittable/hint attach to the result; promotion stays a runtime-path behavior and the tapped element never changes No snapshot in the session, an unresolvable ref, or a missing/unusable rect make the preflight a no-op: it never adds a capture round trip. Registry: the three native-ref gap cells flip to kind 'runtime' and leave the pinned gap list. Refs #1081 |
||
|
|
c506ddf3e7 |
RFC: ADR 0011 — interaction guarantee contract (path × guarantee matrix) (#1080)
* docs+feat: ADR 0011 interaction guarantee contract, Layer-1 registry and gate Design for making interaction guarantees hold across every dispatch path (runtime selector/ref, direct iOS selector, native ref, coordinate, maestro fallback) instead of eroding at path boundaries one incident at a time — every interaction bug this week was a (path, guarantee) cell nobody was watching. Three layers (ADR 0011): declare the path x guarantee matrix as a typed registry whose completeness is a compile error; share one implementation per rule on both sides of the wire with golden fixture tables proving TS/Swift parity; prove every non-waived cell with contract scenarios generated from the registry. This lands Layer 1: the registry with an HONEST initial classification — ten cells are acknowledged gap waivers (direct-path disambiguation/ occlusion/nonHittable/responseFields/errorTaxonomy, native-ref guards, coordinate bounds) — plus the gate test that keeps entries truthful: referenced TS symbols must be exported, runner symbols must exist in the Swift sources, delegations must land on paths that actually enforce the guarantee, and the gap list is pinned so it can only change explicitly in a reviewed diff. * refactor: apply ADR 0011 design review - Frame Layer 1 as an honesty/completeness gate, not a truth gate: it proves every path declared a stance and referenced symbols exist; behavioral parity starts with the Layer-2/3 fixture and scenario work. - Split responseFields into responseConstruction (one shared response construction site — a single Layer-2 refactor) and responseIdentity (which identity fields a path can provide — per-path capability work); note the anticipated errorTaxonomy split (codes vs diagnostics). - Encode the hybrid gap-closure strategy: runner-side parity for geometry-local rules, delegation-on-error for semantic failures (with the explicit caveat that delegation-on-error is NOT success-path parity), and a shared runtime preflight for native-ref where a silent backend success means delegation never triggers. - Gap waivers now require a trackingIssue (gate-enforced URL); all 16 pinned gaps link the umbrella issue #1081. The honest reclassification grew the pin list from 10 to 16 — responseConstruction is a gap on every path including runtime ones, which is exactly the partial progress the coarser guarantee was hiding. - Align ADR wording with the code: parityTable is optional until Layer 3, required once a runner cell claims parity. * fix: address registry review — maestro disambiguation honesty, command-scoped verify 1. maestro-non-hittable-fallback/disambiguation was overclaimed: the guarantee is defined as visible-first/deepest/smallest ranking, but findElement only implements unique-or-ambiguous scanning. Reclassified as an intentional waiver (deliberate Maestro-semantics divergence), mirroring how the direct path keeps its success-path parity gap. 2. verifyEvidence was claimed path-wide on paths that dispatch longpress, which has no --verify. Cells can now be command-scoped via appliesTo (non-empty strict subset of the path's commands, gate-enforced), and the three affected cells scope to press/click/fill. |
||
|
|
07181acc0b |
fix: honor wait budgets, preserve the daemon on polling timeouts, refuse off-screen selector taps (#1075)
* fix: agent-UX fixes from Bluesky dogfooding
Four fixes found by driving the Bluesky dev build end to end as an agent:
1. wait honors its user-supplied budget in the daemon request envelope.
The wait timeout travels as a positional, so the client never extended
the request timeout: any 'wait ... 180000' died at the 90s default.
resolveDaemonRequestTimeoutMs now parses wait positionals (shared
src/core parser) and extends the envelope to budget + 30s margin.
2. wait/find timeouts no longer reset the daemon. They are repeated
snapshot captures, sharing snapshot's stalled-bridge failure mode, but
sat on the daemon-reset path — one timed-out wait destroyed every
session the daemon owned (observed live: wait timeout -> runner
killed -> SESSION_NOT_FOUND on the next command).
3. Off-screen selector targets are refused instead of silently tapped.
'tap label=Explore' against Bluesky's closed drawer reported success
while tapping (-161, 265) — coordinates that cannot land:
- selector disambiguation now prefers candidates whose CENTER lies in
the root viewport (isNodeVisibleOnScreen; edge-grazing containers
poke fractions of a pixel into the viewport and must not count),
so 'press label=Profile' picks the visible tab over the drawer item;
- selector-resolved interactions get the same off-screen guard refs
already had (reason: offscreen_selector, with a scroll/open hint);
- the runner's direct selector tap refuses matches whose center is
outside the main window frame (app.frame unions transformed
subtrees, so a closed drawer inflates it) with ELEMENT_OFFSCREEN,
which falls back to the tree-based path.
4. The non-hittable 'may have had no visible effect' hint is dropped when
--verify evidence proves the interactive tree changed — the warning
sat directly next to data contradicting it.
Live-verified against Bluesky on the iPhone 17 Pro simulator: offscreen
tap -> offscreen_selector error; ambiguous Profile -> on-screen tab
(355, 817); 100s wait survives the old 90s envelope with the session
intact.
* refactor: dedupe resolved-target tails and off-screen guards, decompose disambiguation
Fallow follow-up on the dogfood fixes: extract the shared ref/selector
resolved-node tail (describeResolvedInteractionNode), the shared
off-screen check-and-throw skeleton (throwIfOffscreenInteractionTarget —
per-caller messages/hints preserved), and the candidate-accumulation
step out of analyzeSelectorMatches. No behavior change; suites and the
fallow gate are green.
|
||
|
|
c5f7b7baa4 |
refactor: harden exec failure wrapping end to end (#1074)
* refactor: harden exec failure wrapping end to end Follow-up to #1072, closing the structural gaps that let the missing-processExitError bug class exist: - requireExecSuccess(result, message, extra?) in utils/exec.ts: guards an allowFailure result and throws the curated COMMAND_FAILED (flag set via execFailureDetails) itself. Result-side by design — tool providers and executor overrides return results without throwing, so an ExecOptions knob interpreted at spawn time would silently not fire on those paths. ~30 pure guard-and-throw sites across apple/android/web converted; sites with tolerance branches, cleanup, or exit-0 reachability keep their explicit shape. - Source-scan guard (src/__tests__/exec-wrap-guard.test.ts): fails when an AppError('COMMAND_FAILED', ...) details literal rebuilds the stdout/stderr/exitCode trio inline without the helper; intentional holdouts opt out with a documented exec-guard-allow comment. The guard immediately found three wrap sites the July audit missed (runtime-hints run-as probe, device-ready not_ready branch, perf export table) — now converted. - coerceExecResult at the provider boundaries (executor overrides, apple tool provider scope, android adb provider scope): SDK-supplied callbacks cross an unchecked boundary; coercing once there replaces the per-site String(result.stdout ?? '') defensiveness, which is removed. - normalizeError stderr excerpts now strip noise prefixes (adb:/xcrun:/ simctl:/error:) before rendering; skip/strip lists stay in the kernel deliberately (platforms cannot hook normalizeError) with a comment marking the registry escape hatch if they grow. - AppErrorDetails exported and documented in kernel/errors.ts: the magic keys (hint, processExitError, retriable, reason, diagnosticId, logPath, stdout/stderr/exitCode) now carry types and doc comments. - runIosDevicectl gains tolerateOutput; the devicectl uninstall path in app-install.ts reuses it instead of duplicating the wrap + hint logic (its failure hint now falls back to the devicectl default hint). * fix: reconcile exec hardening with the adb failure classifier Main landed the central adb failure classifier (androidAdbResultError + withAdbFailureHintProvider) while this branch was in flight. Resolution: - Android call sites keep main's androidAdbResultError form — it composes execFailureDetails with the classified adb hint, which is strictly richer than a plain requireExecSuccess conversion for adb invocations. requireExecSuccess remains the shape for non-adb tools and apple/web. - Provider result coercion folds into withAdbFailureHintProvider's enrichment pass (exec/pull/install), so the existing WeakSet memo also prevents coercer stacking. - The reverse-remove wrap in adb-executor now uses androidAdbResultError instead of hand-rolling the trio (flagged by the exec-wrap guard). - Guard-test scan split into helpers (fallow cyclomatic threshold) and the perf artifact-tail clone carries fallow-ignore markers on both platforms, matching the pre-existing marker on the apple side. - Two expectations updated for the stderr noise-prefix strip: classifier compose test and perf sampling reason ('device offline', no 'error:'). |
||
|
|
9aae457533 |
fix(errors): close call-site and consumer gaps around the central error system (#1071)
* fix(errors): close call-site and consumer gaps around the central error system Audit + iOS/Android dogfood findings (see docs/adr/0010-error-system.md): - press/click/fill targets that parse as neither @ref, selector, nor point now fail with INVALID_ARGS grammar guidance (incl. unquoted multi-word selector values) instead of UNKNOWN 'Expected x to be a finite number' - daemon command-input validation throws AppError INVALID_ARGS instead of bare Error surfacing as UNKNOWN - selector-no-match and stale-ref failures carry targeted hints (selectorFailureHint / STALE_REF_HINT) - retriable/supportedOn survive wire rehydration to CLI --json and SDK (previously dropped at throwDaemonError / toDaemonHttpRpcError) - MCP tool errors carry code + hint instead of message-only text - lease busy/capacity use DEVICE_IN_USE (the retriable code) - asAppError(err, fallbackCode) replaces cause-dropping coercions in the Apple runner; new default hints for AMBIGUOUS_MATCH, DEVICE_IN_USE, UNSUPPORTED_PLATFORM, and a distinct UNKNOWN hint - ADR 0010 documents the error-system conventions * fix: format touched files and clear fallow audit gate - privatize SELECTOR_NO_MATCH_HINT / SELECTOR_NOT_UNIQUE_HINT (consumed only via selectorFailureHint in the same module) and integerSchema (only used inside command-input.ts) - dedupe the resolved-node return tail in interaction resolution into describeResolvedNode - extract stringDetail/booleanDetail readers so normalizeError stays under the complexity threshold * fix: reject unquoted trailing text after interaction selectors press/click/longpress positionals like 'press text=Gesture lab' used to silently drop the leftover tokens and act on the truncated selector (text=Gesture), potentially hitting the wrong element. Reject non-empty splitSelectorFromArgs rest with INVALID_ARGS guidance that suggests the merged quoted form (text="Gesture lab"). Fill keeps consuming rest as its text payload; wait/is/replay-heal already handle rest explicitly. |
||
|
|
67703ed863 |
feat: classify adb failures with actionable hints at the executor level (#1067)
* feat: classify adb failures with actionable hints at the executor level Android COMMAND_FAILED errors surfaced raw adb stderr with only the generic retry-with-debug hint; the sole stderr classification lived in snapshot.ts and was retry-only. Add a central classifier in adb-executor.ts that recognizes the common adb failure families (device offline/unauthorized/not found, more than one device, no devices, server version mismatch, connection drops, INSTALL_FAILED_* variants) and attaches the resolved hint — plus retriable for clearly transient families — to the AppError details in both executor funnels (local serial exec and provider-scoped exec), so every adb call site benefits without per-site changes. The adb devices -l discovery call is wrapped too, where server-mismatch/no-devices failures actually surface. snapshot.ts now consumes the shared classifier for its retry decision, keeping only its snapshot-specific dump-race patterns. normalizeError lifts details.retriable to the top-level error (mirroring the hint lift) and the daemon typed-error graft lets a throw-site classification win over the code-level policy. Also fix the port-reverse removal path throwing a bare Error that surfaced as UNKNOWN. Update session-appstate-input-perf.test.ts: its Android sampling-failure fixture uses stderr "error: device offline", which the classifier now recognizes, so error.hint carries the actionable adb-reconnect hint instead of the generic "retry with --debug". The result shape (hint string, details.metric/package, code) is unchanged — only the hint text improved, which is this change's intent — so the test expectation moves to the new hint rather than reverting production. * feat: classify tolerated adb failures and semantic provider methods Review follow-up closing two classifier coverage gaps: 1. allowFailure paths bypassed the classifier: the executor wrapper only enriches THROWN errors, but many Android flows run adb with allowFailure, inspect the nonzero result, and throw a fresh AppError built from its stdout/stderr — so e.g. "device offline" during uninstall still surfaced the generic hint. There was no common construction point for these errors, so androidAdbResultError in adb-executor.ts now IS that point: it builds the COMMAND_FAILED error from a tolerated result and runs it through the classifier, keeping hint strings central. The result-inspecting throw sites (app launch/uninstall, appearance read, keyboard/clipboard queries, package listing, logcat capture, helper APK installs, heap-dump pull, uiautomator dump) route through it; a site hint (heap-dump pull) still wins because attachAdbFailureHint never overwrites. Composes with #1072 rather than replacing it: androidAdbResultError builds its details via execFailureDetails for nonzero exits, so normalizeError still suffixes the curated message with the first stderr line while the classified hint/retriable/adbFailure ride along. Semantic failures thrown at exit 0 (the am start error-on-stdout path) stay unflagged, matching #1072's deliberate exit-0 exclusion. 2. Provider-scoped enrichment only wrapped exec, but the transfer helpers prefer the semantic provider methods, so a provider whose install rejects with INSTALL_FAILED output kept the generic hint. Enrichment now lives in normalizeAndroidAdbProvider — the single funnel every provider passes through (scope installation and explicitly passed providers alike) — wrapping exec plus pull/install/installBundle; a WeakSet keeps repeated normalization from stacking wrappers. The local provider needs no wrap since its methods delegate to the already enriched serial executor. Tests: androidAdbResultError classification, hint-plus-excerpt composition through normalizeError, exit-0 no-excerpt guard, site-hint precedence, and provider install/pull classification in adb-executor.test.ts; an allowFailure regression on a real production path (keyboard state query with a nonzero device-offline result) in device-input-state.test.ts. |
||
|
|
9407839f6e |
feat: tag daemon artifacts with semantic types (#1066)
* feat: tag daemon artifacts with semantic types * Normalize artifact type threading through daemon tracking * Add timeout error to http server artifact test helper * test: restore artifact wait helper behavior * test: fail artifact wait helper on timeout * refactor: model artifactType as optional on wire shapes Applies the design-review tweak: producer-owned APIs (reserveOutput, trackDownloadableArtifact, finalization callbacks) keep the required 'DaemonArtifactType | undefined' form so artifact owners must explicitly decide, while public/wire/result shapes (DaemonArtifact, both artifact inventory entry types) become 'artifactType?:' — missing metadata is valid, JSON drops undefined, and remote/older daemons may omit the field. Construction sites now omit the key for untyped artifacts, and the finalization test asserts key absence (toEqual cannot distinguish absent from explicitly-undefined). --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> |
||
|
|
664b713130 |
feat(interaction): opt-in --verify evidence for press/click/fill (#1064)
* feat(interaction): opt-in --verify evidence for press/click/fill (#1047) Adds an opt-in --verify flag that returns cheap post-action evidence (foregroundApp, nodeCount, interactiveNodeCount, digest, changedFromBefore) instead of requiring a full follow-up snapshot to confirm a mutating command had an effect. The digest hashes the (type, label, identifier) multiset of an interactive-only capture, order-independent so it doesn't flip on harmless re-ordering; the node tree is never serialized back to the client, only the digest and counts. Default behavior (no --verify) is byte-identical to today. Implements the approved design from the #1047 issue comment: - src/utils/ax-digest.ts: new standalone digest module. - Pre-action digest reuses the snapshot the resolution path already captures for ref/selector targets (zero extra cost); point targets opt into one extra baseline capture only when --verify is set. - Post-action: one interactive-only capture through the same capture helper, digested and discarded. - --verify threaded through the CLI flag schema, MCP input schema, interactionResultExtra allowlist, and MCP output schemas, following the same plumbing as --double-tap and the #1040 targetHittable precedent. - The native-ref/direct-iOS-selector fast paths are skipped when --verify is set, since they bypass the resolution/capture path evidence depends on. * test: cover press --verify with a provider scenario; classify the flag CI's architecture-progress gate failed on 1 unclassified public flag. --verify drives real device captures, so it belongs in the device-observable list backed by an actual provider scenario rather than the intentionally-outside bucket: the new scenario asserts evidence (changedFromBefore, digest, nodeCount) on press @ref --verify, that the verify capture's tree is never serialized into the response, and that the transcript completes (snapshot -> tap -> verify snapshot; the @ref path reuses the session snapshot as its baseline, so no extra resolution capture entry exists). * fix: type the verify scenario transcript entries * fix: include interaction extras in the fill @ref response branch The ref branch of dispatchFillViaRuntime rebuilt responseData from backendResult/coordinates, dropping interactionResultExtra(result) — so fill @ref --verify returned no evidence even though the post-action capture ran (live E2E gap found in PR #1064 review). Spreading the extras also gives fill @ref the same ref/refLabel/selectorChain (and conditional targetHittable/hint) fields press @ref already returns. Adds daemon tests for fill @ref --verify evidence and for the no-verify path staying evidence-free with no post-action capture. |
||
|
|
4711bc434b |
fix: report expired daemon resources as expiry, not INVALID_ARGS (#1069)
* fix: report expired daemon resources as expiry, not INVALID_ARGS TTL/GC'd registry entries (downloadable/uploaded artifacts, resumable uploads, materialized paths) surfaced as INVALID_ARGS, so agents got "Check command arguments and run --help" when the resource had simply expired. Add a shared requireTenantOwnedEntry helper that reports these as COMMAND_FAILED with reason RESOURCE_EXPIRED and a per-resource recovery hint, and dedupes the paired wrong-tenant UNAUTHORIZED check. Also: give request cancellation an explicit "canceled intentionally" hint instead of the misleading retry default; replace the ad-hoc INTERNAL_ERROR code (not in the KnownAppErrorCode union) with UNKNOWN plus a report-a-bug hint; and fix materialized-path TTL expiry of tenant-owned entries, which threw UNAUTHORIZED in a void-ed promise (unhandled rejection) and leaked the entry and its temp dir. * fix: report expired resumable upload tickets as expiry, not INVALID_ARGS Route requireResumableUpload() through the shared requireTenantOwnedEntry helper so a missing/expired upload ticket on PUT /upload/direct/:id and POST /upload/finalize returns the COMMAND_FAILED + RESOURCE_EXPIRED shape (with a recovery hint) instead of the misleading INVALID_ARGS/400. Genuinely invalid input (malformed preflight args, bad content-range, incomplete upload, hash mismatch) stays INVALID_ARGS. |
||
|
|
ab55db0b83 |
feat: add wait stable settle primitive (#1059)
* feat: add wait stable settle primitive Add `agent-device wait stable [quietMs] [timeoutMs]` (defaults 500/10000) as a new target kind on the existing wait command/plumbing: it polls the interactive-only tree via the existing selector-snapshot backend at the existing wait poll cadence, and resolves once two or more consecutive captures are unchanged for quietMs, comparing a small local per-node digest (type, label, identifier, rounded rect). Timeout throws the standard wait COMMAND_FAILED shape with capture stats (captures, nodeCount) attached, and the wait-current-surface enrichment now also recognizes the stable timeout message. Successful results add settledAfterMs/captures/nodeCount alongside waitedMs. This rides the same CLI grammar -> command catalog -> daemon handler -> runtime poll loop as `wait text`/`wait selector`; no new runner protocol. Polling snapshots intentionally do not update the session cache, since the interactive-only tree is a settle signal and would otherwise degrade subsequent ref/get/find lookups in the same session. Documents wait stable in `help workflow` as the replacement for guessed sleeps between actions and snapshots in the core loop. Closes #1049 * fix: bound wait stable by the user timeout when a capture stalls The stable loop only checked the deadline between captures, so a stalled backend capture (observed with macOS AX captures) rode the wait into the 90s daemon request timeout instead of the user-supplied timeoutMs. - Race each settle capture against the remaining wait budget with a real timer (test clocks advance synthetic time and cannot represent a hung backend call) and abandon the capture when the deadline wins, throwing wait_stable_timeout with captureStalled: true and a hint. - Skip the current-surface enrichment on the stalled path: it fires another capture, which is just as slow or hung and pushed the response ~8s further past the timeout on macOS. Verified live against macOS System Settings: cold-runner stall now exits in 10.1s for a 10s timeout (previously ~90s); warm settle unchanged. |
||
|
|
1c95bd319b |
fix: accept selector-first is form and print find action confirmations (#1068)
* fix: accept selector-first is form and confirm find actions is <selector> <predicate> failed with a misleading predicate error because every entry point read positionals[0] as the predicate, and the trailing predicate token could not be recovered later: visible/hidden/editable/selected double as selector boolean keys, so greedy selector parsing swallowed it. Normalize both argument orders into the canonical predicate-first shape at the CLI reader and splitIsSelectorArgs, and explain the key/predicate collision in the error hint when nothing parses. find click succeeded silently: the delegated click response (with its 'Tapped @ref (x, y)' message) was discarded, and the find formatter printed fill's raw text field instead of its success message. Carry the success message through find click and print message-first, so find click/fill/focus/ type confirm exactly like their direct counterparts. * style: format cli-grammar test |
||
|
|
83a1eec986 |
fix: surface typed error codes for metro, MCP, and batch validation failures (#1070)
Bare `throw new Error(...)` surfaces to users as code UNKNOWN with a generic hint. Convert the remaining user-reachable clusters from the July 2026 error audit to AppError with the right code, preserving messages: - metro/client-metro.ts: timeouts, bridge failures, and Metro start / not-ready errors are now COMMAND_FAILED (the not-ready error also carries logPath in details); createMetroBridgeRequestError builds on AppError so non-retryable bridge errors that escape via rethrow get the right code too - mcp/command-tools.ts + mcp/router.ts: MCP input validation is now INVALID_ARGS (JSON-RPC responses unchanged; the router only reads .message) - commands/batch/metadata.ts: step validation is now INVALID_ARGS; with the plain-Error opt-out gone, the now-unused BatchStepErrorFactory injection seam is removed from batch-contract.ts - cloud-webdriver/webdriver-source.ts: xml.ts parser failures are wrapped at the consumer boundary into a single COMMAND_FAILED naming what was being parsed |
||
|
|
1009086145 |
fix: make artifact inventory consumption deterministic on download (#1073)
Artifact downloads consumed the inventory entry only from the source stream's 'end' / response 'finish' handlers. But the response carries an explicit content-length, so a client's fetch completes as soon as that many body bytes arrive - which happens right after the final chunk is written to the socket, while the server's read stream still needs one more threadpool read to detect EOF and emit 'end'. Under CPU contention (e.g. the instrumented Coverage CI job) that threadpool completion can land after the client's follow-up GET /artifacts, which then still lists the consumed artifact. Consume the entry as soon as the stream has emitted content-length bytes, before pipe() writes the final chunk to the client. Any client that observed a complete download is now guaranteed to observe the entry as consumed. The 'end'/'finish'/'close' handlers stay as backstops (idempotent via didCleanupArtifact). |
||
|
|
50a6a3533a |
fix: surface stderr excerpts from hand-rolled exec failure wraps (#1072)
* fix: surface stderr excerpts from hand-rolled exec failure wraps
normalizeError only replaces a generic COMMAND_FAILED message with the
first meaningful stderr line when details.processExitError is true, but
only createExitError in utils/exec.ts set that flag. Hand-rolled wrap
sites (throw new AppError('COMMAND_FAILED', msg, {stdout, stderr,
exitCode}) after an allowFailure run) missed it, so users saw messages
like "xcrun exited with code 22" with a generic hint instead of the
actual stderr excerpt.
Add execFailureDetails(result, extra?) to utils/exec.ts returning the
stdout/stderr/exitCode spread plus processExitError: true (extras spread
after the base keys so call sites can keep truncation or String()
normalization), and convert ~42 exit-guarded wrap sites across the apple
platform (apps launch/terminate paths, devicectl, simulator boot, perf,
perf-xctrace, runner modules, dsym/symbolication, macOS host provider)
and android/web (app-helpers, logcat, devices, perf, settings,
snapshot helpers, app-lifecycle, device-input-state, multitouch-helper,
agent-browser provider).
Sites reachable at exit 0 (parse/stdout checks), failure aggregations,
timeout/forced-kill errors, and sites that already build richer messages
(macos helper JSON, runner-contract early-exit, maestro run-script) are
deliberately left unflagged so stderr enrichment never replaces a more
specific message.
* fix: append stderr excerpts to curated wrap messages instead of replacing them
Spreading processExitError to curated call sites made normalizeError
replace specific messages like 'uiautomator dump did not return XML'
with the bare stderr line, losing the operation context (caught by the
provider-failures integration test). Keep wholesale replacement only for
the information-free generic '<tool> exited with code N' wraps; curated
messages now keep their specific reason and gain the stderr excerpt as a
': <excerpt>' suffix (skipped when already present).
Separate commit rather than amend so the semantics change to
maybeEnrichCommandFailedMessage stays individually reviewable on the PR.
|
||
|
|
4a8a7bb895 |
feat(snapshot): dedup inherited labels/identifiers in output; promote diff snapshot (#1063)
* feat(snapshot): dedup inherited labels/identifiers in client output; promote diff snapshot Deep RN/AX trees repeat the same accessibility label across a whole parent chain (ScrollView -> Other -> Button -> inner Button), so agents pay for the same string 3-4x in one snapshot. When a node's label (or, separately, identifier) string-equals its nearest ancestor's, the CLI/JSON client output now omits it and sets an `inheritsLabel`/`inheritsIdentifier: true` marker instead; the human text renderer shows "[same label as parent]" in its place. This is output-only: it runs in snapshotCliOutput (src/commands/capture/output.ts), which feeds both the default text and --json presentations, right before they are handed to the caller. The in-daemon session tree that selector building (buildSelectorChainForNode), wait/is/get matching (selectors-match.ts), and replay/replay-heal read from is never touched, since none of those consumers go through this serializer -- they all read session.snapshot.nodes directly. --raw and non-default MCP responseLevels (e.g. digest, which can omit `nodes` entirely) bypass dedup and stay byte-for-byte. Also promotes `diff snapshot` as the named post-action verification step in the core-loop help text, replacing the generic "verify" step: it diffs the rendered snapshot lines against the previous snapshot in the session and prints only what changed, which is the actual token-cheap way to confirm a mutation today (discovery, not new behavior). * fix: dedup only labels long enough to pay for the marker Review finding: dedup fired on any string-equal duplicate, but the replacement markers cost ~21 chars in JSON and ~23 in text — short duplicated labels (tab bars: Home, Settings) made output BIGGER, not smaller. Only values of 24+ chars dedup now, which is net-positive on both surfaces and keeps short labels readable inline. Fixtures updated to threshold length; added a short-label regression test. |
||
|
|
e3566b638a |
perf: smaller minified output and a lighter eager CLI startup path (#1065)
* build: run SWC minifier compress with 3 passes Measured -9.7 kB raw / -7.0 kB npm tarball on the emitted JS at no runtime cost. splitChunks minSize tuning was also evaluated for the tiny-chunk overhead but measured as a byte-identical no-op (the sub-kB files are dynamic-import boundaries, not split products), so it is intentionally not included. * perf(cli): keep help text, replay reporting, and diff runtime off the eager command path Every CLI invocation eagerly parsed 599 kB of JS. Three static imports dragged in code that most commands never run: - cli.js -> cli-help.js (75 kB of help text) via the usage()/ usageForCommand() builders in parser/args.ts, which are only needed on help and usage-error paths. They now lazy-import cli-help.ts. - cli.ts/generic.ts -> replay/test/reporting.ts (~17 kB), only needed by the test command. Now imported at the call sites. - cli/commands/screenshot.ts -> createAgentDevice (68 kB client-side command runtime chunk incl. screenshot pixel diffing), only needed by the diff screenshot branch. Now imported inside diffCommand. Eager closure of cli.js drops 599 kB -> 431 kB (-28%), ~2.5 ms median module-load per command invocation. --help/--version fast paths in bin.ts are unchanged. Package size is unchanged by design (the code moves to lazy chunks; it does not disappear). |
||
|
|
e833de474d |
refactor: split oversized session and apple app files (#1061)
* refactor: split oversized session and apple app files * fix: satisfy ci after file split |
||
|
|
f98111ef7e |
feat(help): document behavioral guarantees in help workflow (#1051) (#1054)
Add a "Guarantees" section to `agent-device help workflow`: statements of fact for agents to reason from instead of probing behavior with trial commands. Covers selector ambiguity (folds/extends the existing #1040 paragraph instead of duplicating it), hittability/targetHittable, open idempotent-foreground vs --relaunch's single simctl launch --terminate-running-process call, close's runner retention policy plus the AGENT_DEVICE_IOS_RUNNER_IDLE_STOP_MS idle-stop window, ref lifetime (cleared by open/relaunch), diff snapshot's comparison baseline, and wait's polling model. Each statement is traceable to source, noted in the PR body. Adds three doc-assertion tests in cli-help.test.ts mirroring the #1040 pattern. |
||
|
|
2adf3d5535 | refactor: split runner xctestrun module (#1062) | ||
|
|
b92883e883 | fix: clarify prepare ios-runner timing overlap (#1058) | ||
|
|
db07c200f4 |
refactor: centralize known CLI command checks (#1057)
* refactor: centralize known cli command checks * feat: emit drift diagnostic for registered-but-unhandled commands Folds #1055's telemetry into this branch: known-command fall-through now emits cli_known_command_unhandled at error level alongside the distinct user-facing message, so catalog/dispatch drift is visible in diagnostics as well as to the user who hits it. |
||
|
|
b1d77c68af | docs: document conservative revisit triggers (#1056) | ||
|
|
5e8a90a924 | refactor: add shared ttl memo reset registry (#1060) | ||
|
|
016ce8d8a6 |
feat: accept tap as an alias of press (#1053)
* feat: accept tap as an alias of press * test: restore #1036 ordering regression coverage, assert tap dispatches press Making tap a real alias deleted the only tests covering unknown-command-before-flag-validation ordering (they used tap as the unknown command). Restore them with a genuinely unknown command, and strengthen the alias test to assert the dispatched call records press with positionals preserved instead of merely not-erroring. * test: cover tap alias composing with the bare-ref hint Adopted from #1052 (credit: @vku2018): tap e3 must normalize to press and then surface the @e3 suggestion, proving the alias and the bare-ref hint compose instead of masking each other. |
||
|
|
0453826b2e |
fix: report unknown command before flag-support validation (#1038)
* fix: report unknown command before flag-support validation When an unknown command is provided with flags, the parser was validating flag support before checking if the command exists, resulting in misleading "Flags are not supported for command X" errors instead of "Unknown command: X". This fix reorders the validation to check for unknown commands first, reporting more helpful error messages. Additionally, when the unknown command is 'tap', the error now includes a hint suggesting 'press or click' as alternatives. Closes #1036 * refactor: drop as-any casts and note the cli.ts sync requirement Review follow-ups: widen the command lists to readonly string[] instead of narrowing the value, and document that isCommandKnown must stay in sync with the cli.ts dispatch fall-through. |
||
|
|
a79dad69f4 |
feat(interaction): surface targetHittable/hint for non-hittable press/fill targets (#1040)
When a resolved press/fill/click selector or ref target lands on a node that reports hittable: false (after any hittable-ancestor promotion), the success response now carries targetHittable: false and a hint suggesting the interaction may have had no visible effect. Resolution semantics are unchanged: no failing, no filtering, no strict-unique matching — iOS AX hittable flags are unreliable on deep RN trees, and react-navigation's maestro suite depends on today's auto-disambiguation behavior (#1037). Also documents the selector disambiguation policy (deepest node, then smallest area, exact ties fail) in `help workflow`, including that replay/replay-heal apply the same policy for touch/fill/get-text. |
||
|
|
9b19aeee7d |
fix: hint @ref when a bare snapshot ref is passed as interaction target (#1039)
When a user forgets the @ prefix on a snapshot ref (e.g., 'click e3' instead of 'click @e3'), the positional parser would attempt to parse it as coordinates, resulting in a confusing error message "Expected x to be a finite number." Add a check in readInteractionTargetFromPositionals and readFillTargetFromPositionals to detect bare snapshot refs matching /^e\d+$/ and throw INVALID_ARGS with a helpful hint message suggesting the correct @-prefixed form. Closes #1035 |
||
|
|
bff28009e2 | 0.18.3 v0.18.3 |