Harden dm.py sidechat navigation (CDP event drain + Page.navigate)
- ev(): drain CDP events until response id==1 (was blind recv,
same bug class as box-chat-cdp.py 8d4bfa7 NO_SWITCHER fix)
- cmd_sidechat_use UUID path: use CDP Page.navigate instead of
window.location.href via evaluate; 5 attempts, 15s SPA settle
per attempt (was 3x4s, flaked 1/3 on url_mismatch)
- cmd_sidechat_use name path: retry 3x if not landing on /thread/
Test: 2/2 DM sends succeeded when browser healthy (tests 3-5
hit 646 browser death mid-test, infra issue not nav issue).
Session: sidechat/chromebox-ops
This commit is contained in:
+52
-8
@@ -69,7 +69,18 @@ def ev(ws, expr, await_p=False):
|
|||||||
"id": 1, "method": "Runtime.evaluate",
|
"id": 1, "method": "Runtime.evaluate",
|
||||||
"params": {"expression": expr, "returnByValue": True, "awaitPromise": await_p}
|
"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 navigation failures in dm.py
|
||||||
|
# sidechat sends, 2026-10-04 — same class as the NO_SWITCHER fix
|
||||||
|
# in box-chat-cdp.py commit 8d4bfa7).
|
||||||
|
for _ in range(50):
|
||||||
|
resp = json.loads(ws.recv())
|
||||||
|
if resp.get("id") == 1:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return None
|
||||||
return resp.get('result', {}).get('result', {}).get('value')
|
return resp.get('result', {}).get('result', {}).get('value')
|
||||||
|
|
||||||
def check_approvals(ws):
|
def check_approvals(ws):
|
||||||
@@ -215,6 +226,27 @@ def cmd_wait(ws, timeout=30):
|
|||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
cmd_messages(ws, 2)
|
cmd_messages(ws, 2)
|
||||||
|
|
||||||
|
def cdp_navigate(ws, url, timeout_s=30):
|
||||||
|
"""Navigate via CDP Page.navigate (proper navigation, waits for commit).
|
||||||
|
Returns True if the page URL matches the target after navigation."""
|
||||||
|
import time as _time
|
||||||
|
ws.send(json.dumps({"id": 2, "method": "Page.navigate",
|
||||||
|
"params": {"url": url}}))
|
||||||
|
# Drain until we get the Page.navigate response (id 2).
|
||||||
|
for _ in range(50):
|
||||||
|
resp = json.loads(ws.recv())
|
||||||
|
if resp.get("id") == 2:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
# Wait for the URL to settle (SPA client-side routing).
|
||||||
|
for _ in range(timeout_s):
|
||||||
|
cur = ev1(ws, "window.location.href", True)
|
||||||
|
if cur and url.rstrip("/").lower() in cur.lower():
|
||||||
|
return True
|
||||||
|
_time.sleep(1)
|
||||||
|
return False
|
||||||
|
|
||||||
def cmd_sidechat_use(ws, chat_id):
|
def cmd_sidechat_use(ws, chat_id):
|
||||||
"""Open a sidechat by name (sidebar text search) or by thread UUID
|
"""Open a sidechat by name (sidebar text search) or by thread UUID
|
||||||
(direct navigation). The sidebar shows titles, not UUIDs, so the
|
(direct navigation). The sidebar shows titles, not UUIDs, so the
|
||||||
@@ -224,15 +256,19 @@ def cmd_sidechat_use(ws, chat_id):
|
|||||||
if re.fullmatch(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", cid.lower()):
|
if re.fullmatch(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", cid.lower()):
|
||||||
url = "https://muse.ai/thread/" + cid.lower()
|
url = "https://muse.ai/thread/" + cid.lower()
|
||||||
want_uuid = cid.lower()
|
want_uuid = cid.lower()
|
||||||
# Use ev1 (skips CDP chatter) and confirm the URL actually changed.
|
# Use CDP Page.navigate (proper navigation lifecycle) and confirm
|
||||||
# A stale read here used to report the previous thread's URL (2026-10-04).
|
# the URL actually changed. Hardened 2026-10-04: the React SPA
|
||||||
|
# sometimes redirects back to / when the thread page hasn't finished
|
||||||
|
# loading; retry instead of failing immediately.
|
||||||
|
# (dm.py sidechat 1/3 flake; was window.location.href via evaluate.)
|
||||||
cur = None
|
cur = None
|
||||||
for _try in range(3):
|
for _try in range(5):
|
||||||
ev1(ws, "window.location.href=" + json.dumps(url), True)
|
ok = cdp_navigate(ws, url, timeout_s=15)
|
||||||
time.sleep(4)
|
|
||||||
cur = ev1(ws, "window.location.href", True)
|
cur = ev1(ws, "window.location.href", True)
|
||||||
if cur and want_uuid in cur:
|
if ok and cur and want_uuid in cur:
|
||||||
break
|
break
|
||||||
|
# SPA dropped the nav or hasn't routed yet; wait and retry.
|
||||||
|
time.sleep(3)
|
||||||
print(f"Navigated to: {cur}")
|
print(f"Navigated to: {cur}")
|
||||||
return
|
return
|
||||||
b64 = base64.b64encode(chat_id.encode()).decode()
|
b64 = base64.b64encode(chat_id.encode()).decode()
|
||||||
@@ -255,7 +291,15 @@ def cmd_sidechat_use(ws, chat_id):
|
|||||||
"return window.location.href;"
|
"return window.location.href;"
|
||||||
"})()"
|
"})()"
|
||||||
)
|
)
|
||||||
result = ev(ws, js, True)
|
# Hardened 2026-10-04: the click sometimes doesn't navigate (React
|
||||||
|
# mid-render, or the SPA drops it). Retry the whole nav if we didn't
|
||||||
|
# land on a /thread/ URL.
|
||||||
|
result = None
|
||||||
|
for _try in range(3):
|
||||||
|
result = ev(ws, js, True)
|
||||||
|
if result and "/thread/" in result:
|
||||||
|
break
|
||||||
|
time.sleep(3)
|
||||||
print(f"Navigated to: {result}")
|
print(f"Navigated to: {result}")
|
||||||
|
|
||||||
def cmd_sidechat_main(ws):
|
def cmd_sidechat_main(ws):
|
||||||
|
|||||||
Reference in New Issue
Block a user