[eric] browser: reuse the parent's same-host card on re-dispatch, stacked webviews wedge each other

This commit is contained in:
ciregenz
2026-06-05 01:38:58 -07:00
parent ed3ffd2de1
commit 65427f33a0
2 changed files with 120 additions and 11 deletions
+71 -11
View File
@@ -1413,6 +1413,45 @@ async def run_browser_agent(
}
# Cards a sub-agent is actively driving in this process. Reuse must never hand
# two agents one webview (their commands would interleave into chaos).
_active_agent_cards: set[str] = set()
# find+claim+create must be one critical section or two parallel dispatches
# race to claim the same idle card (or both miss and double-create).
_card_pick_lock = asyncio.Lock()
def _find_reusable_card(dashboard_id: str, url: str, parent_session_id: str | None) -> str:
"""An existing same-host spawned card to drive instead of stacking another
webview: concurrent same-site webviews wedge each other (shared-partition
lock contention), so a retry must REUSE, not multiply. The parent's own
card first, else one orphaned by a finished parent. User-created cards
(no spawned_by) are never grabbed implicitly."""
want = browser_skills.host_of(url)
if not (dashboard_id and want):
return ""
try:
from backend.apps.dashboards.dashboards import _load
cards = _load(dashboard_id).layout.browser_cards
except Exception:
return ""
from backend.apps.agents.agent_manager import agent_manager
own, orphan = "", ""
for bid, card in cards.items():
spawned = getattr(card, "spawned_by", None)
if not spawned or bid in _active_agent_cards:
continue
if browser_skills.host_of(getattr(card, "url", "") or "") != want:
continue
if spawned == parent_session_id:
own = own or bid
else:
parent = agent_manager.get_session(spawned)
if parent is None or getattr(parent, "status", "") != "running":
orphan = orphan or bid
return own or orphan
async def _create_browser_card(dashboard_id: str, url: str, parent_session_id: str | None = None) -> str:
"""Create a new browser card on the dashboard and return its browser_id."""
from backend.apps.dashboards.dashboards import _load, _save
@@ -1466,20 +1505,41 @@ async def run_browser_agents(
task_text = task_def.get("task", "")
url = task_def.get("url", "")
reused = False
if not browser_id and dashboard_id:
browser_id = await _create_browser_card(dashboard_id, url, parent_session_id)
await asyncio.sleep(2.0)
async with _card_pick_lock:
browser_id = _find_reusable_card(dashboard_id, url, parent_session_id)
if browser_id:
reused = True
else:
browser_id = await _create_browser_card(dashboard_id, url, parent_session_id)
_active_agent_cards.add(browser_id)
if reused:
logger.info(f"[browser-agent] reusing same-host card {browser_id} instead of stacking another webview")
if url:
# a retry starts from the task's entry URL, never the failed attempt's leftover page state
try:
await execute_browser_tool("BrowserNavigate", {"url": url}, browser_id)
except Exception:
pass
else:
await asyncio.sleep(2.0)
elif browser_id:
_active_agent_cards.add(browser_id)
is_pre_selected = browser_id in pre_selected
return await run_browser_agent(
task=task_text,
browser_id=browser_id,
model=model,
dashboard_id=dashboard_id,
pre_selected=is_pre_selected,
initial_url=url if url and browser_id not in pre_selected else None,
parent_session_id=parent_session_id,
)
try:
return await run_browser_agent(
task=task_text,
browser_id=browser_id,
model=model,
dashboard_id=dashboard_id,
pre_selected=is_pre_selected,
initial_url=url if url and browser_id not in pre_selected else None,
parent_session_id=parent_session_id,
)
finally:
_active_agent_cards.discard(browser_id)
results = await asyncio.gather(*[_run_one(t) for t in tasks], return_exceptions=True)
+49
View File
@@ -1094,3 +1094,52 @@ def test_prior_domain_hint_is_seeded_into_system_prompt(monkeypatch):
if isinstance(system, list):
assert system[-1].get("cache_control", {}).get("type") == "ephemeral"
assert len(aux.calls) == 0 # no exhaustion, no adjudication on a clean run
def test_find_reusable_card_reuses_own_then_orphan_never_user(monkeypatch):
# Concurrent same-site webviews wedge each other, so a re-dispatch must
# reuse the parent's own (or an orphaned) spawned card instead of stacking
# another. User-created cards (no spawned_by) are never grabbed implicitly.
import backend.apps.dashboards.dashboards as dash_mod
import backend.apps.agents.agent_manager as am_mod
class _Card:
def __init__(self, url, spawned_by):
self.url = url
self.spawned_by = spawned_by
class _Layout:
browser_cards = {
"b-user": _Card("https://www.linkedin.com/feed/", None),
"b-orphan": _Card("https://www.linkedin.com/search/x", "dead-parent"),
"b-own": _Card("https://www.linkedin.com/in/y", "p1"),
"b-hn": _Card("https://news.ycombinator.com/", "p1"),
}
class _Dash:
layout = _Layout()
monkeypatch.setattr(dash_mod, "_load", lambda did: _Dash(), raising=True)
class _Done:
status = "completed"
monkeypatch.setattr(am_mod.agent_manager, "get_session", lambda sid: _Done(), raising=False)
target = "https://www.linkedin.com/search/results/people/?keywords=t"
# the parent's own same-host card wins
assert BA._find_reusable_card("d1", target, "p1") == "b-own"
# a different parent skips p1's... unless that parent finished (orphan); first orphan wins
assert BA._find_reusable_card("d1", target, "p2") == "b-orphan"
# never a different host
assert BA._find_reusable_card("d1", "https://example.com/", "p1") == ""
# an actively-driven card is never grabbed
BA._active_agent_cards.update({"b-own", "b-orphan"})
try:
assert BA._find_reusable_card("d1", target, "p1") == ""
finally:
BA._active_agent_cards.clear()
# cards of a still-RUNNING other parent are off limits
class _Running:
status = "running"
monkeypatch.setattr(am_mod.agent_manager, "get_session", lambda sid: _Running(), raising=False)
assert BA._find_reusable_card("d1", target, "p2") == ""