Skip to content

fix(browser): keep cloud browser sessions alive during long browser_exec runs - #83739

Open
pierrenode wants to merge 1 commit into
NousResearch:mainfrom
pierrenode:fix/browser-exec-cloud-session-heartbeat
Open

fix(browser): keep cloud browser sessions alive during long browser_exec runs#83739
pierrenode wants to merge 1 commit into
NousResearch:mainfrom
pierrenode:fix/browser-exec-cloud-session-heartbeat

Conversation

@pierrenode

Copy link
Copy Markdown
Contributor

Summary

browser_exec's Browser Use mode is now the default browser backend (8d8bc85dca, today) whenever the browser-use CLI is runnable. When a cloud provider (Browserbase, Firecrawl, Nous gateway) is configured, _resolve_backend_cdp() calls browser_tool._get_session_info(task_id) exactly once, before the CLI subprocess even starts — that's the only place this call path touches the legacy stack's per-task session activity tracker. It then hands the resolved CDP endpoint to an external browser-use CLI subprocess that drives the browser directly over CDP for up to 1800s (raised from 120s/600s by 92968a5c7d, specifically to support long, multi-step extractions — "split the work into several calls that append to workspace files… so progress survives timeouts"), making zero further calls back into Hermes's Python process for the whole run.

A background thread (_cleanup_inactive_browser_sessions, 30s poll) reaps any session whose last-activity timestamp is older than browser.inactivity_timeout (default 120s) — and this is a real teardown: it sends an agent-browser close command, calls the provider's close_session() (destroying the remote browser instance), and kills the local daemon PID.

Net effect: with a cloud provider configured and no explicit session=, any browser_exec call running longer than ~120-150s has its underlying cloud browser destroyed by the reaper while the browser-use CLI subprocess is still actively connected to it over CDP — breaking the connection mid-task. This directly undermines 92968a5c7d's own raised timeout and "workspace persists across timeouts" design: the failure here is a broken CDP connection, not a clean timeout the workspace-recovery guidance addresses. Local Chrome setups (no cloud provider) are unaffected, since _resolve_backend_cdp returns early without touching session tracking there.

Fix

  • _resolve_backend_cdp() now returns (error, cloud_session_task_id) instead of just error. cloud_session_task_id is the key it registered activity for with the session tracker when it successfully resolved a cloud-provider session, or None when no such session was resolved (local CDP override, no provider configured, or a Browser Use direct-API config that manages its own session).
  • browser_exec() now runs the CLI via a bounded poll loop (subprocess.Popen + background stdout/stderr drain threads, to avoid a pipe-buffer deadlock during a long run) instead of a single blocking subprocess.run(). While waiting, it calls browser_tool._update_session_activity() every _CLOUD_SESSION_HEARTBEAT_INTERVAL_S (20s — well under the 120s default inactivity threshold) whenever a cloud session was resolved, refreshing its activity timestamp for the whole run instead of only at resolution time.
  • Local Chrome and explicit session=/BU_NAME runs are unaffected — cloud_session_task_id is None there, so no heartbeat calls are made.
  • Overall timeout/kill semantics, stdout/stderr capture (including the existing stderr truncation and screenshot detection), and the result dict shape are unchanged.

Test plan

  • New regression test test_long_exec_keeps_cloud_session_activity_fresh: drives a fake CLI subprocess that sleeps across several (tiny, monkeypatched) heartbeat intervals with a resolved cloud-provider session, and asserts browser_tool._update_session_activity is called repeatedly (not just once) with the correct task id.
  • Mutation-verified: stashed the tools/browser_use_cli.py fix and confirmed the new test fails against pre-fix code (the heartbeat mechanism/constant doesn't exist at all pre-fix).
  • Updated the 7 existing _resolve_backend_cdp unit tests for its new (error, cloud_session_task_id) return shape; test_cloud_provider_session_exported additionally asserts the returned key matches the resolved task id.
  • Ran the full tests/tools/test_browser_use_cli.py suite (68 tests, including the pre-existing timeout, non-zero-exit, stderr-capture, stdin-piping, session-naming, and screenshot-detection tests, all of which exercise the real subprocess path against a fake CLI script): all pass, confirming the Popen rewrite is behaviorally equivalent to the old subprocess.run for every existing case.
  • Ran the closely-related session/reaper test files (test_browser_orphan_reaper.py, test_browser_cleanup.py, test_browser_use_session_expiry.py, test_browser_cloud_provider_cache.py, test_browser_command_timeout_race.py, test_browser_provider_plugins.py): all pass.
  • ruff check clean on both changed files.

Conflict note

Two open PRs touch tools/browser_use_cli.py:

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists tool/browser Browser automation (CDP, Playwright) sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 11, 2026
…xec runs

browser_exec's Browser Use mode is now the default backend (8d8bc85,
today) whenever the browser-use CLI is runnable. When a cloud provider
(Browserbase, Firecrawl, Nous gateway) is configured, _resolve_backend_cdp()
calls browser_tool._get_session_info(task_id) exactly once, before the
CLI subprocess starts — that's the only place this call touched the
per-task session tracker's activity timestamp. It then hands the CDP
endpoint to an external browser-use CLI subprocess that drives the
browser directly over CDP for up to 1800s (raised from 120s/600s by
92968a5 specifically to support long, multi-step extractions), making
zero further calls back into hermes's Python process.

A background thread (_cleanup_inactive_browser_sessions, 30s poll) reaps
any session whose last-activity timestamp is older than
browser.inactivity_timeout (default 120s) — a real teardown: it sends
an `agent-browser close` command, calls the provider's close_session()
(destroying the remote browser instance), and kills the local daemon
PID. With a cloud provider configured and no explicit session=, any
browser_exec call running longer than ~120-150s has its underlying
cloud browser destroyed by the reaper while the browser-use CLI
subprocess is still actively connected to it over CDP — breaking the
connection mid-task, directly undermining 92968a5's own raised
timeout and "workspace persists across timeouts" design (this failure
is a broken CDP connection, not a clean timeout the workspace-recovery
guidance addresses).

Fix: _resolve_backend_cdp() now also returns the task-tracker key it
registered activity for (None when no cloud-provider session was
resolved — local CDP override, no provider, or a Browser Use direct-API
config that manages its own session). browser_exec() runs the CLI via
a bounded poll loop (subprocess.Popen + background stdout/stderr drain
threads to avoid pipe-buffer deadlock during a long run) instead of a
single blocking subprocess.run(), and calls
browser_tool._update_session_activity() every
_CLOUD_SESSION_HEARTBEAT_INTERVAL_S (20s, well under the 120s default
threshold) while waiting — refreshing the session's activity timestamp
for the whole run instead of only at resolution time. Local Chrome and
explicit session=/BU_NAME runs are unaffected (cloud_session_task_id is
None there, so no heartbeat calls are made); overall timeout/kill
semantics, stdout/stderr capture, and the result shape are unchanged.
@pierrenode
pierrenode force-pushed the fix/browser-exec-cloud-session-heartbeat branch from 7c605a4 to 87bf53f Compare August 12, 2026 14:38
@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

fix(browser): keep cloud browser sessions alive during long browser_exec runs — the heartbeat-pump fix is the right shape and the regression test is solid. Observations:

  1. _drain_pipe swallows every exception with a bare except Exception: pass. A read failure silently truncates stdout/stderr while success may still report True (the caller only strips/truncates stderr). At minimum logger.debug the exception so truncation is diagnosable.

  2. Lazy from tools.browser_tool import _update_session_activity inside the 20s heartbeat loop (_run_cli_with_activity_heartbeat): it re-runs import machinery on every poll and, if the import fails, retries each heartbeat while logging only at debug level. Hoisting the import to the top of the function (same try/except) makes the failure visible once, at entry.

  3. Timeout-path process-tree cleanup: on overall timeout the code calls proc.kill() then join(timeout=5) on the drain threads. proc.kill() only terminates the direct child — if the browser-use CLI spawned a grandchild that inherits stdout/stderr, the threads stay blocked until the join timeout (dropping the tail of the output), and the grandchild may outlive the call. Consider start_new_session=True + killing the process group on timeout so the whole tree dies and the pipes close promptly.

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

Labels

P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/browser Browser automation (CDP, Playwright) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants