Skip to content

Fix browser tools hanging for minutes on dialogs and stuck pages - #216

Merged
stippi merged 5 commits into
mainfrom
fix/browser-dialogs
Oct 2, 2026
Merged

stippi merged 5 commits into
mainfrom
fix/browser-dialogs

Conversation

@stippi

@stippi stippi commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Problem

The browser tools sometimes hung for minutes and then failed with "Request timed out". The engine is chromiumoxide (CDP), which lacks protections Playwright provides out of the box. A local test reproduced it:

  • A click that opened an alert() did not return within 120 s.
  • After that, every CDP command waited for its timeout. A screenshot took about 16 minutes before "Request timed out".
  • chromiumoxide never answered the dialog, so it stayed open and the next tool call hung the same way.

chromiumoxide's own 30 s command timeout does not hold while the renderer is stuck.

Changes

  • JavaScript dialogs are answered as they open. alert and beforeunload are accepted. confirm and prompt are dismissed unless browser_act gets accept_dialogs: true for that call. Each answered dialog is reported to the model (Note: a confirm dialog "…" was dismissed.).
  • Timeouts: every BrowserSession verb is bounded: 15 s for an interaction, 30 s for a navigation (BrowserTimeouts). A limit that runs out is a typed BrowserTimeout. wait_for and settle are bounded as a whole.
  • Capture: when the page cannot be read, capturing the result stops and reports that the page is not responding, instead of also waiting out the screenshot.
  • Slow loads: a navigation whose load event never fires (a slow iframe or tracker) is no longer a failure. The page is shown as far as it got, with a note. The login handoff tolerates this too, so a slow portal page cannot fail a login the user already completed.
  • Typing: characters outside the US layout (ü, ß, €) used to fail with "Key not found". They are now inserted with Input.insertText.
  • Smaller fixes: navigate() dropped its second wait_for_navigation, which has no timeout in chromiumoxide. LaunchedBrowser::close kills the browser if it does not exit within 10 s.

Notes for reviewers

  • I chose dismiss as the default for confirm on purpose. Accepting could trigger an outward action, such as submitting a form, whose warning the model never saw.
  • WebClient::fetch (the web_fetch tool) has the same unbounded wait_for_navigation after new_page. It is not part of this PR.
  • test_web_search needs network access to DuckDuckGo. It failed locally because the network was unavailable, not because of this change.

Testing

  • New engine tests in crates/web/src/tests.rs:
    • alert and confirm, including the opt-in
    • typing umlauts
    • a navigation that never loads
    • a page stuck in a script loop: every verb must give up within its limit
  • New tool tests in crates/code_assistant_core/src/tools/impls/browser.rs:
    • dialogs reported through browser_act
    • a slow load shown with a note
    • an act on a hung page reported in under 4.5 s
  • cargo test --package web and cargo test --package code_assistant_core pass. cargo clippy --all-targets --all-features -- -D warnings and cargo fmt --check are clean.

stippi added 5 commits October 1, 2026 21:59
Chrome stalls the renderer while an alert/confirm/prompt/beforeunload
dialog is open, and chromiumoxide leaves it open. The click that raised
it did not return, and every later CDP command (evaluate, screenshot)
waited for its timeout: one browser_act took many minutes and ended in
"Request timed out", and the dialog stayed open for the next call.

- BrowserSession answers dialogs as they open: alert and beforeunload
  are accepted, confirm and prompt dismissed unless accepting was
  requested. Answered dialogs are reported in the next observation.
- browser_act takes accept_dialogs for the call; the result notes each
  dialog and whether it was accepted or dismissed.
chromiumoxide's key table only knows the US layout, so type and fill
failed with "Key not found" on the first umlaut, ß or €. Characters
on the layout are still pressed as real keys; all others are inserted
with Input.insertText into the focused element.
- navigate() followed goto with wait_for_navigation, which has no
  timeout in chromiumoxide. goto already waits for the load event; the
  second wait only added a hang when a script redirect started right
  after loading.
- LaunchedBrowser::close waited for the process to exit without a
  limit. After 10 seconds the browser is now killed instead.
chromiumoxide's own 30s command timeout does not hold when the renderer
is stuck: a click on a hung page blocked for minutes, a screenshot for
about 16 minutes. One browser tool call could stall the agent that long.

- BrowserSession bounds every verb: 15s for an interaction, 30s for a
  navigation (BrowserTimeouts, adjustable via with_timeouts). A limit
  that runs out is a typed BrowserTimeout. wait_for and settle are
  bounded as a whole, so a poll that never returns cannot outlast them.
- capture stops at the first unanswered read and reports that the page
  is not responding, instead of waiting out the screenshot as well.
- A navigation whose load event does not fire in time (a slow iframe or
  tracker) is no longer a failure: the page is shown as far as it got,
  with a note. The login handoff tolerates it the same way, so a slow
  portal page cannot fail a login the user already completed.
async-trait up to 0.1.89 added a message-less #[must_use] to every
async trait method, whose boxed future is already must_use. Clippy 1.99
flags that as double_must_use; 0.1.92 no longer emits the attribute.
@stippi
stippi force-pushed the fix/browser-dialogs branch from 14bb9e6 to b0a959e Compare October 2, 2026 08:55
@stippi
stippi merged commit 9106115 into main Oct 2, 2026
5 checks passed
@stippi
stippi deleted the fix/browser-dialogs branch October 2, 2026 09:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant