chore(fleet): sync operator memory, hatch menu dialogs, and watchdog alerts

This commit is contained in:
operator
2026-10-07 00:25:51 +00:00
parent 0065d11e97
commit 34b0ef9fe2
38 changed files with 2205 additions and 444 deletions
+119 -44
View File
@@ -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):