Files
mission-control-v2/backend/radar_lauf.py
T
HitonabiandClaude Opus 5.5 93f6613146 box-wart: Hirn gezielt pruefen, Laden meldet das echte Ergebnis, Radar-Suche als Auftrag
- Waechter (pruefe_kern): prueft jetzt gezielt das Modell mit der Rolle hermes und seinen
  Zustand in llama-swap (/running, v257 listet auch ladende Modelle), statt das Hirn fuer
  bereit zu halten, sobald irgendein Modell laeuft. Ein Ladevorgang ist kein Ausfall, aber
  nur bis 10 Minuten (MC_WAECHTER_HIRN_LADEN_S), sonst bliebe ein immer neu startendes Hirn
  unbemerkt. Sonst die ueblichen Takte (FAIL_AFTER); Knopf "Protokoll des Motors".
- llamaswap: modell_zustaende() und hirn_zustand(); lade_modell() meldet ok false mit
  deutschem Grund und der Meldung der Engine, "laedt noch" nach der Wartezeit ist kein Fehler,
  ein geladenes Modell ohne Chat (embed) zaehlt als geladen. POST /api/models/{id}/load
  reicht das Ergebnis durch (Router duenn).
- Radar "Jetzt suchen": startet die Suche als Auftrag (Job-Engine, Gruppe radar, hoechstens
  einer, radar_lauf.py --nur-suche) und antwortet sofort mit der Auftrags-ID. Die Ansicht
  zeigt "Sucht ..." bis der Auftrag fertig ist, liest danach das Radar neu und nennt eine
  gescheiterte Suche; die Auftragskarte zeigt den Stand.
- Tests: Hirn-Zustaende und Frist, Laden gegen eine llama-swap-Attrappe, Radar-Auftrag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:39:01 +02:00

140 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Nächtlicher Lauf des Modell-Radars (Box-Wart, 09/2026) — gestartet von mc2-radar.timer um 00:30.
Warum ein eigener Einstieg: Der Test lädt ein bis zu ~85 GB großes Modell neben Lucys Hirn. Das
darf nur nachts passieren und muss um 02:30 sicher vorbei sein, weil um 03:00 der NerdQuiz-
Nachtlauf das Hirn braucht. Ablauf:
1. Suchen — höchstens einmal am Tag (Merkliste + Entdeckung).
2. Offene Urteile nachholen („getestet“: damals fehlte die Vergleichsmessung).
3. Testen — nur im Fenster 00:30–02:30 und höchstens ein neuer Kandidat pro Woche.
Die Messungen prüfen die Frist selbst. Hängt trotzdem etwas, zieht fünf Minuten nach der Frist die
Notbremse: Kandidaten-Server und Download werden gestoppt, der Prozess endet hart. systemd
(RuntimeMaxSec) ist die letzte Sicherung. Ist nichts fällig, endet der Lauf sofort mit Code 0.
Mit --nur-suche (seit 24.09.2026) nur Schritt 1, jederzeit: So startet der Knopf „Jetzt suchen“ die Suche als
Auftrag (services/radar.suche_starten). Getestet wird dabei nichts.
"""
import logging
import os
import signal
import subprocess
import sys
import threading
import time
from pathlib import Path
from services import radar
logging.basicConfig(
level=os.environ.get("MC_LOG_LEVEL", "INFO").upper(),
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
)
log = logging.getLogger("radar_lauf")
NOTBREMSE_S = int(os.environ.get("MC_RADAR_NOTBREMSE_S", "300"))
def _beim_beenden(signum, _frame) -> None:
"""SIGTERM (systemd stop, RuntimeMaxSec) → normal auslaufen, damit der Kandidaten-Server gestoppt wird."""
raise SystemExit(f"Signal {signum}")
def _notbremse(frist: float) -> threading.Timer:
def ziehen() -> None:
log.error("Radar: Notbremse – %s s nach der Frist läuft noch etwas, der Lauf wird hart beendet.", NOTBREMSE_S)
try:
radar.notstopp()
finally:
os._exit(3)
bremse = threading.Timer(max(frist - time.time(), 0) + NOTBREMSE_S, ziehen)
bremse.daemon = True
bremse.start()
return bremse
NOTIFY = Path(__file__).resolve().parents[1] / "deploy" / "notify.sh"
def melde_bestanden(test: dict) -> None:
"""Ein bestandener Kandidat wartet auf eine Entscheidung — einmal Bescheid geben, sonst liegt er
unbemerkt herum (Lehre der Pins vom September). notify.sh sammelt Nachtmeldungen für den Morgen."""
k = radar.lade_stand()["kandidaten"].get(test.get("kandidat")) or {}
rolle = "das Hirn" if test.get("rolle") == "hirn" else "den Coder"
text = (f"{k.get('name') or test.get('kandidat')} hat den Nachttest für {rolle} bestanden. "
f"{str(test.get('zusammenfassung') or '')[:300]} "
"In MC2 unter Modelle kannst du ihn übernehmen oder verwerfen.")
try:
subprocess.run(["bash", str(NOTIFY), "-s", "[Modell-Radar]", text], timeout=120, check=False,
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except (OSError, subprocess.SubprocessError):
log.warning("Radar: Meldung über den bestandenen Kandidaten ging nicht raus.", exc_info=True)
def main() -> int:
beginn = radar.jetzt()
if radar.suche_faellig(beginn):
try:
ergebnis = radar.suche()
log.info("Radar: Suche – %s neu, %s warten", len(ergebnis.get("neu") or []), ergebnis.get("wartend"))
except Exception:
log.warning("Radar: Suche fehlgeschlagen", exc_info=True)
else:
log.info("Radar: heute schon gesucht.")
if not radar.im_fenster(radar.jetzt()):
log.info("Radar: außerhalb des Test-Fensters %s–%s, nichts zu testen.", radar.FENSTER_START,
radar.FENSTER_ENDE)
return 0
frist = radar.fenster_ende(radar.jetzt()).timestamp()
signal.signal(signal.SIGTERM, _beim_beenden)
bremse = _notbremse(frist)
try:
if (nachgeholt := radar.nachurteilen(frist)):
log.info("Radar: %s offene Urteile nachgeholt.", nachgeholt)
plan = radar.plane_test(radar.lade_stand(), radar.jetzt())
if not plan.get("kandidat"):
log.info("Radar: nichts fällig (%s).", plan.get("grund"))
return 0
log.info("Radar: teste %s bis spätestens %s.", plan.get("grund"), radar.FENSTER_ENDE)
test = radar.teste(plan["kandidat"], frist)
if test:
log.info("Radar: %s – %s", test.get("ergebnis"), test.get("zusammenfassung"))
if test.get("ergebnis") == "bestanden":
melde_bestanden(test)
return 0
finally:
bremse.cancel()
def nur_suche() -> int:
"""„Jetzt suchen“ aus der Oberfläche: nur die Suche, kein Test. Die letzte Zeile sagt das Ergebnis in Worten —
die Auftragskarte zeigt sie. Code 1, wenn die Suche scheitert oder keine Quelle erreichbar war."""
# Das Protokoll des Auftrags zeigt die Oberfläche: ohne eine Zeile je Hugging-Face-Abruf (samt signierter
# Download-Adressen), nur die Schritte des Radars und das Ergebnis.
logging.getLogger("httpx").setLevel(logging.WARNING)
try:
ergebnis = radar.suche()
except Exception as exc:
log.exception("Radar: Suche fehlgeschlagen")
print(f"Die Suche ist gescheitert: {exc.__class__.__name__}: {exc}", flush=True)
return 1
neu = ergebnis.get("neu") or []
quellen = ergebnis.get("quellen") or {}
teile = [f"{len(neu)} neu ({', '.join(neu)})" if neu else "nichts Neues",
f"{ergebnis.get('wartend', 0)} warten auf den Nachttest"]
if not quellen.get("discover", True):
teile.append("Hugging Face war nicht erreichbar")
if not quellen.get("watchlist", True):
teile.append("die Merkliste war nicht lesbar")
print(f"Suche fertig: {', '.join(teile)}.", flush=True)
return 0 if any(quellen.values()) else 1
if __name__ == "__main__":
sys.exit(nur_suche() if "--nur-suche" in sys.argv[1:] else main())