diff --git a/backend/apps/agents/core/ws_manager.py b/backend/apps/agents/core/ws_manager.py index b5ec0ee5..7d53d64d 100644 --- a/backend/apps/agents/core/ws_manager.py +++ b/backend/apps/agents/core/ws_manager.py @@ -15,6 +15,7 @@ BROWSER_CMD_TIMEOUTS = { "replay_route": 20.0, # an API fetch can be slow "wait": 12.0, # smart-wait already caps itself well under this "perform_action": 35.0, # session-borrow shims pack navigate + wait + scrape into ONE command, so it needs more than navigate alone + "find_composer": 30.0, # packs trigger + scroll-ladder + retop + open-first into ONE command; on the 15s default the last two tiers were unreachable and heavy pages died mid-ladder (measured: linkedin timed out 2 of 3 runs). The in-page routine self-caps well under this. } BROWSER_CMD_REBROADCAST_S = 3.0 # A CPU-starved renderer can briefly drop its WS (a missed heartbeat) and the frontend auto-reconnects a beat later; bridge that gap instead of hard-failing a live run into it. Short enough that a genuinely-closed window still fails quickly (and no LLM turns are ever burned waiting); long enough to ride out a reconnect even on a loaded machine. diff --git a/frontend/src/shared/browserCommandHandler.ts b/frontend/src/shared/browserCommandHandler.ts index 0cc8bb90..dbbe545e 100644 --- a/frontend/src/shared/browserCommandHandler.ts +++ b/frontend/src/shared/browserCommandHandler.ts @@ -570,11 +570,19 @@ async function handleFindComposer(wv: BrowserWebview, params: Record DEADLINE - Date.now(); + const budget = (want) => Math.max(0, Math.min(want, left())); // 1. A compose opener visible up top (Gmail "Compose", LinkedIn "Start a post"). Patient // poll: LinkedIn code-splits its share modal, and under a heavy session the editor // chunk lands past 2.5s (measured: the 2.5s poll missed ~half the time; poll exits the // moment the editable appears, so the patience costs nothing on the happy path). - try { if (clickTrigger(false)) { acts.push('trigger'); hit = await pollFind(6000); } } catch (e) { /* keep going */ } + try { if (clickTrigger(false)) { acts.push('trigger'); hit = await pollFind(budget(6000)); } } catch (e) { /* keep going */ } // 2. Progressive scroll for a below-fold / lazy composer: YouTube comments hydrate on // scroll and start as a placeholder that only becomes editable once clicked, so scroll // a step, re-scan, and re-click the trigger on whatever just entered the viewport, up to @@ -582,13 +590,14 @@ async function handleFindComposer(wv: BrowserWebview, params: Record 7000; step++) { const before = sc.scrollTop; sc.scrollBy(0, Math.round(window.innerHeight * 0.9)); scrolled = true; await sleep(500); hit = findBest(); - if (!hit) { try { if (clickTrigger(true)) hit = await pollFind(1400); } catch (e) { /* keep going */ } } + if (!hit) { try { if (clickTrigger(true)) hit = await pollFind(budget(1400)); } catch (e) { /* keep going */ } } if (sc.scrollTop === before) break; } if (scrolled) acts.push('scroll'); @@ -596,16 +605,16 @@ async function handleFindComposer(wv: BrowserWebview, params: Record 2500) { try { (document.scrollingElement || document.documentElement).scrollTo(0, 0); await sleep(400); - if (clickTrigger(false)) { acts.push('retop'); hit = await pollFind(4000); } + if (clickTrigger(false)) { acts.push('retop'); hit = await pollFind(budget(4000)); } } catch (e) { /* keep going */ } } // 4. Last resort: open the first list item (X DMs / chat lists). Navigational, so it runs // only after trigger+scroll fail, which stops it from yanking YouTube to another video. - if (!hit) { let did = false; try { did = openFirstItem(); } catch (e) { did = false; } if (did) { acts.push('open-first'); hit = await pollFind(2000); } } + if (!hit && left() > 1200) { let did = false; try { did = openFirstItem(); } catch (e) { did = false; } if (did) { acts.push('open-first'); hit = await pollFind(budget(2000)); } } } if (!hit) return { found: false, reveals: acts };