feat(watchdog): auto-escalation של zombie-loops ל-chair — סגירת הלולאה (#218+#222)
מרכיב את הפרוסות שמוזגו לכלל closed-loop: watchdog שרת-צדי (_zombie_escalation_loop ב-lifespan, כל 15 דק') שגוזר בריאות per-issue (#222) ומסלים כל zombie מתמשך (לולאת-recovery, ≥ZOMBIE_ESCALATE_MIN_RECOVERY=2 יקיצות-שחזור) ל-chair דרך escalate_issue הloop-safe (#218). הטלמטריה agent.escalated (#219) נדלקת מה-Port. - מדוע שרת-צדי: סוכן תקוע-בלולאה לא יכול להסלים-עצמו אמין; ה-watchdog (מקביל ל-Deacon של Gastown, אבל דרך ה-reaper הקיים ולא tier חדש) עושה זאת. - אידמפוטנטי מבנייה: issue מוסלם הופך agent-unowned → לא zombie בsweep הבא. - החלטת-ההסלמה (סף+severity) = helper טהור zombie_escalation ב-agent_health (testable). severity: ≥5 יקיצות→high, אחרת→medium. - kill-switch: AGENT_ZOMBIE_AUTO_ESCALATE=0 להשבתה (הבריאות עדיין נצפית, escalate ידני עדיין זמין ב-endpoint). ברירת-מחדל ON — הלולאות שורפות budget והתרופה מוכחת. - 4 בדיקות policy חדשות (11 סה"כ). dry-run חי: 6 issues→0 hzombies→0 הסלמות (blast-radius מינימלי על deploy). Invariants: מקיים G2 (מרחיב את מנגנון-ה-reaper הקיים, לא sweep מקביל; escalate דרך המסלול הקנוני), G12 (הכל ב-web/, המגע מהשער), §6 (כשל-sweep נלכד ולא-קטלני). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -77,3 +77,34 @@ def classify_issue_health(
|
||||
if total_wakeups >= STALL_WAKEUP_THRESHOLD:
|
||||
return "stalled"
|
||||
return "idle"
|
||||
|
||||
|
||||
# A zombie needs at least this many recovery-marker wakeups before the watchdog
|
||||
# auto-escalates it — filters a single transient recovery wake from a genuine,
|
||||
# budget-burning loop. Escalation reassigns the issue to the chair (via
|
||||
# escalate_issue), so an escalated issue is agent-unowned next sweep and never
|
||||
# re-escalates (idempotent by construction).
|
||||
ZOMBIE_ESCALATE_MIN_RECOVERY = 2
|
||||
|
||||
|
||||
def zombie_escalation(item: dict) -> tuple[str, str] | None:
|
||||
"""Decide whether a health item warrants auto-escalation to the chair.
|
||||
|
||||
Pure (no I/O) so the watchdog's policy is unit-tested. Returns
|
||||
``(severity, reason)`` for a persistent zombie, else ``None``. Severity
|
||||
scales with loop intensity — a long-running loop (``>=5`` recovery wakes)
|
||||
is ``high``, a fresh one ``medium``. Only ``zombie`` items escalate: a
|
||||
``stalled``/``working``/``idle`` issue is not a recovery loop.
|
||||
"""
|
||||
if item.get("health") != "zombie":
|
||||
return None
|
||||
recovery = int(item.get("recovery_wakeups", 0))
|
||||
if recovery < ZOMBIE_ESCALATE_MIN_RECOVERY:
|
||||
return None
|
||||
severity = "high" if recovery >= 5 else "medium"
|
||||
reason = (
|
||||
f"auto-escalation (watchdog): זוהתה לולאת-recovery — {recovery} יקיצות-שחזור "
|
||||
f"על {item.get('identifier')} ({item.get('agent_name')}); ה-issue נמסר לחיים "
|
||||
f"במקום להמשיך לשרוף budget."
|
||||
)
|
||||
return severity, reason
|
||||
|
||||
Reference in New Issue
Block a user