* feat: expose web network dump through agent-browser
* fix: reduce web network mapper complexity
* fix: gate web network headers by include mode
* test: assert compact web network summary
* refactor: simplify web network dump mapping
* refactor: trim web network coverage
* refactor(ios): snapshot capture plans with a structured quality verdict
Implements ADR 0004's explicit-strategies decision as architecture
(candidates 1+2 of the snapshot pipeline review):
- Snapshot backend seam: three adapters (recursive tree, query sweep,
private AX) behind one captureWithBackend dispatch. Each strategy
declares its chain as data (regular: tree→queries→private-ax,
compact: queries→private-ax, raw: tree→private-ax) and one plan
runner walks it under a 20s umbrella budget so chained recovery can
never stack past the 30s main-thread watchdog. Terminal policy is
per-plan: raw rethrows AX failures (diagnostics preserve errors),
interactive fails closed with runnerFatal invalidation.
- Single quality classifier: one sparsePayloadReason predicate (with
reason codes), one collapsed-leaf detector, replacing the three
divergent sparse detectors (Swift structural, daemon count==1, CLI
count<=3) that each patched a different failure shape.
- Structured snapshot quality verdict on the wire (state, backend,
reason, reasonCode, effectiveDepth, collapsedLeafIndexes): the daemon
and CLI render warnings from it instead of re-deriving degradation
from node shapes; budget starvation is no longer blamed on the app's
accessibility. Legacy runner messages and daemon-side detectors stay
behind a verdict-absent gate for mixed-version compat.
- The verdict surfaces in --json (snapshotQuality) for agents; the
generic sparse CLI hint is suppressed when a verdict explains it.
Threading the verdict exposed two more hand-copy field drops
(captureInteractionOutcomeAwareSnapshot, serializeSnapshotResult,
client response mapping) - now carried alongside warnings everywhere.
Verified live: Settings healthy (tree, no warnings), Settings compact
under load (recovered/private-ax/budget), production login (sparse
best-effort with honest warning), collapse fixture (healthy +
collapsedLeafIndexes -> @ref warning), Bluesky Home (recovered/
private-ax, 24 nodes in 2s). Full unit suite 2327 passed, fallow clean,
runner builds.
* fix(ios): correct recovered-snapshot viewport and private-AX scope semantics
Review follow-ups on the capture-plan refactor:
- The query-sweep synthetic root doubles as the daemon's viewport
(find.ts prefers on-screen matches inside nodes[0].rect), but it was
built from candidate bounds, so off-screen controls below the screen
could inflate it and win duplicate-label resolution. The root now
uses the real finite viewport, falling back to candidate bounds only
when viewport capture failed.
- The private-AX backend applied --scope as a per-node text filter,
hiding the matched container's children — diverging from regular
snapshot scope semantics and contradicting the depth-cap hint that
recommends scoped re-runs. Scope now selects the matched subtree:
descendants inherit the match and only the normal option filters
apply to them (in-bundle test covers a non-matching descendant).
Verified live on Bluesky Home: scope homeScreen returns the 52-node
subtree including non-matching descendants; compact root rect equals
the screen (0,0,402,874).
* fix(ios): fail closed on interactive AX failure, stamp fatal verdict, validate parser
Three review findings on the capture-plan terminal path:
- P1: the fail-closed guard required `best == nil`, but the query-sweep
tier always returns a synthetic-root sparse payload that sets `best` —
so an interactive recursive-tree AX serialization failure that no
backend recovered returned a sparse snapshot instead of invalidating
the cached target. Reaching the terminal already means no backend
produced a usable tree, so the sparse `best` must not suppress the
fail-closed path. Extracted the decision into a pure, unit-tested
`resolveSnapshotPlanTerminal` (closes the terminal-ordering testability
gap the architecture review flagged).
- P2: `snapshotAccessibilityUnavailable` returned a payload with no
`snapshotQuality`, leaving one planned sparse result on the
legacy-message path. It now carries a sparse/ax-rejected verdict like
every other planned snapshot, so downstream sparse handling keys off
the verdict.
- P2: `readSnapshotQualityVerdict` cast any string state/backend into the
union, so a malformed object suppressed the legacy node-shape
detectors. State and backend are now validated against their unions
(unknown → verdict-absent → legacy detectors run); an unknown
reasonCode is dropped rather than rejecting the whole verdict, so a
forward-version runner still yields a usable verdict.
Unit-covered: Swift resolveSnapshotPlanTerminal matrix + fatal-verdict
assertion; TS parser accept/reject/forward-compat. Full suite 249 files
/ 2449 tests, fallow, lint, runner build green.
* refactor(ios): consolidate series batching onto the sequence runner command
Closes#767
Routes every Apple multi-press variant (plain, double-tap, hold, jitter)
and swipe series through budget-chunked sequence requests, retiring the
daemon-side tapSeries and dragSeries senders:
- Add a doubleTap step kind to the sequence allowlist on both ends,
mirroring the retired tapSeries doubleTapAt branch.
- The single doubleTap interactor sends a one-step sequence and parses
the result, surfacing step failures as errors.
- Swipe series unroll ping-pong daemon-side into per-step endpoints;
the runner's coordinate-drag path ignores durationMs exactly as the
daemon-sent (non-synthesized) dragSeries did.
- Extract runIosSequenceChunks so press and swipe share the chunking,
aggregation, and global step-index rebasing.
- Keep tapSeries/dragSeries runner handlers for wire compatibility with
older daemons, annotated like interactionFrame; remove both from the
preflight-skip allowlist (daemon never sends them) and update ADR
0005 / protocol-optimizations docs.
This also closes the latent watchdog exposure where press --count N
--interval-ms M routed to tapSeries and executed all pauses inside one
30s-watchdog main-thread block with no chunking.
Behavior note: plain tap series now use the synthesized HID tap path on
iOS non-tv (with runner-side tapAt fallback), matching the individual
tap command instead of the retired tapSeries' XCUICoordinate taps.
https://claude.ai/code/session_01VokBZWESTDgcnbYwS4DkJo
* refactor(ios): drop dead series wire surface from the daemon
- Remove chunkRunnerSequenceSteps: superseded by the budget-aware
chunker; no production callers remained.
- Remove tapSeries/dragSeries from the RunnerCommand union along with
their orphaned fields (count, intervalMs, doubleTap, pauseMs,
pattern) and protocol fixtures: this type is the send surface of the
current daemon, which no longer sends either command. The Swift
runner keeps serving both for wire compatibility with older daemons.
- Retarget the ready-mutation preflight test from tapSeries to
sequence.
https://claude.ai/code/session_01VokBZWESTDgcnbYwS4DkJo
* refactor(ios): remove retired series and frame wire commands entirely
Drops the runner-side wire compatibility for tapSeries, dragSeries, and
interactionFrame now that no daemon path sends them (series fuse into
sequence since this branch; interactionFrame was fused into scroll in
#760):
- Swift: delete the three handler cases, performDragSeries, runSeries
(no remaining callers), the CommandType enum cases, journal-retention
and traits entries, and the Command fields (count, intervalMs,
doubleTap, pauseMs, pattern) that existed only for them. The
never-sent synthesized dragSeries branch goes with it.
- TS: drop interactionFrame from the RunnerCommand union and
isReadOnlyRunnerCommand, and its protocol fixture.
- Update stale perf scenario labels referencing the retired commands.
Verified dead before removal: no dynamic command construction anywhere
(runner-command-recovery only echoes in-flight command ids), no
raw-string references in Swift, no docs references. Helpers shared with
live paths (synthesizedDragAt, doubleTapAt, keyboardAvoidingDragPoints,
sleepFor) all retain callers.
Compat: an old daemon paired with a runner built from these sources
gets a CommandType decode rejection; the source-fingerprint check
rebuilds a matching runner on the next session.
https://claude.ai/code/session_01VokBZWESTDgcnbYwS4DkJo
---------
Co-authored-by: Claude <noreply@anthropic.com>
* perf(ios): add lifecycle-safe runner sequence command for hot press series
Adds a narrow 'sequence' runner command that batches an explicit
allowlist of coordinate steps (tap, longPress, drag) into one
lifecycle-tracked request with stop-on-first-failure and small bounded
per-step results. iOS press series with hold/jitter now issue one
sequence request per ~20-step chunk (also budgeted to stay under the
runner's 30s main-thread watchdog) instead of one request per press.
Sequence responses are journaled and retained, so lost-response recovery
returns observed results without replaying the gesture sequence.
Closes#669
* fix: perform every press in direct press series
runDirectPressSeries guarded the awaited interaction itself with ??=,
so presses 2..N were silently skipped once the first result was kept
(affects Android series and doubleTap series; introduced in #512).
The kept-first-result shape is preserved.
* chore: unexport internal sequence chunk budget constant
* perf(ios): make sequence eligible for readiness preflight skip
Rebased onto main with #763 (healthy-mutation preflight skip) and #760
(fused scroll). Per the merge-order note, add 'sequence' to
PREFLIGHT_SKIP_ELIGIBLE_RUNNER_COMMANDS so a successful sequence earns
the next hot-command skip instead of always taking the
conservative_command path. Extend the per-family skip tests and the
allowlist enumeration in ADR 0005 and the protocol-optimizations doc.
https://claude.ai/code/session_01VokBZWESTDgcnbYwS4DkJo
---------
Co-authored-by: Claude <noreply@anthropic.com>
* perf(ios): fuse scroll frame resolution and drag into one runner command
Non-tvOS scroll now sends a single mutating 'scroll' runner command. The
Swift runner resolves the interaction frame and executes the same
non-synthesized drag path, eliminating the separate read-only
interactionFrame request per scroll. The command is lifecycle-journaled
with retained response JSON so lost-response recovery returns the result
without replaying the gesture.
Closes#668
* perf(ios): make fused scroll eligible for readiness preflight skip
#763 landed the healthy-mutation preflight skip with a note that the
fused scroll command should join the allowlist once it exists. Add
'scroll' to PREFLIGHT_SKIP_ELIGIBLE_RUNNER_COMMANDS, drop the
now-resolved code note, extend the per-family skip tests, and update
the allowlist enumeration in ADR 0005 and the protocol-optimizations
doc.
https://claude.ai/code/session_01VokBZWESTDgcnbYwS4DkJo
* test: complete scroll plan parity vector mirror
Address review on the cross-language parity vectors:
- mirror the Swift pixels-plan vector (down, 120px @ 300x600) in the
vitest suite so every vector exists in both languages
- add amount > 1 clamp and tiny-frame (2x2) vectors to both suites;
the tiny frame engages every max(1, ...) floor and the .5 rounding
cases where JS half-up and Swift half-away-from-zero must agree
https://claude.ai/code/session_01VokBZWESTDgcnbYwS4DkJo
---------
Co-authored-by: Claude <noreply@anthropic.com>
Reintroduces the #662 adaptive readiness-preflight skip with guardrails
for the #702 failure modes. Recency is recorded only from healthy
(non-runnerFatal) responses to an explicit mutating-interaction allowlist
(tap, tapSeries, longPress, drag, dragSeries, swipe), scoped to the same
appBundleId, capped at a 5s freshness window, and lives on the session
object so it dies with every invalidation. Startup, no-recent-success,
stale, app-switch, and non-allowlisted commands still preflight. A
transport failure after a skip clears recency, carries the skip context
through status recovery, and never routes into restart-and-replay.
Closes#667