diff --git a/docs/browser-tab.md b/docs/browser-tab.md index 4f20e493..3bce6843 100644 --- a/docs/browser-tab.md +++ b/docs/browser-tab.md @@ -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 `` 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. diff --git a/docs/ceilings.md b/docs/ceilings.md index e59e7a2f..2796af30 100644 --- a/docs/ceilings.md +++ b/docs/ceilings.md @@ -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 `` 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 diff --git a/internal/browser/chrome.go b/internal/browser/chrome.go index a058ce36..ee76d125 100644 --- a/internal/browser/chrome.go +++ b/internal/browser/chrome.go @@ -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)) } @@ -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 diff --git a/internal/browser/downloads_live_test.go b/internal/browser/downloads_live_test.go index 4ee48726..0290b8ad 100644 --- a/internal/browser/downloads_live_test.go +++ b/internal/browser/downloads_live_test.go @@ -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) diff --git a/internal/browser/session_downloads.go b/internal/browser/session_downloads.go index 22f17d72..1ea0ac99 100644 --- a/internal/browser/session_downloads.go +++ b/internal/browser/session_downloads.go @@ -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 , 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 +// 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.