daemon: give each named daemon its own tab on shared browsers - #616
daemon: give each named daemon its own tab on shared browsers#616teknium1 wants to merge 1 commit into
Conversation
BU_NAME already isolates a daemon's socket, log, and pid, but every local/CDP daemon still ran attach_first_page() against the same shared Chrome — so parallel named daemons all attached to the SAME tab and fought over navigations (browser-use#375, browser-use#582; also reported against downstream integrations). Now a named daemon (BU_NAME != "default") on a local/CDP browser creates its own dedicated about:blank tab via Target.createTarget and attaches to that, instead of grabbing the first existing page. On shutdown the daemon closes the tab it created, so parallel workers don't leak tabs into the shared browser. Unchanged behavior: - the default daemon keeps attaching to the first real page (single-user flow: reuse the tab the user is looking at); - named cloud daemons (BU_BROWSER_ID) keep first-page attach — the remote browser is exclusive to the daemon, and creating a tab there would just leak one. Verified live against a shared headless Chrome: two named daemons ("wa"/"wb") created distinct targets, set different page titles, read them back intact across calls, and the owned tab was closed on meta:shutdown (tab count returned to baseline). tests/unit: 109 passed.
There was a problem hiding this comment.
4 issues found across 2 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/browser_harness/daemon.py">
<violation number="1" location="src/browser_harness/daemon.py:376">
P2: On a stale-session re-attach, a named daemon creates a brand-new dedicated tab and overwrites `owns_target`/`target_id`, so the previous tab it created in `attach_first_page()` is never closed. Only the last tab is closed at shutdown, so each re-attach leaks one `about:blank` into the shared browser. Track the previous owned target and `Target.closeTarget` it (best-effort) before re-creating a tab when `self.owns_target and self.target_id` is already set.</violation>
<violation number="2" location="src/browser_harness/daemon.py:377">
P2: If target creation succeeds but attach or startup setup fails, the daemon exits before its cleanup block and leaves an orphan tab in the shared browser. Record the created ID immediately and clean it up from the attach/startup failure path.</violation>
<violation number="3" location="src/browser_harness/daemon.py:386">
P2: When startup follows the Chrome remote-debugging recovery flow, this early return skips inspect-tab cleanup and marker removal. Run the same cleanup for the named local path before returning.</violation>
<violation number="4" location="src/browser_harness/daemon.py:642">
P1: After a named daemon switches tabs, `self.target_id` no longer identifies the tab it created. Shutdown therefore closes the selected tab and leaks the owned tab; track a separate `owned_target_id` and close that ID.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| # A named daemon owns the tab it created — close it on shutdown so | ||
| # parallel workers don't leak about:blank/leftover tabs into the | ||
| # shared browser. Best-effort: the WS may already be gone. | ||
| if d.owns_target and d.target_id: |
There was a problem hiding this comment.
P1: After a named daemon switches tabs, self.target_id no longer identifies the tab it created. Shutdown therefore closes the selected tab and leaks the owned tab; track a separate owned_target_id and close that ID.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/browser_harness/daemon.py, line 642:
<comment>After a named daemon switches tabs, `self.target_id` no longer identifies the tab it created. Shutdown therefore closes the selected tab and leaks the owned tab; track a separate `owned_target_id` and close that ID.</comment>
<file context>
@@ -615,7 +633,21 @@ async def handler(reader, writer):
+ # A named daemon owns the tab it created — close it on shutdown so
+ # parallel workers don't leak about:blank/leftover tabs into the
+ # shared browser. Best-effort: the WS may already be gone.
+ if d.owns_target and d.target_id:
+ try:
+ await asyncio.wait_for(
</file context>
| # already exclusive to this daemon, so first-page attach stays. | ||
| if NAME != "default" and not REMOTE_ID: | ||
| tid = (await self.cdp.send_raw("Target.createTarget", {"url": "about:blank"}))["targetId"] | ||
| self.owns_target = True |
There was a problem hiding this comment.
P2: If target creation succeeds but attach or startup setup fails, the daemon exits before its cleanup block and leaves an orphan tab in the shared browser. Record the created ID immediately and clean it up from the attach/startup failure path.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/browser_harness/daemon.py, line 377:
<comment>If target creation succeeds but attach or startup setup fails, the daemon exits before its cleanup block and leaves an orphan tab in the shared browser. Record the created ID immediately and clean it up from the attach/startup failure path.</comment>
<file context>
@@ -360,12 +360,30 @@ def __init__(self):
+ # already exclusive to this daemon, so first-page attach stays.
+ if NAME != "default" and not REMOTE_ID:
+ tid = (await self.cdp.send_raw("Target.createTarget", {"url": "about:blank"}))["targetId"]
+ self.owns_target = True
+ log(f"named daemon {NAME}: created dedicated tab ({tid})")
+ page = {"targetId": tid, "url": "about:blank", "type": "page"}
</file context>
| self.target_id = tid | ||
| log(f"attached {tid} (about:blank) session={self.session}") | ||
| await self._enable_default_domains(self.session) | ||
| return page |
There was a problem hiding this comment.
P2: When startup follows the Chrome remote-debugging recovery flow, this early return skips inspect-tab cleanup and marker removal. Run the same cleanup for the named local path before returning.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/browser_harness/daemon.py, line 386:
<comment>When startup follows the Chrome remote-debugging recovery flow, this early return skips inspect-tab cleanup and marker removal. Run the same cleanup for the named local path before returning.</comment>
<file context>
@@ -360,12 +360,30 @@ def __init__(self):
+ self.target_id = tid
+ log(f"attached {tid} (about:blank) session={self.session}")
+ await self._enable_default_domains(self.session)
+ return page
targets = (await self.cdp.send_raw("Target.getTargets"))["targetInfos"]
pages = [t for t in targets if is_real_page(t)]
</file context>
| # daemon its own dedicated tab instead. REMOTE_ID (cloud) browsers are | ||
| # already exclusive to this daemon, so first-page attach stays. | ||
| if NAME != "default" and not REMOTE_ID: | ||
| tid = (await self.cdp.send_raw("Target.createTarget", {"url": "about:blank"}))["targetId"] |
There was a problem hiding this comment.
P2: On a stale-session re-attach, a named daemon creates a brand-new dedicated tab and overwrites owns_target/target_id, so the previous tab it created in attach_first_page() is never closed. Only the last tab is closed at shutdown, so each re-attach leaks one about:blank into the shared browser. Track the previous owned target and Target.closeTarget it (best-effort) before re-creating a tab when self.owns_target and self.target_id is already set.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/browser_harness/daemon.py, line 376:
<comment>On a stale-session re-attach, a named daemon creates a brand-new dedicated tab and overwrites `owns_target`/`target_id`, so the previous tab it created in `attach_first_page()` is never closed. Only the last tab is closed at shutdown, so each re-attach leaks one `about:blank` into the shared browser. Track the previous owned target and `Target.closeTarget` it (best-effort) before re-creating a tab when `self.owns_target and self.target_id` is already set.</comment>
<file context>
@@ -360,12 +360,30 @@ def __init__(self):
+ # daemon its own dedicated tab instead. REMOTE_ID (cloud) browsers are
+ # already exclusive to this daemon, so first-page attach stays.
+ if NAME != "default" and not REMOTE_ID:
+ tid = (await self.cdp.send_raw("Target.createTarget", {"url": "about:blank"}))["targetId"]
+ self.owns_target = True
+ log(f"named daemon {NAME}: created dedicated tab ({tid})")
</file context>
Problem
BU_NAMEalready isolates a daemon's IPC socket, log, and pid — but on a shared local/CDP browser, every named daemon still runsattach_first_page()against the same Chrome, so parallel daemons all attach to the same tab and clobber each other's navigations. This is the root of the parallel-subagent pain in #375 and #582 (and reported against downstream integrations embedding the harness — e.g. @shantanugoel's report against Hermes Agent'sbrowser_exec).Change
A named daemon (
BU_NAME != "default") on a local/CDP browser now creates its own dedicatedabout:blanktab viaTarget.createTargetand attaches to that, instead of grabbing the first existing page. On shutdown it closes the tab it created (owns_target), so workers don't leak tabs into the shared browser.Unchanged:
BU_BROWSER_ID) keep first-page attach — the remote browser is exclusive to the daemon; creating a tab there would just leak one.This makes
BU_NAMEthe complete parallelism primitive the SKILL.md already implies: name → own socket → own tab. Combined with per-name tab identity work (#604) operators can also tell workers apart.Validation
tests/unit: 109 passed (3 new: named-creates-dedicated-tab, default-keeps-first-page, named-cloud-keeps-first-page).wa,wb) created distinct targets, set differentdocument.titles, read them back intact across separate invocations;meta:shutdownclosed the owned tab (tab count returned to baseline, "closed owned tab" in the daemon log).Smallest diff that fixes the clobber: one guard clause in
attach_first_page()plus best-effort cleanup inmain().Summary by cubic
Named daemons on shared local/CDP browsers now create and attach to their own about:blank tab instead of reusing the first existing page, preventing parallel daemons from clobbering each other’s navigations. The default daemon and named cloud daemons keep first-page attach; named local daemons close their owned tab on shutdown to avoid leaks.
Review notes
Written for commit a1aded1. Summary will update on new commits.