mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-05 17:27:42 +02:00
[eric] browser: reuse the parent's same-host card on re-dispatch, stacked webviews wedge each other
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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") == ""
|
||||
|
||||
Reference in New Issue
Block a user