Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/browser-tab.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ and size, after waiting up to ten seconds for it; one still running answers
`browser_navigate` on a URL that answers with a file download it the same way:
Chrome aborts that navigation and the page stays where it was, so the answer
is the handle with `downloads` rather than an error. That answer also
waits until 1.5 seconds after the download began, because Chrome aborts a
navigation started sooner and reloads the page. `browser-download` tells the
waits until 2.5 seconds after the download began, because Chrome reloads the
page about 1.5 seconds after an `<a download>` and the reload aborts a
navigation started before it. `browser-download` tells the
window when a file is complete, and the window shows a toast naming it with a
button that opens its folder. The same holds in a window opened with Open in a
window.
Expand Down
19 changes: 11 additions & 8 deletions docs/ceilings.md
Original file line number Diff line number Diff line change
Expand Up @@ -992,14 +992,17 @@ work when nobody knows it and that the call site never shows. The mechanism and
live in the window: closing it, or the browser, loses them.

- **A session browser's downloads go straight to the user's Downloads folder** (`internal/browser/downloads.go`,
`session_downloads.go`, `docs/browser-tab.md`): `XDG_DOWNLOAD_DIR` on Linux, `~/Downloads` on macOS and
Windows, even for a Windows user who moved that known folder. While one runs, a file named by Chrome's
download id sits there; it takes its real name once complete. Chrome aborts a navigation the page starts
within about a second of a download (`net::ERR_ABORTED`, then it reloads the page; Chromium 155, with
downloads on or off), so an agent's click, key press, open or navigate that downloaded holds its answer
1.5 seconds; a navigation the person starts in the pane is not held. A download still running after ten
seconds is reported in progress and named again when it ends. No tool lists past downloads or cancels one,
nothing caps their size, and closing the browser cancels one still running.
`session_downloads.go`, `docs/browser-tab.md`): `XDG_DOWNLOAD_DIR` on Linux, `~/Downloads` on macOS and Windows,
even for a Windows user who moved that known folder. While one runs, a file named by Chrome's download id sits
there; it takes its real name once complete. The download behaviour is the whole browser's, so what Chrome
downloads on its own, with no page, lands there too under its id and is never renamed: the optimization guide's
model downloads are switched off (`disabledFeatures`, `chrome.go`), and a background download a later Chrome adds
would land. Chrome reloads the page 1.46 to 1.70 seconds after an `<a download>` begins, and the reload aborts a
navigation started before it (`net::ERR_ABORTED`; Chromium 155, with downloads on or off), so an agent's click,
key press, open or navigate that downloaded holds its answer 2.5 seconds; a navigation the person starts in the
pane is not held. A download still running after ten seconds is reported in progress and named again when it
ends. No tool lists past downloads or cancels one, nothing caps their size, and closing the browser cancels one
still running.

## Languages

Expand Down
12 changes: 11 additions & 1 deletion internal/browser/chrome.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ func chromeLaunch(dir string, headed bool) (Page, error) {
return nil, err
}
opts := append([]chromedp.ExecAllocatorOption{}, chromedp.DefaultExecAllocatorOptions[:]...)
opts = append(opts, chromedp.ExecPath(exe), chromedp.UserDataDir(dir), chromedp.WSURLReadTimeout(launchWait))
opts = append(opts, chromedp.ExecPath(exe), chromedp.UserDataDir(dir), chromedp.WSURLReadTimeout(launchWait),
chromedp.Flag("disable-features", disabledFeatures))
if headed {
opts = append(opts, chromedp.Flag("headless", false))
}
Expand All @@ -66,6 +67,15 @@ func chromeLaunch(dir string, headed bool) (Page, error) {
return p, nil
}

// disabledFeatures replaces chromedp's own disable-features list, which it
// repeats, adding the optimization guide's model downloads. Those go through
// the profile's download manager with no page and no download event, so with
// downloads allowed Chrome saved them into the user's Downloads as files
// named by a GUID, 4 KB to 33 MB each (Chrome on the Windows and macOS CI
// runners, 2026-10-27).
const disabledFeatures = "site-per-process,Translate,BlinkGenPropertyTrees," +
"OptimizationGuideModelDownloading,OptimizationTargetPrediction"

type chromePage struct {
ctx context.Context
cancel context.CancelFunc
Expand Down
10 changes: 7 additions & 3 deletions internal/browser/downloads_live_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,18 @@ func TestChromeDownloadsLandInDownloadsAndThePageStays(t *testing.T) {
}
want := filepath.Join(dir, id+".csv")
got := info.Downloads
for deadline := time.Now().Add(10 * time.Second); len(got) == 0 && time.Now().Before(deadline); {
// A new tab hands its URL to the page after the click returns.
for deadline := time.Now().Add(10 * time.Second); (len(got) == 0 || got[0].State == api.DownloadInProgress) && time.Now().Before(deadline); {
// A new tab hands its URL to the page after the click returns,
// and a read names a download still running, then again once
// it ends.
time.Sleep(100 * time.Millisecond)
read, err := s.Read(t.Context(), api.SessionIDOptions{ID: "s"})
if err != nil {
t.Fatal(err)
}
got = read.Downloads
if len(read.Downloads) > 0 {
got = read.Downloads
}
}
if len(got) != 1 || got[0].Path != want || got[0].State != api.DownloadCompleted {
t.Fatalf("downloads %+v, want %s completed", got, want)
Expand Down
11 changes: 6 additions & 5 deletions internal/browser/session_downloads.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ const downloadPoll = 50 * time.Millisecond
const downloadNotice = time.Second

// downloadQuiet is how long after a download begins a click or a key press
// holds its answer. Chrome aborts a navigation the page starts within about a
// second of a download (net::ERR_ABORTED, then it reloads the page): the
// agent's next step failed 10 times in 10 right after an <a download>, and
// none in 50 with this gap (Chromium 155, measured 2026-10-27).
const downloadQuiet = 1500 * time.Millisecond
// holds its answer. Chrome reloads the page 1.46 to 1.70 seconds after an
// <a download> begins (24 samples, Chromium 155, measured 2026-10-27), and
// the reload aborts a navigation the agent started before it
// (net::ERR_ABORTED). A 1.5-second hold sat inside that spread and the next
// navigate failed 2 times in 40; this one clears it.
const downloadQuiet = 2500 * time.Millisecond

// download is one of a session's downloads and the state the agent last
// heard it in, "" before it heard of it.
Expand Down
Loading