Repository navigation
refactor(snapshot): assign and scope the Wave 4 shared presentation debt (#1739) #1983
Description
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 inselector-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
- one named owner;
- one explicit package/facet boundary, with presentation vocabulary moved out of
src/daemon/; - golden conformance tables under
contracts/fixtures/as the shared proof; - Swift acquisition separated from presentation;
- Android native-a11y → snapshot: conform to the acquire/present engine contracts (sibling of #1797) #1832 folded into this architectural track rather than progressing as an independent sibling.
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
- Part of refactor(daemon): platform-free daemon — broader migration waves (successor to #1696) #1739 (Wave 4 shared capture debt). Blocks Wave 5's migration of the freshness call sites.
- Related: Android native-a11y → snapshot: conform to the acquire/present engine contracts (sibling of #1797) #1832 (Android sibling), iOS snapshot backends: make interpretation divergence impossible by construction (acquire/present split) #1797 (closed — the iOS acquire/present split), test(snapshot): add cross-runtime presentation conformance #1973
(cross-runtime conformance tests).
Activity
refactor(snapshot): type the acquisition producer beside the platform…
refactor(snapshot): type the acquisition producer beside the platform…
refactor(snapshot): type the acquisition producer beside the platform…
thymikee commentedon Aug 24, 2026
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.selector-capture-runtime.ts:13anddeferred-interaction-outcome.ts:13still importandroid-snapshot-freshness.ts. So "must be assigned before Wave 5 migrates them" no longer gates anything; the assignment is simply late, and this debt is now the largest un-owned daemon→platform edge cluster outside the named Wave 6 units.src/daemon/snapshot-presentation/ios/currently holds 8 production modules plus tests/fixtures (18 files), andandroid-snapshot-freshness.ts,android-snapshot-timeout-evidence.ts,screenshot-overlay-android.tsanddirect-ios-selector.tsare all still present.SnapshotProduceraxis explicitly as this track's types-before-semantics step, giving the host-side presenter wiring and double-presentation removal something to key on.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
refactor(snapshot): type the acquisition producer beside the platform…
thymikee commentedon Aug 24, 2026
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, sosrc/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.tskeeps only what genuinely needs a session — reading and retiring the window on store-ownedSessionState, and choosing the comparison baseline from snapshot lineage — and remains the declared R7 owner ofandroidSnapshotFreshness. The two call sites #1739 named as the Wave 5 blockers,selector-capture-runtime.tsanddeferred-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 publisheddetails.androidSnapshotTimeoutScreenshotpayload 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 sharedcapturefacade — routing it throughcaptureadded one module to eleven eager-closure budgets includingsrc/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 includesisAndroidUnlabeledClickableSourceand its excluded-types table, which were sitting inscreenshot-overlay.tsrather than in the-androidfile. 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.tsgeneralizes from the presentation subtree to the whole facet — nothing undersrc/snapshot/may importsrc/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.tscarries no presentation policy.isLocalIosRunnerSessionandreadSimpleIosSelectorTargetare session routing (device family, provider ownership, the stabilization window).deriveDirectIosNodeSelectorandisDirectIosSelectorFallbackErrorare 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.mdglossary 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-adoptionandapp-log-session-resource, reproduce identically on unmodifiedmain— they assert onchmodrestrictions that do not bind root in a container);pnpm check:toolinggreen end to end, including layering (R7 owner table repointed, R11 now 108 subpaths),check:fallow, eager-closure budgets with zero deltas, andcheck:package.Ask for the reviewer — device evidence.
check-affectedselects 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
Extract snapshot policy from daemon to host-side facet (#1983) (#2014)
refactor(apple): colocate the XCUITest runner client into packages/pl…
docs(adr): model the runner subtree as a durable platform-owned facet
refactor(apple): colocate the XCUITest runner client into packages/pl…
thymikee commentedon Aug 27, 2026
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.