mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-12 12:47:42 +02:00
[eric] browser: treat a hung tab (repeated timeouts) as card-unavailable so it aborts fast, not a 20-min spin
This commit is contained in:
@@ -1144,12 +1144,14 @@ async def run_browser_agent(
|
||||
)
|
||||
break
|
||||
|
||||
# The card's webview is gone (closed / dashboard not open). The agent
|
||||
# can't bring it back, so stop retrying and report it honestly below.
|
||||
# The card is unusable, gone (closed) OR hung (commands keep timing
|
||||
# out / the page never responds). Either way the agent can't make
|
||||
# progress, so stop retrying after a short streak and report honestly,
|
||||
# instead of the 20-minute spin on a wedged tab.
|
||||
if card_gone_streak >= _CARD_GONE_LIMIT:
|
||||
logger.warning(
|
||||
f"[browser-agent {session_id}] browser card {browser_id} is gone "
|
||||
f"({card_gone_streak} consecutive misses); aborting fast"
|
||||
f"[browser-agent {session_id}] browser card {browser_id} is unusable "
|
||||
f"({card_gone_streak} consecutive gone/hung results); aborting fast"
|
||||
)
|
||||
break
|
||||
|
||||
@@ -1198,7 +1200,7 @@ async def run_browser_agent(
|
||||
# looked around), report the truth instead of a ghost "completed". A gone
|
||||
# card gets its own precise reason instead of the generic verdict.
|
||||
if card_gone_streak >= _CARD_GONE_LIMIT:
|
||||
honest, dishonest_reason = False, "the browser card is no longer open (it was closed or never opened)"
|
||||
honest, dishonest_reason = False, "the browser became unresponsive (the tab hung or was closed); it needs a fresh browser to continue"
|
||||
else:
|
||||
honest, dishonest_reason = completion_is_honest(action_log)
|
||||
final_status = "completed" if honest else "error"
|
||||
|
||||
@@ -200,12 +200,18 @@ _READ_TOOLS = {
|
||||
}
|
||||
|
||||
|
||||
# A card whose webview is gone is UNRECOVERABLE by the agent (it cannot resurrect
|
||||
# the card), unlike a missing selector it could route around. The frontend
|
||||
# returns these only AFTER a 2s re-register grace, so they mean the card is truly
|
||||
# gone (closed) or the dashboard was never open. Retrying just burns the turn
|
||||
# budget (the multi-minute spins we measured), so the caller fails fast instead.
|
||||
_CARD_GONE_MARKERS = ("not an electron webview", "no dashboard is connected")
|
||||
# A card the agent can't make progress on, EITHER gone (closed/dashboard not open;
|
||||
# unrecoverable) OR hung (a wedged tab where every command times out / the page
|
||||
# never responds). Both look the same to the agent: retrying just burns time (the
|
||||
# 20-minute LinkedIn spin), so we fail fast. The streak (reset on any good result)
|
||||
# absorbs a one-off transient; only a SUSTAINED pattern trips it, so a merely-busy
|
||||
# page that recovers is never mistaken for dead.
|
||||
_CARD_GONE_MARKERS = (
|
||||
"not an electron webview", # card closed / destroyed
|
||||
"no dashboard is connected", # dashboard view not mounted
|
||||
"command timed out", # hung: the command never came back
|
||||
"page unresponsive", # hung: smart-wait gave up probing the tab
|
||||
)
|
||||
_CARD_GONE_LIMIT = 2 # consecutive misses before we give up (absorbs a transient)
|
||||
|
||||
|
||||
|
||||
@@ -633,7 +633,41 @@ def test_dead_browser_card_aborts_fast_without_spinning(monkeypatch):
|
||||
))
|
||||
assert len(primary.calls) <= 3, "a dead card must fail fast, not spin the whole budget"
|
||||
assert captured.get("status") == "error"
|
||||
assert "no longer open" in r["summary"].lower()
|
||||
assert "unresponsive" in r["summary"].lower()
|
||||
|
||||
|
||||
def test_hung_browser_card_aborts_fast_not_a_20_minute_loop(monkeypatch):
|
||||
# THE regression from the user's 20-min LinkedIn freeze: a HUNG tab returns
|
||||
# "Browser command timed out" on every command (not "card not found"), so the
|
||||
# gone-detector never tripped and the agent spun for minutes. Now a hung card
|
||||
# feeds the same fast-fail streak and aborts in a couple of turns.
|
||||
import backend.apps.agents.browser.browser_skills as SK
|
||||
SK.clear()
|
||||
BH._browser_history.clear()
|
||||
primary = FakeLLM(
|
||||
[Resp([_rp("read"), _tu("BrowserGetText")]) for _ in range(8)]
|
||||
+ [Resp([Blk("text", "done")], stop_reason="end_turn")]
|
||||
)
|
||||
_install(monkeypatch, primary, FakeAux())
|
||||
|
||||
async def _hung(request_id, action, browser_id, params, tab_id=""):
|
||||
return {"error": "Browser command timed out"} # what a wedged tab returns
|
||||
monkeypatch.setattr(BA.ws_manager, "send_browser_command", _hung, raising=False)
|
||||
captured = {}
|
||||
orig = BA.ws_manager.send_to_session
|
||||
|
||||
async def _cap(session_id, event, payload):
|
||||
if event == "agent:status":
|
||||
captured["status"] = payload.get("status")
|
||||
return await orig(session_id, event, payload)
|
||||
monkeypatch.setattr(BA.ws_manager, "send_to_session", _cap, raising=False)
|
||||
|
||||
r = asyncio.run(BA.run_browser_agent(
|
||||
task="Read the page", browser_id="b1", model="sonnet", initial_url=DOC_URL,
|
||||
))
|
||||
assert len(primary.calls) <= 3, "a hung card must abort fast, not spin for 20 minutes"
|
||||
assert captured.get("status") == "error"
|
||||
assert "unresponsive" in r["summary"].lower()
|
||||
|
||||
|
||||
def test_perception_is_frontloaded_into_first_turn(monkeypatch):
|
||||
|
||||
@@ -159,6 +159,11 @@ def test_card_is_unavailable_only_for_unrecoverable_errors():
|
||||
# a gone card is unrecoverable (fail fast); a missing selector is not (route around)
|
||||
assert card_is_unavailable({"error": "Browser card 'b1' not found or not an Electron webview"})
|
||||
assert card_is_unavailable({"error": "No dashboard is connected. Open the dashboard to use browser tools."})
|
||||
# a HUNG card (the 20-min LinkedIn freeze) also counts: commands time out, the
|
||||
# page never responds, retrying is pointless -> same fast-fail streak as gone
|
||||
assert card_is_unavailable({"error": "Browser command timed out"})
|
||||
assert card_is_unavailable({"error": "page unresponsive"})
|
||||
# but normal, recoverable problems do NOT (the agent can route around these)
|
||||
assert not card_is_unavailable({"error": "Element not found: '.submit'"})
|
||||
assert not card_is_unavailable({"text": "ok", "url": "http://x"})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user