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,
|
"params": {"expression": expr, "returnByValue": True,
|
||||||
"awaitPromise": await_p},
|
"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", {})
|
res = resp.get("result", {})
|
||||||
if res.get("subtype") == "error":
|
if res.get("subtype") == "error":
|
||||||
raise RuntimeError("JS error: %s" % str(res.get("description"))[:200])
|
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); }
|
if (nav.getAttribute('aria-current') !== 'page') { nav.click(); await sleep(3000); }
|
||||||
const panelOpen = () => !!document.querySelector('[data-testid="hatch-chat-compose"]');
|
const panelOpen = () => !!document.querySelector('[data-testid="hatch-chat-compose"]');
|
||||||
if (!panelOpen()) {
|
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';
|
if (!sw) return 'NO_SWITCHER';
|
||||||
sw.click(); await sleep(2500);
|
sw.click(); await sleep(2500);
|
||||||
if (!panelOpen()) return 'PANEL_CLOSED';
|
if (!panelOpen()) return 'PANEL_CLOSED';
|
||||||
@@ -250,12 +271,19 @@ def main(argv):
|
|||||||
agent, op = argv[1], argv[2]
|
agent, op = argv[1], argv[2]
|
||||||
ws = None
|
ws = None
|
||||||
try:
|
try:
|
||||||
ws = connect(agent)
|
# CDP evaluate can resolve null when the page is mid-navigation
|
||||||
# CDP evaluate can rarely resolve null when the page is mid-navigation;
|
# (agent actively using browser). Reconnect fresh on each retry
|
||||||
# one retry covers the flake (observed 2026-10-04).
|
# 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
|
data, last_err = None, None
|
||||||
for attempt in range(2):
|
for attempt in range(3):
|
||||||
try:
|
try:
|
||||||
|
if ws is not None:
|
||||||
|
try:
|
||||||
|
ws.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
ws = connect(agent)
|
||||||
if op == "threads":
|
if op == "threads":
|
||||||
data = op_threads(ws)
|
data = op_threads(ws)
|
||||||
ok = isinstance(data, dict) and isinstance(
|
ok = isinstance(data, dict) and isinstance(
|
||||||
|
|||||||
Reference in New Issue
Block a user