From 2c55d17f50f8f2853f31d847eca6fa07696b961f Mon Sep 17 00:00:00 2001 From: ciregenz Date: Fri, 5 Jun 2026 15:56:38 -0700 Subject: [PATCH] [eric] browser: dead-dashboard dispatches fail in 1s instead of burning turns, recovery skips systemic causes --- backend/apps/agents/agent_manager.py | 9 +++++++-- backend/apps/agents/browser/browser_agent.py | 13 +++++++++++++ backend/apps/agents/browser/browser_fast_path.py | 6 ++++++ backend/tests/test_browser_fast_path.py | 12 ++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/backend/apps/agents/agent_manager.py b/backend/apps/agents/agent_manager.py index 10b8e60a..18835ce3 100644 --- a/backend/apps/agents/agent_manager.py +++ b/backend/apps/agents/agent_manager.py @@ -3386,8 +3386,13 @@ class AgentManager: if not text: text = await _dispatch(browser_fast_path.compose_task(prompt, brief)) if browser_fast_path.dispatch_failed(text): - logger.info(f"[browser-fast-path] first dispatch failed for {session_id}; one recovery dispatch") - text = await _dispatch(browser_fast_path.recovery_task(prompt, text)) + # Retry only transient failures; a dead dashboard fails the + # retry identically, so skip it and tell the user instead. + if ws_manager.global_connections: + logger.info(f"[browser-fast-path] first dispatch failed for {session_id}; one recovery dispatch") + text = await _dispatch(browser_fast_path.recovery_task(prompt, text)) + else: + text = browser_fast_path.NO_DASHBOARD_REPLY if not text: text = "The browser agent couldn't complete this and gave no report." except asyncio.CancelledError: diff --git a/backend/apps/agents/browser/browser_agent.py b/backend/apps/agents/browser/browser_agent.py index d00d4eee..9dcf5e88 100644 --- a/backend/apps/agents/browser/browser_agent.py +++ b/backend/apps/agents/browser/browser_agent.py @@ -1596,6 +1596,19 @@ async def run_browser_agents( """ pass # Browser agent launch captured via session dump + # No dashboard renderer means every browser command is dead on arrival; + # failing here saves the 2-5 LLM turns a sub burns narrating timeouts at a + # corpse before card-gone detection trips. + if not ws_manager.global_connections: + logger.warning("[browser-agent] dispatch refused: no dashboard connected") + return [{ + "summary": ( + "Error: no dashboard window is connected, so browser tools cannot run. " + "Tell the user to open the OpenSwarm window and try again; do not retry until they do." + ), + "action_log": [], "final_screenshot": None, + } for _ in tasks] + pre_selected = set(pre_selected_browser_ids or []) async def _run_one(task_def: dict) -> dict: diff --git a/backend/apps/agents/browser/browser_fast_path.py b/backend/apps/agents/browser/browser_fast_path.py index e49e6268..e83200ba 100644 --- a/backend/apps/agents/browser/browser_fast_path.py +++ b/backend/apps/agents/browser/browser_fast_path.py @@ -125,6 +125,12 @@ def dispatch_failed(summary: str) -> bool: return not ("OUTCOME: DONE" in s.upper() or "learned skill replay" in s) +NO_DASHBOARD_REPLY = ( + "I can't drive a browser right now because no OpenSwarm window is connected. " + "Open the app window and send this again." +) + + def recovery_task(prompt: str, first_report: str) -> str: """One informed retry, replacing the orchestrator's recovery role. Verify- first wording keeps a maybe-already-sent irreversible step from repeating.""" diff --git a/backend/tests/test_browser_fast_path.py b/backend/tests/test_browser_fast_path.py index a7592d88..15176b56 100644 --- a/backend/tests/test_browser_fast_path.py +++ b/backend/tests/test_browser_fast_path.py @@ -92,3 +92,15 @@ def test_text_normalizes_to_message_without_phone_number(): assert _normalize_for_classifier(sms) == sms count = "count messages containing the exact text r10-os" assert "message r10-os" in _normalize_for_classifier(count) + + +def test_dispatch_refused_instantly_when_no_dashboard_connected(): + import asyncio + from backend.apps.agents.browser.browser_agent import run_browser_agents + from backend.apps.agents.core.ws_manager import ws_manager + + assert not ws_manager.global_connections + results = asyncio.run(run_browser_agents(tasks=[{"task": "go to example.com"}], model="sonnet")) + assert len(results) == 1 + assert results[0]["summary"].startswith("Error: no dashboard window is connected") + assert dispatch_failed(results[0]["summary"])