mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
main
304 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6db1a4270f |
fix(android): report the clip an Android recording really captured (#2566)
* fix(android): report the clip an Android recording really captured Android `screenrecord` encodes a frame only when the screen changes, so a window that ends on an unchanged screen returns a video far shorter than the requested duration, and `record stop` had nothing to say about it: the reported `durationMs` is host wall clock from `record start` until the export finished, which is not the length of the file that was just pulled. `record stop` now measures the pulled MP4 timelines and reports them as `capturedDurationMs`, and warns with the clip length against the window when the video is two or more seconds short. The window is measured on the device's own elapsed clock, read before the stop signal and at launch, because host wall clock drifts against the clock the encoder timestamps frames with; an unreadable clock or a chunk that answers no duration costs the caller the claim, never the recording. Measuring the timeline needed an ISO-BMFF box walk, which now lives in `@agent-device/capture-kit/recording-mp4-duration` and replaces the private top-level atom scan that MP4 container detection was doing. Stop replay through daemon recovery carries the field too, so a completion read back from the session resource reports the same numbers it did live. * chore(gates): enumerate the capture-kit MP4 subpaths the layering scan holds `@agent-device/capture-kit/recording-mp4-duration` and its fixture sibling are new declared package subpaths, so the boundary enumeration that holds every exported workspace subpath has to name them for the layering scan to accept the Android recorder's read of a pulled clip's timeline. * fix(capture-kit): evaluate the MP4 box scan only when a file is validated The Coverage job's ADR-0019 eager-closure probe failed: `recording/video.ts` evaluated 25 modules on import where the merge-base evaluated 24, because the MP4 container gate statically imported the box walk it now shares with the clip-duration read, and `recording/overlay.ts` grew by the same module. An entry the merge-base already carries gets no growth budget, so the edge moves behind a function-scoped `await import`: the scan is something recording completion asks for, and importing this module for `waitForStableFile` or WebM detection should not evaluate a box walker. The alternative the probe offered -- hosting the walker in a module both growing entries already evaluate -- would have put an ISO-BMFF walk in `swift-cache.ts` or `video-webm.ts`, or made the duration read import the Swift validator machinery that sits behind `video.ts`. * refactor(android): bracket the recording window with the host clock Human review of #2566: the device-clock read defended against host-vs-encoder drift that does not matter at this threshold. Quartz drifts by tens of ppm, so a 30-minute chunked recording moves the window under 100 ms against a 2s warning threshold, while the read cost a transport operation, its own probe budget, and two adb round trips per recording. The window is now the host elapsed time between `Date.now()` immediately before the recorder launches and `Date.now()` immediately before the stop signal, so the contract change and the extra device I/O are gone, and the one case where the clocks genuinely diverge -- a host that sleeps mid-recording -- reports a shorter window and misses the warning rather than inventing one. The surviving clock arithmetic is one subtraction, so it lives in the window module that already owns that concern instead of a module of its own. A stop recovered through daemon recovery now passes the manifest's own start instant, which is the first host timestamp the recording ever had, so a recovered stop gets the same comparison a live stop gets. |
||
|
|
775163bb35 |
fix(record): end the recorder a loaded host could not confirm (#2565)
* fix(record): end the recorder a loaded host could not confirm `record stop` refused to signal a recorder whose identity probe it could not afford, and the recording left in `cleanup-pending` then blocked `record start` until the daemon restarted. - Read the identity facts that decide who may be signaled off the event loop, with a budget the caller names, instead of one synchronous `ps` per field. - Keep "the host would not answer" apart from "the host named a different process": the first is unknown evidence, the second is a refusal. - End the recorder this owner spawned through its own handle while the host is not answering, and report any marker it cannot account for. - Re-drive a termination or a finish the host refused instead of replaying the rejection to every later stop. Closes #2549 * test(daemon): answer the ownership facts seam in the recovery fixture |
||
|
|
04052fdcd2 |
fix(android): retire recording evidence stranded by a re-adopted device id (#2564)
* fix(android): retire recording evidence stranded by a re-adopted device id Android `record start` refused forever with "native recovery evidence already exists" once an emulator was re-adopted under a new serial: the device-side marker names the device identity that wrote it, reconciliation retired evidence only when that identity matched, and the leftover classified as neither recoverable nor retireable — an `UNKNOWN` internal error whose hint asked for a bug report, while `record stop` owned nothing to clear. Start reconciliation now retires evidence whose recording is terminal or whose device identity the transport can no longer address, and only after every artifact it names is proven released: a committed recorder through process inspect, an uncommitted pending artifact against the recorders running on the device. A recorder still writing is never deleted; the refusal names the writer in `details.writer`. Remaining refusals — unreadable evidence, the other transport mode, and an open recording this identity owns — are typed errors carrying the marker path and the command that clears it. Closes #2550 * fix(android): keep an unreadable recorder off the delete path The writer probe filtered candidate processes down to the ones it could prove were recorders, so a caller that read an empty list as "nothing writes this path" removed an artifact from under a recorder whose /proc entries could not be read, along with the marker that named it. A process table it could not read at all threw a bare Error, which reached callers as an unclassified failure. The transport answers a writer search with `clear`, `found`, or `uncertain`. Only `clear` proves an artifact is free: start retirement refuses with `native_recording_recorder_unproven` and owned cleanup keeps the evidence pending, so both wait for a conclusive answer instead of deleting on doubt. * fix(android): keep a mixed writer scan inconclusive The writer search answered `found` as soon as it identified one recorder, which hid the candidates it could not read. Start reconciliation already refuses a found writer, but owned cleanup stops only the recorders it was handed and then removes the artifact and marker — deleting under an unreadable recorder still writing that same path. A search now reports the writers it identified together with whether every candidate was read. Cleanup requires both halves: an identified recorder does not prove the others are gone, so an inconclusive scan retains the evidence before anything is signalled or removed. * test(android): record retirement side effects through one evidence rig 21 reconciliation scenarios each rebuilt the same marker reader and the same recording transport stubs. One rig holds the marker and the ordered side-effect log, so a test names only the probe outcome it is about. * test(android): keep legacy reconciliation scenarios as they were The scenarios that already covered retirement were rewritten into a shared rig, which spent most of this PR's churn budget on moving lines around. They read from the device marker again as before; only the recorder-state table names the outcome each state now produces, and the stale row that expected evidence naming another device identity to be kept is gone, since retiring it is this fix. New scenarios use the rig. * test(android): table-drive the inconclusive writer scans Each inconclusive writer-scan scenario rebuilt the same scoped adb fake and the same cleanup transport. The transport cases now differ only in which candidate reads fail, and the owned-cleanup cases differ only in the scan they return, so both run from one table against the same assertions. |
||
|
|
508b750fbd |
fix(apple): carry the runner's sparse verdict through a viewport refusal (#2572)
* fix(apple): carry the runner's sparse verdict through a viewport refusal A payload the runner declared sparse carries the backend, reason code, and reason that explain it, yet the daemon dropped all of it while reconstructing a viewport from the synthetic root that sparse payloads always carry. Callers saw only an internal engine invariant. The verdict now travels as error.details.snapshotQuality and the hint composes the shared sparse-capture advice with the presented surface host. * fix(replay): keep the capture quality verdict in divergence details The replay failure wrapper rebuilds cause details from a four-key allowlist, so the verdict a device-facing capture produced could not reach the agent that replayed the step. |
||
|
|
15644b6f0d | feat(devices): answer when a device's current boot began (#2575) | ||
|
|
d77eff452d |
refactor(daemon): cut the last edges into the CLI schema layer (#2543) (#2562)
* refactor(command-registry): own command defaults; cut the daemon's CLI-schema edges The daemon resolved routes through the command registry but still reached two symbols across the layer boundary — recordedFlagKeys and applyCommandDefaults — both imported from src/cli-schema/command-schema.ts, and the apps filter default was declared a second time in the apps facet. The registry is now the single source of command defaults: a COMMAND_DEFAULTS table typed on CommandFlags plus applyCommandDefaults sit in registry.ts, where every other command default (the defaultValue descriptors) is already declared. The CLI parser and the daemon request scope both import it from @agent-device/command-registry/registry, the one module already in both callers' eager closure. Registry and CLI closure are unchanged: the table adds a literal, not a new import, so no entry grows. The apps facet default and CommandSchema.defaults go away; DEFAULT_APPS_FILTER collapses into resolveAppsFilter's provider fallback and stops being a public export. session-action-recorder reads the recorded-flag vocabulary straight from @agent-device/command-registry/flag-registry, and removing that import also drops command-schema's now-unconsumed recordedFlagKeys re-export. Part of #2545 / #2543. * chore(gates): forbid the daemon importing the CLI schema layer R10 (scripts/layering/daemon-modularity) now rejects any daemon import of src/cli-schema/, value or type. The last two value edges into command-schema.ts are gone, so a total boundary is available, and an explicit rule beats a rank that would let the next daemon module reach the CLI schema layer again. Part of #2545 / #2543. |
||
|
|
4f5a87b6b3 |
refactor(command-registry): move CLI flag grammar, text and command aliases down (#2561)
Move the vocabulary that both the CLI and commands read but no command's runtime depends on into the package below both: flag types, registry, groups and the four flag-definitions files, command-text, and cli-command-aliases. These are pure moves; only their import specifiers change. No compat re-export at the old paths — every consumer switches to the owning subpath. The per-command defaults stay where they are for now (the daemon's edge into the facet resolver is the harder cut and belongs with the daemon-closure work). Part of #2545 / #2543. |
||
|
|
3394d5b89c |
fix(android): keep a scroll's swipe out of the IME window (#2514)
* fix(android): keep a scroll's swipe out of the IME window Android is the case the clip exists for beyond iOS: an `adjustPan` or `adjustNothing` activity keeps a window whose recorded bounds already run under the IME, so a plan built from them aims at the keys. The helper now reports the largest input method window beside the application window, in absolute screen pixels like the window next to it, and `scroll` clips its band with the shared rule or refuses when the keyboard owns the surface. An older helper reports no keyboard keys, and a provider-supplied viewport has no IME channel at all. Both read as "nothing to avoid", which is what the shared rule already does with a missing frame; neither turns into a refusal. `UiAutomation.getWindows()` answers with an empty list until the service asks for interactive windows, so the read applies the seam the tree capture already uses rather than depending on a snapshot capture having run first in the same instrumentation; the one-shot fallback below it has no such neighbour. Measured on a Pixel 7 emulator with an `adjust=pan` contact editor, the application window keeps its full 2400px height while the IME window reports `[0,1517][1080,2400]`, and `scroll down` answers with `referenceHeight: 1505`, `keyboardMinY: 1517`, `keyboardAvoided: true` and a swipe ending at 301 instead of starting at 1920 under the keys. * fix(android): clear the composer, not just the key plane, before a scroll swipes The helper kept the largest `TYPE_INPUT_METHOD` rectangle as the keyboard. A composer bar and its key plane can arrive as separate windows and the key plane is the larger one, so the earlier top edge was discarded and the clipped band still ended inside the composer: the swipe landed on keys the rule exists to keep it off. The read now copies every input method window and unions the ones the swipe's centre line crosses, which is the same line the shared clip rule tests. A candidate strip at the edge of the screen that the swipe can never reach no longer shortens the band either. The selection runs on plain window edges, because `Rect` is a device type whose constructors throw off-device, so the two-window case is a unit test rather than a simulator-only path. * refactor(android): pass the measured occlusion explicitly and drop the duplicate rect check The clip rule already fails open on a keyboard frame it cannot measure, so the helper reader no longer needs its own copy of the check, and the gesture-viewport validator goes back to its original body. The refusal names its three numbers instead of spreading the clip variant, so the discriminator never leaks into error details. |
||
|
|
5cf6414fd2 |
fix(contracts): state the scroll keyboard clip once for every platform (#2537)
* fix(contracts): state the scroll keyboard clip once for every platform * fix(apple): surface the scroll keyboard clip as evidence and a typed reason * refactor(contracts): state the scroll keyboard refusal details once and keep the runner's message The Apple scroll owner rebuilt the refusal per command, discarding the runner's measured message and carrying an unmeasured variant of the error builder for it. The shared reason and hint are now one frozen object in scroll-gesture; the Apple owner adds it to the runner's own error (matched on the typed runner code, transport details kept), and the error builder takes a plain measured occlusion, which only Android produces in-process. The help text names the behaviour in one clause; the hint carries the recovery at the moment it matters. |
||
|
|
7a25a02f6d |
fix(record): replay the finished export from a retried record stop (#2534)
* fix(record): replay the finished export from a retried record stop A remote record stop can outlive its client window while the daemon is still exporting. The finished manifest was then read as no active recording, and its metadata carried no client output path, so the caller had no way to collect the file. A repeated record stop now serves the completed export and says so in the timeout hint. * refactor(record): declare each completion codec once A mapped codec per completion property drives encoding and decoding from one declaration, and the declaration fails to typecheck if a property has no codec. * fix(record): keep manifest encoding inside the session resource module Session teardown reaches the recording resource definition while it loads, and that eager closure takes no new module. Writing a completion is property reads only, so the field map and writers now live with the resource definition; reading one back needs the recording vocabulary and stays behind the stop path. * test(client): give the request timeout hint its own mirror file The hint assertions had outgrown the aggregate client test past its size ratchet; they mirror src/daemon-client/daemon-client-timeout.ts, so they move rather than shrink. * refactor(record): store the finished stop response under one manifest key The manifest now holds the completion as the one object record stop returned, so a replay cannot lose a field between an encoder and a decoder, and the reader lives with the stop path that needs it. Recovery still refuses a response whose served path or caller-side paths are not whole. * refactor(record): reuse the scope guard and record path their owners declare A stored scope is checked by isRecordingScope next to the vocabulary it validates, and a session's durable record path comes from the factory that names it instead of being re-derived at each read. * refactor(client): hand the timed-out request to its timeout handler Command, session, and action all come from the same request, so they are passed as one request instead of three more positional arguments. * refactor(client): name the timed-out request fields the handler reads The client timeout handler stays off the daemon request shape: R10 daemon-modularity holds external importers of that module at the merge-base count, so the fields arrive as named properties instead of the request object. * refactor(client): read a timed-out request's fields once for both transports A socket timeout and an HTTP timeout described the same request with two copies of the same mapping. |
||
|
|
b92b6ca70c |
fix(cloud): cancel a screen read that its caller gave up on (#2516)
* fix(cloud): cancel a screen read that its caller gave up on (#2509) A hosted page-source read that outran its request was dropped by the client while the driver kept walking the UI tree. Nothing at the wire said the answer was no longer wanted, so the read stayed in flight and held the session it was blocking. On a screen that never goes still -- a looping video, a live ticker, continuous animation -- every later command then queued behind a capture nobody was waiting for, which is what makes one stuck `snapshot -i` look like a frozen session. Bind the read to the request that asked for it, on both Android and iOS, and say what a source read that runs out of budget was waiting for. The timeout keeps its `webdriver_request_timeout` reason and gains a hint naming a screen that never goes still, so the rented minutes end with a cause rather than a silent hang. Closes #2509 * fix(cloud): stop advising a timeout the source read never sees The hint and the AWS docs both told the caller to retry with a larger `--timeout`. That flag widens the command envelope around the read; the read's own budget is the transport's, so the advice could not work and cost rented minutes to discover. The report on #2509 shows exactly that experiment failing at 65 seconds. Say whose budget it is, and offer the two things that do work: a `screenshot`, which never reads the tree, and the `@refs` an earlier snapshot captured. * test(cloud): claim only what the cancellation layer proves The scenario comment credited this layer with the lease-gone symptom, which a ten-minute cloud WebDriver lease rules out for the reported run. And one assertion message said the driver's own tree walk had been cancelled, when what the test observes is our request being hung up at the wire. Behaviour and coverage are unchanged; the test now says what it measures. * docs(snapshot): put the never-idle screen rule where snapshot advice lives The read that fails is shared hosted WebDriver behaviour, so it belongs on the Snapshots page rather than only under one provider. The provider page keeps the part that is about being metered. Both pages name the two dead ends, since both were tried on the reported run: a larger `--timeout`, which widens the command around the read, and `settings animations`, which hosted WebDriver sessions do not implement. * docs(snapshot): bound the cancellation to the waiting this side controls Hanging up the client read proves agent-device stops waiting and stops holding the session. It says nothing about the provider, whose tree walk can keep running and can still occupy that session's queue server-side. The paragraph claimed the recovery the fixture does not prove. |
||
|
|
b8d70aa8a1 |
fix(daemon): keep a discarded rejection from shutting the daemon down (#2532)
An aborted Apple simulator recording start discarded the transport promise: `void started.then(rollback)` has no rejection handler, and the local transport rejects with the abort reason after its dynamic imports settle. The rejection reached the daemon's only unhandledRejection handler, which exits with code 1 and kills every open session. End the rollback chain in a handler at both abandon sites so a rejected acquisition, or a rollback that itself fails, cannot escape. Sweep the remaining single-handler `void … .then(` sites: a rejected renewal now fences as unconfirmed authority instead of settling as a normal renewal, which would re-arm renewal work with no backoff, and the Limrun drain drops its discarded continuation by deleting from `pending` when the deferred settles. |
||
|
|
8e8eeb2ced |
refactor(snapshot): drop the bridge truncation dimension; stop promising --scope for depth caps (#2511)
The dimension and limit the bridge adapter inferred for a cut capture had no renderer and one consumer, the comparison-identity string, where the kind alone gives the same comparability. The runner never produced them. The depth-cap warning suggested --scope to read deeper content; on iOS scope narrows presentation and acquisition stays scope-blind. |
||
|
|
1527146507 |
feat(snapshot): disclose a cut capture on every platform; raise the iOS bridge node cap to 5000 (#2510)
Every backend sets truncated: true when it cuts a capture at one of its limits, but only JSON carried it. One shared warning now renders from that flag in the cross-platform warnings assembly and tells the agent what fell off (what comes last in document order) and what to do. The iOS Simulator AX bridge cap moves from 1500 to 5000 nodes, the Android helper's bound. Measured on a synthetic 600-row screen, acquisition time did not change with the cap while the 1500 cut dropped the on-screen footer. |
||
|
|
076234e2eb |
perf(screenshot): read the crop region instead of decoding the whole capture (#2504)
`screenshot --crop-on` paid for a full PNG decode and an RGBA re-encode of the capture before keeping a frame. One worker job now turns the captured bytes into the cropped bytes: a region reader that reconstructs pixels only down to the box's last row and allocates only the box's pixels, and a truecolor writer that drops the alpha channel when the cropped pixels carry none. The reader claims the 8-bit non-interlaced truecolor layout that iOS simulator and Android emulator captures arrive in, and only for a file it can vouch for: the IHDR and every chunk checksum are verified, an unrecognised critical chunk name is a decline, and every row's filter byte is read whether or not the box reaches that row. Everything else — palette, grayscale, interlaced, 16-bit, a checksum that does not match — falls through to the general PNG reader, which keeps owning the canonical decode error and the previous RGBA output. A box covering the whole image reads through that general reader too, so an unchanged answer is only reported for a file that reader accepts. Cropped bytes verify pixel-for-pixel against ImageMagick's own crop across RGB, RGBA, grayscale, palette, 16-bit, interlaced, and translucent sources, on both iOS simulator and Android emulator captures. |
||
|
|
37d67de776 |
fix(android): carry accessibility selected state into snapshots (#2515)
The snapshot helper never serialized `selected`, and the host reads only the
helper's XML, so no later layer could recover it: `get attrs` had no `selected`
field, no snapshot node was marked selected, `is selected` could not match, and
a Maestro `assertVisible {id, selected: true}` failed with "Maestro visible
condition did not match" for a visible element while `selected: false` matched
every Android node (#2462).
The helper now emits both answers, like `enabled` and `password`, so an
unselected control answers `false` and a helper older than the attribute answers
nothing. The parser, the Android hierarchy node, and the published snapshot node
carry it to `get attrs` and the `[selected]` marker.
Snapshot lines render that marker on the default formatter path too: `--settle`
and `diff` already compared selection, and a line that weighs a fact it cannot
print turns a tab tap into a changed pair whose two lines look identical.
|
||
|
|
49fbaf61b8 | fix(aws): defer Android app launch until open (#2512) | ||
|
|
b7c82ea152 |
fix(test): preserve colliding diagnostic artifacts (#2507)
Co-authored-by: PLASMA-FR <173463847+PLASMA-FR@users.noreply.github.com> |
||
|
|
eefe37b51e |
docs(adr-0011): narrow the offscreen rescue comment to the per-request surface policy (#2465)
* docs(adr-0011): note the offscreen live rescue reads the tree's surface The offscreen guarantee's live rescue runs the runner's direct querySelector, which consumes the single activeApp that prepareActiveCommandContext resolved for the snapshot tree as well, including an in-place system surface. Record that the tree and the rescue read the same surface so the cell's rationale stays accurate. Closes #2452 * docs(adr-0011): narrow the offscreen rescue comment to the per-request surface policy The snapshot capture and the live rescue are separate runner requests, and each calls prepareActiveCommandContext on its own, so only the selection policy is shared, not the surface instant. State that, and say plainly that a surface appearing or dismissing between the two requests is not detected. Closes #2452 * docs(adr-0011): limit the shared surface seam to runner-routed captures An eligible iOS simulator snapshot is served by the host AX bridge (packages/platform-apple/src/snapshot-route.ts), which never reaches the runner's prepareActiveCommandContext. The rescue's direct querySelector always does, so the two requests share that surface policy only when the capture is runner-routed too - which is the case #2448 forces for the system surface. Keep the unchanged statement that no surface identity crosses the two requests. |
||
|
|
3cd6341388 |
fix(scroll): pace edge passes to rest and stop a stuck surface (#2499)
* fix(scroll): pace edge passes to rest and stop a stuck surface A rubber-band bounce keeps shifting the surface after a fling, so the next edge pass captured a phantom new offset, stacked another fling on top of the bounce, and a single stuck container could fling 40 times without net progress. `runScrollEdgePasses` now waits for the scoped scroll surface to hold between passes before deciding or flinging again, and stops with `scroll_edge_no_progress` when a container still reports hidden content but its descendants never shift. The end pass-limit keeps its own `scroll_edge_pass_limit` reason. `scroll --until` shares the same stuck-signature window and raises `scroll_until_no_progress`. The stuck-window bookkeeping and surface fingerprint are shared from capture-kit so both loops read one definition of "stuck" rather than a copy. * fix(scroll): count recycled-cell and fresh-signature progress as movement Two false stops survived the first cut: - The surface signature coalesced to `identifier ?? label ?? value`, so a recycled cell that keeps its identifier and slot while its text changes looked unchanged and a genuinely advancing list was reported stuck. The signature now keys on identifier, label, and value together. - `scrollSurfaceIsStuck` accepted any four captures with at most two distinct signatures, including `A,A,A,B`, and both loops stopped on the pass that finally advanced. It now also requires the newest signature to be one the window already showed, so rubber-banding trips but a fresh signature continues. |
||
|
|
de8703b6a0 |
fix(selectors): resolve a wrapper chain's control for uniqueness reads (#2501)
A control reported through its own accessibility wrapper answers a selector twice, and a regular iOS snapshot omits unverified hittability, so the ladder that relates a wrapper to its control cannot fire. #2482 collapsed that chain for mutating resolution only: `press` tapped the toolbar button while `is visible` and `get attrs` reported "Selector did not match" and `screenshot --crop-on` refused the same screen as two nodes. Export the collapse beside the classification that asks for it and apply it where a read row's answer was a refusal. Rows that resolve before any refusal are untouched, and a candidate set the rule does not recognize as one control - a cell and the button inside it, or matches in distinct subtrees - still refuses. Replay verifies a recorded target by resolving its recorded selector again under the same row's refusal rules, so a step whose screen had not changed verified as an identity mismatch on its first replay. Verification names the collapsed control too, which is the node dispatch acted on and the node the recorded identity carries. |
||
|
|
d80fb35ec3 |
fix(browserstack): carry the full provider-allocation config over the lease_allocate envelope (#2494) (#2495)
* fix(browserstack): forward provider session metadata over the lease envelope `--provider-project`, `--provider-build`, and `--provider-session-name` are stored in the connection profile and reach the daemon on the line transport (which forwards the whole request), but the compact JSON-RPC lease envelope carried only `providerApp`. Over HTTP/remote daemons the daemon's lease-lifecycle provider therefore saw no session-naming metadata and created BrowserStack sessions as "Untitled Project" / "Untitled Build" with an empty name. Read the four provider session-metadata flags through one shared projection (`readLeaseAllocateProviderMetadata`) used by both the client's `buildHttpRpcPayload` and the daemon's `toLeaseDaemonRequest`, so the transports agree and the producer and consumer cannot drop a sibling again. Closes #2494 * chore(gates): pin lease_allocate wire digests for the forwarded provider metadata (#2494) * test(daemon-http): move the lease provider-metadata check into its own file The provider-scenario daemon-http-server test is over the 1000-line tripwire and may not grow; the lease_allocate metadata assertion now lives in daemon-http-lease-allocate.test.ts, mirroring the lease projection in http-server.ts. * fix(browserstack): carry the full provider-allocation config over the lease envelope The lease envelope named only the session-label fields, so a fresh remote allocation still failed in prepareSession before the names could take effect: device selection (platform, device), providerOsVersion, and the configured device-feature/AWS knobs were dropped on the HTTP transport, while the line transport forwards the whole request for free. readLeaseAllocateProviderMetadata becomes readLeaseAllocateProviderFlags and projects the full set the lease-lifecycle provider reads, pinned exhaustive against CloudProviderProfileFields so a new field cannot silently miss it. The producer and consumer already share this reader, so both transports agree. Covered end to end in cloud-webdriver-lease-http.test.ts: the exact client envelope is driven through a real daemon HTTP server into the real BrowserStack prepareSession, asserting the capabilities that reach the hub. * chore(gates): re-pin lease_allocate wire digests for the broadened projection (#2494) |
||
|
|
0feb4e26a0 |
feat(ios): drive ASWebAuthenticationSession sign-in sheets in place (#2438) (#2448)
* 0.21.1 * feat(ios): drive ASWebAuthenticationSession sign-in sheets in place (#2438) iOS apps that sign in via ASWebAuthenticationSession present the identity provider in com.apple.SafariViewService, out of the app's process. Two facts, both verified live on the iOS 26.2 Simulator, made these flows unautomatable: activating or launching the host cancels the auth session, and the host AX bridge cannot see the sheet because the app stays the AX primaryApp. Serve and drive the sheet in place. A closed registry names the host (shared by the TypeScript and Swift sides under a parity test); the runner reads and drives it without activation and never adopts it as the session target; and the Simulator route detects a running host with a cheap device-scoped ps probe and takes the runner path, since the bridge would serve the occluded app tree as if healthy. open refuses to launch a registered host, and captures carry a system-surface disclosure. Presence is foreground state, not tree content: a torn-down host serves a richer tree than a live one, so content heuristics cannot tell them apart. The never-activate guard is what keeps the foreground predicate sound, which also makes the stale-tree failure mode unrepresentable for this flow. Closes #2438 * chore(gates): register contracts/ios-system-surface in the export snapshot * fix(ios): close the system-surface correctness gaps from review Presence probe: absence and probe failure are no longer reported as "no surface". The probe returns present/absent/unknown and the route takes the runner for anything but a proven absent, so a sheet opened between two captures, or a probe that cannot answer, can no longer fall through to a bridge capture that would answer confidently from the occluded app tree. Only a positive observation is memoized. The probe now matches with pgrep and reads only a matched pid's environment, which is ~3x cheaper than the previous full process-environment dump and stops copying every process's environment. Open guard: the refusal moved to every resolved-host launch and terminate, so the URL, deep-link and launch-args branches that returned before the old check can no longer launch the host. Terminating a host is refused too, since that cancels the presented session just as launching it does. Comparison: the surface identity now reaches SnapshotState, and tap-failure corroboration refuses outright when a baseline and a post-action capture disagree about it, instead of letting app and sheet captures meet in legacy same-presentation matching. Selector routes disclose an iOS system surface through the shared disclosure seam rather than reading only the Android field. The contracts import in the launch path is deferred so the app-lifecycle facade's eager closure stays flat, and the runner's comment prose is trimmed because apple/runner ships to npm as uncompiled source. * fix(ios): route the system-surface probe through the Apple tool provider The probe shelled out with runCmd, so every eligible capture spawned a real process even in provider-backed tests that stub the Apple tool seam — 17 real spawns in one scenario file, which is both wasted work and added latency on timing-sensitive settle paths. It now goes through runAppleToolCommand like the sibling ps probe, so a stubbed provider answers instead of spawning. * fix(ios): disclose a skipped bridge when the surface probe cannot answer Routing an unprovable probe to the runner is right, but the early return also skipped runFallback, so the response lost its warning and kept an identity that could still be compared against a bridge publication. An unknown probe now falls back through the same disclosed path as a bridge failure, with its own reason. * fix(ios): keep surface identity through comparison, find, and probe scope A ps read that carries no SIMULATOR_UDID at all was reported as absence, so an unreadable or truncated environment could route a live sheet to the occluded app tree. Only a scope naming a different device is a real negative now; a missing one stays unknown. The shared post-gesture comparison token used comparisonKey or the backend alone, so an app capture and a sheet capture — both XCTest — compared equal and a sheet appearing or dismissing read as a stable surface. The token now carries the surface, which covers stabilization, verify and settle through the one path they share. Mutating find rebuilt its capture without iosSystemSurfaceBundleId, so the shared disclosure helper could not report the sheet on either outcome. It is preserved now. Each fix has a regression that fails without it. * fix(ios): keep surface identity in verify and settle comparisons `--verify` compared node digests and `--settle` diffed node-only baselines, so an app baseline and an in-place system-surface capture (a web sign-in sheet) were treated as one presentation: a meaningless changed verdict, and a whole-surface replacement presented as an in-surface diff with refs. The pre-action baseline now travels with the surface its capture described, from the resolution and the session frame through to the settled capture, and one module owns the comparison for both routes. Across a surface change no same-surface claim is made: evidence reports the transition instead of a digest comparison, the settled diff and its refs are withheld, and both payloads disclose the transition. * refactor(test): move the cross-surface settle tests onto their source mirror The #2438 cross-surface cases were appended to `settle.test.ts`, taking it over the test-file size ratchet (2528 lines, 2359 at the merge-base). They assert the comparison `post-action-surface.ts` owns, so they move to that module's mirror test file, and the device double plus the trees both files drive move to a sibling fixtures module under `__tests__/` rather than being duplicated. Pure move: every test and every assertion is unchanged, and `settle.test.ts` is back under its merge-base length. * test(daemon): cover the cross-surface settle refusal on the generic route `scroll --settle` and `back --settle` plumb the baseline's surface identity through `baselineSurfaceBundleId`, but nothing asserted it: the generic route had zero coverage of the #2438 refusal, so a regression there would have been silent while the element-targeted route stayed green. Assert the same contract the targeted route guarantees, in both directions and for both commands: no diff is attached across an app/sheet boundary — therefore no tail and no `refsGeneration` — the transition is disclosed, and the settle observation still reports its own verdict alongside that disclosure. Each direction falsifies a different half of the plumbing, so both are needed: dropping the baseline's surface identity fails only the sheet-to-app tests (an app baseline has no surface id to lose), and dropping the settled capture's fails only the app-to-sheet tests. No production change: the plumbing was correct, only untested. * refactor(ios): inline the single-caller surface disclosure wrapper iosSystemSurfaceDisclosure() only mapped provenance-or-nothing onto the shared constant for one caller, so the caller now reads the constant directly and the wrapper is gone. Its test becomes a test of the transition disclosure, which is the function that still earns its place (the "sheet is gone" sentence). readAppleSnapshotResult also called readSystemSurfaceProvenance twice inside one spread; it is bound to a local and read once. * docs(adr): state that a presented surface outranks a requested bundle id prepareActiveCommandContext checks for a presented system surface before it resolves or activates command.appBundleId, so a command naming a different app is still served the sheet. That is intended, but the code does not read that way; the amendment now says it plainly. * refactor(ios): carry the system surface in the capture's comparison lineage A capture of an in-place system surface (a web sign-in sheet) describes a different presentation than a capture of the app, so it must never compare equal to one. The `present` branch of the iOS snapshot route returned a bare fallback, so that capture carried no comparison identity at all, and two comparison sites hand-rolled the distinction from `iosSystemSurfaceBundleId` instead. The probe now reports which host it matched, and the `present` branch goes through `runFallback` like the `unknown` branch beside it, lineaged to `<device>:<host bundle>`. The comparison key then differs from an app capture's by construction, so the surface branch in `hasMatchingPresentation` and the surface concatenation in `snapshotComparisonKey` are gone: both sites are plain key equality again, and neither knows that system surfaces exist. Two captures of the same surface still share a lineage, so they stay comparable with each other. A presented surface is not a bridge failure, so it gets its own warning wording: the bridge is inapplicable here, not unavailable. * refactor(interaction): carry the pre-action baseline as one surface-scoped value The same pre-action tree travelled as a flattened nodes/surface pair at every boundary, and each boundary rebuilt it with a conditional spread. Carry SurfaceScopedNodes itself instead: - ResolvedInteractionTarget gets preAction?: SurfaceScopedNodes, replacing the preActionNodes/preActionSurfaceBundleId pair and the PreActionBaselineFields intersection on all three arms of the union. - SettleObservationCommandOptions gets baseline: SurfaceScopedNodes, replacing baselineNodes/baselineSurfaceBundleId. - RefResolution carries tree: SurfaceScopedNodes instead of nodes plus a loose surfaceBundleId. That retires preActionBaselineFields(), preActionBaseline(), evidenceBaseline(), the local SettleBaseline type, the split-then-reassemble in settleObservationCommand, and the 'preActionNodes' in resolved narrowing tests. SurfaceScopedNodes moves to contracts, where ResolvedInteractionTarget can name it; only two sites now mint one from a SnapshotState. Behaviour is unchanged: the cross-surface guarantees keep their existing tests. * fix(ios): identify a surface capture by what the runner served The `present` path stamped the capture's comparison lineage from the host-side presence probe. That probe answers about a host PROCESS and deliberately stays positive while a dismissed host lingers, so during that window the runner truthfully returned APP content while the route lineaged it to the HOST: the sheet capture before the dismissal and the app capture after it compared equal, and a post-gesture poll could read the transition as a stable surface. Derive the identity from the returned capture's `systemSurface` instead - the runner stamps the surface it actually served - and say which of the two the capture holds in the warning. The probe's host is now evidence only: it names the matched host in a route diagnostic so a lingering window is legible in the daemon log. Other reasons keep their lineage and wording byte for byte. Captures that bypass the route's planning (a pinned backend, a custom-actions read) also reach the runner, and the runner serves the sheet there too. They carried no comparison key at all, so a sheet and app content fell through to legacy presentation matching as one presentation and could corroborate a tap across the two. The capture owner now gives those a surface-scoped identity as well, with no fallback-source residue: nothing fell back. An app capture off the route is untouched. * fix(ios): derive a served surface identity at the one stamping point A runner fallback's comparison identity was decided per call site. The `present` path and the off-route path read the runner's `systemSurface` stamp, but the plain `runFallback` path did not: it stamped the app lineage the route had planned, whatever the runner returned. The probe and the capture are separate observations, so a sheet can appear in the gap between them. With the bridge circuit already disabled for the generation, an app capture and a later sheet capture both received the same app-generation key, so tap corroboration could treat two different surfaces as comparable. `stampFallback` now owns the decision for every runner fallback: the surface the runner served outranks the app lineage the route planned. The reason the bridge was skipped survives either way, and app-generation evidence leaves with the app lineage it describes, so two captures of the same sheet still compare equal. `runSurfaceFallback` keeps only the reason, which is the one thing that path decides. |
||
|
|
1fb276448f |
fix(wait): poll through a retriable runner refusal instead of surrendering the budget (#2493)
* fix(wait): poll through a retriable runner refusal instead of surrendering the budget The iOS Smoke lane started failing on main at the merge of #2486: the new smoke:webview-remote-content scenario ended with `wait text "Jump to form" 20000` failing after 288 ms with RUNNER_BUSY. Three defects stacked up. The scenario reused acceptDeepLinkConfirmationIfPresent, whose readiness landmark was hard-coded to the Automation lab's text. Off that route it can never match, so the helper always fell through to its `alert get` probe — and an XCTest alert query against a live WKWebView screen exceeds the runner's 30 s main-thread execution watchdog (measured 10.1 s to fail locally, 10.6 s in CI), abandoning main-thread work and leaving the runner refusing every following command as RUNNER_BUSY. The landmark is now a parameter and each caller passes its own route's, so the probe runs only when the destination genuinely did not arrive. The depth-frontier scenario carried the same mismatch and is fixed with it. A `wait` is a budgeted retry loop, but it abandoned its whole budget on the first retriable refusal. A poll whose failure the producer itself marked retriable is now ridden out like an unreadable capture: the wait keeps polling to its deadline, records the poll as `retriable` in its timeout evidence, and surfaces the refusal only when no readable capture ever completed. RUNNER_WEDGED is not retriable and still ends the wait at once. That classification was also missing on the path the failure actually took. A runner error recovered from the lifecycle journal after a lost transport response was built with a bare toAppErrorCode, so RUNNER_BUSY reached callers as a RUNNER_BUSY wire code with no `retriable` flag, while the live-response path published it as COMMAND_FAILED plus details.runnerErrorCode and retriable: true. Both paths now read the runner's code through one classifier in runner-contract. Live-validated on a booted iPhone 17 Pro simulator against the fixture app: the destination landmark resolves in 389 ms with no alert probe, the page wait succeeds in 81 ms, and the snapshot still carries Link "Jump to form", the "Email address" field label, and the remote-content-boundary XCTest fallback warning. Driving the old sequence first reproduces the wedge, after which the fixed wait polls its full 20 s in `retriable` polls instead of failing instantly. * test(apple-runner): move journaled runner-code classification to the recovery test The two new cases landed in runner-command-retry.test.ts, which was already over the 1,000-line test-file tripwire, so the size ratchet refused its growth. They assert runnerStatusFailureError's reading of the lifecycle journal, so their home is runner-command-recovery.test.ts, which mirrors that module and drives recovery through the real stack against a scripted fake runner. * test(e2e): let a deep-link route mount before probing for its confirmation alert The 2500 ms destination budget was tuned to the Automation lab on a warm simulator. On CI the WebView lab rendered correctly but was not in the bridge tree that fast, so the helper fell through to its `alert get` probe — and that XCTest query against a live WKWebView exceeds the runner's execution watchdog, leaving every later command refused as RUNNER_BUSY. Measured on a freshly created simulator: with the confirmation alert up the probe is correct and costs 1.6 s, because the alert blocks the route and there is no web view to query; with no alert the landmark resolves in 0.1-1.7 s. The budget only has to outlast an honest mount, and overshooting it costs nothing when a confirmation really is up, since that route never renders until it is accepted. Cold-simulator run of the whole scenario: landmark 400 ms, page wait 613 ms, snapshot keeps the page link, the field label, and the XCTest fallback warning. * fix(wait): keep the poll timeline on a wait exhausted by retriable refusals Review finding on #2493: a wait that spent its whole budget being refused threw the last refusal raw, so the common all-RUNNER_BUSY case carried no captures, waitedMs or polls and could not show where its budget went — contradicting the evidence this PR documents. The mirror gap existed on the other exhaustion shape: when the deadline cancelled the final poll, the wait reported a generic stall and dropped the runner code and retry details instead. Both shapes now raise one error that keeps the producer's code, message, hint and retry details and carries the wait's own evidence, with reason wait_capture_stalled and the original as its cause. A content verdict is still preserved untouched, since it already describes the capture it came from, and whether it outranks the stall verdict stays the caller's policy (wait absent). Live-verified against a genuinely wedged simulator runner: COMMAND_FAILED, retriable true, runnerErrorCode RUNNER_BUSY, reason wait_capture_stalled, captures 6, readableCaptures 0, waitedMs 8041, polls retriable,retriable,retriable,retriable,retriable,deadline. |
||
|
|
c94e66e9b8 |
fix(selectors): collapse an unverified-hittability wrapper chain to its control (#2482)
* fix(selectors): collapse an unverified-hittability wrapper chain to its control A SwiftUI toolbar wrapper and its control share one identifier, and regular iOS snapshots omit hittability evidence. findPreferredActionableDescendant requires verified hittability, and the wrapper's rect differs by under a point per edge, so press/fill saw two distinct actionable elements for one control and refused with AMBIGUOUS_MATCH. Resolve the deepest semantic touch target when every candidate lacks hittability evidence and all rects agree within sub-pixel slack. Candidates carrying any hittability fact keep the existing rules. * fix(selectors): keep the wrapper-collapse fallback to non-actionable wrappers Review follow-up on #2482. The unverified-hittability collapse accepted any ancestry chain whose rects agreed within a point, so a cell and the button inside it (both actionable, no hittability evidence) collapsed to the descendant: a silent wrong-control press where the previous rules refused as ambiguous. The fallback now requires every candidate above the control to be a non-actionable wrapper, and a negative regression covers the semantic-ancestor case next to the captured Other/Button success case. Gate: pnpm check:affected --run - 304 files / 2011 tests, all runnable checks passed. * perf(selectors): keep the wrapper collapse inside its budgeted closure The extracted module grew the eager closure of three budgeted entries by one module each -- interaction-targeting.ts 13 -> 14, selector-pipeline.ts 25 -> 26, absence-observation-resolution.ts likewise -- and the eager-closure gate ratchets that closure against the merge-base: an entry surface that drags more of the repo onto the import path is a loading-shape regression whatever the reason. The rule has exactly one consumer, so it now lives beside the classification that asks it and is no longer an exported surface. Its tests move to the owning module's test file and exercise `classifyActionableTouchCandidates`, the boundary the command actually calls. Each of the four refusals fails when its own guard is mutated: the hittability condition, the 1 pt slack, the non-actionable-wrapper condition, and the semantic-control condition. --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> |
||
|
|
fd4cee83f8 |
fix(android): retire completed recording evidence after pid reuse (#2487)
* fix(android): retire completed recording evidence after pid reuse (#2476) A reused emulator can reassign a recorded screenrecord pid to an unrelated process. The transport proves that replacement with `ownership-lost`, but completed-evidence retirement and reattach accepted only `missing`, so they treated proven termination like an uncertain live recorder: `record start` refused forever on the retained marker, and `record stop` could not return the already-finalized completion. Classify the declared ownership observations once, in the contract that declares them, and ask that question instead of comparing to `missing`. Retirement still refuses a live or unreadable recorder and never signals a pid it proved is not its own. * fix(android): read proven termination in the recovery warning too Review follow-up. Classify the recovered chunk's recorder with the owning observation predicate, so a recorder proven gone through pid reuse also discloses that the MP4 may be truncated instead of only a pid directory that went absent. Attribute each `ownership-lost` producer — reassigned executable, foreign remote path, exited task with no command line — in the transport test, and state them in the contract comment the classification rests on. * fix(android): retain completed recording evidence while a replacement recorder writes its path Review follow-up. A reused pid that runs screenrecord on the recorded remote path with a different start time proved the old recorder gone, and retirement read that as permission to remove the artifact — deleting the replacement recording's active MP4. Classify that observation as foreign-writer in the contract: it still proves termination, so recovery and the truncation warning keep reading it, but it never proves the path unclaimed, so retirement retains the marker and artifact until the replacement ends and never signals it. Stop-wait refuses it like ownership-lost. |
||
|
|
eb0d791957 |
fix(orientation): disclose an unconfirmed rotation instead of asserting it (#2483)
* fix(orientation): disclose an unconfirmed rotation instead of asserting it executeSetOrientation fell back to the requested rotation when the owner reported no resulting orientation, then reported 'Rotated to <request>' as a success claim. Keep the requested rotation for compatibility, but mark the claim unconfirmed and warn. * fix(orientation): carry the unconfirmed rotation through the journal and public surface Review follow-up on #2483. The daemon disclosed `confirmed: false` plus a warning, but the surfaces that consume the result still asserted a rotation: - `buildOrientationActionSummary` rebuilt "Rotated to <orientation>" from the orientation field alone, so a session journal recorded an unconfirmed rotation as fact. It now records "Requested <orientation> (unconfirmed)" when the owner reported nothing; the journal regression fails on the previous commit. - `OrientationCommandResult` declares the optional `confirmed` and `warning` fields, and the MCP output schema advertises them so clients can consume the distinction (the navigation schema parity test covers the lockstep). The disclosed-warning shape stays: no hard failure. Gate: pnpm check:affected --run - 332 files / 2158 tests, all runnable checks passed. |
||
|
|
47b1cae548 |
fix(ios): refuse Simulator bridge trees that end at a web view's remote content (#2484) (#2486)
* 0.21.1 * fix(ios): refuse Simulator bridge trees that end at a web view's remote content (#2484) Since 0.21.0 the host AX bridge is the snapshot source for local iOS Simulators. It reads one process, and a WebKit page lives in another: Safari and WKWebView screens were published as chrome plus empty webview nodes, with no ref reaching the page. The decoder now counts AXRemoteElement leaves that sit under a WebView ancestor and reach the viewport, and the source refuses such a tree as remote-content-boundary. The existing route fallback serves XCTest, which resolves remote elements, for the rest of the app generation and discloses the switch in the snapshot warning. Frameless leaves are refused; zero-area and off-screen ones are published. Adds a fixture-backed smoke scenario that drives the WebView lab through the default route, amends ADR 0004 and the bridge README, and shares the e2e snapshotNodes helper. |
||
|
|
f57b42166a |
fix(network): report iOS requests that reused a keep-alive connection (#2433)
* fix(network): report iOS requests that reused a keep-alive connection CFNetwork logs a request URL only on the `com.apple.network:connection` line that opens a connection. A request that reuses a keep-alive connection emits a task summary carrying status, timing, and byte counts but no URL anywhere in the log, so a URL-keyed reader dropped it and the dump silently omitted a request that did happen. An "assert this endpoint was called on startup" check therefore read as a definite fail. Correlate a reused task summary with the connection it names and report it against that connection's origin, with `pathUnavailable` set, its status, and its timing. The request path is not in the log at all, so the dump also notes how many requests it could not name — a gap in observation now reads as a gap rather than as a negative observation. Also stop a URL parsed out of a log line from carrying the punctuation that follows it, so an entry's `url` compares equal to the endpoint under test instead of failing on a trailing comma. The correlation lives in the reader rather than a sibling module because `packages/capture-kit/src/index.ts` may not grow its eager import closure. Refs callstack/agent-device#2430 * fix(network): count keep-alive requests the reader cannot name at all Review of the parent commit found the same definite-negative it fixes, one level down: a reused task summary whose connection was opened before the scanned window resolves to no origin, so it produced no entry and no signal — an empty dump reporting "No HTTP(s) entries were found" for a window that demonstrably carried traffic. Count those in the dump's `unnamedRequests` and say so in the notes, so an unnameable request is still a reported observation. Also order the Apple note builders so the keep-alive note no longer trips the `notes.length === 0` guard that suppresses lifecycle guidance, and give the android-backend test a fixture an Apple dump would actually resolve, so the backend gate it names is the thing it proves. * fix(network): scope connection correlation to the process that opened it Review findings on the parent commits: three ways the reader still answers with something other than what it observed. A connection number is only meaningful within one process, but the index keyed on the number alone, so an app that relaunched and reopened the same number inherited the origin its predecessor had contacted — a request attributed to a host it never reached, which is worse than dropping it. Key the index by the compact log's `name[pid]` and the connection number together; a line whose process cannot be read correlates to nothing and its traffic stays unnamed. The simulator recovery pass merged its dump only when it carried entries, so a recovery window holding nothing but unnameable reused-task summaries discarded that count and the response still reported an empty window. Merge whenever the pass observed traffic in either form, and reserve the "none looked like HTTP traffic" note for a pass that found neither. The trailing-separator strip was global, so a valid URL ending in punctuation became a different endpoint. Take the URL from the delimited `url:` field where the format establishes the separator, and leave a bare URL exactly as matched. Regressions cover each: the same connection number under a different pid, an unreadable process identity, recovery-only unnamed traffic, and a path that legitimately ends in a period. * fix(network): reconcile unnamed keep-alive requests across scan windows The app log and the simulator recovery pass cover different, sometimes overlapping windows, so taking the larger of their two unnamed counts was wrong in both directions: two unnameable requests in one window and three in the other reported three rather than five, and a request the recovery pass resolved stayed counted as unnamed from the app log. Carry the identities instead of a count. Every CFNetwork line names its request as `Task <UUID>.<seq>`, scoped here to the emitting process, so the same request seen in two windows is recognisable as one. A merge unions the unnamed identities and subtracts anything either window managed to name, and a resolved reused request carries its identity as `packetId` so that subtraction has something to key on. `NetworkDump.unnamedRequests` becomes `unnamedRequestIds`, since a list of identities is what makes the reconciliation exact rather than a lower bound. Regressions cover disjoint windows, overlapping windows, and a request one window named while the other could not. * fix(network): keep unnamed-request identities out of the response `unnamedRequestIds` collected every unresolved task in the scan window and was spread straight into the response, so `network dump 1` could answer with thousands of task ids: an output whose size tracked the log rather than the requested entry limit. The identities exist to reconcile two scan windows, which is a step that finishes before a dump is returned. Keep them there. `NetworkDump` carries `unnamedRequests` as a count again, bounded by construction; the identities ride `ScannedNetworkDump`, the internal widening that the reader and the merge speak, and the Apple runtime projects them away with `withoutScanIdentities` on the way out. Reconciliation is unchanged: overlapping windows still collapse to one request and disjoint windows still sum, because the merge still sees the identities and recomputes the count from them. Regression: five unnameable tasks against `maxEntries: 1` reports all five and exposes no identity list. * fix(network): return scan identities beside the dump, not on it The Apple route stopped leaking task identities into its response, but Limrun and WebDriver return the scanner result directly and both serve Apple sessions, so an iOS `network dump 1` through either still answered with every unresolved task id in the scan window. Projecting at one producer was never going to hold: `ScannedNetworkDump` was assignable to `NetworkDump`, so returning the scanner result compiled everywhere and each producer had to remember not to. Take the shape away instead. `readRecentNetworkTrafficFromText` returns a `NetworkScan` — `{ dump, unnamedRequestIds }` — so identities sit beside the public dump rather than on it, and `mergeNetworkScans` reconciles the pair. A route returning `scan.dump` cannot carry them out, and a route that forgets does not compile. All four producers are updated; the response shape is unchanged. Regressions cover the Apple, Limrun and WebDriver routes: five unnameable tasks against `maxEntries: 1` report the count and expose no identity list. All three fail if the identities are put back on the dump. |
||
|
|
72ccf1a476 |
fix(maestro): use canonical deep-link classification for exports (#2463)
Co-authored-by: PLASMA-FR <173463847+PLASMA-FR@users.noreply.github.com> |
||
|
|
f4c8f3ddda | refactor(apple): carry one phase Deadline through the runner interfaces and test cancellation as a matrix (#2473) | ||
|
|
fea7ca8a43 |
chore(layering): runner modules reach host-kit only through the runner host port (#2470)
R77 apple-runner-host-port bans a direct @agent-device/host-kit/* value import from packages/platform-apple/src/runner/**; the port at runner/host.ts, bound in core/runner-host.ts, is the only door. runner/** sits in the eager closure of seven Apple facade entries eager-closure-budgets.ts holds at a fixed size, so a direct import grows all seven at once (#2423 measured one candidate import adding 5 modules to runner/index.ts's closure, 13 -> 18, after two review rounds spent rediscovering this). |
||
|
|
6d08de4609 |
feat(scroll): find off-screen targets in one command with --until (#2436)
* refactor(interaction): extract the scroll command runtime out of gestures.ts * feat(scroll): add --until <selector>, report honored travel, fix web amount units * test(scroll): cover --until through the provider-backed integration path * perf(selectors): keep the scroll-until predicate off the eager import path * fix(scroll): refuse an unreadable capture instead of reporting end-of-content * fix(selectors): keep the capture-readability check off the eager import path * test(selectors): use a declared snapshot quality state in the capture fixtures * fix(scroll): read the capture quality verdict under the spelling the backend uses * refactor(scroll): collapse --until onto the one route that runs it * refactor(scroll): drop unexported until types and duplicated guidance prose * test(scroll): fix the climbing fixture and drop duplicated route-level cases * refactor(scroll): delete the dead command-runtime executor and reuse canonical predicates * refactor(interaction): keep requireResolvedPoint local to the gesture runtime |
||
|
|
da76aa4f1e |
refactor(commands): declare project-config admission and recorder sanitization on the flag declaration (#2453)
* refactor(commands): declare project-config admission and recorder sanitization on the flag declaration Move the two fail-closed flag properties — may a key be set from a project `agent-device.json`, and does the session recorder copy it into `SessionAction.flags` — off the hand-maintained allowlists and onto each `FlagDefinition` as required `projectConfig` / `recorded` fields. Omitting either is now a type error, so the compiler holds the fail-closed property a list held by omission. - 156 declarations carry both fields; the 6 screenshot-specific definitions carry them too. Populated to match the old sets exactly (one-off diff empty: 85 project-config and 39 recorded keys, byte-for-byte). - `cli-config.ts` and `session-action-recorder.ts` derive their sets from the registry and no longer list keys; `RECORDED`/`PROJECT_CONFIG` derivations recomputed per call so a consumer builds its set at its own module load. Recorder reaches the derivation through the `cli-schema/command-schema.ts` seam (daemon may not import `commands/`). - Planted-divergence tests, per #2421: flipping one declaration's field moves the admission/sanitization outcome through the production derivation, plus a compile-time pin that an incomplete declaration does not build. - `docs/agents/cli-flags.md` now points at the declaration fields, not the allowlist. Refs #2445 * refactor(commands): return the recorded keys as a set, matching project-config Both derivations answer the same question — the set of flag keys a surface admits — so both return ReadonlySet<FlagKey>. Drops a needless set-then-spread on the recorder path; consumers already iterate the value. Refs #2445 * fix(commands): keep the CommandFlags guard on recorded flag declarations The deleted `SANITIZED_FLAG_KEYS` was `satisfies readonly (keyof CommandFlags)[]`, so every recorded key had to be a `CommandFlags` key. The derived set returns `FlagKey` and the recorder indexed it through a cast, so `recorded: true` on a CLI-only key (`daemonAuthToken`, `help`, …) compiled and could leak an uncarrable value into a recorded action. State the constraint on the declaration: `FlagDefinition` is a union that locks `recorded` to `false` for a `NonRecordableFlagKey = Exclude<FlagKey, keyof CommandFlags>`. `recordedFlagKeys()` returns `ReadonlySet<RecordableFlagKey>` via a narrowing predicate, so `sanitizeFlags` drops its cast. Adds a `@ts-expect-error` test that a CLI-only key cannot opt into recording. Refs #2445 |
||
|
|
2d109e5cc5 |
refactor(contracts): additive capability facts on the clipboard family (#2464)
* refactor(contracts): make clipboard capability facts additive Additive capability facts on the clipboard family (#2443, family 3). clipboardRuntimeOperationFacts took exactly two cells, read and write, and both are required. The halves stay separate claims — a WebDriver provider whose Appium clipboard extension exposes only a getter is a real owner with one half and not the other, and `clipboard read` must not be refused because the write half is missing — so both are optional and `unsupported` names the denial an unnamed half reports. A call naming no denial is still refused: omission is a classified refusal, never an unclassified half and never an implied success. Six of the eight owners drive both halves from one shell command set or one leaf gate, so they now state that once: web, HarmonyOS, and Vega name a single denial, and the shared unavailable record carries one clipboard cell where it carried two. The owners that serve clipboard keep naming both halves, and the one install-source double that enumerated the two keys by hand now goes through the family builder like every other construction site. Fact values are unchanged: every owner's clipboard cells are byte-identical, and the family's own test pins an unnamed half reporting the stated denial verbatim, an owner naming nothing answering with the exhaustive shape, and the freeze. * fix(contracts): derive clipboard family denials from the served cell Review follow-up on the clipboard family. Apple restated its leaf branches to choose a clipboard denial, so the leaf split lived twice in one file. It now reads the leaf's own refusal, keeping a leaf-scoped placeholder only where the leaf serves clipboard and therefore has no clipboard refusal to state. Android named a build-level shell verdict as its family denial, which is the claim its own probe refuses to make when the probe did not complete; the denial now follows the probed cell, so an unnamed half reports the unknown rather than a verdict. Pin the shared unavailable record's clipboard fan-out the way the keyboard commit pinned its own: one input cell, two operations, each with its reason. |
||
|
|
1131fb3118 |
refactor(contracts): additive capability facts on the gesture family (#2460)
* refactor(contracts): make gesture capability facts additive Additive capability facts on the gesture family (#2443, family 2). gestureRuntimeOperationFacts took one fact per tier, and it had five: plan, directionalFling, multiTouch, targetAuthoredDrag, viewport. The tiers split exactly where an owner's mechanics split — no tier is one every owner serves, and several owners serve none — so every tier is optional now and `unsupported` names the denial an omitted tier reports. The type still refuses a call that names no denial at all: omission is a classified refusal, never an unclassified tier and never an implied success, and an owner cannot leave the family blank. An owner states a tier only to say something the family denial does not. Web refuses every tier with one cell and names only the drag tier whose wording came from a retired closure. Vega names the two tiers whose refusals came from separate closures. Limrun's session-less and iOS branches name one denial each, where each wrote five. The owners that serve gestures keep naming what they serve: the win is the next tier, which an owner can now ignore entirely. Fact values are unchanged: every owner's gesture cells are byte-identical, and the family's own test pins an omitted tier reporting the stated denial verbatim, an owner that names nothing answering with the exhaustive shape, and the freeze. * fix(platform-apple): name the gesture family denial by leaf Review follow-up on the gesture family. Apple refuses gestures for two reasons depending on the leaf: watchOS and visionOS have no gesture surface at all, while every other leaf without a touch kind is the wrong device kind. One constant cannot be truthful for both, and the family denial is what the first unnamed tier will report, so it now follows the leaf. The Limrun gesture call collapsed to one source for the session-less branch, so pin the reason and hint it reports there rather than availability alone, and cover the directional-fling tier the loop had skipped. |
||
|
|
d80b021fa5 |
refactor(contracts): make keyboard capability facts additive (#2459)
The keyboard family's facts builder took one required cell per operation, so an operation only one owner implements still cost a hand-written denial in every other owner. The builder now takes the family's denial once, as `unsupported`, and every operation cell is optional: an owner names what it serves and omission reports that denial verbatim, with the reason and hint the owner would otherwise have repeated per cell. Omission stays a classified refusal, never an unclassified cell and never an implied success: `unsupported` is required, so a call that leaves the family blank does not compile. An owner that names every operation still states the family refusal, and it must refuse the family rather than one operation of it — whatever the owner leaves unnamed reports that cell verbatim. The shared unavailable-facts input collapses `keyboardStatus`/`keyboardDismiss`/ `keyboardEnter` into one required `keyboard` cell, so the owners that answered the family with raw keys (webdriver, vega, linux, limrun's no-session binding) now answer it through the builder, which is the family's one entry point. Facts are unchanged for every owner, so the per-platform admission assertions hold untouched. Two owner tests now assert the keyboard reason as well as the availability, because one family cell reports one reason for all three operations: the WebDriver inactive session and the stale Limrun identity. Refs #2443 |
||
|
|
bbd53d6c79 |
fix(ios): budget cold toolchain probes for the first-exec signature stall (#2423)
* fix(ios): budget cold toolchain probes for the first-exec signature stall xcodebuild/xcrun toolchain probes in cache-identity.ts and runner-cache-metadata.ts were budgeted for a warm toolchain (10s/5s), below the ~18-19s syspolicyd signature-verification stall on the first exec after a fresh macOS host boots. Share one 30s floor constant between both call sites and retry once after a timeout while the deadline allows, since the second exec is instant. Closes #2422 * fix(apple): duplicate the cold-toolchain-probe budget instead of a shared module toolchain-probe-budget.ts sat outside every platform-apple facade's eager closure, but runner-cache-metadata.ts (imported from it) sits inside all seven -- so the new import added one module to each, tripping the eager-closure-budgets no-growth gate (#2422). Delete the shared module. cache-identity.ts keeps the canonical constant inline (it was already outside the gated closures); runner-cache-metadata.ts declares its own copy, guarded by a new unit test that asserts the two stay equal. * fix(apple): bound the cold toolchain probes by the owning request budget Three synchronous probes could each retry once at 30 s, so a wedged toolchain host blocked a request for ~180 s with no deadline and no cancellation check. The runner cache decision now takes the owning request's budget (remaining ms + abort signal) and builds one clock per fingerprint read: every attempt runs at min(per-call ceiling, remaining), the retry is skipped once the budget is spent, an exhausted budget fails the decision without starting another probe, and an aborted signal surfaces the cancellation instead of retrying. `ensureXctestrunArtifact` passes the build budget and signal, session reuse passes the startup budget and the request signal, and lease adoption passes the startup budget; a caller with neither is still capped at 45 s total, so the worst case falls from ~180 s to 45 s. Error codes, texts, and the probe hint are unchanged. Both retry classifiers now read the exec layer's structured timeout detail instead of matching "timed out after Nms" in the message. The predicate is exported once from host-kit's command surface and reaches `runner-cache-metadata.ts` through the Apple runner host port, so the file's eager closure is unchanged. Tests use a fake clock that only advances when a probe actually blocks for the timeout it was given, so the exhausted-budget and cancellation cases have to spend the budget to pass; both consumers also pin that an error saying "timed out after 10ms" without the structured detail is not retried. Refs #2422 * fix(apple): spend one deadline across the toolchain probes and the step they precede The Apple runner cache decision runs up to three blocking toolchain probes before the step that needs the decision. Those probes were handed the phase's timeout and the step was then handed the same number again, so a cold-start probe stall added its 30 to 45 seconds on top of the phase budget instead of coming out of it. RunnerCacheProbeBudget now carries the phase's deadline rather than a timeout number, and each caller creates exactly one: - ensureXctestrunArtifact: the probes and xcodebuild read the same clock, and a phase with nothing left fails before the spawn. - ensureRunnerSession: the reuse probe spends from the startup clock, and the new session gets what it left. - tryAdoptRunnerSessionFromLease: the fingerprint probe spends from the caller's clock, and the adopted session gets the remainder. COLD_TOOLCHAIN_PROBE_TIMEOUT_MS now has one owner, core/config.ts. Snapshot source imports it; runner-cache-metadata reads it through the Apple runner host port, because core/config.ts is missing from one of the seven eager closures that evaluate that file and a direct import would grow it. createSnapshotSourceDeadline takes an injectable clock so a test can prove that a probe which blocked for its whole timeout leaves the retry only the remainder. * fix(apple): keep cancellation typed after a probe timeout and simplify the probe budget * fix(apple): check cancellation before the warm toolchain fingerprint cache * refactor(apple): guard each toolchain probe attempt in one place Fold the toolchain probe's three duplicated cancellation/budget guard sites (runToolchainProbe's pre-check, runToolchainProbeCommand's retry pre-check, and execToolchainProbeCommand's timeout computation) into one: attemptToolchainProbe checks cancellation and the remaining budget before every exec, first attempt and retry alike. The outer runToolchainProbe now rethrows cancellation and a spent budget instead of swallowing them into a probe failure, and only genuine probe errors become one. * fix(apple): keep cancellation typed when the final toolchain probe fails The guard fold left one gap: a request that aborts while the last probe is in flight and then fails with a non-timeout error has no next attempt whose guard could see the abort, so the catch classified it as an unreadable toolchain. The catch checks the signal again before classifying, as it did before the fold. * refactor(apple): own the toolchain probe budget in platform-apple and trim narration COLD_TOOLCHAIN_PROBE_TIMEOUT_MS moves from @agent-device/host-kit/command to runner/apple-runner-platform.ts, beside the SDK names the probes are run against. Both Apple toolchain probers import it directly, so the runner host port no longer carries a coldToolchainProbeTimeoutMs() accessor for a plain number. isCommandTimeoutError stays in host-kit, where the exec layer stamps the detail it reads. The comments that narrated control flow the code already shows are gone; the cold-start stall rationale (on the constant), the spawnSync cancellation limitation (on the probe clock) and one line per phase-deadline creation site remain. |
||
|
|
805ffb4690 |
fix(ios): budget snapshot bridge test compile from the build ceiling (#2454)
The host bridge unit test compiled the snapshot bridge under a fixed 45 s budget that a cold macOS runner trips on during the first `xcrun` (the signature-scan stall plus clang). Budget both `beforeAll` compiles from a deadline sized to production's build ceiling via `createSnapshotSourceDeadline` and `remainingSnapshotSourceMs`, so the unit lane is never stricter than the preparation path it mirrors. `BUILD_TIMEOUT_MS` is now `@internal`-exported as the single source of truth for that ceiling instead of a second magic number. Closes #2439 |
||
|
|
41e2633f10 |
fix(wait): retire native selector bypass and recover text observations (#2440)
* fix(apple): resolve selector waits through canonical capture * fix(wait): retire dead selector observations and recover native text failures |
||
|
|
8dd1f6c51a |
fix(check): honor Vitest worker configuration (#2437)
* fix(check): honor Vitest worker configuration * test(apple): freeze the default readiness budget clock --------- Co-authored-by: PLASMA-FR <173463847+PLASMA-FR@users.noreply.github.com> |
||
|
|
bd42b2602f |
fix(ios): serve regular --depth from every snapshot backend (#2431)
* fix(ios): serve regular --depth from every snapshot backend A regular depth-capped request was refused on every runner backend but the recursive tree: the query sweep past depth 1 and private AX at any depth returned no capture, so a plan pinned or deferred to private AX (custom actions, a private AX verdict on the session, the XCTest channel penalty) fell through to the synthetic sparse root, which the daemon then rejected as "regular iOS snapshot presentation requires a valid viewport". Presentation already applies the presented-depth cut to whatever hierarchy a backend acquired, and a depth-capped regular capture is a subset of the unscoped one from the same backend, so the refusal protected nothing the unscoped answer did not already disclose through truncated/effectiveDepth. Delete the gate, declare private AX as regular-depth=presentation-cut, and record the rule in ADR 0004. Closes #2403 * test(ios): prove a private-AX-pinned plan serves regular --depth through acquisition The presentation-package test passes with the old backend depth gate restored, because it calls presentation directly. This runner-bundle test pins private AX, asks for regular depth 1 against the launched host app, and requires the plan to reach acquisition and presentation: a private-ax verdict that is not sparse, more than one node, a real root rect, and a payload no larger than the unscoped capture from the same backend. With the gate restored the plan logs SNAPSHOT_BACKEND_DEPTH_UNSUPPORTED and returns the zero-rect sparse root, and the test fails. |
||
|
|
6ca66c9fad | fix(android): ease controlled scrolls within the requested duration (#2393) | ||
|
|
ad79461c72 |
refactor(commands): declare a command option once and derive its surfaces (#2421)
* refactor(commands): declare a command option once and derive its surfaces Collapses the pass-through hops the #2410 hop trace named, so an option is stated once and every surface reads that statement instead of a copy. - The snapshot option/flag pair (`customActions` <-> `snapshotCustomActions` and its seven siblings) is declared once in `@agent-device/kernel/snapshot` with both projections, generalising the shipped screenshot pair. The nine hand-written renames now read the declaration. - The dispatch pass-through flags are declared once as `DISPATCH_CONTEXT_FLAG_KEYS`; `DispatchContext` and the daemon's `contextFromFlags` both derive from it, so an identically-named flag no longer costs two files. The mapper keeps only what it decides. - The snapshot capture option keys are one type (`SnapshotCommandOptionFields`), referenced by the public SDK options, the internal request bag and the command runtime options instead of four copies. - An option's prose has one owner: its `FlagDefinition` now carries both audiences (`usageDescription` for --help, `inputDescription` for the tool and SDK field), and `optionField` derives the command's input field -- its JSON schema shape, bounds and description -- from that declaration. The duplicate doc comments in contracts are gone. Adds planted-divergence tests: a divergence planted in the one declaration must move the derived surface, which a second hand-written copy could not do. No help text, MCP tool schema or wire shape changes. * fix(contracts): re-pin SNAPSHOT_OPTION_FLAGS against CommandFlags The kernel declares SNAPSHOT_OPTION_FLAGS but sits below contracts and cannot name CommandFlags, so nothing pinned its values to real flag keys after the `satisfies Record<RoutedSnapshotOption, keyof CommandFlags>` guard on the old src/backend-snapshot-options.ts table was dropped. A misspelled or renamed flag key would compile and snapshotOptionsFromFlags would read `undefined` at every seam, silently dropping the option. Re-pin it in contracts, the lowest point that can name both. * fix(contracts): stop exporting the SNAPSHOT_OPTION_FLAGS pin The compile-time pin added against CommandFlags was exported, which the layering gate's facade-exhaustiveness check flagged: packages/contracts/src/ facades/command.ts re-exports command-flags.ts but omitted it, an accidental narrowing. The pin is not API -- nothing reads its value, it only exists to fail the build on a renamed or misspelled flag key. Keep it unexported and consume it with `void` so oxlint's no-unused-vars stays satisfied without widening the facade. * test(commands): plant the option divergence before the surfaces are built The planted-divergence tests called optionField after command metadata had already been constructed, so they proved only that optionField reads its own argument: a command that went back to a hand-written booleanField('...') would still have passed. The plant now lands on the option declaration in a fresh module graph BEFORE the builders run, and both derived surfaces -- the command metadata table and the MCP tool schema built from it -- are asserted against it. Also restores the public SDK documentation the branch dropped. AppOpenOptions .foreground and CaptureSnapshotOptions.customActions carry their JSDoc again: a .d.ts is read in an editor where no FlagDefinition resolves, and this repo has no step that generates those docs. The prose is the option's one declaration verbatim, and a unit test pins each SDK field's JSDoc to that declaration's inputDescription so the two cannot drift. * fix(kernel): stop bundling SNAPSHOT_OPTION_FLAGS' rationale into the public .d.ts Its JSDoc was internal refactor rationale, not SDK documentation, but rolldown-plugin-dts inlines it wherever CaptureSnapshotOptions resolves through SnapshotCommandOptionFields. Move it to a source comment (stripped from declaration output) and keep one short hover line. Cuts the packaged size growth from +3.3 kB/+2.2 kB to +2.3 kB/+1.8 kB (unpacked/download); no behavior change. |
||
|
|
edbd96ea30 |
refactor(contracts): additive capability facts pilot on touch family (#2420)
* refactor(contracts): make touch capability facts additive The touch family's facts builder took one required cell per operation, so an operation only one owner implements still cost a hand-written denial in every other owner. The builder now takes the family's denial once, as `unsupported`, and every operation cell is optional: an owner names what it implements and omission reports that denial verbatim, with the reason and hint the owner would otherwise have repeated per cell. Omission stays a classified refusal, never an unclassified cell and never an implied success: `unsupported` is required, so a call that leaves the family blank does not compile. Facts are unchanged for every owner, so the per-platform admission assertions hold untouched. Refs #2412 * fix(contracts): keep tap/longPress/fill required in touch facts Making every touch cell optional dropped the compile-time guard on the three operations every owner implements, with nothing replacing it: an owner could silently drop `tap` and nothing would fail. Only the truly per-owner cells (tapRef, hover, hoverRef, fillRef, tapElementSelector) need `unsupported` as their default. Restoring the required fields caught a real regression this refactor introduced: platform-web's touch facts had silently lost their explicit `longPress: readinessUnavailable` cell, so longPress reported the family's `unsupported` fallback instead of the owner's own readiness reason. Restored it, and fixed the other now-required-field call sites (the fully-unavailable owner fixtures, and the screenshot-runtime-fixture test double, which had also silently lost its longPress/fill distinction). Refs #2412 * fix(coverage): repoint hover evidence off the retired runtime.ts source line The touch family's additive-facts refactor dropped every owner's explicit hover: unavailable cell in favor of the family-level unsupported fallback, so the coverage manifests' literal-substring checks against packages/platform-apple/src/runtime.ts and packages/platform-linux/src/runtime.ts broke: that source line no longer exists. Point the macOS, iOS, tvOS, and Linux hover rows at new tests that assert the typed unavailable denial directly against each owner's bound facts, instead of re-adding a hand-written denial line the refactor was meant to retire. Refs #2412 * test(coverage): point hover evidence at the typed-denial tests in the declaration table |
||
|
|
52420d77e0 |
perf(ios): learn generation-scoped native depth hints in the host AX source (#2427)
The host source remembers the native levels a finished recovery accepted, keyed by resolved target id, app generation, and producer, and sends them as nativeLevelsHint so the guest's first request skips the known rejection. The hint changes request strategy only: delivered depth, node bounds, and completeness rules are unchanged. It is learned only after the delivered tree validated, expires after eight hinted captures, is never renewed by a hinted success, and never crosses apps, generations, or producers; explicit raw-depth requests neither use nor teach it. The route's generation circuit stays the only lifecycle owner. The guest reports its request accounting (requests, rejected, continuations, accepted levels) as recovery, which the host emits as the ios_snapshot_source_recovery diagnostic; the source version moves to v1.5.5. The shared recovery fixture gains the host column of every hint case and the hinted recovery cases, replayed through the source adapter itself. The test app gains a deep-tree screen that reproduces the native depth rejection for paired benchmarks. |
||
|
|
4d9e7ffe8f |
test(ios): add a shared AX recovery conformance fixture with a host adapter (#2425)
Add contracts/fixtures/ios-ax-recovery-conformance.json, a shared executable recovery contract for the host AX bridge and the XCTest runner's private AX bridge. Every expectation names the outcome, the native request accounting, and the delivered tree as a canonical preorder signature with its retained node count, so a producer that drops, duplicates, reorders, or re-parents nodes cannot pass as complete. The fixture records per-producer expectations and documents the intentional differences (depth vocabulary, ladders, frontier evidence, budgets, ownership and deadlines, hint lifetime). The host adapter is an Objective-C driver over captureSnapshotTree, compiled and run per case by native-runtime.test.ts. The runner adapter and the accepted-depth memory characterization follow in a stacked PR. |
||
|
|
0dfd65f6a2 |
perf(ios): speed up deep snapshots and keep first taps reliable (#2414)
* perf(ios): recover deep snapshots and isolate optional tap probes * chore(gates): enforce snapshot assets and optional probe lifecycle * fix(ios): preserve capture bounds and local probe recovery * chore(gates): validate base package assets with its own policy * chore(gates): verify recovery failures respect launch observation policy * fix(ios): fail closed on unknown snapshot frontier completeness |
||
|
|
4d7d9be21e |
fix(host-kit): resolve the code-signature root so a symlinked checkout stamps its own files (#2429)
walkDaemonCodeGraph resolved manifests through realpath but left the root and the entry as given. Under a symlinked prefix — macOS /tmp, a symlinked checkout — the two then sat on either side of the link: every workspace package was labelled by the route out of the repository and back in (../../../private/tmp/...), and isInstalledDependencyPath read every installed dependency as a workspace package and walked its whole closure. Route the root, the entry, and the manifest through one resolver, and give the cache the same resolved pair so a document's entry label still matches the walk that wrote it. The cache's private copy of that resolver goes away. The fixtures now build a resolved root, which is the shape a production caller passes (findProjectRoot derives it from an already-resolved import.meta.url). Naming a root through a link is covered on purpose instead, by two tests that fail without this change on any platform. |
||
|
|
342e98cff9 |
refactor(daemon): separate open-target policy from platform mechanics (#2416)
* refactor(daemon): separate open-target policy from Android mechanics Move resolveAndroidPackageForOpen/inferAndroidPackageAfterOpen behind the Android owning seam in packages/platform-android. resolveSessionAppBundleIdForTarget now lazily reaches Android mechanics itself instead of taking an injected resolver function, so open-prepare and selector-dispatch import only the neutral open plan/result surface from platform-runtime-open-target.ts. Reclassifies the two R74 inventory edges to daemon-policy-essential and updates ADR 0022. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167UVzrdzVMCZqXgxtzWoTD * refactor: address adversarial review findings on open-target seam Restore try/catch around the Android-mechanics lazy load so a module load failure still resolves to undefined instead of throwing. Rename the unrelated private resolveAndroidPackageForOpen in app-lifecycle.ts to requireAndroidPackageForOpen to remove the naming collision with the new exported function. Add a planted-violation regression test for reintroducing Android mechanics on the selector-dispatch edge. Tighten ADR/inventory wording that overstated which files consume the neutral resolver. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167UVzrdzVMCZqXgxtzWoTD * fix(platform-android): keep the mechanics facade lazy for the new open-target exports resolveAndroidPackageForOpen/inferAndroidPackageAfterOpen were re-exported statically from mechanics.ts, which eagerly evaluates open-target-resolution.ts on import and tripped the eager-closure-budgets gate (177 -> 178 modules). Wrap them as lazy async functions, matching the existing pattern used for listAndroidAppsWithAdb/captureAndroidLogcatWithAdb in the same file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167UVzrdzVMCZqXgxtzWoTD * fix(android-tools): keep inferOpenedAppBundleId best-effort on a mechanics load failure Loading Android mechanics moved from the near-infallible root platform-runtime-open-target.ts to the real adb-backed mechanics module, but the wrapper call stayed unguarded. A loader failure now throws instead of leaving the app-bundle identity unset, even for a targetless open that never needed the loaded module. Wrap the load and delegate in try/catch so it degrades to the current bundle id, matching the pre-refactor behavior, and add a regression test with the loader rejecting on a targetless open. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167UVzrdzVMCZqXgxtzWoTD * perf(android-tools): skip loading Android mechanics when app-bundle identity is known inferOpenedAppBundleId always loaded Android mechanics before delegating, even when currentAppBundleId already made the delegate's own fast-return a no-op. Check it first so the load is skipped entirely once the identity is already known, and add a regression test asserting the loader is never called in that case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167UVzrdzVMCZqXgxtzWoTD --------- Co-authored-by: Claude <noreply@anthropic.com> |