diff --git a/backend/gateway_app.py b/backend/gateway_app.py new file mode 100644 index 0000000..8e30f23 --- /dev/null +++ b/backend/gateway_app.py @@ -0,0 +1,64 @@ +""" +MC2-Gateway — der /v1-Datenpfad als EIGENER Prozess (UMBAU v3, P1). + +Befund der Live-Inspektion 15.07.2026: Jeder LLM-Aufruf der Box (Lucys Haupt-Hirn, +Nacht-Crons, Worker-Delegation, Vision, Decomposer/Specifier/Curator) lief durch den +Steuerpult-Prozess auf :9001 — den am häufigsten neu gestarteten Dienst des Stacks. +Dieser Einstieg hebt denselben Gateway-Router (routers/gateway_proxy.py) UNVERÄNDERT +in einen bewusst winzigen, langweiligen Prozess: Unit mc2-gateway.service, Loopback +:9010, Restart=always. Das Steuerpult darf beliebig neu starten — die Wirbelsäule steht. + +Bewusst NICHT hier: weitere Router, Hintergrund-Loops, CORS, Frontend-Auslieferung. +LAN-Clients (IDE-Lane) erreichen /v1 weiter über MC2 :9001, das roh hierher +durchreicht (routers/gateway_forward.py, MC_V1_UPSTREAM). +""" + +import logging +import os +from contextlib import asynccontextmanager +from typing import AsyncGenerator + +import httpx +from fastapi import FastAPI, Request + +from config import LLAMA_SWAP_URL, VERSION +from routers import gateway_proxy + +logging.basicConfig( + level=os.environ.get("MC_LOG_LEVEL", "INFO").upper(), + format="%(asctime)s %(levelname)-7s %(name)s: %(message)s", +) +log = logging.getLogger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: + # Gleiche Client-Parameter wie zuvor in app.py: Keep-Alive/Pooling statt neuer + # Client pro Anfrage (Sockets/TIME_WAIT unter parallelen Agent-Strömen). + app.state.gw_client = httpx.AsyncClient( + timeout=httpx.Timeout(connect=10.0, read=None, write=None, pool=10.0), + limits=httpx.Limits(max_keepalive_connections=100, max_connections=200), + ) + log.info("mc2-gateway bereit (Engine: %s)", LLAMA_SWAP_URL) + try: + yield + finally: + await app.state.gw_client.aclose() + + +app = FastAPI(title="MC2 Gateway", version=VERSION, lifespan=lifespan) +app.include_router(gateway_proxy.router) + + +@app.get("/gw/health") +async def health(request: Request): + """Eigener Health-Pfad (nicht /api/health — das gehört dem Steuerpult): + beweist Prozess UND Engine-Erreichbarkeit, für deploy.sh/stack-postcheck.sh.""" + engine = False + try: + r = await request.app.state.gw_client.get(f"{LLAMA_SWAP_URL}/v1/models", timeout=5.0) + engine = r.status_code == 200 + except httpx.HTTPError: + pass + return {"status": "ok", "service": "mc2-gateway", "version": VERSION, + "engine_reachable": engine} diff --git a/backend/services/token_stats.py b/backend/services/token_stats.py index f19d7df..e098c39 100644 --- a/backend/services/token_stats.py +++ b/backend/services/token_stats.py @@ -26,9 +26,22 @@ _lock = threading.Lock() _stats: dict | None = None _dirty = False _last_flush = 0.0 +# mtime der Datei beim letzten eigenen Laden/Schreiben — seit dem Gateway-Auszug +# (UMBAU v3 P1) schreibt der mc2-gateway-Prozess die Datei, das Steuerpult liest nur +# noch: ohne mtime-Vergleich zeigte es ab Prozessstart eingefrorene Zahlen. +_disk_mtime: float | None = None + + +def _stat_mtime() -> float | None: + try: + return STATS_FILE.stat().st_mtime + except OSError: + return None def _load_from_disk() -> dict: + global _disk_mtime + _disk_mtime = _stat_mtime() if not STATS_FILE.exists(): return dict(_BASELINE) try: @@ -51,19 +64,31 @@ def _ensure_loaded() -> dict: def _write(stats: dict) -> None: + global _disk_mtime try: STATS_FILE.parent.mkdir(parents=True, exist_ok=True) tmp = STATS_FILE.with_suffix(".tmp") with open(tmp, "w", encoding="utf-8") as f: json.dump(stats, f) tmp.replace(STATS_FILE) + _disk_mtime = _stat_mtime() # eigener Write ist kein Fremd-Update except OSError: log.warning("token_stats: Schreiben fehlgeschlagen", exc_info=True) def get_stats() -> dict: - """Aktueller Stand (inkl. noch nicht geflushter Inkremente) als Kopie.""" + """Aktueller Stand (inkl. noch nicht geflushter Inkremente) als Kopie. + + Multi-Prozess-fähig: Hat ein ANDERER Prozess (mc2-gateway) die Datei inzwischen + geschrieben und liegen hier keine ungeflushten Inkremente, wird frisch geladen. + Im Gateway-Prozess selbst ist nach jedem Flush Datei == Speicher → der + mtime-Vergleich lädt dort nie unnötig nach.""" + global _stats with _lock: + if _stats is not None and not _dirty: + mtime = _stat_mtime() + if mtime is not None and mtime != _disk_mtime: + _stats = _load_from_disk() return json.loads(json.dumps(_ensure_loaded())) diff --git a/deploy/deploy.sh b/deploy/deploy.sh index d67d7f1..a446bf9 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -57,6 +57,9 @@ systemctl --user disable --now hermes-webui 2>/dev/null || true rm -f "$HOME/.config/systemd/user/hermes-terminal.service" "$HOME/.config/systemd/user/hermes-webui.service" # Box-Konsole (ttyd -> Login-Shell auf :7682), in MC2 als Konsole-Seite eingebettet. cp "$SRC/deploy/box-console.service" "$HOME/.config/systemd/user/box-console.service" +# MC2-Gateway (UMBAU v3 P1): der /v1-Datenpfad als eigener Prozess (Loopback :9010, +# Restart=always) — Lucys Hirn + Nacht-Autonomie überleben damit jeden MC2-Neustart. +cp "$SRC/deploy/mc2-gateway.service" "$HOME/.config/systemd/user/mc2-gateway.service" # Mem0-Sidecar-Unit (nur wenn das venv existiert). [ -x "$HOME/.mem0/venv/bin/python" ] && cp "$SRC/deploy/mem0-service.service" "$HOME/.config/systemd/user/mem0-service.service" # Voice-Sidecar-Unit (nur wenn das venv existiert). @@ -171,6 +174,7 @@ if [ -d "$_HUI_WEB" ] && [ -x "$HOME/.hermes/node/bin/npx" ]; then fi systemctl --user daemon-reload systemctl --user enable mission-control-2 >/dev/null 2>&1 || true +systemctl --user enable mc2-gateway >/dev/null 2>&1 || true systemctl --user enable box-console >/dev/null 2>&1 || true systemctl --user enable mem0-service >/dev/null 2>&1 || true systemctl --user enable voice-service >/dev/null 2>&1 || true @@ -191,11 +195,22 @@ loginctl enable-linger "$USER" >/dev/null 2>&1 || true # reset-failed, damit ein zuvor am Start-Limit gestorbener Dienst wieder anläuft. systemctl --user reset-failed hermes-builtin-ui 2>/dev/null || true systemctl --user restart hermes-builtin-ui 2>/dev/null || true +# Gateway VOR dem Steuerpult (re)starten — MC2s /v1-Weiterleiter braucht ihn sofort. +# KEIN `|| true`: ohne Gateway ist der LLM-Datenpfad tot, das MUSS den Deploy stoppen. +systemctl --user restart mc2-gateway systemctl --user restart mission-control-2 command -v ttyd >/dev/null 2>&1 && systemctl --user restart box-console 2>/dev/null || true sleep 2 echo "--- Health ---" +# Gateway-Kaltstart-Fenster: bis ~12 s tolerieren, DANN hart scheitern (ohne Gateway +# ist der LLM-Datenpfad tot — das muss den Karten-Runner rot machen). +for _i in $(seq 1 10); do + curl -sf -m 3 http://127.0.0.1:9010/gw/health >/dev/null 2>&1 && break + [ "$_i" -eq 10 ] && { echo "FEHLER: mc2-gateway (:9010) antwortet nicht"; exit 1; } + sleep 1 +done +curl -sf http://127.0.0.1:9010/gw/health && echo curl -sf http://127.0.0.1:9001/api/health && echo # Warm-Set (Hirn + Augen/vision + Gedächtnis/embed) nach dem Deploy nachladen — ein Deploy bzw. diff --git a/deploy/gateway-cutover.sh b/deploy/gateway-cutover.sh new file mode 100755 index 0000000..d7da286 --- /dev/null +++ b/deploy/gateway-cutover.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Gateway-Cutover (UMBAU v3 P1, Stufe 2 = Unabhängigkeit): +# Hermes' LLM-Endpunkte vom Steuerpult-Umweg (127.0.0.1:9001/v1) auf den +# eigenständigen Gateway (127.0.0.1:9010/v1) umstellen — Top-Level-config.yaml +# UND alle Profil-Configs (werkstatt/betrieb). Danach überleben Lucys Hirn und +# die komplette Nacht-Autonomie jeden MC2-Neustart. +# +# BEWUSST NICHT in deploy.sh: ~/.hermes/config.yaml ist Zwei-Schreiber-sensibel +# (Hermes schreibt sie selbst). Einmalig von Hand bzw. per angenommener Karte: +# bash ~/mission-control-v2/deploy/gateway-cutover.sh # umstellen +# bash ~/mission-control-v2/deploy/gateway-cutover.sh --revert # zurück +# Je geänderter Datei entsteht ein Backup *.bak-gwcut-. +# Danach: hermes-gateway-Neustart + kurzer Funktionsnachweis. +set -euo pipefail + +if [ "${1:-}" = "--revert" ]; then + ALT="127.0.0.1:9010/v1"; NEU="127.0.0.1:9001/v1" + PROBE="http://127.0.0.1:9001/api/health" # Rückweg: MC2 muss /v1 wieder selbst bedienen +else + ALT="127.0.0.1:9001/v1"; NEU="127.0.0.1:9010/v1" + PROBE="http://127.0.0.1:9010/gw/health" # Hinweg: Gateway-Prozess muss leben +fi + +# Sicherung: NIE auf ein totes Ziel umstellen — das würde Lucy das Hirn abschneiden. +if ! curl -sf -m 5 "$PROBE" >/dev/null 2>&1; then + echo "ABBRUCH: Ziel $NEU antwortet nicht ($PROBE). Nichts geändert." + exit 1 +fi + +STAMP="$(date +%Y%m%d-%H%M%S)" +ALT_RE="${ALT//./\\.}" # Punkte für sed escapen +geaendert=0 +for f in "$HOME/.hermes/config.yaml" "$HOME"/.hermes/profiles/*/config.yaml; do + [ -f "$f" ] || continue + grep -q "$ALT" "$f" || continue + cp "$f" "$f.bak-gwcut-$STAMP" + sed -i "s|$ALT_RE|$NEU|g" "$f" + echo "umgestellt: $f (Backup: $f.bak-gwcut-$STAMP)" + geaendert=1 +done + +if [ "$geaendert" -eq 0 ]; then + echo "Nichts zu tun — '$ALT' kommt in keiner Hermes-Config vor." + exit 0 +fi + +echo "hermes-gateway neu starten …" +systemctl --user restart hermes-gateway +sleep 3 +if systemctl --user is-active --quiet hermes-gateway; then + echo "hermes-gateway läuft." +else + echo "WARN: hermes-gateway ist nach dem Neustart NICHT aktiv!" + echo " journalctl --user -u hermes-gateway -n 30 und ggf.: $0 --revert" + exit 1 +fi + +echo "Empfohlener Nachweis: cd ~/.hermes/hermes-agent && venv/bin/hermes doctor" +echo "FERTIG. Rückweg jederzeit: $0 --revert" diff --git a/deploy/mc2-gateway.service b/deploy/mc2-gateway.service new file mode 100644 index 0000000..a5ea4c5 --- /dev/null +++ b/deploy/mc2-gateway.service @@ -0,0 +1,26 @@ +# systemd-USER-Unit für den MC2-Gateway — der /v1-Datenpfad als eigener Prozess (UMBAU v3 P1). +# Durch diesen Prozess läuft JEDER LLM-Aufruf der Box (Lucys Hirn, Nacht-Crons, Worker- +# Delegation, Vision) — er ist bewusst winzig, wird praktisch nie angefasst und startet +# sich immer selbst neu (Restart=always: die Wirbelsäule steht, bevor es jemand merkt). +# Loopback-only (:9010): alle kritischen Verbraucher sind box-lokal; LAN-Clients +# (IDE-Lane) kommen weiter über MC2 :9001/v1, das roh hierher durchreicht. +# Sudo-frei wie alle MC2-Units: ~/.config/systemd/user/ + systemctl --user. + +[Unit] +Description=MC2 Gateway (/v1-Datenpfad: model:auto-Routing + Bild-Weiche) +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +WorkingDirectory=%h/mission-control-v2/backend +ExecStart=%h/mission-control-v2/backend/.venv/bin/python -m uvicorn gateway_app:app --host 127.0.0.1 --port 9010 +Environment=PYTHONPATH=%h/mission-control-v2 +Environment=MC_LLAMA_SWAP_URL=http://127.0.0.1:8080 +Environment=MC_CONFIG_PATH=/etc/llama-swap/config.yaml +Environment=MC_MODELS_DIR=/srv/models +Restart=always +RestartSec=2 + +[Install] +WantedBy=default.target diff --git a/deploy/mission-control-2.service b/deploy/mission-control-2.service index e1bf86c..a9a6521 100644 --- a/deploy/mission-control-2.service +++ b/deploy/mission-control-2.service @@ -21,6 +21,10 @@ Environment=MC_CONFIG_PATH=/etc/llama-swap/config.yaml Environment=MC_MODELS_DIR=/srv/models # Geteiltes Gedächtnis = die bestehende v1-DB (Kontinuität bis/über Cutover). Environment=MC_MEMORY_DB=/srv/models/mission-control-memory.db +# Gateway-Auszug (UMBAU v3 P1): /v1 roh an den eigenständigen mc2-gateway-Prozess +# durchreichen. Diese Zeile entfernen (+ daemon-reload + restart) = Rollback, MC2 +# bedient /v1 wieder selbst. +Environment=MC_V1_UPSTREAM=http://127.0.0.1:9010 # KEIN MC_ENGINE_UPDATE_CMD-Override mehr: Das alte /usr/local/bin/update-llamacpp zog den # ROCm-Build nach /opt/llamacpp (totes Rollback-Dir) statt des aktiven Vulkan-Builds → Updates # liefen ins Leere ("DONE", aber nichts passierte). Ohne Override nutzt das Backend den Default diff --git a/docs/wissen/UMBAUPLAN-ABLOESUNG.md b/docs/wissen/UMBAUPLAN-ABLOESUNG.md index 9698a06..2d16593 100644 --- a/docs/wissen/UMBAUPLAN-ABLOESUNG.md +++ b/docs/wissen/UMBAUPLAN-ABLOESUNG.md @@ -17,6 +17,7 @@ Recherche). DIE Arbeitsliste für die Zeit nach Claude. Leser: der User, die Box - Warm-Set schlank: Hirn 17,5 GB + embed 2 GB + reranker 2,2 GB ≈ 22 GB. - Repo: lokal = Gitea = Box @ main. Live-Checkout wieder sauber auf `main`. - **Autonomie-Beweis der Nacht 14.07.:** Idle-Radar erhob Journal-Befund (stop-sigterm-Timeout) → Decomposer zerlegte → werkstatt baute Restart-Skript+systemd-Override → betrieb testete live (~6 s Recovery) → done. Ohne Menschen, ohne Claude. +- **★ Nachmittags-Befund (Greenfield-Review + Live-Inspektion 15.07.):** JEDER LLM-Aufruf der Box läuft durch den EINEN MC2-Prozess auf :9001 — Lucys Haupt-Hirn (`model.base_url`, config.yaml ~Z. 430), alle auxiliary-Rollen (vision/curator/kanban_decomposer/triage_specifier), die komplette `delegation` (jeder Worker-Auftrag) und 3 MCP-Server (MC_URL). Der am häufigsten neu gestartete Dienst ist zugleich die Wirbelsäule des gesamten Stacks; das `Restart=always`-Override ist rückblickend ein Symptom dieser Kopplung. Konsequenz: **Abschnitt 3b · UMBAU v3.** ## 1 · Heute im Review erledigt (15.07.) @@ -49,6 +50,43 @@ c) **sudo-Runde (einmal Passwort):** 2. Root-Warmup-Kopie aktualisieren: `sudo install -m 0755 ~/mission-control-v2/deploy/warmup.sh /usr/local/bin/llama-swap-warmup.sh` (stale seit 07.07. — sonst wärmt ein llama-swap-Restart falsch) d) **Daily-Briefing-Cron** (dein eigener, 08:00): steht auf `Repeat: 9/41` — **läuft nach 41 Läufen aus**. Wenn dauerhaft gewollt: auf ∞ stellen (`hermes cron edit 095d53c1e99e`). +## 3b · UMBAU v3 — Architektur-Feinschliff (NEU 15.07. nachmittags) + +**Woher:** Greenfield-Gedankenexperiment („wie sähe ein Komplett-Rework aus?") + Live-Inspektion +der Box. Ergebnis: KEIN Rewrite (die Box wartet sich nachweislich selbst — während der Inspektion +lief Karte `t_ab6754f1` und lud das Gemma-4-Bench-Modell), sondern vier Inkremente, die je für +sich Wert haben. Reihenfolge ist von der Topologie diktiert, nicht verhandelbar: P1 ent-riskiert +alles Weitere. Recherche-Verdikte dazu (15.07.): FastAPI bleibt (Litestar erwogen — Ökosystem/ +Coder-Modell-Wissen gewinnen), React bleibt (Svelte/Solid schneller, aber irrelevant für +1-Nutzer-LAN), SSE ist 2026 der klare Dashboard-Standard (einweg, Auto-Reconnect, ~95 % der Fälle). + +- **P1 · Gateway-Auszug** ✅ GEBAUT (Branch `umbau/p1-gateway-auszug`, wartet auf Klick): + Der /v1-Datenpfad wird eigener Prozess `mc2-gateway.service` (Loopback **:9010**, + `Restart=always`, gleicher Router-Code `routers/gateway_proxy.py`, neuer Einstieg + `backend/gateway_app.py`). MC2 :9001/v1 wird dünner Roh-Weiterleiter (`MC_V1_UPSTREAM` in der + Unit; Zeile entfernen = Rollback), damit LAN-Clients (IDE-Lane) nichts merken. Token-Zählung + passiert genau EINMAL (im Gateway); `token_stats.get_stats()` lädt bei Fremd-Änderung per + mtime nach (Steuerpult zeigt sonst eingefrorene Zahlen). **Stufe 2 = Unabhängigkeit:** + `deploy/gateway-cutover.sh` stellt ~/.hermes-Configs (Top-Level + beide Profile) von + `127.0.0.1:9001/v1` auf `127.0.0.1:9010/v1` um (Backups, `--revert`, bewusst NICHT im + deploy.sh — config.yaml ist Zwei-Schreiber-sensibel). Danach überlebt Lucys Hirn + die ganze + Nacht-Autonomie jeden MC2-Neustart. Postcheck prüft den Gateway mit (nur wenn Unit enabled). +- **P2 · Steward** (Karte, nach P1): die 4 Endlos-Loops (warmer, sentry, reminders, Mem0-dedupe) + aus dem Webserver-Lifespan in einen eigenen Mini-Dienst — ein Steuerpult-Neustart darf keinem + Wächter Timing/Gedächtnis nehmen. Danach gibt es genau ZWEI Zeitplan-Systeme mit klaren Rollen: + systemd-Timer (Box-Pflege) + Hermes-Crons (Agenten-Arbeit). +- **P3 · Steuerpult-Modernisierung** (Kartenserie, reine UI/API-Arbeit, berührt nach P1 nie mehr + den Datenpfad): (a) EIN SSE-Eventstrom `/api/events` ersetzt die 53 `refetchInterval`-Poller in + `queries.ts`; (b) TanStack Router → echte URLs (`/modelle/coder`, `/auftrag/t_…`), Deep-Links, + Browser-Zurück; (c) TypeScript-Typen aus OpenAPI GENERIEREN (openapi-typescript) statt + handgepflegtem `api.ts` (stiller Drift-Fehlerherd). +- **P4 · State-Konsolidierung** (optional, NUR bei Schmerz): die 10 `mc2-*.json/jsonl` in + /srv/models → eine SQLite (WAL) mit Event-Tabelle (Chronik/Zeitmaschine = Abfragen statt + Eigenbau; Backup = eine Datei). Nutzen real, aber Risiko/Aufwand hoch — bewusst hinten. + +**Bewusst NICHT geändert:** FastAPI, React/Vite/Tailwind, kein Electron, kein Chat in MC2, +LAN-Trust ohne Auth, Appliance-Philosophie (user-services, deploy.sh, Postcheck, Rollback). + ## 4 · ROBUSTHEIT (Priorität 1 — Karten für die Queue) - **R1 · Worker-Protokoll-Erinnerung** (klein): `box-steckbrief-inject.sh` um einen Satz ergänzen: „Am Ende IMMER kanban_complete oder kanban_block aufrufen — sauberes Text-Ende zählt nicht." Genau daran blockierte die Nacht-Karte.