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:
Hitonabi
2026-07-20 15:22:07 +02:00
parent 77ae504b58
commit e3b3725b6f
5 changed files with 376 additions and 1 deletions
@@ -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