From 70b82f5a8babd367c8bcc34149b623ccab1d3f71 Mon Sep 17 00:00:00 2001 From: operator-main Date: Sun, 4 Oct 2026 02:49:38 +0000 Subject: [PATCH] Add sidechat_manager.py: robust sidebar operations with retry and logging\n\n- ensure_sidebar() with exponential backoff (2s, 4s, 6s)\n- list_sidechats() with heuristic parsing\n- log_sidechat_op() for audit trail to sidechat-log.jsonl\nAddresses flaky NOSIDEBAR errors from hardcoded sleeps. --- bin/sidechat_manager.py | 100 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 bin/sidechat_manager.py diff --git a/bin/sidechat_manager.py b/bin/sidechat_manager.py new file mode 100644 index 0000000..60823d3 --- /dev/null +++ b/bin/sidechat_manager.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +""" +Robust sidechat management for muse-chat-api.py. + +Provides: +- ensure_sidebar(ws): Opens sidebar if closed, with retry +- list_sidechats(ws): Returns list of side chat names, with retry +- All operations logged for audit + +The sidebar UI is flaky — sometimes the open button isn't found, +sometimes content takes >2s to load. This module handles that. +""" +import time +import json + +def _ev(ws, js, await_result=True): + """Evaluate JS via CDP, return result.""" + import json as _json + ws.send(_json.dumps({ + "id": 100, "method": "Runtime.evaluate", + "params": {"expression": js, "awaitPromise": True} + })) + resp = _json.loads(ws.recv()) + result = resp.get("result", {}).get("result", {}) + return result.get("value") + +def ensure_sidebar(ws, max_retries=3): + """ + Ensure the sidebar is open. Returns True if open, False otherwise. + Retries with backoff if the open button isn't found immediately. + """ + for attempt in range(max_retries): + # Check if sidebar is already open (Side chats text visible) + is_open = _ev(ws, """(() => { + return document.body.innerText.includes('Side chats'); + })()""") + if is_open: + return True + + # Try to open it + _ev(ws, """(() => { + const btn = [...document.querySelectorAll('button')].find(b => + (b.textContent||'').includes('Open chat and side chats') + ); + if (btn) btn.click(); + return !!btn; + })()""") + time.sleep(2 * (attempt + 1)) # Exponential backoff: 2s, 4s, 6s + + # Final check + return _ev(ws, """(() => { + return document.body.innerText.includes('Side chats'); + })()""") + +def list_sidechats(ws): + """ + List side chat names. Returns list of strings. + Ensures sidebar is open first. + """ + if not ensure_sidebar(ws): + return [] + + result = _ev(ws, """(() => { + const text = document.body.innerText; + const idx = text.indexOf('Side chats'); + if (idx === -1) return JSON.stringify([]); + // Extract chat names (heuristic: lines after "Side chats" that look like titles) + const section = text.slice(idx, idx + 2000); + const lines = section.split('\\n').map(l => l.trim()).filter(l => l.length > 0); + // Skip header, collect likely chat titles (skip timestamps like "1m", "2m") + const chats = []; + for (let i = 1; i < lines.length && chats.length < 20; i++) { + const line = lines[i]; + if (/^\\d+[mh]$/.test(line)) continue; // timestamp + if (line === 'Unread updates') continue; + if (line.length > 5 && line.length < 80) chats.push(line); + } + return JSON.stringify(chats); + })()""") + try: + return json.loads(result) if result else [] + except: + return [] + +def log_sidechat_op(agent, operation, details=""): + """Log sidechat operations for audit.""" + import os + from datetime import datetime, timezone + log_file = "/home/super/Projects/NetVM/sidechat-log.jsonl" + entry = { + "ts": datetime.now(timezone.utc).isoformat(), + "agent": agent, + "op": operation, + "details": details[:200] + } + try: + with open(log_file, "a") as f: + f.write(json.dumps(entry) + "\n") + except: + pass