Fix transient NO_SWITCHER / CDP race in box-chat-cdp.py
Three fixes for flaky main-chat reads (pip/opm): 1. ENSURE: retry chat-switcher lookup 4x with 2s waits instead of immediate NO_SWITCHER (React may still be rendering). 2. ev(): drain CDP events until matching command id arrives; previously the first recv() could grab a browser event instead of our evaluate response, returning None. 3. main(): reconnect fresh websocket on each retry (3 attempts); reusing a stale ws after page navigation gave dead JS contexts. Verified: 7/8 reads succeed across all 4 agents; the 1 failure was THREAD_NOT_FOUND while opm was actively in a side chat, self-healed on next attempt. Session: sidechat/chromebox-ops
This commit is contained in:
+34
-6
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user