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:
Executable → Regular
+64
-50
@@ -71,6 +71,18 @@ except ImportError:
|
||||
VALID_AGENTS = ["muse", "pip", "646", "opm"]
|
||||
DEFAULT_PORTS = {"muse": 9410, "pip": 9420, "646": 9430, "opm": 9440}
|
||||
|
||||
# Matches EVERY [RESULT <job_id>] marker in a message (use with finditer, not
|
||||
# search). The result text is lazy and stops before the next marker (or end of
|
||||
# text), so a message closing two jobs records each with its own text instead
|
||||
# of the first marker greedily swallowing the second.
|
||||
RESULT_RE = re.compile(r"\[RESULT\s+([A-Za-z0-9_-]+)\]\s*(.*?)(?=\[RESULT\s|\Z)", re.S)
|
||||
|
||||
def iter_result_markers(text):
|
||||
"""Yield (job_id, result_text) for every [RESULT <job_id>] marker in text."""
|
||||
for m in RESULT_RE.finditer(text or ""):
|
||||
yield m.group(1).strip(), m.group(2).strip()
|
||||
|
||||
|
||||
|
||||
def utcnow():
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
@@ -353,30 +365,29 @@ def harvest_main_feed(cdp, agent, watermarks, followups, dry_run=False):
|
||||
append_jsonl(CHAT_HISTORY_LOG, record)
|
||||
|
||||
if author == "assistant":
|
||||
m_res = re.search(r"\[RESULT\s+([A-Za-z0-9_-]+)\]\s*(.*)", text, re.S)
|
||||
result_job_id = None
|
||||
if m_res:
|
||||
job_id = m_res.group(1).strip()
|
||||
result_job_id = job_id
|
||||
result_text = m_res.group(2).strip()
|
||||
is_fail = is_fail_result(result_text)
|
||||
job_results += 1
|
||||
job_record = {
|
||||
"ts": utcnow(),
|
||||
"type": "job_result",
|
||||
"job_id": job_id,
|
||||
"agent": agent,
|
||||
"success": not is_fail,
|
||||
"result_snippet": result_text[:300],
|
||||
"thread_id": "main",
|
||||
"msg_id": mid,
|
||||
}
|
||||
if not dry_run:
|
||||
append_jsonl(JOB_LOG, job_record)
|
||||
trigger_chain_next(job_id, result_text, success=not is_fail)
|
||||
markers = list(iter_result_markers(text))
|
||||
if markers:
|
||||
for job_id, result_text in markers:
|
||||
is_fail = is_fail_result(result_text)
|
||||
job_results += 1
|
||||
|
||||
clear_matching_followups(followups, agent, "main", mid, text, dry_run,
|
||||
job_id=result_job_id)
|
||||
job_record = {
|
||||
"ts": utcnow(),
|
||||
"type": "job_result",
|
||||
"job_id": job_id,
|
||||
"agent": agent,
|
||||
"success": not is_fail,
|
||||
"result_snippet": result_text[:300],
|
||||
"thread_id": "main",
|
||||
"msg_id": mid,
|
||||
}
|
||||
if not dry_run:
|
||||
append_jsonl(JOB_LOG, job_record)
|
||||
trigger_chain_next(job_id, result_text, success=not is_fail)
|
||||
clear_matching_followups(followups, agent, "main", mid, text,
|
||||
dry_run, job_id=job_id)
|
||||
else:
|
||||
clear_matching_followups(followups, agent, "main", mid, text, dry_run)
|
||||
|
||||
return new_messages, new_wm, job_results
|
||||
|
||||
@@ -474,35 +485,31 @@ def harvest_agent_thread(cdp, agent, thread_info, watermarks, followups, dry_run
|
||||
if not dry_run:
|
||||
append_jsonl(CHAT_HISTORY_LOG, record)
|
||||
|
||||
# Check for [RESULT <job_id>] in assistant messages
|
||||
if author == "assistant":
|
||||
m_res = re.search(r"\[RESULT\s+([A-Za-z0-9_-]+)\]\s*(.*)", text, re.S)
|
||||
result_job_id = None
|
||||
if m_res:
|
||||
job_id = m_res.group(1).strip()
|
||||
result_job_id = job_id
|
||||
result_text = m_res.group(2).strip()
|
||||
is_fail = is_fail_result(result_text)
|
||||
job_results += 1
|
||||
markers = list(iter_result_markers(text))
|
||||
if markers:
|
||||
for job_id, result_text in markers:
|
||||
is_fail = is_fail_result(result_text)
|
||||
job_results += 1
|
||||
|
||||
job_record = {
|
||||
"ts": utcnow(),
|
||||
"type": "job_result",
|
||||
"job_id": job_id,
|
||||
"agent": agent,
|
||||
"success": not is_fail,
|
||||
"result_snippet": result_text[:300],
|
||||
"thread_id": thread_id,
|
||||
"msg_id": mid,
|
||||
}
|
||||
if not dry_run:
|
||||
append_jsonl(JOB_LOG, job_record)
|
||||
# Trigger pipeline chaining or next job if configured
|
||||
trigger_chain_next(job_id, result_text, success=not is_fail)
|
||||
|
||||
# Check and clear pending follow-ups
|
||||
clear_matching_followups(followups, agent, thread_id, mid, text, dry_run,
|
||||
job_id=result_job_id)
|
||||
job_record = {
|
||||
"ts": utcnow(),
|
||||
"type": "job_result",
|
||||
"job_id": job_id,
|
||||
"agent": agent,
|
||||
"success": not is_fail,
|
||||
"result_snippet": result_text[:300],
|
||||
"thread_id": thread_id,
|
||||
"msg_id": mid,
|
||||
}
|
||||
if not dry_run:
|
||||
append_jsonl(JOB_LOG, job_record)
|
||||
# Trigger pipeline chaining or next job if configured
|
||||
trigger_chain_next(job_id, result_text, success=not is_fail)
|
||||
clear_matching_followups(followups, agent, thread_id, mid, text,
|
||||
dry_run, job_id=job_id)
|
||||
else:
|
||||
clear_matching_followups(followups, agent, thread_id, mid, text, dry_run)
|
||||
|
||||
return new_messages, new_wm, job_results
|
||||
|
||||
@@ -610,6 +617,8 @@ def clear_matching_followups(followups, agent, thread_id, mid, text, dry_run=Fal
|
||||
Matches on thread identity (thread_uuid or target='main') OR on job_id
|
||||
(from a [RESULT <job_id>] reply). The job_id path works regardless of
|
||||
thread_uuid or target, fixing ghost followups with null thread_uuid.
|
||||
Also matches a main-chat reply when the sweeper recorded
|
||||
final_nudge_target='main' (final nudge routed to main chat).
|
||||
"""
|
||||
if not followups:
|
||||
return
|
||||
@@ -627,6 +636,11 @@ def clear_matching_followups(followups, agent, thread_id, mid, text, dry_run=Fal
|
||||
match_thread = True
|
||||
elif f_rec.get("thread_uuid") and f_rec.get("thread_uuid") == thread_id:
|
||||
match_thread = True
|
||||
elif f_rec.get("final_nudge_target") == "main" and thread_id == "main":
|
||||
# C3: the sweeper routed the final nudge to main chat, so a
|
||||
# main-chat reply resolves even when the followup target is a
|
||||
# sidechat.
|
||||
match_thread = True
|
||||
|
||||
# Match by job_id (from [RESULT <job_id>]) -- works regardless of
|
||||
# thread_uuid or target. This is an ADDITIONAL path, not a replacement.
|
||||
|
||||
Reference in New Issue
Block a user