From 99237432194ec5df56bec69a950758a32dbf5f85 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Sun, 28 Jun 2026 11:29:32 +0200 Subject: [PATCH] =?UTF-8?q?Feat:=20Stack-Funktionspr=C3=BCfung=20nach=20OS?= =?UTF-8?q?-/Engine-/Router-Update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bisher hatte nur das Hermes-Update einen echten Post-Check; OS/Engine/Router liefen mit Exit 0 durch, auch wenn der neue Build den Stack zerschoss (gruener Job trotz totem Stack). Neu: deploy/stack-postcheck.sh prueft nach jedem Update funktional — llama-swap aktiv, /v1/models 200, ECHTE 1-Token-Inferenz auf dem Hirn-Modell (beweist Laden+Generieren), MC2 /api/health engine_reachable, Mem0 erreichbar. Eingehaengt als ` && bash stack-postcheck.sh` in os/engine/ swap-update-job → Exit 1 macht den jobengine-Job ROT. Pendant zu hermes-postcheck.sh. Co-Authored-By: Claude Opus 4.8 --- backend/services/maintenance.py | 16 ++++++-- deploy/stack-postcheck.sh | 69 +++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 deploy/stack-postcheck.sh diff --git a/backend/services/maintenance.py b/backend/services/maintenance.py index b00aed9..d344bfc 100644 --- a/backend/services/maintenance.py +++ b/backend/services/maintenance.py @@ -38,6 +38,10 @@ SWAP_BIN = os.environ.get("MC_SWAP_BIN", "/usr/local/bin/llama-swap") SWAP_REPO = os.environ.get("MC_SWAP_REPO", "mostlygeek/llama-swap") _swap_cache = {"ts": 0.0, "avail": False} +# Stack-Funktionsprüfung NACH jedem Update (OS/Engine/Router): verifiziert per echter +# Inferenz, dass der Stack noch läuft → Job wird rot, wenn ein Update etwas zerschossen hat. +STACK_POSTCHECK = os.path.join(_REPO_ROOT, "deploy", "stack-postcheck.sh") + def _installed_engine_build() -> int | None: """Build-Nummer der installierten llama-server-Binary (z.B. 9821), oder None. @@ -475,7 +479,9 @@ def check_updates_job(sudo_password: str | None = None) -> dict: def os_update_job(sudo_password: str | None = None) -> dict: if err := check_sudo_needs_password(sudo_password): return err - cmd = "sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y" + # Nach dem apt-Upgrade den Stack funktional prüfen (Job wird rot, wenn etwas kaputt ging). + cmd = ("sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y " + f"&& bash {STACK_POSTCHECK}") job_id = jobengine.start_job(["bash", "-c", cmd], "OS-Update (apt)", sudo_password=sudo_password) return {"ok": True, "job_id": job_id} @@ -490,7 +496,9 @@ def engine_update_job(sudo_password: str | None = None) -> dict | None: _engine_cache.update(ts=0.0, avail=False) # Cache leeren → frischer Build-Vergleich # update-engine.sh läuft via sudo als root und startet llama-swap am Ende selbst neu. - job_id = jobengine.start_job(["bash", "-c", ENGINE_UPDATE_CMD], + # Danach Stack-Funktionsprüfung (echte Inferenz) → Job wird rot, wenn der neue Build kaputt ist. + cmd = f"{ENGINE_UPDATE_CMD} && bash {STACK_POSTCHECK}" + job_id = jobengine.start_job(["bash", "-c", cmd], "Engine-Update (llama.cpp Vulkan)", on_done=on_done, sudo_password=sudo_password) return {"ok": True, "job_id": job_id} @@ -506,7 +514,9 @@ def swap_update_job(sudo_password: str | None = None) -> dict | None: _swap_cache.update(ts=0.0, avail=False) # Cache leeren → frischer Versions-Vergleich # update-swap.sh läuft via sudo als root, ersetzt die Binary und startet llama-swap neu. - job_id = jobengine.start_job(["bash", "-c", SWAP_UPDATE_CMD], + # Danach Stack-Funktionsprüfung (echte Inferenz) → Job wird rot, wenn der Router kaputt ist. + cmd = f"{SWAP_UPDATE_CMD} && bash {STACK_POSTCHECK}" + job_id = jobengine.start_job(["bash", "-c", cmd], "Router-Update (llama-swap)", on_done=on_done, sudo_password=sudo_password) return {"ok": True, "job_id": job_id} diff --git a/deploy/stack-postcheck.sh b/deploy/stack-postcheck.sh new file mode 100644 index 0000000..1e025ba --- /dev/null +++ b/deploy/stack-postcheck.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Stack-Post-Update-Check: läuft NACH jedem Update (OS / Engine / Router) und verifiziert, +# dass der komplette Inferenz-Stack noch FUNKTIONIERT — nicht nur "Befehl lief durch". +# Exit 0 = alles ok, sonst 1 → der jobengine-Job wird im UI ROT (state=failed). +# +# Pendant zu hermes-postcheck.sh (prüft das Gehirn/Mem0); dieser prüft Router+Engine+MC2 +# inkl. einer ECHTEN 1-Token-Inferenz (beweist, dass ein Modell wirklich lädt & generiert). +set -uo pipefail + +SWAP_URL="${MC_LLAMA_SWAP_URL:-http://127.0.0.1:8080}" +MC_URL="${MC_URL:-http://127.0.0.1:9001}" +MEM0_URL="${MEM0_SERVICE_URL:-http://127.0.0.1:8765}" +BRAIN="${MC_WARMUP_MODELS:-fast}"; BRAIN="${BRAIN%% *}" # erstes Modell, falls Liste +fail=0 + +echo "=== Stack Post-Update: Funktionsprüfung ===" + +# 0. Auf llama-swap warten — ein Engine/Router-Update startet den Dienst neu, der Erststart +# (+ erstes Modell-Laden) kann dauern. Max ~120s. +swap_up=0 +for _ in $(seq 1 60); do + curl -sf -m 5 "$SWAP_URL/v1/models" >/dev/null 2>&1 && { swap_up=1; break; } + sleep 2 +done + +# 1. Router-Dienst aktiv? (is-active ist read-only → kein sudo nötig) +if systemctl is-active --quiet llama-swap; then + echo "PASS · llama-swap-Dienst aktiv" +else + echo "FAIL · llama-swap-Dienst NICHT aktiv"; fail=1 +fi + +# 2. Engine erreichbar (Modell-Liste)? +if [ "$swap_up" -eq 1 ]; then + echo "PASS · Engine /v1/models antwortet" +else + echo "FAIL · Engine /v1/models antwortet nicht (nach 120s)"; fail=1 +fi + +# 3. Echte Inferenz: lädt ein Modell und generiert es ein Token? +resp="$(curl -s -m 240 -X POST "$SWAP_URL/v1/chat/completions" \ + -H 'Content-Type: application/json' \ + -d "{\"model\":\"$BRAIN\",\"max_tokens\":1,\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}]}" 2>/dev/null)" +if printf '%s' "$resp" | grep -q '"choices"'; then + echo "PASS · Inferenz auf '$BRAIN' liefert eine Antwort" +else + echo "FAIL · Inferenz auf '$BRAIN' fehlgeschlagen (Modell lädt/generiert nicht)"; fail=1 +fi + +# 4. MC2 selbst gesund (Engine + Gateway erreichbar)? +if curl -sf -m 8 "$MC_URL/api/health" 2>/dev/null | grep -qE '"engine_reachable":[[:space:]]*true'; then + echo "PASS · MC2 /api/health: engine_reachable=true" +else + echo "FAIL · MC2 /api/health meldet Engine nicht erreichbar"; fail=1 +fi + +# 5. Mem0-Sidecar (Gedächtnis) erreichbar? +if curl -sf -m 5 "$MEM0_URL/health" >/dev/null 2>&1; then + echo "PASS · Mem0-Sidecar erreichbar" +else + echo "FAIL · Mem0-Sidecar NICHT erreichbar"; fail=1 +fi + +if [ "$fail" -eq 0 ]; then + echo "=== STACK OK ✓ ===" +else + echo "=== STACK-CHECK FEHLGESCHLAGEN — bitte prüfen ===" +fi +exit "$fail"