Commit graph

2 commits

Author SHA1 Message Date
Lahfir
32175e44c5
fix: return observed trees and stop demanding renderer activation from shallow walks (#117)
* fix: return observed trees, stop demanding activation from shallow walks

Six defects found by measuring the observation and action paths against
real applications. Each was code computing something correct and then
discarding it.

Snapshot discarded a fully-observed tree when its budget expired. Finder
observed 247 nodes, threw away all of them, and returned TIMEOUT with
zero refs. It now returns what it observed with `complete: false`, and
every node whose descendants were cut carries `subtree_truncated`, which
propagates to its ancestors so a reader can walk from the root to the
boundary. Only a full snapshot may be partial: a drill-down replaces refs
inside an existing map, so it still requires a complete observation
rather than destroying descendants it may not be able to re-allocate.

`kAXErrorIllegalArgument` was classified as retryable. It is the window
bridge rejecting an element outright, which no amount of retrying will
change, so strict resolution burned its full budget on a call that could
not succeed and then blamed the application. Finder refs went from 0 of 9
resolving at ~814ms to 6 of 7 at ~120ms.

A depth-clamped observation was treated as evidence that a renderer had
no accessibility surface. A shallow walk stops above the web content by
design, so every skeleton snapshot of a Chromium application demanded an
activation it did not need and re-walked the tree until the deadline
expired. Slack `--skeleton` went from failing at 3.4s to 0.2s, and depth
now scales monotonically instead of shallower being slower than deeper.
The retry that follows a genuine activation also backs off, because each
attempt costs a full tree walk: a 3s budget spent about 120 of them and
now spends 11.

A boundary node is read for its child count alone. On a renderer that
materialises children lazily that count can cost more than the traversal
it describes, so it is now best-effort; a boundary that cannot afford one
is still reported as truncated, just without a number.

Refmap retention kept 512 snapshots and swept every one of them for
orphaned temporary files on every save. Retention is now 128 with
eviction to 96 so the sort-and-stat pass is amortised, the per-save sweep
covers only directories that save could have written, and the exhaustive
sweep runs with eviction. `session end` drops the ref scaffolding it
accumulated, but only under `ArtifactsMode::Full` where the trace keeps
its own copy; the default mode never copies them, and discarding them
there would sever snapshot resolution for anyone reading the trace after.

`is --property` read element bounds for properties that never use them.

BREAKING CHANGE: ENVELOPE_VERSION is now 2.2. `data.complete` is present
on every successful snapshot, and a snapshot that exhausts its budget
returns `ok: true` with `complete: false` where it previously returned a
TIMEOUT error. Callers that branched on TIMEOUT to detect an oversized
tree must read `complete` instead.

* docs: correct solution docs that contradicted the code and each other

A refresh pass over docs/solutions/ against the current tree. Three of
the four corrections were internal contradictions that reading the doc
alone could not reveal.

The pointer-action doc cited `resolve_point_with_deadline`, which has no
matches in the repository; hover and drag resolve in two phases, before
and under the interaction lease. It also now distinguishes the two
hit-tests that exist, since the shared battery's multi-candidate check
has grown to cover the click family while the pointer pipeline keeps its
own single-point check.

The drag-abort doc claimed the release guard "arms only after mouse-down
is posted". It arms before, which is what the doc's own prevention rule
requires and what the code does.

The progressive-snapshot contract described one truncation path. There
are two, and the second was silently dropping descendants with no marker
at all, violating that doc's own rule.

CONCEPTS.md gains Interaction Lease, Partial Observation and Delivery
Semantics, and corrects Interaction Policy, which claimed ref commands
expose exactly two modes when a third is the base policy of an explicit
key press and is directly selectable by language bindings.

* fix: only discard refmaps the trace actually copied

Being in ArtifactsMode::Full was treated as proof that every refmap had
been duplicated into the trace, so ending a session deleted the whole
snapshot store. It is not proof. The artifact byte budget rejects a copy
once a session's refmaps exceed it, and a serialisation failure skips one
too; both report success to the caller. A long full-artifacts session
therefore holds snapshots whose only refmap lives in the store, and
ending it destroyed them permanently, severing snapshot resolution for
anyone reading that trace afterwards.

Each snapshot directory is now removed only against its own duplicate in
the trace, and the latest-snapshot pointer survives unless the snapshot
it names is gone.

The existing full-artifacts test encoded the defect: it seeded a snapshot
with no trace copy and asserted the store was emptied. It now seeds the
copy it claims exists, and a new test covers the case that was losing
data — a refmap the trace never copied must survive.
2026-08-02 01:02:50 -04:00
Lahfir
3f322728b4
feat!: implement Playwright-grade foundation contract
Settle the Playwright-grade reliability contract in agent-desktop-core
before the Windows/Linux adapters are built, so they inherit it instead
of redesigning it. Every command now observes, waits, verifies, and
reports honestly instead of firing blindly.

Highlights: capability-supertrait split of PlatformAdapter with
not_supported() defaults; canonical role/state vocabulary with live
`is --property visible`; display enumeration (`list-displays`) and honest
`--screen` with scale factor; truthful Automation permission; `native_id`
identity spine; window-id-first resolution; serializable `LocatorQuery`
with live `find`; default-on auto-wait before every ref action; three-way
`hit_test` occlusion gate; `scroll_into_view` in core; core accessible-name
precedence; typed `ActionStep` delivery tier; `ProcessState` and
`APP_UNRESPONSIVE`; `LaunchOptions`; baseline-diff desktop signals
(`wait --event`); typed clipboard (`Text`/`Image`/`FileUrls`); mouse
modifier chords and `mouse-wheel`. Hardened through a 35-reviewer pass with
independent validation and a green live e2e gate (109/0), plus a
head-vs-main performance comparison harness.

BREAKING CHANGE: default-on auto-wait changes the timing of every
previously-untouched ref-action call (bounded 5000 ms default; `--timeout-ms 0`
restores single-shot). `ENVELOPE_VERSION` is now `2.1` (adds the
`APP_UNRESPONSIVE` code and process state in error details). FFI ABI major
is `3` (append-only struct evolution; `wait --event` is intentionally not
exposed over FFI). The legacy string clipboard API is removed in favor of
typed content. `key-down`/`key-up` fail closed until daemon-owned held input
exists. `close-app` verifies termination and the osascript fallback path is
removed. `--text` matching is subtree containment: `find --text X --first`
returns the outermost matching container.
2026-07-20 00:21:38 -07:00