- 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>
140 lines
5.8 KiB
Python
140 lines
5.8 KiB
Python
"""
|
||
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())
|