feat(main-loop): digest response protocol — actionable digests, reply verbs, closure metrics

Session: sidechat/main-loop-protocol
This commit is contained in:
operator-main
2026-10-05 00:48:38 +00:00
parent 664b4da952
commit 86f0082ffc
3 changed files with 461 additions and 22 deletions
+71 -14
View File
@@ -4,8 +4,9 @@ response-harvester.py — Fleet agent readback and response harvesting daemon.
Monitors Chromebox agents (muse, pip, 646, opm), harvests incoming messages from
Main Chat and registered sidechats, maintains persistent watermarks, appends to
chat-history.jsonl, resolves pending follow-ups, and records [RESULT] completions
in job-log.jsonl.
chat-history.jsonl, resolves pending follow-ups (verb-aware: [ACK|CLAIM] ->
acknowledged, [RESULT|DECLINE|NO-ACTION] -> resolved, outcome recorded), and
records [RESULT] completions in job-log.jsonl.
Features:
- Direct CDP over host veth interfaces (fast, no sudo needed).
@@ -83,6 +84,20 @@ def iter_result_markers(text):
yield m.group(1).strip(), m.group(2).strip()
# Verb markers for the digest response protocol:
# [ACK|CLAIM|RESULT|DECLINE|NO-ACTION <job_id>].
# ACK/CLAIM acknowledge a digest (nudge-suppressed, NOT closed);
# RESULT/DECLINE/NO-ACTION close the digest. Every verb match records
# outcome=<verb> on the followup record.
VERB_RE = re.compile(r"\[(ACK|CLAIM|RESULT|DECLINE|NO-ACTION)\s+([A-Za-z0-9_-]+)\]")
def iter_verb_markers(text):
"""Yield (verb, job_id) for every [VERB <job_id>] marker in text."""
for m in VERB_RE.finditer(text or ""):
yield m.group(1), m.group(2).strip()
def utcnow():
return datetime.now(timezone.utc).isoformat()
@@ -366,7 +381,8 @@ def harvest_main_feed(cdp, agent, watermarks, followups, dry_run=False):
if author == "assistant":
markers = list(iter_result_markers(text))
if markers:
verbs = list(iter_verb_markers(text))
if markers or verbs:
for job_id, result_text in markers:
is_fail = is_fail_result(result_text)
job_results += 1
@@ -385,7 +401,12 @@ def harvest_main_feed(cdp, agent, watermarks, followups, dry_run=False):
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)
dry_run, job_id=job_id, verb="RESULT")
for verb, job_id in verbs:
if verb == "RESULT":
continue # resolved via the result-marker path above
clear_matching_followups(followups, agent, "main", mid, text,
dry_run, job_id=job_id, verb=verb)
else:
clear_matching_followups(followups, agent, "main", mid, text, dry_run)
@@ -487,7 +508,8 @@ def harvest_agent_thread(cdp, agent, thread_info, watermarks, followups, dry_run
if author == "assistant":
markers = list(iter_result_markers(text))
if markers:
verbs = list(iter_verb_markers(text))
if markers or verbs:
for job_id, result_text in markers:
is_fail = is_fail_result(result_text)
job_results += 1
@@ -507,7 +529,12 @@ def harvest_agent_thread(cdp, agent, thread_info, watermarks, followups, dry_run
# 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)
dry_run, job_id=job_id, verb="RESULT")
for verb, job_id in verbs:
if verb == "RESULT":
continue # resolved via the result-marker path above
clear_matching_followups(followups, agent, thread_id, mid, text,
dry_run, job_id=job_id, verb=verb)
else:
clear_matching_followups(followups, agent, thread_id, mid, text, dry_run)
@@ -611,7 +638,7 @@ def trigger_chain_next(job_id, result_text, success=True):
def clear_matching_followups(followups, agent, thread_id, mid, text, dry_run=False,
job_id=None):
job_id=None, verb=None):
"""Resolve follow-up records if an assistant message is detected in the thread.
Matches on thread identity (thread_uuid or target='main') OR on job_id
@@ -619,13 +646,27 @@ def clear_matching_followups(followups, agent, thread_id, mid, text, dry_run=Fal
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).
When verb is given ([ACK|CLAIM|RESULT|DECLINE|NO-ACTION <job_id>]),
matching is job_id-scoped for non-RESULT verbs (a verb marker names the
digest it answers, so it must not touch unrelated pending followups that
merely share the thread); RESULT keeps the historical thread-or-job
matching. Every verb match records outcome=<verb>. ACK/CLAIM set status
'acknowledged' (nudge-suppressed, NOT closed) instead of 'resolved', and
also match already-'acknowledged' records so an ACK -> RESULT lifecycle
closes correctly.
"""
if not followups:
return
modified = False
for f_id, f_rec in followups.items():
if f_rec.get("status") != "pending":
# Verb replies can follow an ACK (ACK -> RESULT lifecycle), so verbs
# also match 'acknowledged' records; plain replies only match pending.
if verb:
if f_rec.get("status") not in ("pending", "acknowledged"):
continue
elif f_rec.get("status") != "pending":
continue
if f_rec.get("recipient") != agent:
continue
@@ -642,17 +683,33 @@ def clear_matching_followups(followups, agent, thread_id, mid, text, dry_run=Fal
# 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.
# Match by job_id (from [RESULT <job_id>] or [VERB <job_id>]) --
# works regardless of thread_uuid or target. This is an ADDITIONAL
# path, not a replacement.
match_job = False
if job_id and f_rec.get("job_id") and f_rec.get("job_id") == job_id:
match_job = True
# Non-RESULT verbs are job-scoped: they must not acknowledge/resolve
# unrelated pending followups that merely share the thread. RESULT
# keeps the historical thread-or-job matching.
if verb and verb != "RESULT" and not match_job:
continue
if match_thread or match_job:
f_rec["status"] = "resolved"
f_rec["resolved_at"] = utcnow()
f_rec["resolved_by_mid"] = mid
f_rec["resolved_snippet"] = text[:150]
if verb:
f_rec["outcome"] = verb
if verb in ("ACK", "CLAIM"):
# Acknowledged: sweeper nudges stop (status != pending), but
# the digest is NOT closed until a closing verb arrives.
f_rec["status"] = "acknowledged"
f_rec["acknowledged_at"] = utcnow()
f_rec["acknowledged_by_mid"] = mid
else:
f_rec["status"] = "resolved"
f_rec["resolved_at"] = utcnow()
f_rec["resolved_by_mid"] = mid
f_rec["resolved_snippet"] = text[:150]
modified = True
if modified and not dry_run: