Restore followup/DM reliability fixes wiped by 19:43Z tree-clean

Re-applies three workstreams lost when 793d3d7 committed over uncommitted
edits, reconciled against the parallel track's committed dm.py changes:
- followup-sweeper.py: backfill thread_uuid after successful nudge sends;
  record final_nudge_target=main on final-nudge routing (C1/C2)
- response-harvester.py: resolve followups on main-chat replies when
  final_nudge_target=main (C3); harvest ALL [RESULT] markers per message
- dm.py: pre-send placement gate (fail closed when post-nav URL lacks the
  target thread UUID; skips main) — purely additive over 793d3d7+f268d3d
- sidechat_manager.py: wait_for_chat_list() settle-poll for list population
  race (sidebar button renders before titles load)
- new: bin/tests/test_followup_fixes.py (25 tests), bin/placement-audit.py,
  bin/dm-log-taxonomy.py, bin/session-probe.py,
  docs/SIDECHAT-RELIABILITY.md, docs/UUID-ROTATION.md

Verified: 25/25 tests pass, py_compile clean, sweeper/harvester dry-runs clean.
Known limitation: gate catches wrong-placement, not wrong-mapping (false
autoprovision adopting the parked thread needs a creation check).
This commit is contained in:
operator-main
2026-10-04 20:06:58 +00:00
parent b5c5e2ff4c
commit ad9dbca7eb
10 changed files with 1984 additions and 51 deletions
+40 -1
View File
@@ -4,6 +4,7 @@ Robust sidechat management for muse-chat-api.py.
Provides:
- ensure_sidebar(ws): Opens sidebar if closed, with retry
- wait_for_chat_list(ws, name=None): Polls until sidebar list content loads
- list_sidechats(ws): Returns list of side chat names, with retry
- All operations logged for audit
@@ -56,13 +57,51 @@ def ensure_sidebar(ws, max_retries=3):
return !!document.querySelector('[data-testid="hatch-chat-compose"]');
})()""")
def wait_for_chat_list(ws, name=None, timeout_s=15):
"""Poll until the sidebar chat list content has loaded (and optionally
contains `name`, case-insensitive substring match).
Readiness requires at least one plausible chat-title line under the
"Side chats" header -- the header itself renders before items populate,
and the compose button that ensure_sidebar() keys on renders earlier
still. Clicking a chat title before the list is ready is a known
nav-miss contributor (the click lands nowhere and the SPA stays on /).
Per-poll CDP errors are treated as not-ready (browser may be mid-render
or mid-flap). Returns True when ready, False on timeout. Note: an
account with genuinely zero sidechats will always time out here.
"""
import re as _re
want = (name or "").strip().lower()
deadline = time.time() + timeout_s
while time.time() < deadline:
try:
text = _ev(ws, "document.body.innerText") or ""
except Exception:
text = ""
idx = text.find("Side chats")
if idx != -1:
section = text[idx:idx + 2000].lower()
lines = [l.strip() for l in section.split("\n")]
titles = [l for l in lines[1:21]
if 5 < len(l) < 80
and not _re.fullmatch(r"\d+[mh]", l)
and l != "unread updates"]
if titles and (not want or want in section):
return True
time.sleep(1)
return False
def list_sidechats(ws):
"""
List side chat names. Returns list of strings.
Ensures sidebar is open first.
Ensures sidebar is open first, then waits for the list content to
populate before scraping (the open signal fires before content loads).
"""
if not ensure_sidebar(ws):
return []
if not wait_for_chat_list(ws):
return []
result = _ev(ws, """(() => {
const text = document.body.innerText;