From ad9bb8439e8697091a38ac97830d89a0d0704f2d Mon Sep 17 00:00:00 2001 From: ciregenz Date: Mon, 6 Jul 2026 15:22:53 -0700 Subject: [PATCH] [eric] browser: cut send-path waits (composer-fill poll 6s->2.4s after 3/3 full-timeout misses; skip the 4s text-confirm on send-class clicks the receipt already proves) --- backend/apps/agents/browser/browser_agent.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/backend/apps/agents/browser/browser_agent.py b/backend/apps/agents/browser/browser_agent.py index 96842ab2..ca51577e 100644 --- a/backend/apps/agents/browser/browser_agent.py +++ b/backend/apps/agents/browser/browser_agent.py @@ -450,8 +450,8 @@ async def post_action_state( lst = None p_send_si = None if p_composer_fill: - # The Send button lazy-renders a beat LATER than the text commits (measured: not in the AX tree even at 2.5s, worse under load), and a single re-list races that and loses, the old handoff never fired (send-ready=0 across 10 A/B legs). So POLL the actual interactives list for a REAL Send button and stop the instant it appears; this checks for the exact thing we hand over, not just 'Send' text. - p_deadline = time.monotonic() + 6.0 + # The Send button lazy-renders a beat LATER than the text commits (measured: not in the AX tree even at 2.5s, worse under load), and a single re-list races that and loses, the old handoff never fired (send-ready=0 across 10 A/B legs). So POLL the actual interactives list for a REAL Send button and stop the instant it appears; this checks for the exact thing we hand over, not just 'Send' text. Deadline cut 6s->2.4s: recent sends ran the FULL 6s and still found nothing (send_button_found=False 3/3), so the long tail bought pure wall time; two polls catch the lazy-render case, the model finds Send itself past that. + p_deadline = time.monotonic() + 2.4 while True: try: p_l = await asyncio.wait_for( @@ -1716,7 +1716,19 @@ async def run_browser_agent( # Act-and-confirm: if the agent declared the change it expects, VERIFY it actually happened, success is observed, never assumed. A hit returns fast (act + confirm in one turn); a miss is a clear "may not have worked" (and a wedge surfaces as a clean not-confirmed, not a blind 20s timeout), so the agent never claims a success it didn't see or re-fires blindly. p_expect = (str(tu.input.get("expect") or "").strip() if isinstance(tu.input, dict) else "") - if p_expect and "error" not in result and tu.name in P_CONFIRM_TOOLS: + # A send-class click's text-probe is documented-unreliable (sent text renders late/split/scrolled off) and the composer-clear receipt supersedes it, so don't burn the 4s probe timeout on exactly the clicks that never confirm by text. + p_is_send_click = (task_is_send and "error" not in result and tu.name in P_CONFIRM_TOOLS + and (browser_batch_replay.is_send_completed( + {"action": "click", "name": result.get("clickedName") or "", + "role": result.get("clickedRole") or ""}) or any( + browser_batch_replay.is_send_completed( + {"action": "click", "name": r.get("clickedName") or "", + "role": r.get("clickedRole") or ""}) + for r in (result.get("results") or [])))) + if p_is_send_click and p_expect: + result["confirmed"] = True + result["text"] = f"{result.get('text') or ''}\nConfirmed by receipt: the send-class click registered." + elif p_expect and "error" not in result and tu.name in P_CONFIRM_TOOLS: # target_only: wait for the expected text to actually appear, don't call it 'not confirmed' just because the page settled first (a sent message lands in the thread a beat after settle, esp. under load) p_conf = await browser_wait.smart_wait(p_wait_exec, browser_id, tab_id, 4000, until=p_expect, target_only=True)