Electron Stagewrightdocs

ADR-005: Snapshot schema v1 + renderer-injected tool layer

Context

Agents need a structured, low-token view of the renderer to decide what to do — roles, names, state, bounding boxes, and stable handles (refs) they can act on. The pure snapshot module (walker, accname, fingerprint, diff, reconcile, find) shipped earlier as framework-agnostic functions over a Document. What was deferred was the tool layer: running that walker inside the Electron renderer and threading per-session state so an agent can call snapshot({ since: 'last' }).

Decision

  1. Schema (v1)Snapshot is { schemaVersion, entries[], meta }. Each SnapshotEntry carries ref (number | null), role, name, state envelope, bbox, fingerprint, interactive, recently_changed. Refs are matched across snapshots by fingerprint (reconcileRefs) for stability. Everything is plain JSON (no Map/Set/Date) so it round-trips to the agent.

  2. Renderer injection by bundle, not hand-serialisation — the walker plus its helpers are bundled (esbuild, IIFE) into dist/snapshot/injected-walker.js. The electron_snapshot / electron_find tools read that artifact and inject it via session.evaluate('renderer', …), which installs globalThis.__stagewrightWalk. Serialising the walker with Function.prototype.toString was rejected: it drops the function's imported dependencies. The bundle keeps the walker the single source of truth.

  3. Ref resolution by data-sw-ref tagging — during the renderer walk, each interactive element that receives a ref is tagged data-sw-ref="<ref>" (via the walker's refAttribute option). A later interaction tool resolves ref: N to the [data-sw-ref="N"] selector for real input. The tag is a renderer-only DOM mutation; it is re-applied each walk and gone after a reload.

  4. Stateful since:'last' — a per-session SnapshotStore holds the last FULL (unfiltered) snapshot. since:'last' returns the delta (diffSnapshots

    • recently_changed); a detected reload (detectRendererReload) forces a full snapshot and sets renderer_reloaded. interactiveOnly / maxEntries filter only the RETURNED snapshot, never the stored baseline (so diffs stay accurate).

Alternatives considered

Alternative Why rejected
Hand-serialise the walker via Function.prototype.toString Drops imported helpers; fragile and duplicative.
Ship refs with no DOM resolution Decorative — interaction could not act on a ref.
A separate snapshot_diff tool ADR-007 P7: diff is a parameter (since:'last'), not a second tool.
Store the filtered snapshot as the diff baseline Produces spurious added/removed entries when filters differ between calls.

Consequences

Status Update — 2026-06-10

snapshot({ since: 'last' }) now defaults to a COMPACT diff encoding at the tool layer; the wire Snapshot shape and schemaVersion: 1 are unchanged.

Status Update — 2026-07-10: marker-first renderer installation

The original version marker prevented the renderer from executing the walker bundle more than once per document, but the server still sent that bundle with every snapshot, find, read, probe, and live similar-ref walk. The injection protocol now has two paths:

The sentinel is consumed inside the server helper and never changes a tool's public response. Snapshot schema v1, ref reconciliation, DOM retagging, error mapping, and Injector's renderer-evaluation boundary are unchanged. The marker is memoized alongside the cached bundle on the server, while its validity is always checked against the actual renderer global, not session-local state.