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:
@@ -33,7 +33,8 @@ TRIAGE (19.07. — Drei-Bahnen-Regel): Ordne deine Karte ZUERST einer Bahn zu un
|
|||||||
PROTOKOLL-PFLICHT (15.07.): Beende deine Aufgabe IMMER mit dem Tool-Aufruf kanban_complete (fertig) oder kanban_block (Frage/Sackgasse). Eine Text-Zusammenfassung OHNE diesen Aufruf zaehlt als Absturz (Protokollverstoss) und blockiert die Karte — genau so ging am 14.07. eine fertige Arbeit verloren.
|
PROTOKOLL-PFLICHT (15.07.): Beende deine Aufgabe IMMER mit dem Tool-Aufruf kanban_complete (fertig) oder kanban_block (Frage/Sackgasse). Eine Text-Zusammenfassung OHNE diesen Aufruf zaehlt als Absturz (Protokollverstoss) und blockiert die Karte — genau so ging am 14.07. eine fertige Arbeit verloren.
|
||||||
REPO-WEGWEISER mission-control-v2 (NICHT raten/suchen — so liegt es): backend/app.py = Einstieg + Router-Mounting · backend/routers/*.py = REST-Endpunkte · backend/services/*.py = Logik · backend/gateway_app.py = /v1-Gateway-Prozess (:9010) · frontend/src/views/*.tsx = Ansichten · frontend/src/lib/api.ts = Typen+Fetch · frontend/src/lib/queries.ts = TanStack-Query-Hooks (Polling) · frontend/src/nav.ts = Sidebar · deploy/ = Skripte/Units/Hooks/Skills · Projekt-Regeln: AGENTS.md (Wurzel) + docs/wissen/.
|
REPO-WEGWEISER mission-control-v2 (NICHT raten/suchen — so liegt es): backend/app.py = Einstieg + Router-Mounting · backend/routers/*.py = REST-Endpunkte · backend/services/*.py = Logik · backend/gateway_app.py = /v1-Gateway-Prozess (:9010) · frontend/src/views/*.tsx = Ansichten · frontend/src/lib/api.ts = Typen+Fetch · frontend/src/lib/queries.ts = TanStack-Query-Hooks (Polling) · frontend/src/nav.ts = Sidebar · deploy/ = Skripte/Units/Hooks/Skills · Projekt-Regeln: AGENTS.md (Wurzel) + docs/wissen/.
|
||||||
FRONTEND-BAUEN (seit 15.07. abends): Node LTS liegt sudo-frei unter ~/.local (node/npm im Worker-PATH). Bei Frontend-Aenderungen baust DU das dist MIT: cd frontend && npm ci --no-audit --no-fund && npm run build — und committest frontend/dist im selben Branch (die Box zeigt sonst nach Annahme den alten Stand; Bau-Beweis 15.07.: kompletter Build in ~4 s).
|
FRONTEND-BAUEN (seit 15.07. abends): Node LTS liegt sudo-frei unter ~/.local (node/npm im Worker-PATH). Bei Frontend-Aenderungen baust DU das dist MIT: cd frontend && npm ci --no-audit --no-fund && npm run build — und committest frontend/dist im selben Branch (die Box zeigt sonst nach Annahme den alten Stand; Bau-Beweis 15.07.: kompletter Build in ~4 s).
|
||||||
KONTEXT-BUDGET & ETAPPEN-REGEL (15.07. — vier Worker starben an finish_reason=length): Dein Kontext ist ENDLICH. Lies gezielt ueber den Wegweiser statt ls-Ketten und Volltexte; grosse Dateien nur in Ausschnitten. Merkst du, dass die Aufgabe mehr als ~5 Dateien oder mehrere Baustellen umfasst: NICHT durchziehen — schneide Etappen (eine Etappe = ein Worker-Lauf): lege Teil-Karten an (kanban_create, falls verfuegbar) und schliesse deine Karte per kanban_complete mit dem Etappen-Plan ab; alternativ kanban_block mit dem Etappen-Vorschlag als Frage.
|
REPO FEHLT? (20.07.): Soll dein Ergebnis in ein Gitea-Repo, das noch NICHT existiert (eigenes/neues Projekt), dann lege es SELBST an — NICHT den Commander fragen, NICHT git init: bash ~/.hermes/scripts/gitea-repo-create.sh <name> "<kurze Beschreibung>" (immer privat, letzte Ausgabezeile = CLONE <url>), dann voll klonen und darin arbeiten. NUR mission-control-v2 und das Lucy-Repo NIE neu anlegen/veraendern.
|
||||||
|
KONTEXT-BUDGET & ETAPPEN-REGEL (15.07./20.07. — Worker starben an finish_reason=length UND an vollgelaufenem Kontext): Dein Kontext ist ENDLICH und fuellt sich vor allem durch GROSSE Datei- und Terminal-Ausgaben. Lies NIE eine grosse Datei am Stueck — nutze grep und Zeilenbereiche (read mit offset/limit); Datei-Reads sind auf 50k Zeichen gedeckelt, aber auch viele mittlere summieren sich. Lies gezielt ueber den Wegweiser statt ls-Ketten und Volltexte. Merkst du, dass die Aufgabe mehr als ~5 Dateien oder mehrere Baustellen umfasst: NICHT durchziehen — schneide Etappen (eine Etappe = ein Worker-Lauf): lege Teil-Karten an (kanban_create, falls verfuegbar) und schliesse deine Karte per kanban_complete mit dem Etappen-Plan ab; alternativ kanban_block mit dem Etappen-Vorschlag als Frage.
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
|
|
||||||
|
|||||||
@@ -191,6 +191,18 @@ done
|
|||||||
for _prof in "$HOME"/.hermes/profiles/*/config.yaml; do
|
for _prof in "$HOME"/.hermes/profiles/*/config.yaml; do
|
||||||
[ -f "$_prof" ] && python3 "$SRC/deploy/ensure-profile-hooks.py" "$_prof" || true
|
[ -f "$_prof" ] && python3 "$SRC/deploy/ensure-profile-hooks.py" "$_prof" || true
|
||||||
done
|
done
|
||||||
|
# Hermes-Runtime-Patch-Traeger (20.07.2026): MC2-eigene Fixes im Hermes-Quellcode
|
||||||
|
# (needs_input-wartet-auf-Commander, Decompose-erst-bei-Startreife, Orphan-Guard,
|
||||||
|
# Projekt-Kontext-Vererbung, Etappen-Auto-Kette) idempotent (re-)eintragen — so
|
||||||
|
# ueberleben sie ein `hermes update`, das den Quellbaum sonst zuruecksetzt.
|
||||||
|
python3 "$SRC/deploy/hermes-patches/apply.py"; _patch_rc=$?
|
||||||
|
if [ "$_patch_rc" -eq 10 ]; then
|
||||||
|
# Patches haben den Quellcode geaendert → Gateway muss die neue Laufzeit laden.
|
||||||
|
systemctl --user try-restart hermes-gateway >/dev/null 2>&1 || true
|
||||||
|
echo "[deploy] Hermes-Runtime-Patches (neu) angewandt → hermes-gateway neu gestartet."
|
||||||
|
elif [ "$_patch_rc" -ne 0 ]; then
|
||||||
|
echo "[deploy] ‼ WARNUNG: hermes-patches/apply.py meldete Fehler — ein Patch passt nicht mehr (Update hat den Kontext geaendert?). Fixes pruefen!"
|
||||||
|
fi
|
||||||
# Eingebaute Hermes-Web-GUI (`hermes serve`) für die Einbettung in MC2 (routers/hermes_ui.py →
|
# Eingebaute Hermes-Web-GUI (`hermes serve`) für die Einbettung in MC2 (routers/hermes_ui.py →
|
||||||
# same-origin /hermes-ui/) vorbereiten: das SPA-Bundle mit Vite-base=/hermes-ui/ (neu) bauen, sonst
|
# same-origin /hermes-ui/) vorbereiten: das SPA-Bundle mit Vite-base=/hermes-ui/ (neu) bauen, sonst
|
||||||
# kollidieren seine absoluten Pfade (/assets, /api) mit MC2s eigenen. Läuft auch nach jedem
|
# kollidieren seine absoluten Pfade (/assets, /api) mit MC2s eigenen. Läuft auch nach jedem
|
||||||
|
|||||||
@@ -0,0 +1,259 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Hermes-Runtime-Patch-Traeger — haelt MC2-eigene Fixes im Hermes-Quellcode
|
||||||
|
(``~/.hermes/hermes-agent``), damit sie ein ``hermes update`` ueberleben.
|
||||||
|
|
||||||
|
Warum: Der Hermes-Agent wird aus git aktualisiert; ein Direkt-Edit der Laufzeit
|
||||||
|
wuerde beim naechsten Update still ueberschrieben. Dieses Skript wird von
|
||||||
|
``deploy/deploy.sh`` UND ``deploy/hermes-postcheck.sh`` aufgerufen und traegt die
|
||||||
|
Patches (re-)ein.
|
||||||
|
|
||||||
|
Mechanik: JEDER Patch ist eine idempotente String-Ersetzung (robust gegen
|
||||||
|
Zeilenverschiebungen durch Updates, anders als ``git apply``):
|
||||||
|
* ``marker`` schon im Quelltext -> Patch bereits drin, ueberspringen.
|
||||||
|
* ``old`` genau 1x vorhanden -> ersetzen, dann ``py_compile`` pruefen.
|
||||||
|
* ``old`` 0x oder >1x -> Kontext hat sich durch ein Update
|
||||||
|
geaendert -> LAUT scheitern (Exit 1), damit
|
||||||
|
der Postcheck rot wird und niemand denkt,
|
||||||
|
der Fix sei noch aktiv.
|
||||||
|
|
||||||
|
Bei Fehlschlag: Exit 1 (der Deploy/Postcheck behandelt das als Fehler).
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import py_compile
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
HA = Path(os.environ.get("HERMES_AGENT_DIR", str(Path.home() / ".hermes" / "hermes-agent")))
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
# --- Zusatzmodule, die in die Hermes-Laufzeit kopiert werden (update-fest) ---
|
||||||
|
# (source relativ zu diesem Skript, dest relativ zum Hermes-Agent-Dir)
|
||||||
|
COPIES = [
|
||||||
|
("mc2_kanban_reaper.py", "hermes_cli/mc2_kanban_reaper.py"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Patch-Definitionen (chronologisch, Fan-out-/Muenz-Schleifen-Fixes 20.07.2026) ---
|
||||||
|
PATCHES = [
|
||||||
|
{
|
||||||
|
"name": "0001-needs-input-wait",
|
||||||
|
"file": "hermes_cli/kanban_db.py",
|
||||||
|
"marker": 'kind not in ("needs_input", "capability")',
|
||||||
|
"old": " if recurrences >= BLOCK_RECURRENCE_LIMIT:\n",
|
||||||
|
"new": (
|
||||||
|
" # MC2-Patch (20.07.2026): needs_input/capability sind genuin\n"
|
||||||
|
" # human-only — sie muessen in ``blocked`` auf den Commander warten\n"
|
||||||
|
" # (Antwort via Auftragsbuch), NICHT nach triage eskalieren. Sonst\n"
|
||||||
|
" # zerlegt der Auto-Decomposer sie neu = Duplikat-Karten-Schleife\n"
|
||||||
|
' # ("Muenz-Schleife", Fan-out-Vorfall 20.07.). Der Loop-Breaker bleibt\n'
|
||||||
|
" # fuer transient/untyped Blocks aktiv.\n"
|
||||||
|
' if recurrences >= BLOCK_RECURRENCE_LIMIT and kind not in ("needs_input", "capability"):\n'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "0002-decompose-at-readiness",
|
||||||
|
"file": "hermes_cli/kanban_decompose.py",
|
||||||
|
"marker": "Startreife-Gate",
|
||||||
|
"old": (
|
||||||
|
" cfg = _load_config()\n"
|
||||||
|
" orchestrator = _resolve_orchestrator_profile(cfg)\n"
|
||||||
|
),
|
||||||
|
"new": (
|
||||||
|
" # MC2-Patch (20.07.2026): Startreife-Gate. Eine triage-Karte mit noch\n"
|
||||||
|
" # OFFENEN Vorgaengern NICHT zerlegen — sonst raet der Decomposer die\n"
|
||||||
|
" # Schnittstellen der Vor-Etappe ins Blaue, bevor die gebaut hat\n"
|
||||||
|
" # (Fan-out-Vorfall 20.07.). Billiger Parent-Check VOR dem teuren\n"
|
||||||
|
" # LLM-Aufruf; erst zerlegen, wenn alle Eltern done/archived.\n"
|
||||||
|
" with kb.connect_closing() as _pconn:\n"
|
||||||
|
" _parents = [r[0] for r in _pconn.execute(\n"
|
||||||
|
' "SELECT parent_id FROM task_links WHERE child_id = ?", (task_id,))]\n'
|
||||||
|
" if _parents:\n"
|
||||||
|
' _ph = ",".join("?" * len(_parents))\n'
|
||||||
|
" _open = _pconn.execute(\n"
|
||||||
|
' "SELECT COUNT(*) FROM tasks WHERE id IN (%s) "\n'
|
||||||
|
" \"AND status NOT IN ('done', 'archived')\" % _ph, _parents\n"
|
||||||
|
" ).fetchone()[0]\n"
|
||||||
|
" if _open:\n"
|
||||||
|
" return DecomposeOutcome(\n"
|
||||||
|
" task_id, False,\n"
|
||||||
|
' f"defer: {_open} unfinished parent(s) — decompose when start-ready",\n'
|
||||||
|
" )\n"
|
||||||
|
" cfg = _load_config()\n"
|
||||||
|
" orchestrator = _resolve_orchestrator_profile(cfg)\n"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "0003-orphan-guard",
|
||||||
|
"file": "gateway/kanban_watchers.py",
|
||||||
|
"marker": "mc2_kanban_reaper",
|
||||||
|
"old": (
|
||||||
|
" pids = await asyncio.to_thread(_kb.reap_worker_zombies)\n"
|
||||||
|
" if pids:\n"
|
||||||
|
" logger.info(\n"
|
||||||
|
' "kanban dispatcher: reaped %d zombie worker(s), pids=%s",\n'
|
||||||
|
" len(pids),\n"
|
||||||
|
" pids,\n"
|
||||||
|
" )\n"
|
||||||
|
),
|
||||||
|
"new": (
|
||||||
|
" pids = await asyncio.to_thread(_kb.reap_worker_zombies)\n"
|
||||||
|
" if pids:\n"
|
||||||
|
" logger.info(\n"
|
||||||
|
' "kanban dispatcher: reaped %d zombie worker(s), pids=%s",\n'
|
||||||
|
" len(pids),\n"
|
||||||
|
" pids,\n"
|
||||||
|
" )\n"
|
||||||
|
" # MC2-Patch (20.07.2026): Orphan-Guard. Beende lebende\n"
|
||||||
|
" # Worker, deren Task nicht mehr 'running' ist (extern\n"
|
||||||
|
" # blockiert/reset) — sonst unterlaufen sie max_in_progress\n"
|
||||||
|
" # und verbrennen Rechenzeit.\n"
|
||||||
|
" try:\n"
|
||||||
|
" from hermes_cli import mc2_kanban_reaper as _mc2reaper\n"
|
||||||
|
" _orph = await asyncio.to_thread(\n"
|
||||||
|
" _mc2reaper.reap_orphaned_workers, _kb.connect_closing\n"
|
||||||
|
" )\n"
|
||||||
|
" if _orph:\n"
|
||||||
|
" logger.info(\n"
|
||||||
|
' "kanban dispatcher: reaped %d orphaned worker(s): %s",\n'
|
||||||
|
" len(_orph), _orph,\n"
|
||||||
|
" )\n"
|
||||||
|
" except Exception:\n"
|
||||||
|
' logger.exception("kanban dispatcher: orphan reaper failed")\n'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "0004a-context-inherit-compute",
|
||||||
|
"file": "hermes_cli/kanban_decompose.py",
|
||||||
|
"marker": '_parent_ctx = ""',
|
||||||
|
"old": (
|
||||||
|
" children: list[dict] = []\n"
|
||||||
|
" for idx, entry in enumerate(raw_tasks):\n"
|
||||||
|
),
|
||||||
|
"new": (
|
||||||
|
" children: list[dict] = []\n"
|
||||||
|
" # MC2-Patch (20.07.2026): Projekt-/Repo-Kontext vererben. Der Parent-Body\n"
|
||||||
|
' # traegt oft harte Randbedingungen (Ziel-Repo, Tabus, "eigenes Projekt"),\n'
|
||||||
|
" # die bei der Zerlegung sonst verloren gehen (Worker klonte das falsche\n"
|
||||||
|
" # Repo, Fan-out-Vorfall 20.07.). Jeder Kind-Body bekommt den voran.\n"
|
||||||
|
' _parent_ctx = ""\n'
|
||||||
|
' if getattr(task, "body", None) and task.body.strip():\n'
|
||||||
|
" _parent_ctx = (\n"
|
||||||
|
' "## PROJEKT-KONTEXT (von der uebergeordneten Aufgabe — gilt fuer dich)\\n"\n'
|
||||||
|
" + task.body.strip()[:1500]\n"
|
||||||
|
' + "\\n\\n## DEIN TEILAUFTRAG\\n"\n'
|
||||||
|
" )\n"
|
||||||
|
" for idx, entry in enumerate(raw_tasks):\n"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "0004b-context-inherit-apply",
|
||||||
|
"file": "hermes_cli/kanban_decompose.py",
|
||||||
|
"marker": "_parent_ctx + body",
|
||||||
|
"old": (
|
||||||
|
' body = entry.get("body")\n'
|
||||||
|
" if not isinstance(body, str):\n"
|
||||||
|
' body = ""\n'
|
||||||
|
),
|
||||||
|
"new": (
|
||||||
|
' body = entry.get("body")\n'
|
||||||
|
" if not isinstance(body, str):\n"
|
||||||
|
' body = ""\n'
|
||||||
|
" body = _parent_ctx + body # MC2 (20.07.): Projekt-Kontext vererben\n"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "0005-create-chain-guard",
|
||||||
|
"file": "tools/kanban_tools.py",
|
||||||
|
"marker": "_MC2_RECENT_ROOTS",
|
||||||
|
"old": (
|
||||||
|
" new_task = kb.get_task(conn, new_tid)\n"
|
||||||
|
" subscribed = _maybe_auto_subscribe(conn, new_tid)\n"
|
||||||
|
),
|
||||||
|
"new": (
|
||||||
|
" # MC2-Patch (20.07.2026): Etappen-Auto-Kette. Legt dieselbe\n"
|
||||||
|
" # Session mehrere ROOT-triage-Karten (ohne parents) im selben\n"
|
||||||
|
" # 120s-Fenster an, verkette sie — so greift das Startreife-Gate\n"
|
||||||
|
" # (Karte 2 wartet auf Karte 1) statt alle sofort zu zerlegen\n"
|
||||||
|
" # (Fan-out 20.07.). Unter max_in_progress:1 ist eine Fehl-\n"
|
||||||
|
" # Verkettung harmlos (laeuft eh seriell).\n"
|
||||||
|
" try:\n"
|
||||||
|
" if triage and not parents and session_id:\n"
|
||||||
|
" import time as _mc2t\n"
|
||||||
|
' _roots = globals().setdefault("_MC2_RECENT_ROOTS", {})\n'
|
||||||
|
" _prev = _roots.get(session_id)\n"
|
||||||
|
" _now = _mc2t.monotonic()\n"
|
||||||
|
" if _prev and (_now - _prev[1]) <= 120 and _prev[0] != new_tid:\n"
|
||||||
|
" try:\n"
|
||||||
|
" kb.link_tasks(conn, _prev[0], new_tid)\n"
|
||||||
|
" except Exception:\n"
|
||||||
|
" pass\n"
|
||||||
|
" _roots[session_id] = (new_tid, _now)\n"
|
||||||
|
" if len(_roots) > 64:\n"
|
||||||
|
" _roots.clear()\n"
|
||||||
|
" except Exception:\n"
|
||||||
|
" pass\n"
|
||||||
|
" new_task = kb.get_task(conn, new_tid)\n"
|
||||||
|
" subscribed = _maybe_auto_subscribe(conn, new_tid)\n"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if not HA.is_dir():
|
||||||
|
print(f"hermes-patches: Hermes-Agent-Dir fehlt ({HA}) — nichts zu tun.")
|
||||||
|
return 0
|
||||||
|
applied, skipped, failed = [], [], []
|
||||||
|
|
||||||
|
# --- Zusatzmodule kopieren (idempotent: nur wenn Inhalt abweicht) ---
|
||||||
|
for src_rel, dst_rel in COPIES:
|
||||||
|
src, dst = HERE / src_rel, HA / dst_rel
|
||||||
|
if not src.is_file():
|
||||||
|
failed.append((src_rel, f"Quelle fehlt: {src}"))
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
same = dst.is_file() and dst.read_bytes() == src.read_bytes()
|
||||||
|
if same:
|
||||||
|
skipped.append(f"copy:{dst_rel}")
|
||||||
|
continue
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
if dst.suffix == ".py":
|
||||||
|
py_compile.compile(str(dst), doraise=True)
|
||||||
|
applied.append(f"copy:{dst_rel}")
|
||||||
|
except Exception as exc:
|
||||||
|
failed.append((src_rel, f"Kopie/Compile fehlgeschlagen: {exc}"))
|
||||||
|
for p in PATCHES:
|
||||||
|
f = HA / p["file"]
|
||||||
|
if not f.is_file():
|
||||||
|
failed.append((p["name"], f"Datei fehlt: {f}"))
|
||||||
|
continue
|
||||||
|
src = f.read_text(encoding="utf-8")
|
||||||
|
if p["marker"] in src:
|
||||||
|
skipped.append(p["name"])
|
||||||
|
continue
|
||||||
|
n = src.count(p["old"])
|
||||||
|
if n != 1:
|
||||||
|
failed.append((p["name"], f"erwartete 1 Vorkommen von old, fand {n} "
|
||||||
|
"(Update hat den Kontext geaendert?)"))
|
||||||
|
continue
|
||||||
|
f.write_text(src.replace(p["old"], p["new"]), encoding="utf-8")
|
||||||
|
try:
|
||||||
|
py_compile.compile(str(f), doraise=True)
|
||||||
|
except Exception as exc:
|
||||||
|
failed.append((p["name"], f"py_compile fehlgeschlagen: {exc}"))
|
||||||
|
continue
|
||||||
|
applied.append(p["name"])
|
||||||
|
|
||||||
|
print(f"hermes-patches: applied={applied} skipped={skipped} failed={[n for n, _ in failed]}")
|
||||||
|
for name, why in failed:
|
||||||
|
print(f" FEHLER {name}: {why}", file=sys.stderr)
|
||||||
|
if failed:
|
||||||
|
return 1 # Fehler → Deploy/Postcheck rot
|
||||||
|
if applied:
|
||||||
|
return 10 # etwas geaendert → Aufrufer sollte hermes-gateway neu starten
|
||||||
|
return 0 # nichts zu tun (alles schon drin)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -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
|
||||||
@@ -30,6 +30,25 @@ else
|
|||||||
echo "FAIL · memory.provider nicht mehr gesetzt (Config vom Update überschrieben?)"; fail=1
|
echo "FAIL · memory.provider nicht mehr gesetzt (Config vom Update überschrieben?)"; fail=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- Hermes-Runtime-Patch-Traeger (20.07.2026): ein Update setzt den Quellbaum ---
|
||||||
|
# --- zurueck → unsere Fixes (needs_input-wartet, Startreife-Gate, Orphan-Guard, ---
|
||||||
|
# --- Kontext-Vererbung, Etappen-Kette) hier RE-eintragen. Exit 10 = etwas neu ---
|
||||||
|
# --- angewandt → Gateway neu laden; Exit 1 = ein Patch passt nicht mehr → ROT. ---
|
||||||
|
PATCH_APPLY="${MC2_SRC:-$HOME/mission-control-v2}/deploy/hermes-patches/apply.py"
|
||||||
|
if [ -f "$PATCH_APPLY" ]; then
|
||||||
|
python3 "$PATCH_APPLY" >/dev/null 2>&1; _prc=$?
|
||||||
|
if [ "$_prc" -eq 10 ]; then
|
||||||
|
systemctl --user try-restart hermes-gateway >/dev/null 2>&1 || true
|
||||||
|
echo "PASS · Hermes-Runtime-Patches nach Update RE-angewandt (Gateway neu geladen)"
|
||||||
|
elif [ "$_prc" -eq 0 ]; then
|
||||||
|
echo "PASS · Hermes-Runtime-Patches unveraendert vorhanden"
|
||||||
|
else
|
||||||
|
echo "FAIL · Hermes-Runtime-Patches: ein Patch passt nicht mehr zum aktualisierten Quellcode — pruefen"; fail=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "WARN · hermes-patches/apply.py nicht gefunden ($PATCH_APPLY)"
|
||||||
|
fi
|
||||||
|
|
||||||
if ( cd "$HERMES/hermes-agent" && HERMES_HOME="$HERMES" ./venv/bin/python -c \
|
if ( cd "$HERMES/hermes-agent" && HERMES_HOME="$HERMES" ./venv/bin/python -c \
|
||||||
"import sys; sys.path.insert(0,'.'); from plugins.memory import load_memory_provider; p=load_memory_provider('mc2-memory'); assert p and p.name()=='mc2-memory'" \
|
"import sys; sys.path.insert(0,'.'); from plugins.memory import load_memory_provider; p=load_memory_provider('mc2-memory'); assert p and p.name()=='mc2-memory'" \
|
||||||
>/dev/null 2>&1 ); then
|
>/dev/null 2>&1 ); then
|
||||||
|
|||||||
Reference in New Issue
Block a user