Hermes-Runtime-Patch-Traeger + Fan-out-/Kontext-Fixes
Neuer update-fester Patch-Traeger (deploy/hermes-patches/apply.py) haelt
MC2-eigene Fixes im Hermes-Quellcode, idempotent, in deploy.sh + postcheck
verdrahtet (Exit 10 = Gateway-Neustart, Exit 1 = rot):
0001 needs_input/capability warten auf Commander (keine triage-Eskalation
-> keine Muenz-Schleife)
0002 Decompose erst bei Startreife (Parents done)
0003 Orphan-Guard: Reaper killt Worker, deren Task nicht mehr running
(mc2_kanban_reaper.py) -> max_in_progress leckt nicht
0004 Projekt-/Repo-Kontext vererbt sich auf Kind-Karten
0005 Etappen-Auto-Kette: Bulk-Root-triage-Karten einer Session verketten
Steckbrief: 'Repo fehlt -> selbst anlegen' + verschaerfte Kontext-Regel.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""MC2-Zusatzmodul (20.07.2026) — Orphan-Reaper für Kanban-Worker.
|
||||
|
||||
Wird vom Patch-Träger (``deploy/hermes-patches/apply.py``) nach
|
||||
``~/.hermes/hermes-agent/hermes_cli/`` kopiert und vom Gateway-Dispatcher
|
||||
(``gateway/kanban_watchers.py``, per Patch 0003) jeden Tick aufgerufen.
|
||||
|
||||
Problem: Wird eine ``running``-Karte von außen blockiert/zurückgesetzt (z. B.
|
||||
über das Auftragsbuch oder die No-Progress-Bremse), setzt ``block_task``
|
||||
``worker_pid = NULL`` — der Worker-PROZESS läuft aber weiter. Die DB zählt ihn
|
||||
nicht mehr, also unterläuft der Orphan ``kanban.max_in_progress`` (Doppel-Worker
|
||||
möglich) und verbrennt Rechenzeit. Dieser Reaper beendet lebende Worker, deren
|
||||
Task nicht mehr ``running`` ist.
|
||||
|
||||
Linux-only (/proc-Scan). Best-effort: jeder Fehler → leere Liste, nie werfen.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import sys
|
||||
|
||||
_TASK_RE = re.compile(rb"work kanban task (t_[0-9a-fA-F]+)")
|
||||
|
||||
|
||||
def reap_orphaned_workers(connect_closing) -> "list[tuple[int, str]]":
|
||||
"""Beende lebende Kanban-Worker, deren Task nicht mehr ``running`` ist.
|
||||
|
||||
``connect_closing`` ist die gleichnamige Kontextmanager-Factory aus
|
||||
``kanban_db`` (durchgereicht, um Import-Zyklen zu vermeiden). Gibt die Liste
|
||||
der beendeten ``(pid, task_id)`` zurück.
|
||||
"""
|
||||
if sys.platform != "linux":
|
||||
return []
|
||||
candidates: "dict[int, str]" = {}
|
||||
try:
|
||||
entries = os.listdir("/proc")
|
||||
except OSError:
|
||||
return []
|
||||
for entry in entries:
|
||||
if not entry.isdigit():
|
||||
continue
|
||||
try:
|
||||
with open(f"/proc/{entry}/cmdline", "rb") as fh:
|
||||
cmd = fh.read().replace(b"\x00", b" ")
|
||||
except OSError:
|
||||
continue
|
||||
m = _TASK_RE.search(cmd)
|
||||
if m:
|
||||
candidates[int(entry)] = m.group(1).decode()
|
||||
if not candidates:
|
||||
return []
|
||||
|
||||
task_ids = list(set(candidates.values()))
|
||||
placeholders = ",".join("?" * len(task_ids))
|
||||
running: "set[str]" = set()
|
||||
try:
|
||||
with connect_closing() as conn:
|
||||
running = {
|
||||
row[0]
|
||||
for row in conn.execute(
|
||||
f"SELECT id FROM tasks WHERE id IN ({placeholders}) "
|
||||
"AND status = 'running'",
|
||||
task_ids,
|
||||
)
|
||||
}
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
killed: "list[tuple[int, str]]" = []
|
||||
for pid, task_id in candidates.items():
|
||||
if task_id in running:
|
||||
continue
|
||||
# Prozessgruppe zuerst (Worker starten start_new_session=True → eigene
|
||||
# Gruppe; killpg räumt selbst gestartete Kinder wie uvicorn gleich mit).
|
||||
try:
|
||||
os.killpg(os.getpgid(pid), signal.SIGTERM)
|
||||
except OSError:
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
except OSError:
|
||||
continue
|
||||
killed.append((pid, task_id))
|
||||
return killed
|
||||
Reference in New Issue
Block a user