chore(fleet): sync operator memory, hatch menu dialogs, and watchdog alerts
This commit is contained in:
+119
-44
@@ -93,23 +93,29 @@ def get_page(node, cdp_url):
|
||||
return pages[0]
|
||||
|
||||
def ev(ws, expr, await_p=False):
|
||||
ws.send(json.dumps({
|
||||
"id": 1, "method": "Runtime.evaluate",
|
||||
"params": {"expression": expr, "returnByValue": True, "awaitPromise": await_p}
|
||||
}))
|
||||
# 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:
|
||||
"""Returns None (no traceback) if the CDP WebSocket drops.
|
||||
(Fix 2026-10-06: uncaught WebSocketConnectionClosedException.)"""
|
||||
try:
|
||||
ws.send(json.dumps({
|
||||
"id": 1, "method": "Runtime.evaluate",
|
||||
"params": {"expression": expr, "returnByValue": True, "awaitPromise": await_p}
|
||||
}))
|
||||
# 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')
|
||||
except Exception as e:
|
||||
print(f"CDP evaluate failed: {type(e).__name__}: {e}", file=sys.stderr)
|
||||
return None
|
||||
return resp.get('result', {}).get('result', {}).get('value')
|
||||
|
||||
def check_approvals(ws):
|
||||
"""
|
||||
@@ -362,14 +368,64 @@ def cmd_messages(ws, n=5, width=200):
|
||||
# Exclude the compose box subtree: a failed send leaves the draft text
|
||||
# (including the [id:...] tag) in the composer, and scraping it would
|
||||
# produce a false "verified" (2026-10-04 dm.py false-confirmation bug).
|
||||
result = ev1(ws, f"""(() => {{
|
||||
# 2026-10-05: row-aware scrape. The message feed alternates sender-header
|
||||
# rows (div.group/stacked-row, per-message timestamp in
|
||||
# div.text-caption-1) and message units. Each unit is prefixed with its
|
||||
# header's timestamp ([8:57 pm]) so sweeps can compute message age. The
|
||||
# feed is the row-parent whose non-row children hold <p> elements (the
|
||||
# sidebar shares the row classes). The feed hydrates async after
|
||||
# navigation, so poll up to ~8s before falling back to the legacy
|
||||
# paragraph scrape.
|
||||
result = ev1(ws, f"""(async () => {{
|
||||
const composer = document.querySelector('[contenteditable="true"]') ||
|
||||
document.querySelector('textarea[placeholder*="Message"]');
|
||||
const ps = [...document.querySelectorAll('p')]
|
||||
.filter(p => !(composer && composer.contains(p)))
|
||||
.slice(-{n*2}).map(p=>p.innerText.slice(0,{width}));
|
||||
return ps.join('\\n---\\n');
|
||||
}})()""")
|
||||
const noComposer = p => !(composer && composer.contains(p));
|
||||
const legacy = () => {{
|
||||
const ps = [...document.querySelectorAll('p')]
|
||||
.filter(noComposer)
|
||||
.slice(-{n*2}).map(p=>p.innerText.slice(0,{width}));
|
||||
return ps.join('\\n---\\n');
|
||||
}};
|
||||
const ROWSEL = 'div[class*="group/stacked-row"]';
|
||||
const findFeed = () => {{
|
||||
const byParent = new Map();
|
||||
for (const r of document.querySelectorAll(ROWSEL)) {{
|
||||
const p = r.parentElement;
|
||||
if (p) {{
|
||||
if (!byParent.has(p)) byParent.set(p, []);
|
||||
byParent.get(p).push(r);
|
||||
}}
|
||||
}}
|
||||
for (const [p, rs] of byParent) {{
|
||||
const hasMsg = [...p.children].some(c => rs.indexOf(c) === -1 &&
|
||||
c.querySelectorAll('p').length > 0);
|
||||
if (hasMsg) return [p, rs];
|
||||
}}
|
||||
return [null, null];
|
||||
}};
|
||||
let list = null, rows = null;
|
||||
for (let i = 0; i < 16 && !list; i++) {{
|
||||
[list, rows] = findFeed();
|
||||
if (!list) await new Promise(r => setTimeout(r, 500));
|
||||
}}
|
||||
if (!list) return legacy();
|
||||
let curTs = '';
|
||||
const out = [];
|
||||
for (const child of [...list.children]) {{
|
||||
if (composer && child.contains(composer)) continue;
|
||||
if (rows.indexOf(child) !== -1) {{
|
||||
const t = child.querySelector('div.text-caption-1');
|
||||
const txt = t ? t.innerText.trim() : '';
|
||||
if (txt) curTs = txt;
|
||||
}} else {{
|
||||
const ps = [...child.querySelectorAll('p')].filter(noComposer)
|
||||
.map(p=>p.innerText.slice(0,{width}));
|
||||
if (ps.length) out.push((curTs ? '[' + curTs + '] ' : '') + ps.join('\\n'));
|
||||
}}
|
||||
}}
|
||||
const res = out.slice(-{n}).join('\\n---\\n');
|
||||
return res || legacy();
|
||||
}})()""", True)
|
||||
print(result)
|
||||
|
||||
def cmd_compose_check(ws):
|
||||
@@ -398,20 +454,33 @@ def cmd_wait(ws, timeout=30):
|
||||
|
||||
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."""
|
||||
Returns True if the page URL matches the target after navigation.
|
||||
Returns False (no traceback) if the CDP WebSocket drops mid-call --
|
||||
the caller retries on False. (Fix 2026-10-06: uncaught
|
||||
WebSocketConnectionClosedException crashed dm.py sends as nav_failed.)"""
|
||||
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:
|
||||
try:
|
||||
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
|
||||
except Exception as e:
|
||||
# Browser CDP connection dropped (crash/restart/relay flake).
|
||||
# Fail cleanly so dm.py logs nav_failed without a traceback.
|
||||
print(f"CDP navigate failed: {type(e).__name__}: {e}", file=sys.stderr)
|
||||
return False
|
||||
# Wait for the URL to settle (SPA client-side routing).
|
||||
for _ in range(timeout_s):
|
||||
cur = ev1(ws, "window.location.href", True)
|
||||
try:
|
||||
cur = ev1(ws, "window.location.href", True)
|
||||
except Exception as e:
|
||||
print(f"CDP read failed: {type(e).__name__}: {e}", file=sys.stderr)
|
||||
return False
|
||||
if cur and url.rstrip("/").lower() in cur.lower():
|
||||
return True
|
||||
_time.sleep(1)
|
||||
@@ -619,18 +688,24 @@ def ev1(ws, expr, await_p=False):
|
||||
"""Runtime.evaluate that skips CDP event chatter while awaiting its
|
||||
response. ev() reads a single message and can catch an event
|
||||
instead (the known None-result quirk); uploads do several DOM
|
||||
calls first, so chatter is likely."""
|
||||
ws.send(json.dumps({
|
||||
"id": 1, "method": "Runtime.evaluate",
|
||||
"params": {"expression": expr, "returnByValue": True,
|
||||
"awaitPromise": await_p}
|
||||
}))
|
||||
for _ in range(30):
|
||||
resp = json.loads(ws.recv())
|
||||
if resp.get("id") != 1:
|
||||
continue
|
||||
return resp.get("result", {}).get("result", {}).get("value")
|
||||
return None
|
||||
calls first, so chatter is likely.
|
||||
Returns None (no traceback) if the CDP WebSocket drops.
|
||||
(Fix 2026-10-06: uncaught WebSocketConnectionClosedException.)"""
|
||||
try:
|
||||
ws.send(json.dumps({
|
||||
"id": 1, "method": "Runtime.evaluate",
|
||||
"params": {"expression": expr, "returnByValue": True,
|
||||
"awaitPromise": await_p}
|
||||
}))
|
||||
for _ in range(30):
|
||||
resp = json.loads(ws.recv())
|
||||
if resp.get("id") != 1:
|
||||
continue
|
||||
return resp.get("result", {}).get("result", {}).get("value")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"CDP evaluate failed: {type(e).__name__}: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def cmd_url(ws):
|
||||
|
||||
Reference in New Issue
Block a user