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
Executable → Regular
+71
View File
@@ -62,6 +62,9 @@ TAG_NUDGES_MAX = 10
TAG_NUDGES_DEFAULT = 2
TAG_ROUTE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$")
TAG_THREAD_RE = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
# Thread-UUID pattern, shared by nav parsing and the pre-send gate.
UUID_RE = (r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-"
r"[0-9a-f]{4}-[0-9a-f]{12}")
# Trailing-token form: [reply:expected] [reply:timeout=7200] ...
TAG_TOKEN_RE = re.compile(r"\[([A-Za-z][A-Za-z0-9:]*)"
r"(?:=([^\]]*))?\]\s*$")
@@ -365,6 +368,52 @@ def verify_placement(recipient, msg_id, target, thread_uuid):
detail["error"] = str(e)[:200]
return False, detail
def assert_pre_send_placement(recipient, target, thread_uuid, direct_nav_done=False):
"""Pre-send placement gate (2026-10-04; restored after 19:43Z clobber).
Independently samples the recipient browser's URL and requires the
expected thread UUID in it BEFORE any send. Fails closed: returns
(False, detail) and the caller must abort the send. 'main' skips.
"""
detail = {"target": target}
if target == "main":
return True, detail
if not thread_uuid:
# Fresh autoprovision: SPA assigns UUID only on first send.
# Strongest pre-send signal: browser must be on a /thread/ page
# (the incident was a send parked on the muse.ai landing page).
_u_rc, _u_out, _u_err = run_full(
f"{NETVM_EXEC} {recipient} -- python3 {API} --account {recipient} url")
actual = (_u_out or "").strip()
detail["actual_url"] = actual[:200]
if "/thread/" not in actual:
detail["reason"] = "not_on_thread_page"
detail["expected"] = "a /thread/ URL (fresh sidechat, UUID assigned on first send)"
return False, detail
return True, detail
expected = thread_uuid.lower()
detail["expected_uuid"] = expected
if not direct_nav_done:
# Title-search nav only: re-navigate by direct /thread/<uuid> URL.
_rc, _out, _err = run_full(
f"{NETVM_EXEC} {recipient} -- python3 {API} --account {recipient} sidechat use {expected}")
if _rc != 0 or "NOTFOUND" in _out or expected not in _out.lower():
detail.update({"reason": "direct_nav_failed", "rc": _rc,
"out": _out[:200], "err": _err[:200]})
return False, detail
time.sleep(2)
else:
time.sleep(1)
_u_rc, _u_out, _u_err = run_full(
f"{NETVM_EXEC} {recipient} -- python3 {API} --account {recipient} url")
actual = (_u_out or "").strip()
detail["actual_url"] = actual[:200]
if expected not in actual.lower():
detail["reason"] = "url_mismatch"
return False, detail
return True, detail
def dm_send(agent, target, message, verify=True, raw=False,
to_agent=None, tags=None, nudge_meta=None,
allow_main_chat=False):
@@ -444,6 +493,7 @@ def dm_send(agent, target, message, verify=True, raw=False,
thread_uuid = None
thread_url = None
is_new_sidechat = False
nav_is_uuid = False
# Resolve well-known aliases or dynamic thread mappings to UUIDs.
nav_target = resolve_sidechat_target(target)
if nav_target != target:
@@ -454,6 +504,7 @@ def dm_send(agent, target, message, verify=True, raw=False,
else:
# Reset-at-Begin: If searching sidebar by title (not a direct UUID), reset to main first to expose the sidebar
is_uuid = bool(re.fullmatch(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", nav_target.strip().lower()))
nav_is_uuid = is_uuid
if not is_uuid:
run_full(f"{NETVM_EXEC} {recipient} -- python3 {API} --account {recipient} sidechat main")
time.sleep(1)
@@ -479,6 +530,12 @@ def dm_send(agent, target, message, verify=True, raw=False,
file=sys.stderr)
sys.exit(1)
is_new_sidechat = True
# If create returned a real thread URL (not /thread/new),
# pin it now so the pre-send gate can assert it.
_cm = re.search(r"/thread/(" + UUID_RE + r")", _c_out, re.I)
if _cm:
thread_uuid = _cm.group(1).lower()
thread_url = "https://muse.ai/thread/" + thread_uuid
log_event({"type": "nav_ok", "id": msg_id, "agent": agent, "to": recipient,
"target": target, "status": "sidechat_created_pending_uuid",
"browser_url": thread_url})
@@ -509,6 +566,20 @@ def dm_send(agent, target, message, verify=True, raw=False,
"target": target, "thread_uuid": thread_uuid,
"browser_url": thread_url})
# Pre-send placement assertion: independently confirm the browser is on
# the target thread BEFORE any send. Failure fails loudly (no ghost sends).
_gate_uuid = thread_uuid or (nav_target.lower() if nav_is_uuid else None)
_ps_ok, _ps_detail = assert_pre_send_placement(
recipient, target, _gate_uuid, direct_nav_done=nav_is_uuid)
if not _ps_ok:
log_event({"type": "pre_send_assert_failed", "id": msg_id, "agent": agent,
"to": recipient, **_ps_detail})
print(f"DM {msg_id} from {agent} to {recipient}/{target}: FAILED "
f"(pre-send placement assertion failed: {_ps_detail.get('reason')}; "
f"expected={_ps_detail.get('expected_uuid') or _ps_detail.get('expected')}; "
f"actual_url={_ps_detail.get('actual_url')})", file=sys.stderr)
sys.exit(1)
time.sleep(2)
# Send (raw mode: no truncation — signatures must survive intact)