Skip to content

refactor(snapshot): assign and scope the Wave 4 shared presentation debt (#1739) #1983

Closed

Description

@thymikee

Outcome

Give the Wave 4 shared capture/presentation debt a named owner and an explicit boundary, so it stops
being the one unassigned block on #1739's parallel-safe track.

#1739 already states the case and the urgency:

Wave 4 shared capture debt remains and is still unassigned: the 10 production files under
src/daemon/snapshot-presentation/ios/, android-snapshot-freshness.ts,
android-snapshot-timeout-evidence.ts, screenshot-overlay-android.ts, and their
observation/interaction call sites. […] This is the one remaining block of work that is
file-disjoint from the descriptor units
, so it is the natural parallel track; it must be assigned
before Wave 5 migrates the freshness call sites in selector-capture-runtime.ts /
deferred-interaction-outcome.ts.

This issue exists to carry that assignment and its scope. It is not a new wave unit and does not
change #1739's denominator.

Why now — churn evidence

From a co-change analysis of the last 200 commits on main (2026-08-10 → 2026-08-23, HEAD
d1fc80f):

  • 37 of 200 commits (18%) are snapshot or presentation work.
  • 19 of those edit presentation logic in two or more zones.
  • 7 of the 21 Swift-presentation commits (33%) also edit TypeScript presentation in the same
    commit.
  • The heaviest file co-change pairs in the whole repository sit inside this cluster.

Presentation behavior currently lives in four places, with types-only sharing beneath them:

Location Lines Contents
packages/kernel/src/snapshot.ts 304 types only
apple/runner/… (Swift) ~10 files in-runner presentation
src/daemon/snapshot-presentation/ios/ 2,013 iOS presentation rules
src/platforms/android/snapshot-presentation.ts 277 Android fold
src/platforms/web/agent-browser-snapshot.ts 223 web fold

CONTEXT.md already specifies the intended end state — the clip fold as "the regular projection's
single visibility interpreter, run inside presentation for every backend […] Platform differences
enter as a fold policy, never as a backend exception." That interpreter does not exist yet. #1973
added cross-runtime presentation conformance tests, which detect divergence between the
implementations rather than removing the reason they diverge; that is the right interim instrument
and the wrong end state.

Structurally, src/daemon/snapshot-presentation/ios/ is iOS platform behavior inside the daemon —
the edge class #1739's terminal gate exists to reject.

Scope

Explicitly not in scope yet

Do not commit to a single implementation across Swift, Android and web. Swift and TypeScript sit
either side of a process and language boundary, and nothing measured here shows that shipping raw AX
trees across it preserves latency, payload size, fallback behavior, or backend-quality reporting.
The accepted outcome is:

One canonical presentation contract and policy model, with the fewest implementations the runtime
boundary permits.

Suggested sequence: consolidate the TypeScript folds and extract shared policy/vocabulary first, then
decide Swift-versus-host with payload and timing evidence in hand.

One measurement this issue does not yet have: the 277/223 line counts above are file sizes, not a
claim about how much of each is genuinely fold logic versus acquisition. The consolidation step
should measure that properly before choosing a target shape.

Blocked by / blocks

Activity

  1. thymikee commented on Aug 24, 2026

    @thymikee
    MemberAuthor

    Status refresh at 957a672 (2026-08-24): the ordering constraint this issue inherited from #1739 is now moot in one direction and overdue in the other.

    Still needed from this issue's own scope: a named owner and the package/facet boundary decision (consolidate the TypeScript folds first; Swift-versus-host stays evidence-gated).


    Generated by Claude Code

  2. thymikee commented on Aug 24, 2026

    @thymikee
    MemberAuthor

    Wave 4's three remaining policies now have neutral host seams, on branch claude/trusting-archimedes-0uljz6 (bc95d7f). Not closing this issue — the DoD's device evidence is outstanding, see the ask at the bottom.

    #2005 established the ownership boundary and moved the iOS presentation policies out of src/daemon/, but left freshness recovery, timeout evidence and screenshot-overlay policy behind their existing daemon adapters — ADR 0004 said so explicitly. That gap is now closed, so src/snapshot/ owns host-side snapshot policy generally rather than presentation alone.

    Freshness recovery. The window vocabulary, the Android staleness classification and its thresholds, and the retry loop move to src/snapshot/snapshot-freshness/. The loop is parameterized by a classifier and a retry schedule, so "how long may a backend lag behind a real transition" is a policy input rather than a constant the loop owns. src/daemon/session-snapshot-freshness.ts keeps only what genuinely needs a session — reading and retiring the window on store-owned SessionState, and choosing the comparison baseline from snapshot lineage — and remains the declared R7 owner of androidSnapshotFreshness. The two call sites #1739 named as the Wave 5 blockers, selector-capture-runtime.ts and deferred-interaction-outcome.ts, now reach freshness through the seam.

    Timeout evidence. Whether a failure is the accessibility-timeout shape becomes a policy in src/snapshot/snapshot-timeout-policy.ts. The published details.androidSnapshotTimeoutScreenshot payload becomes vocabulary in @agent-device/contracts/snapshot-timeout-evidence, built through constructors so an assembly site cannot publish a fifth, undeclared arm. It got its own subpath rather than riding the shared capture facade — routing it through capture added one module to eleven eager-closure budgets including src/cli.ts; the subpath left every budget untouched. Typed details, diagnostics and screenshot evidence are unchanged.

    Screenshot-overlay policy. Which Android nodes earn an overlay ref, and what rectangle an overlay covers, move to src/snapshot/screenshot-overlay/. That includes isAndroidUnlabeledClickableSource and its excluded-types table, which were sitting in screenshot-overlay.ts rather than in the -android file. The daemon keeps approved artifact and ref assembly only: ranking, projection to screenshot pixels, drawing and PNG IO.

    Boundary test. scripts/layering/snapshot-presentation-boundary.test.ts generalizes from the presentation subtree to the whole facet — nothing under src/snapshot/ may import src/daemon/ — and gains a positive control, because a filter that stopped matching would look identical to a boundary being obeyed.

    Residual call-site audit. direct-ios-selector.ts carries no presentation policy. isLocalIosRunnerSession and readSimpleIosSelectorTarget are session routing (device family, provider ownership, the stabilization window). deriveDirectIosNodeSelector and isDirectIosSelectorFallbackError are pure and daemon-independent, but they are selector derivation and ADR 0011 delegation-on-error — their owner would be the selector pipeline governed by R19, not this facet, so moving them here would widen ADR 0004 to a boundary it does not decide. Left in place deliberately; ADR 0004 records the finding so it does not have to be re-derived. The observation/interaction consumers all reach freshness through the facet or its session binding.

    Docs: ADR 0004's ownership section now describes the completed split and the audit; the CONTEXT.md glossary entry became Snapshot policy facet (kept inside the 12,000-byte budget).

    Verified locally: full unit suite 7,991 passed (2 failures, durable-capture-resource-adoption and app-log-session-resource, reproduce identically on unmodified main — they assert on chmod restrictions that do not bind root in a container); pnpm check:tooling green end to end, including layering (R7 owner table repointed, R11 now 108 subpaths), check:fallow, eager-closure budgets with zero deltas, and check:package.

    Ask for the reviewer — device evidence. check-affected selects every replay lane for this change (replay-ios, replay-ios-device, replay-macos, replay-linux, replay-android); the new facet paths carry no platform directory segment, so they are treated as shared runtime surface owning all lanes. All five are GitHub-authoritative and cannot run in a cloud session — there is no simulator, emulator or Xcode here. Could someone with hardware run them (pnpm test:replay:android, test:replay:ios, test:replay:ios-device, test:replay:macos, test:replay:linux), or push this to CI where those lanes execute? The Android lane is the one that actually exercises the migrated freshness, timeout-evidence and overlay paths. This issue stays open until that evidence is in.


    Generated by Claude Code

  3. thymikee commented on Aug 27, 2026

    @thymikee
    MemberAuthor

    Completed by #2005 and #2014. The snapshot presentation owner and shared policy surface are in place, the Android sibling work in #1832 is closed, and the focused presentation/conformance suite passes on current main. Separate Swift and TypeScript implementations remain intentionally pending measurement; that is outside this issue’s acceptance criteria.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions