diff --git a/bin/box-chat-cdp.py b/bin/box-chat-cdp.py index 16cdd18..122d2ad 100755 --- a/bin/box-chat-cdp.py +++ b/bin/box-chat-cdp.py @@ -55,7 +55,22 @@ def ev(ws, expr, await_p=False): "params": {"expression": expr, "returnByValue": True, "awaitPromise": await_p}, })) - resp = json.loads(ws.recv()) + # Drain CDP events until we get our command response (id 1). + # The browser can emit events (Runtime.executionContextCreated, etc.) + # at any time; taking the first recv() blindly returns None on a + # busy page (observed as transient NO_SWITCHER / "unexpected + # messages payload" on pip/opm 2026-10-04). + resp = None + drained = 0 + for _ in range(50): + raw = ws.recv() + resp = json.loads(raw) + if resp.get("id") == 1: + break + drained += 1 + else: + raise RuntimeError("CDP: no response to Runtime.evaluate (drained %d)" % drained) + pass # drained count available in `drained` if needed res = resp.get("result", {}) if res.get("subtype") == "error": raise RuntimeError("JS error: %s" % str(res.get("description"))[:200]) @@ -79,7 +94,13 @@ ENSURE = """(async () => { if (nav.getAttribute('aria-current') !== 'page') { nav.click(); await sleep(3000); } const panelOpen = () => !!document.querySelector('[data-testid="hatch-chat-compose"]'); if (!panelOpen()) { - const sw = document.querySelector('[data-testid="hatch-chat-switcher-trigger"]'); + // Retry: the switcher may not be rendered yet if the SPA is still + // settling (observed transient NO_SWITCHER on pip/opm 2026-10-04). + let sw = null; + for (let k = 0; k < 4 && !sw; k++) { + sw = document.querySelector('[data-testid="hatch-chat-switcher-trigger"]'); + if (!sw) await sleep(2000); + } if (!sw) return 'NO_SWITCHER'; sw.click(); await sleep(2500); if (!panelOpen()) return 'PANEL_CLOSED'; @@ -250,12 +271,19 @@ def main(argv): agent, op = argv[1], argv[2] ws = None try: - ws = connect(agent) - # CDP evaluate can rarely resolve null when the page is mid-navigation; - # one retry covers the flake (observed 2026-10-04). + # CDP evaluate can resolve null when the page is mid-navigation + # (agent actively using browser). Reconnect fresh on each retry + # so we attach to the current page, not a stale JS context. + # Observed 2026-10-04: opm's browser navigates during reads. data, last_err = None, None - for attempt in range(2): + for attempt in range(3): try: + if ws is not None: + try: + ws.close() + except Exception: + pass + ws = connect(agent) if op == "threads": data = op_threads(ws) ok = isinstance(data, dict) and isinstance(