Queue-Steuerung: Stopp/Neuer-Versuch/Prio + Haenger-Warnung (UI+Telegram)
Karten lassen sich gezielt anhalten (generischer Block + Prozessbaum-Kill), frisch starten (Retry beendet Worker samt Kindern - uvicorn-Falle) und per Prio-Pfeilen umsortieren (Dispatcher zieht priority DESC nativ). Laufende Karten ohne Heartbeat >10 min bekommen ein Warn-Badge; der Ketten-Digest alarmiert nach 15 min per Telegram, inkl. Kontext-Verdichtungs-Hinweis. Ausloeser: Scaffold-Worker hing 45 min an selbst gestartetem uvicorn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,9 +31,27 @@ log = logging.getLogger(__name__)
|
||||
ENABLED = os.name == "posix" and os.environ.get("MC_KETTEN_DIGEST", "1") != "0"
|
||||
INTERVAL = int(os.environ.get("MC_KETTEN_DIGEST_INTERVAL", "300")) # Prüf-Tick: 5 min
|
||||
PULS_SEKUNDEN = int(os.environ.get("MC_KETTEN_DIGEST_PULS", "3600")) # Zwischenstand: 60 min
|
||||
# Hänger-Alarm: laufende Karte ohne Lebenszeichen (Heartbeat) länger als diese Schwelle
|
||||
# → sofortige Telegram-Warnung (einmal pro Karten-Lauf). Live-Fall 20.07.: Worker wartete
|
||||
# 45 min auf einen selbst gestarteten uvicorn — „pid_alive" hielt den Claim ewig frisch.
|
||||
HANG_SEKUNDEN = int(os.environ.get("MC_KETTEN_HANG", "900"))
|
||||
KANBAN_LOGS = Path(os.environ.get("MC_KANBAN_LOGS", "~/.hermes/kanban/logs")).expanduser()
|
||||
STATE_PATH = Path(os.environ.get("MC_KETTEN_DIGEST_STATE", str(MODELS_DIR / "mc2-ketten-digest.json")))
|
||||
|
||||
|
||||
def _kontext_druck(task_id: str) -> int:
|
||||
"""Wie oft der Worker zuletzt den Kontext verdichten musste (Log-Marker) —
|
||||
mehrfaches „Compacting context" heißt: die Aufgabe sprengt das Fenster."""
|
||||
try:
|
||||
p = KANBAN_LOGS / f"{task_id}.log"
|
||||
with p.open("rb") as f:
|
||||
f.seek(max(0, p.stat().st_size - 200_000))
|
||||
tail = f.read().decode("utf-8", errors="replace")
|
||||
return tail.count("Compacting context")
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def _load_state() -> dict:
|
||||
try:
|
||||
d = json.loads(STATE_PATH.read_text(encoding="utf-8"))
|
||||
@@ -92,6 +110,33 @@ def _tick() -> None:
|
||||
gemeldete_fragen[key] = now
|
||||
dirty = True
|
||||
|
||||
# 1b) Hänger-Alarm: laufende Karte ohne Lebenszeichen → einmal pro Karten-Lauf warnen.
|
||||
gemeldete_haenger: dict = state.setdefault("haenger", {})
|
||||
for it in items:
|
||||
if it.get("status") != "running":
|
||||
continue
|
||||
puls = it.get("puls_alter")
|
||||
if puls is None or puls < HANG_SEKUNDEN:
|
||||
continue
|
||||
key = f"{it['id']}:{it.get('gestartet') or 0}"
|
||||
if key in gemeldete_haenger:
|
||||
continue
|
||||
minuten = int(puls / 60)
|
||||
text = (f"Karte scheint zu hängen: „{(it.get('titel') or '')[:120]}“ — "
|
||||
f"seit {minuten} min kein Lebenszeichen vom Worker.")
|
||||
druck = _kontext_druck(it["id"])
|
||||
if druck >= 2:
|
||||
text += (f"\nDer Worker musste {druck}× den Kontext verdichten — "
|
||||
"die Aufgabe ist womöglich zu groß geschnitten.")
|
||||
text += "\nIn der Zentrale: „Neuer Versuch“ startet sie frisch, „Stopp“ hält sie an."
|
||||
_melden("[Werkstatt]", text)
|
||||
gemeldete_haenger[key] = now
|
||||
dirty = True
|
||||
if len(gemeldete_haenger) > 200:
|
||||
for k in sorted(gemeldete_haenger, key=gemeldete_haenger.get)[:100]:
|
||||
gemeldete_haenger.pop(k, None)
|
||||
dirty = True
|
||||
|
||||
# Fragen-Gedächtnis klein halten (Karten verschwinden irgendwann ins Archiv).
|
||||
if len(gemeldete_fragen) > 200:
|
||||
for k in sorted(gemeldete_fragen, key=gemeldete_fragen.get)[:100]:
|
||||
|
||||
Reference in New Issue
Block a user