Skip to content

Add a networkquiet wait, and pierce shadow roots in selector snapshots - #2

Closed
YellowSnnowmann wants to merge 6 commits into
tinyhumansai:mainfrom
YellowSnnowmann:tinycomputer/network-quiet
Closed

YellowSnnowmann wants to merge 6 commits into
tinyhumansai:mainfrom
YellowSnnowmann:tinycomputer/network-quiet

Conversation

@YellowSnnowmann

@YellowSnnowmann YellowSnnowmann commented Oct 7, 2026 •

Copy link
Copy Markdown

Summary

tinycomputer links agent-browser as a library and drives Chrome through it. These changes let it read a page sooner after an action without reading it too early, and read a web component's controls:

  1. networkquiet load state. Like networkidle, but it counts its 500 ms quiet window from the start of the wait, polling every 100 ms. networkidle waits for a first 600 ms receive window before it starts counting, so an idle page costs about 1.1 s; under networkquiet it resolves after 500 ms.

  2. It counts only page-changing requests:

    • It counts the page's own document, scripts, stylesheets, and XHR or fetch data.
    • It ignores analytics pings, pictures, fonts, media, prefetches, streams, and other frames' documents.
    • Only a counted request's end restarts the quiet window.

    On amazon.in, every action that opened a page sent 100+ requests for over 2 s. Some of them (pings, and the documents of ad and helper frames in other processes) never report finishing to the page's session, so the wait always ran to its cap.

  3. It waits for what the action before it sent. A click's requests start while the click runs, before a separate waitforloadstate can subscribe, so the wait never saw them: a navigation or fetch slower than the quiet window resolved it on the page as it was before the click.

    • The daemon now keeps each page session's page-changing requests that have not finished, from the events it drains (InFlight).
    • A networkquiet wait subscribes, drains the events that came before, and starts with the requests sent in the last 3 s (CDP wallTime) as pending.
    • A long poll older than that is not waited for, and past 256 entries the old ones are forgotten.
  4. Selector snapshots read through shadow roots. DOM.describeNode now takes pierce: true, so a web component's controls are part of the selector's subtree. On lenskart.com, a consent banner's shadow host yielded two DOM nodes and no accessibility node, and the snapshot failed with "No accessibility node found for selector". With pierce, the same subtree holds 36 accessibility nodes, its buttons among them.

  5. Docs and parity. networkquiet is named in the wait and diff url help, the MCP wait_for_load tool's enum, the README, the core skill's command reference, and the docs site. A networkquiet timeout names its own state.

networkidle behaves as before.

Tests

  • test_network_quiet_counts_from_the_start and test_network_quiet_waits_for_requests_in_flight cover the new state.
  • test_network_quiet_counts_only_what_changes_the_page: a ping, a picture and another frame's document that never finish leave the page quiet, and a finish for a request the wait never saw does not restart the window.
  • test_network_quiet_waits_for_what_was_in_flight_before_it, test_in_flight_keeps_recent_page_changing_requests and test_in_flight_forgets_old_requests_past_its_limit cover the requests kept in flight.
  • test_network_quiet_timeout_names_the_state, test_changes_page_reads_the_request_type_and_frame and test_wait_until_reads_network_quiet cover the rest of the wait.
  • tool_schema_wait_for_load_states_match_cli checks that every state the MCP tool offers parses as itself.
  • a_selector_snapshot_describes_its_subtree_through_shadow_roots covers the describeNode parameters.

Run locally (macOS, stable Rust):

  • cargo fmt --manifest-path cli/Cargo.toml -- --check: clean.
  • cargo clippy --manifest-path cli/Cargo.toml -- -D warnings: clean.
  • cargo test --manifest-path cli/Cargo.toml --lib: 1,403 passed, 2 failed, 143 ignored. The two failures are stream::http tests, in code this branch does not touch: they bind a Unix socket under this Mac's temporary folder, whose path is longer than a socket path may be (path must be shorter than SUN_LEN). On one run test_execute_unknown_command failed under load as well; it passes on its own and passed on the next full run.

Merging

tinycomputer pins this branch's head (ebf10fb) through its vendor/agent-browser gitlink, in tinyhumansai/tinycomputer#77. Please merge with a merge commit or a rebase, so that commit stays on main; after a squash merge, the gitlink has to move to the squashed commit first.

waitforloadstate networkidle starts its 500 ms of quiet only after a
first 600 ms receive window passes empty, so even an already idle page
waits about 1.1 s. networkquiet counts the 500 ms from the start of the
wait and polls every 100 ms: an idle page resolves after 500 ms (never
instantly, as vercel-labs#846 requires), and a page with requests in flight still
waits for them. networkidle is unchanged.
On Amazon every action that opened a page waited the whole 2 s cap: of
the 100+ requests such a page sends, analytics pings and the documents of
ad and helper frames in other processes never report finishing to the
page's session, so nothing ever counted as quiet. And the end of any
request the wait never saw start reset its quiet timer.

networkquiet now counts only requests that can change what the page
shows: the page's own document (a page target's main frame shares its
target id), scripts, stylesheets, and XHR or fetch data. Pings, pictures,
fonts, media, prefetches, streams, and other frames' documents are not
counted, and only a counted request's end restarts the quiet window.
networkidle is unchanged.
A snapshot rooted at a selector collects the backendNodeIds under the
matched element with DOM.describeNode, then keeps the accessibility nodes
among them. Without `pierce`, describeNode stops at a shadow host, so a
web component's controls were never in the subtree. On Lenskart, a
consent banner's host (display: contents) yielded two DOM nodes and no
accessibility node at all, and the snapshot failed with "No accessibility
node found for selector"; with `pierce`, the same subtree holds 36
accessibility nodes, its "Allow Selection" and "Allow all" buttons among
them.
describe_subtree_params was inserted between the comment and the function
it describes, so its rustdoc opened with the wrong sentence.
A click's requests start while the click runs, before a separate
waitforloadstate can subscribe, so networkquiet never saw them: a
navigation or a fetch slower than its 500 ms window resolved the wait on
the page as it was before the action.

The daemon now keeps each page session's page-changing requests that have
not finished, from the events it drains. A networkquiet wait subscribes,
drains what came before, and starts with those sent in the last 3 s
(CDP's wallTime) as pending; a long poll older than that is not waited
for, and past 256 entries the old ones are forgotten. networkidle is
unchanged.

A networkquiet timeout now says networkquiet, and its tests allow up to
1,050 ms, still under networkidle's 1.1 s floor, so a slow machine does
not fail them.
wait --load networkquiet and diff url --wait-until networkquiet already
reached it through the CLI parser; the MCP wait_for_load enum refused it
and no help or docs page named it. A test now checks that every state
the MCP tool offers parses as itself.
@coderabbitai

coderabbitai Bot commented Oct 7, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: fc5f7a7d-cde9-4a08-9ec7-1d9238203ecc
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@tinysweeper

tinysweeper Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Tiny Sweeper review

Tiny Sweeper reviewed this change across 6 lane(s) and found 3 active actionable finding(s). Detailed lane evidence and any incomplete work are listed below.

State: Changes requested
Priority: high
Reviewed head: ebf10fb3685d
Updated: 1791384771 (Unix time)

Review snapshot

Change surface Files Review signal Count
Production 6 Active findings 3
Tests 0 Noted findings 0
Documentation 3 Resolved findings 0
Configuration 0 Pending checks/questions 2

Completeness: Complete
Test assessment: No supported feature-to-test mapping was available; this does not mean tests are absent or passed.

What changed

The review could not produce a supported behavioral summary; inspect the cited changed surface and lane details below.

Features

None identified with supported citations.

Tests

No supported feature-to-test mapping was produced. Test execution is not inferred.

Findings

  • high · critique · Pass the main frame ID instead of the target ID — `changes_page` compares a Document request's `frameId` with `main_frame`, but `page_target_id` returns the CDP page target ID. These are different CDP identifiers in Chrome, so the (cli/src/native/browser\.rs:1238)
  • medium · critique · Document all request types excluded from networkquiet — This description makes the counted categories sound exhaustive while only naming analytics pings, images, and other frame documents as excluded. The existing command documentation (docs/src/app/commands/page\.mdx:144)
  • medium · critique · Key in-flight requests by session and request ID — `InFlight` receives events from multiple sessions, but the map is keyed only by `requestId`. CDP request IDs are scoped to a session/target, so two pages can concurrently report th (cli/src/native/browser\.rs:2361)

Pending checks: Native E2E Tests, Windows Integration Test

Before merge

  • Address Pass the main frame ID instead of the target ID (cli/src/native/browser\.rs).
  • Wait for Native E2E Tests, Windows Integration Test.
Agent review details

critique

  • Conclusion: Failure
  • Scope reviewed: all assigned evidence
  • Lane summary: Reviewed 9 files; 3 findings. _The code index for this repository is cold, so this review saw the diff alone._ _Memory was unavailable (model: cortex: v1/recall: error sending request for url (http://cortexdb:3141/v1/recall\)\), so this review ran without it._
  • Evidence: cli/src/native/browser\.rs — Pass the main frame ID instead of the target ID
  • Evidence: docs/src/app/commands/page\.mdx — Document all request types excluded from networkquiet
  • Evidence: cli/src/native/browser\.rs — Key in-flight requests by session and request ID

security

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: Reviewed 7 files; 0 findings. 2 files were not security-reviewed: README.md (prose or tabular data), skill-data/core/references/commands.md (prose or tabular data). _The code index for this repository is cold, so this review saw the diff alone._ _Memory was unavailable (model: cortex: v1/recall: error sending request for url (http://cortexdb:3141/v1/recall\)\), so this review ran without it._

tests

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The change adds a `networkquiet` load state (counting only page-changing requests, from the start of the wait) and pierces shadow roots in selector snapshots. The new polling logic, InFlight tracking, and request classification are well covered by unit tests that would fail on regression, the MCP schema gets a parity test against the CLI parser, and docs/help are updated throughout. One small cross-session bookkeeping bug in the new InFlight map; otherwise this looks safe to merge. _The code index for this repository is cold, so this review saw the diff alone._ _Memory was unavailable (model: cortex: v1/recall: error sending request for url (http://cortexdb:3141/v1/recall\)\), so this review ran without it._

commits

  • Conclusion: Neutral
  • Scope reviewed: all assigned evidence
  • Lane summary: Nothing sensitive found in what this pull request commits.

description

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The description accurately matches the diff: the `networkquiet` load state, its filtering of page-changing requests, the `InFlight` bookkeeping for pre-wait requests, the `pierce: true` selector snapshot change, and the docs/MCP/help parity updates are all present and behave as described, with tests covering each. One minor correctness nit in the new `InFlight` bookkeeping; otherwise this looks safe to merge. _The code index for this repository is cold, so this review saw the diff alone._ _Memory was unavailable (model: cortex: v1/recall: error sending request for url (http://cortexdb:3141/v1/recall\)\), so this review ran without it._

e2e

  • Conclusion: Neutral
  • Scope reviewed: all assigned evidence
  • Lane summary: This PR adds a `networkquiet` load state (CLI, MCP schema, and a new wait path) and pierces shadow roots in selector snapshots. Both have external surfaces, but no end-to-end test was added or changed; the only coverage is in-process unit tests, and the Native E2E / Windows Integration CI jobs are still pending. The behavioural changes look plausible but their user-facing paths are not driven end to end by anything in this diff. (2 findings discarded for not matching a changed line) Waiting on end-to-end jobs: `Native E2E Tests`, `Windows Integration Test`.
  • Unresolved questions/checks: Native E2E Tests, Windows Integration Test
Evidence and run details
  • Models: gpt-5.6-luna, glm-5.3-flash
  • Spend: $0.025941
  • Tokens: 372719 input · 22357 output · 35114 cached · 0 embedding
Head State Pass summary
ebf10fb3685d changes requested 3 active finding(s), 0 resolved finding(s) (at 1791384771)

tinysweeper 0.1.0

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: 1 lane(s) blocking, worst finding is high.

Fix or reply to the findings below and push. The next review clears this automatically once they are gone — you should not need to dismiss anything by hand.

             $0.0259 · 372,719 in / 22,357 out · 35,114 cached (9%)  · gpt-5.6-luna, glm-5.3-flash
critique:    $0.0166 · 186,728 in / 10,962 out · 19,237 cached (10%) · gpt-5.6-luna, glm-5.3-flash
security:    $0.0086 · 104,243 in / 4,626 out  · 8,901 cached (9%)   · gpt-5.6-luna
tests:       $0.0003 · 33,170 in  / 2,690 out  · 3,712 cached (11%)  · glm-5.3-flash
description: $0.0001 · 16,403 in  / 660 out    · 1,408 cached (9%)   · glm-5.3-flash
e2e:         $0.0001 · 16,381 in  / 871 out    · 1,728 cached (11%)  · glm-5.3-flash

Comment thread cli/src/native/browser.rs
rx,
timeout,
Quiet::FromStart {
main_frame: self.page_target_id(session_id),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority high critique confident

Pass the main frame ID instead of the target ID

changes_page compares a Document request's frameId with main_frame, but page_target_id returns the CDP page target ID. These are different CDP identifiers in Chrome, so the main document normally fails the comparison and is excluded from networkquiet. For a navigation whose only request is the main document, the wait can therefore return after 500 ms while the document is still loading. Obtain and pass the page's actual main frame ID, or change the comparison to use the appropriate identifier.

[RULE] wrong-identifier ·

```

After a page change, prefer a selector, text, URL, or JavaScript condition that represents the state you need. Use `networkidle` only when the page is known to become quiet, because SSE, WebSockets, polling, and long-polling can keep it from resolving.
After a page change, prefer a selector, text, URL, or JavaScript condition that represents the state you need. Use `networkidle` only when the page is known to become quiet, because SSE, WebSockets, polling, and long-polling can keep it from resolving. `networkquiet` counts only the requests that can change what the page shows (the page's document, scripts, stylesheets, XHR and fetch), so analytics pings, images, and other frames' documents do not hold it up.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique confident

Document all request types excluded from networkquiet

This description makes the counted categories sound exhaustive while only naming analytics pings, images, and other frame documents as excluded. The existing command documentation also specifies that fonts, media, and prefetches are not counted; those requests can affect or support page rendering but will not hold networkquiet open. Users relying on this page may therefore form an incorrect expectation about which in-flight requests are waited on. Align this sentence with the fuller behavior description, including the 500 ms quiet period and the exclusions for fonts, media, and prefetches.

[RULE] incomplete-documentation ·

Comment thread cli/src/native/browser.rs
.get("wallTime")
.and_then(Value::as_f64)
.unwrap_or(now);
self.requests

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique confident

Key in-flight requests by session and request ID

InFlight receives events from multiple sessions, but the map is keyed only by requestId. CDP request IDs are scoped to a session/target, so two pages can concurrently report the same ID. The later requestWillBeSent overwrites the first request, and a loadingFinished event from either session removes the shared entry. A subsequent networkquiet wait can then omit a still-running page-changing request. Use (session, requestId) as the key, or otherwise keep separate per-session maps.

[RULE] request-id-collision ·

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant