fix(android): stamp status-bar chrome during the walk instead of reconstructing it downstream (#1319)#1359
Conversation
… to --settle (#1319) #1319 asked whether the systemui run-condemnation rule leaves `--settle` blind to a fully expanded quick-settings shade, the way it blinded the replay divergence in #1318. It does not, and this pins the reason. The settle loop captures `interactiveOnly: true` (`stable-capture.ts`). That walk drops the structural systemui window spine (`legacy_window_root`, `notification_panel`, `qs_frame`, the quick-settings ComposeView chain) — and those are exactly the nodes that merge the shade into ONE contiguous run in the `--raw` / non-raw shape #1318 measured. Under settle's shape the same capture arrives as five runs, only `split_shade_status_bar` carries a marker, and the 29 quick-settings nodes survive into both diff sides. So settle and divergence do not read one tree differently, as #1318 framed it: they consume different capture shapes, and settle already gets the outcome that layer wants — shade content diffs, status-bar churn does not. Separately, the quiet-detection loop digests the UNFILTERED capture (`digestSnapshotNodes` in `stable-capture.ts`), so the shade would reset the quiet window even in the hypothetical where chrome stripping had emptied the diff. Both halves of the question come out clean. No product change. The behavior is correct but rested on an untested structural coincidence: retaining the systemui spine in the interactive walk would re-merge the runs and make `--settle` report a full-cover shade as bare removals with no added content and no hint. The test fails if that happens (verified by flipping the helper to `interactiveOnly: false`). Verified live on emulator-5554 (Pixel 9 Pro XL API 37, deskclock), both directions #1319 asked about and both halves the test asserts: - shade OPENING mid-settle: settled after 6917ms: +28 -25 (added: brightness seekbar, Wi-Fi/Bluetooth/Mobile data/Quick Share/ Modes/Wallet tiles) - shade CLOSING mid-settle: settled after 6919ms: +25 -28 (mirror image) - the shade's own status bar ("Tue, Jul 21, Wifi signal full., T-Mobile") is absent from both settle diffs while `snapshot -i` of the identical screen still lists it — the run rule filters the churn rather than sitting inert. The archived #1318 capture run through the real interactive-only walk reproduces the live tree node-for-node (35 nodes, 29 kept, 6 stripped).
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
|
Coordinator review: clean at The #1319 regression is non-vacuous: it drives the archived Quick Settings capture through production All completed checks are green; iOS smoke is still in progress. No code findings. |
…g is ruled out on device Follow-up to the #1319 investigation, acting on review feedback that a paragraph justifying fragile behavior means the code is wrong. Two candidate fixes for the capture-shape-dependent chrome classification were tested, and BOTH are dead. Recording that here so the next person does not re-walk them: 1. The window-type approach this file's own TODO proposed. Measured against the live helper XML (emulator-5554, Pixel 9 Pro XL API 37): systemui reports `window-type=3` (TYPE_SYSTEM) both collapsed and expanded, `TYPE_STATUS_BAR` (2000) never appears, the helper stamps window metadata on window ROOTS only (1 of 169 nodes in an expanded-shade capture), and an expanded shade is ONE window hosting the status icons AND the quick-settings tiles. No window-level signal separates them. The TODO promised a fix the device data rules out. 2. Replacing run-condemnation with "condemn each marked node's subtree plus fully-condemned ancestors". Shape-independent as intended, and it keeps the tiles in both walks — but on the COLLAPSED status bar it leaks the unmarked chrome the walk re-parents next to the markers ("Battery 100 percent.", the notification-icon summary, neither carrying any resource-id). That is the ticking-clock regression #1319 explicitly warned against. So run-condemnation is not a lazy workaround: it reconstructs identity the walk already discarded when it drops the `status_bar*` wrappers and re-parents chrome leaves next to content. That upstream information loss is the actual root cause, and a provenance-preserving walk is the real fix — larger than this PR, and it would let #1318's divergence fallback be revisited. Also trims the #1319 test's doc comment from a long justification of the current behavior down to the finding, the defect, and what the test holds in place.
Review feedback applied: the long comment was the smellPushed 1. The window-type approach — this repo's own TODO promises something impossible
2. Subtree-condemnation instead of run-condemnationThe obvious fix: stop letting one A subtree rule keeps the battery percentage and the notification-icon summary — churn on every capture. That is precisely the ticking-clock regression #1319 warned against. What this meansRun-condemnation is not a lazy workaround. It is reconstructing identity the walk already discarded: The real fix is a provenance-preserving walk: stamp descendants of a Diff changes since first push
No behavior change; still test + comments only. Lint, typecheck, format and the touched test file all pass. |
…nstructing it downstream Chrome classification gave opposite answers about the same screen depending on capture shape: an expanded quick-settings shade was 100% chrome under the `--raw`/non-raw walk (#1318 — every tile condemned, needing a divergence-local fallback) and ~17% chrome under the interactive-only walk (#1319). Two layers were papering over one broken classifier. Root cause: `shouldIncludeStructuralAndroidNode` drops the `status_bar*` / `navigation_bar*` containers — the only nodes that identify the region — and re-parents their leaves next to real content. Everything downstream was reconstructing identity the walk had already discarded, and reconstruction is what depended on shape. Fix: record it while it still exists. `walkUiHierarchyNode` threads `ancestorSystemChrome` exactly like the `ancestorHittable` it already carries, and stamps `systemChrome` on the emitted node; `androidUiNodes` tracks the same subtree for the streaming content-recovery pass. Classification is then per node and intrinsic, so `--raw` and non-raw agree by construction. This deletes what was compensating for the loss: - the 18 hand-picked marker leaf ids and their justification comment (what enumerates them is "descendant of a status/nav-bar container" — now stamped); - `collectAndroidSystemChromeRunIndexes`, the run-condemnation rule that let one `clock` node condemn 95 unrelated ones; - the leaf-id/prefix split, replaced by one container predicate that matches `status_bar`/`navigation_bar` as an id SEGMENT so the shade's own `split_shade_status_bar` counts. Net −53 lines across 8 files, almost all of it classifier and prose. Two alternatives were measured and rejected before this one (both recorded in #1319 so they are not re-attempted): - keying off AOSP window types, which this file's own TODO proposed: impossible. On a live device systemui reports `window-type=3` (TYPE_SYSTEM) collapsed AND expanded, `TYPE_STATUS_BAR` never appears, metadata is stamped on window roots only (1 of 169 nodes), and an expanded shade is ONE window holding the status icons and the tiles. - condemning marked subtrees plus fully-condemned ancestors: leaks the unmarked chrome the walk re-parents beside the markers ("Battery 100 percent.", the notification-icon summary) — the ticking-clock regression #1319 warned about. Test fixtures that modelled the old mechanism now model the device instead: the synthetic status bar gets the `status_bar_launch_animation_container` root the real capture has, and the content-recovery XML nests its chrome leaves inside their container. Assertions were not weakened — settle.test.ts stamps via the production predicate, and the #1319 test now asserts both walks classify IDENTICALLY, which is the property that was missing. Verified live on emulator-5554 (Pixel 9 Pro XL API 37, deskclock): - shade opening mid-settle: settled after 7655ms: +28 -25, tiles present, zero status-bar chrome (unchanged from before — settle was already right) - ordinary action, no shade: +1 -1, no chrome leak - real captures: collapsed status bar 9/9 chrome; expanded shade keeps every tile under BOTH walks, which is the behavior that changed
Fixed the code insteadPushed The actual defectChrome classification gave opposite answers about the same screen depending on capture shape: an expanded shade was 100% chrome under the Root cause: The fix: record it while it still exists
That let the compensating machinery go:
Two alternatives measured and rejected first
On the test changesFixtures that modelled the old mechanism now model the device: the synthetic status bar gets the Assertions were not weakened to fit. VerificationReal archived captures: collapsed status bar 9/9 chrome (including the previously-leaking unmarked Live on emulator-5554 (Pixel 9 Pro XL API 37, deskclock):
Full unit suite passes except the usual Follow-up this opens#1318's divergence-local fallback was added because the raw shape condemned everything. That premise is gone — it should now be reachable without the fallback, and worth checking whether it can be removed. Not touched here. |
|
Re-review of new head
The PR body also still says ‘no product change’ and ‘test-only,’ but this head changes eight production files. Update the description and attach exact-head Android-helper evidence for the changed public routes: non-raw snapshot/replay selection must keep shade tiles while excluding status chrome, collapsed/no-shade controls must still exclude clock/battery/signal, content recovery must remain correct, and public JSON must contain no internal provenance field. All other exact-head checks are green. |
…mplify androidUiNodes Review blockers on 8615771: 1. `systemChrome` reached CLI/SDK/MCP payloads: `serializeSnapshotResult` passed `result.nodes` straight through, so an internal classification became a de-facto public contract and per-node bytes. Added `publicSnapshotNodes` next to the field it strips, mirroring the existing `publicSnapshotCaptureAnnotations` projection, and applied it at that boundary (the single serializer both the CLI and capture output route through). The regression test asserts the published nodes deep-equal their input minus the field AND that the serialized JSON never contains the string; it fails if the projection is removed. 2. Fallow complexity on `androidUiNodes` (10 cyclomatic / 19 cognitive): extracted `createAndroidChromeSubtreeTracker`, which owns the depth and chrome-subtree state, leaving the generator a token loop. Not baselined. Note the tracker keeps open/close as distinct operations on purpose: a self-closing tag leaves at the depth it entered, a container descends and is left by its `</node>`, so collapsing them silently breaks nesting.
|
Re-review of new head
Keep the provenance in an Android-internal node/sidecar type, project it away at the daemon/public response boundary used by CLI, Node, and MCP, and add a production-route regression proving all published snapshot surfaces omit it. The PR description also still says “Test-only”; update it for the production change and attach exact-head Android-helper/public JSON evidence requested in the prior review. Do not apply labels. |
Blockers addressed —
|
| route | result |
|---|---|
--settle, shade raised mid-settle |
settled after 7108ms: +28 -25 — brightness seekbar + Wi-Fi/Bluetooth/Mobile data/Quick Share/Modes/Wallet tiles, 0 status-bar chrome lines |
--settle, shade closing mid-settle |
+25 -28, mirror image |
| ordinary action, no shade (control) | +1 -1, 0 systemui nodes surfaced, no clock/battery/signal leak |
| replay divergence, shade up (non-raw selection) | state: available, refs: 20 (cap, truncated: true), Display brightness + tiles, 0 status-bar chrome among refs |
| content recovery | systemSurfaceOnly disclosure still fires on the shade |
| public JSON | systemChrome absent from every payload measured — interactive, non-raw, and divergence |
The divergence numbers match #1318's post-fix result exactly (20 refs at the cap, truncated, Display brightness + tiles), so that route is preserved while the classifier underneath it changed.
Note on reading the raw capture numbers: a snapshot of the shade legitimately contains the status-bar nodes (4 interactive / 6 non-raw) — snapshot publishes the tree, and the chrome filter is what excludes them on the settle and divergence routes. That is why the evidence above is end-to-end rather than a node count.
…the test-side stamping helper Review: the remaining long comments read as workarounds. Three were, and each pointed at code worth changing rather than explaining. 1. `collectAndroidSystemChromeIndexes` required the walk-stamped `systemChrome` even when the tree still held the container that defines it. Hand-built trees (every settle unit test) therefore classified as "no chrome at all", which is why they needed `withAndroidSystemChrome` — a test helper re-implementing the production descent, with a comment explaining why it had to exist. The classifier now reads the container from whichever source still has it: the parent chain when the capture kept it, the stamped flag when the walk dropped it. The helper, its type and its comment are deleted; no test re-states production logic, and any caller holding an intact tree is now classified correctly regardless of how it was produced. 2. `createAndroidChromeSubtreeTracker` tracked depth with arithmetic subtle enough to need a comment about self-closing tags — and subtle enough that an earlier revision of it was wrong. Replaced with a stack of open elements: push on descent, pop on `</node>`, nothing to explain. 3. `walkInteractiveOnlyAndroidFixture`'s 16-line comment still described the run-classification this branch deleted — stale, not merely long. Cut to what the helper does. Also trimmed the contract, kernel, classifier and serialization-test comments to the fact each states. Comment lines in the branch drop from 116 to 51. No behavior change beyond (1), which strictly widens correct classification. 4318/4318 unit tests, lint, typecheck, build and smoke pass.
The comments were hiding three fixable things —
|
| route | result |
|---|---|
--settle, shade mid-settle |
+28 -25, tiles present, 0 status-bar chrome |
| ordinary action, no shade (control) | +1 -1, 0 chrome leak |
| replay divergence, shade up | available, refs: 20 (cap, truncated), Display brightness + tiles |
| public JSON | systemChrome absent |
|
Re-review of
Keep the stamped provenance in a distinct internal node type, project at the daemon/public response boundary before Node/MCP receive it, and add daemon→Node plus MCP structured-content regressions. The PR body’s device evidence is also for
|
|
Re-review of The daemon now retains provenance only in the operational/ref-frame tree and strips it from the published snapshot copy. Exact-head Android evidence at Do not apply labels; the coordinator will apply |
|
Review of head Two findings, one confirmed with a repro. 1. Confirmed:
|
|
Re-review of Finding 1 (provenance leak) — fixed and enumerated. The strip now lives at both node-embed sites ( One minor placement note, non-blocking: the invariant sits in Finding 2 (fallback deletion) — resolved via the insurance option. The hittable fallback is restored verbatim in No remaining blockers from my side. |
|
Closes #1319.
This PR changes production behavior. It began as a test-only answer to #1319 and became a fix once the test exposed that Android chrome classification changed with capture shape.
The defect
The same expanded Quick Settings shade was classified in opposite ways:
--settleretained the tiles, which initially made --settle may be blind to a fully expanded quick-settings shade (same run-condemnation rule) #1319 look like a non-issue.Root cause:
shouldIncludeStructuralAndroidNodedrops thestatus_bar*/navigation_bar*containers—the nodes that identify those regions—and reparents their leaves beside real content. Downstream code then tried to reconstruct identity after the tree relationship had been discarded, and the answer depended on the capture shape.The fix
Record provenance while the relationship still exists.
walkUiHierarchyNodethreadsancestorSystemChromealongside the existingancestorHittable, and the streamingandroidUiNodesrecovery path tracks the same subtree. Classification is now intrinsic and per-node, so every capture shape agrees by construction.That removes the obsolete classifier machinery:
collectAndroidSystemChromeRunIndexes, where one clock node could condemn 95 unrelated nodes.The narrowly gated #1318 fallback remains as insurance for unverified older Android/OEM hierarchy shapes. It activates only when classification would otherwise leave no meaningful refs, and still admits hittable nodes only. The verified Pixel hierarchy does not need it.
The shared container predicate matches
status_bar/navigation_baras complete id segments, includingsplit_shade_status_barwithout false positives such asstatus_barometer.systemChromeis Android-internal provenance, not publicRawSnapshotNodeAPI. It remains available in daemon session snapshots for settle/diff/replay and is stripped at the two public node publication seams: snapshot trees and selector read payloads (get/ read-onlyfind). The provider harness rejectssystemChromeat the shared request-handler output before responses fan out to either direct RPC or the SDK transport, so a future response path cannot silently expose it. The CLI, Node SDK, and MCP inherit the same clean daemon contract without client-side defenses.Alternatives measured and rejected
window-type=3(TYPE_SYSTEM) both collapsed and expanded;TYPE_STATUS_BARnever appeared. One window contained status icons and tiles, so window type cannot distinguish them.Battery 100 percent.and the notification-icon summary—the ticking status content --settle may be blind to a fully expanded quick-settings shade (same run-condemnation rule) #1319 is about.Live device evidence (
7cd3ab91fclassifier implementation)Pixel 9 Pro XL API 37 emulator, Google Clock, Android helper
0.19.3. The follow-up commits change only daemon publication, types, tests, and the pre-existing replay safety fallback; they do not change the device-side classifier exercised here.--settle, shade raised mid-settle+1 -1; no SystemUI content surfacedstate: available, 20 refs at the cap, starting withDisplay brightness; actionable tiles retained; no status chromesystemSurfaceOnlystill disclosed; helper backend remainedandroid-helpersystemChromeThe manual session was closed after verification.
Test and validation changes
Fixtures now model the device tree: the synthetic status bar has the real
status_bar_launch_animation_container, and content-recovery XML nests chrome leaves below their container.The regression suite now asserts:
get attrs @ref, andfind … get attrsomit provenance;Revert-sensitivity was checked by disabling provenance classification: six focused tests failed, including collapsed status, navigation, Quick Settings parity, and replay divergence.
Local validation: format, lint, typecheck, layering, Fallow, build, Android helper build, focused tests, provider integration, and live Android verification. The complete provider scenario project passes with the global invariant active when serialized to avoid the repository's documented parallel host-load contention: 37 files, 141 tests. The Android lifecycle provider file (12 tests) and replay-divergence suite (14 tests) also pass in focused runs.
Review blockers addressed
get, andfindnode payloads. A provider-wide serialized-response guard makes this an enumerating contract.createAndroidChromeSubtreeTrackerowns the streaming depth/subtree state; no baseline or allowlist was added.