Auftragsbuch: 'feature/p2-steward' angenommen (Ein-Klick-Gate)

This commit is contained in:
Auftragsbuch
2026-07-15 14:29:20 +02:00
8 changed files with 204 additions and 5 deletions
+24
View File
@@ -19,6 +19,8 @@ import threading
import time
from pathlib import Path
import httpx
from config import MODELS_DIR
log = logging.getLogger(__name__)
@@ -28,6 +30,12 @@ NOTIFY_SH = str(Path(__file__).resolve().parent.parent.parent / "deploy" / "noti
STORE_PATH = Path(os.environ.get("MC_ANNOUNCE_STORE", str(MODELS_DIR / "mc2-announce.json")))
MAX_ITEMS = int(os.environ.get("MC_ANNOUNCE_MAX", "200"))
# Steward-Auszug (UMBAU v3 P2): Läuft der Absender AUSSERHALB des MC2-Prozesses
# (mc2-steward), darf er den Store NICHT direkt schreiben — Datei + In-Memory-Cache
# gehören exklusiv dem Steuerpult. Gesetzt (Unit-Env, z. B. http://127.0.0.1:9001)
# liefert add() die Meldung stattdessen per HTTP am /api/voice/announce-Endpunkt ab.
ANNOUNCE_HTTP = os.environ.get("MC_ANNOUNCE_HTTP", "").rstrip("/")
_lock = threading.Lock()
_state: dict | None = None # {"next_id": int, "items": [...]}
@@ -58,6 +66,22 @@ def add(text: str, subject: str = "", source: str = "", priority: str = "normal"
text = (text or "").strip()
if not text:
raise ValueError("Leere Meldung.")
if ANNOUNCE_HTTP:
# Fremdprozess-Modus (mc2-steward): per HTTP beim Steuerpult abliefern.
payload = {"text": text[:4000], "subject": (subject or "").strip()[:120],
"source": (source or "").strip()[:60], "priority": priority}
try:
with httpx.Client(timeout=5.0) as c:
r = c.post(f"{ANNOUNCE_HTTP}/api/voice/announce", json=payload)
if r.status_code == 200:
return (r.json() or {}).get("item") or payload
except Exception:
pass
# MC2 down/Endpunkt weg: Alarm ist nicht verloren — der Telegram-Direktweg des
# Aufrufers (notify.sh) läuft separat; hier bleibt nur der Log-Nachweis.
log.warning("announce: HTTP-Abgabe an %s fehlgeschlagen — nur im Log: [%s] %.120s",
ANNOUNCE_HTTP, subject or "-", text)
return payload
with _lock:
state = _load()
item = {
+14
View File
@@ -85,6 +85,20 @@ CHECKS: dict[str, tuple] = {
}
# Steward-Modus (UMBAU v3 P2): Läuft der Wächter als EIGENER Prozess (mc2-steward),
# beobachtet er zusätzlich das Steuerpult selbst und den mc2-gateway — genau die zwei
# Ausfälle, die der alte In-Process-Wächter prinzipbedingt nie melden konnte (er starb mit).
if os.environ.get("MC_SENTRY_WATCH_MC2", "") == "1":
_MC2_URL = os.environ.get("MC_SENTRY_MC2_URL", "http://127.0.0.1:9001")
_GW_URL = os.environ.get("MC_SENTRY_GATEWAY_URL", "http://127.0.0.1:9010")
CHECKS["mc2"] = (lambda: _reach(_MC2_URL, "/api/health"),
"Das Steuerpult ist ausgefallen — Dashboard, Briefkasten und Auftragsbuch gehen gerade nicht.",
"Das Steuerpult ist wieder da.")
CHECKS["gateway"] = (lambda: _reach(_GW_URL, "/gw/health"),
"Der Modell-Gateway ist ausgefallen — meine Denk-Anfragen und die der Worker hängen gerade.",
"Der Modell-Gateway läuft wieder.")
class _Watch:
__slots__ = ("fails", "alerted", "alert_ts")
+80
View File
@@ -0,0 +1,80 @@
"""
MC2-Steward — die Wächter-Loops als EIGENER Prozess (UMBAU v3, P2).
Bisher hingen Re-Warm-Wächter, Health-Wächter (sentry) und Mem0-Auto-Dedupe am
Lebenszyklus des Steuerpult-Webservers (app.py-Lifespan): jeder MC2-Neustart riss
den Wächtern Timing und Flanken-Gedächtnis weg — und ein TOTES Steuerpult konnte
sich prinzipbedingt nicht selbst melden. Hier laufen DIESELBEN Loops (unveränderte
Module) als eigener Mini-Dienst (mc2-steward.service, Restart=always):
• warmer.rewarm_loop — Warm-Set nachladen (+ Config-Watch ersetzt den
In-Process-Nudge aus llamaswap.write_config)
• sentry.sentry_loop — Health-Flanken → Briefkasten (HTTP an MC2) + Telegram;
beobachtet im Steward-Modus AUCH MC2 selbst + mc2-gateway
• memory.auto_dedupe_loop — Gedächtnis-Dubletten (HTTP an den Mem0-Sidecar)
BEWUSST NICHT hier: reminders_loop — der teilt sich Datei UND CRUD-Pfade mit dem
/api/reminders-Router (Zwei-Schreiber-Risiko auf mc2-reminders.json); er bleibt im
Steuerpult. Der Briefkasten-Store gehört weiter EXKLUSIV dem MC2-Prozess — dieser
Prozess liefert Meldungen per HTTP ab (announce.py, MC_ANNOUNCE_HTTP).
Die Loop-Schalter (MC_REWARM_ENABLED / MC_SENTRY_ENABLED / MC_MEM_DEDUPE_ENABLED)
stehen in der MC2-Unit auf 0 und hier auf Default 1 — reiner Konfig-Split, kein
Verhaltens-Code im Steuerpult angefasst. Zeilen dort entfernen = Rollback.
"""
import asyncio
import logging
import os
from config import CONFIG_PATH
from services import memory as memory_svc
from services import sentry, warmer
logging.basicConfig(
level=os.environ.get("MC_LOG_LEVEL", "INFO").upper(),
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
)
log = logging.getLogger("steward")
# Wie oft nach einer llama-swap-Config-Änderung geschaut wird (mtime-Watch). Ersetzt
# warmer.nudge() aus dem MC2-Prozess: ein -watch-config-Reload verwirft das Warm-Set,
# wir sehen die Änderung binnen Sekunden statt erst beim nächsten 90s-Tick.
CONFIG_WATCH_S = int(os.environ.get("MC_STEWARD_CONFIG_WATCH_S", "5"))
async def config_watch() -> None:
last: float | None = None
while True:
try:
mtime = CONFIG_PATH.stat().st_mtime
except OSError:
mtime = None
if last is not None and mtime is not None and mtime != last:
log.info("steward: %s geändert → Warm-Set-Nudge", CONFIG_PATH)
warmer.nudge()
last = mtime
await asyncio.sleep(CONFIG_WATCH_S)
async def main() -> None:
tasks: list[asyncio.Task] = []
if warmer.ENABLED:
tasks.append(asyncio.create_task(warmer.rewarm_loop()))
tasks.append(asyncio.create_task(config_watch()))
log.info("Re-Warm-Wächter aktiv (Intervall %ss, Config-Watch %ss)",
warmer.INTERVAL, CONFIG_WATCH_S)
if sentry.ENABLED:
tasks.append(asyncio.create_task(sentry.sentry_loop()))
if memory_svc.AUTO_DEDUPE_ENABLED:
tasks.append(asyncio.create_task(memory_svc.auto_dedupe_loop()))
log.info("Mem0-Auto-Dedupe aktiv (alle %ss, Schwelle %s)",
memory_svc.AUTO_DEDUPE_INTERVAL, memory_svc.AUTO_DEDUPE_THRESHOLD)
if not tasks:
log.warning("steward: alle Loops per Env deaktiviert — nichts zu tun, Ende.")
return
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(main())
+13
View File
@@ -60,6 +60,9 @@ cp "$SRC/deploy/box-console.service" "$HOME/.config/systemd/user/box-console.ser
# 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"
# MC2-Steward (UMBAU v3 P2): Wächter-Loops (Re-Warm/Sentry/Dedupe) als eigener Dienst —
# überleben MC2-Neustarts und melden erstmals auch ein totes Steuerpult (Telegram).
cp "$SRC/deploy/mc2-steward.service" "$HOME/.config/systemd/user/mc2-steward.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).
@@ -175,6 +178,7 @@ 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 mc2-steward >/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
@@ -198,6 +202,8 @@ 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
# Steward (Wächter) — unabhängig vom Steuerpult; ohne ihn wacht niemand über die Box.
systemctl --user restart mc2-steward
systemctl --user restart mission-control-2
command -v ttyd >/dev/null 2>&1 && systemctl --user restart box-console 2>/dev/null || true
@@ -212,6 +218,13 @@ for _i in $(seq 1 10); do
done
curl -sf http://127.0.0.1:9010/gw/health && echo
curl -sf http://127.0.0.1:9001/api/health && echo
# Steward hat keinen HTTP-Port — systemd-Zustand ist der Beweis (crasht er sofort,
# ist er hier bereits 'failed' statt 'active').
if systemctl --user is-active --quiet mc2-steward; then
echo "mc2-steward aktiv (Wächter laufen)"
else
echo "FEHLER: mc2-steward läuft nicht (journalctl --user -u mc2-steward -n 30)"; exit 1
fi
# Warm-Set (Hirn + Augen/vision + Gedächtnis/embed) nach dem Deploy nachladen — ein Deploy bzw.
# watch-config-Reload verwirft es sonst und die erste Anfrage wäre kalt (D16d). warmup.sh ist
+27
View File
@@ -0,0 +1,27 @@
# systemd-USER-Unit für den MC2-Steward — die Wächter-Loops als eigener Prozess (UMBAU v3 P2).
# Re-Warm-Wächter, Health-Sentry und Mem0-Dedupe überleben damit jeden Steuerpult-Neustart,
# und der Sentry kann erstmals auch das Steuerpult SELBST + den mc2-gateway überwachen
# (MC_SENTRY_WATCH_MC2=1). Meldungen gehen per HTTP in den MC2-Briefkasten (MC_ANNOUNCE_HTTP);
# ist MC2 down, greift weiterhin der Telegram-Direktweg (notify.sh).
# Sudo-frei wie alle MC2-Units: ~/.config/systemd/user/ + systemctl --user.
[Unit]
Description=MC2 Steward (Wächter: Re-Warm, Health-Sentry, Mem0-Dedupe)
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 steward.py
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
Environment=MC_ANNOUNCE_HTTP=http://127.0.0.1:9001
Environment=MC_SENTRY_WATCH_MC2=1
Restart=always
RestartSec=3
[Install]
WantedBy=default.target
+7
View File
@@ -25,6 +25,13 @@ Environment=MC_MEMORY_DB=/srv/models/mission-control-memory.db
# durchreichen. Diese Zeile entfernen (+ daemon-reload + restart) = Rollback, MC2
# bedient /v1 wieder selbst.
Environment=MC_V1_UPSTREAM=http://127.0.0.1:9010
# Steward-Auszug (UMBAU v3 P2): Die Wächter-Loops (Re-Warm, Health-Sentry, Mem0-Dedupe)
# laufen jetzt im eigenen Dienst mc2-steward.service — hier deshalb AUS. Diese drei
# Zeilen entfernen (+ daemon-reload + restart) = Rollback (Loops wieder im Steuerpult).
# Der reminders_loop bleibt bewusst HIER (teilt Datei+CRUD mit /api/reminders).
Environment=MC_REWARM_ENABLED=0
Environment=MC_SENTRY_ENABLED=0
Environment=MC_MEM_DEDUPE_ENABLED=0
# 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
+20
View File
@@ -65,6 +65,26 @@ else
echo "FAIL · MC2 /api/health meldet Engine nicht erreichbar"; fail=1
fi
# 4b. MC2-Gateway (eigenständiger /v1-Datenpfad, UMBAU v3 P1) gesund? Nur prüfen, wenn
# die Unit enabled ist — Postchecks können auf Ständen vor dem Gateway-Auszug laufen.
GW_URL="${MC2_GATEWAY_URL:-http://127.0.0.1:9010}"
if systemctl --user is-enabled --quiet mc2-gateway 2>/dev/null; then
if curl -sf -m 8 "$GW_URL/gw/health" 2>/dev/null | grep -qE '"engine_reachable":[[:space:]]*true'; then
echo "PASS · mc2-gateway gesund (Prozess + Engine)"
else
echo "FAIL · mc2-gateway antwortet nicht/Engine weg — LLM-DATENPFAD BETROFFEN"; fail=1
fi
fi
# 4c. MC2-Steward (Wächter-Dienst, UMBAU v3 P2) aktiv? (nur wenn Unit enabled)
if systemctl --user is-enabled --quiet mc2-steward 2>/dev/null; then
if systemctl --user is-active --quiet mc2-steward; then
echo "PASS · mc2-steward aktiv (Wächter laufen)"
else
echo "FAIL · mc2-steward NICHT aktiv — Box ohne Wächter"; fail=1
fi
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"
+19 -5
View File
@@ -60,7 +60,8 @@ alles Weitere. Recherche-Verdikte dazu (15.07.): FastAPI bleibt (Litestar erwoge
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):
- **P1 · Gateway-Auszug** ✅ GEBAUT (Karte `feature/von-allein-und-gateway-p1` @ ccc9a25,
wartet auf Klick — Annahme AKTIVIERT Stufe 1):
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
@@ -71,10 +72,23 @@ Coder-Modell-Wissen gewinnen), React bleibt (Svelte/Solid schneller, aber irrele
`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).
**Nach der Annahme prüfen (Fahrplan):**`curl 127.0.0.1:9010/gw/health` (Box) + Dashboard
normal; ② self-smoke Check 4 „Repo-Wächter" grün; ③ Bild-Weiche-E2E durch den NEUEN Prozess:
Bild an `model:"coder"` → Header `x-mc-route-reason: Bild beschrieben (Vision) -> bleibt beim
Coder`; ④ DANN Stufe 2: `bash ~/mission-control-v2/deploy/gateway-cutover.sh` + `hermes doctor`
+ Beweis „MC2-Neustart tut Lucy nichts": `systemctl --user restart mission-control-2`, dabei
1-Token-Call auf `127.0.0.1:9010/v1` → antwortet weiter. Rückweg: `--revert`.
- **P2 · Steward** ✅ GEBAUT (Karte `feature/p2-steward`, baut auf P1-Karte auf — Annahme
schließt P1 mit ein; Reihenfolge P1→P2 ist sauberer): Re-Warm-Wächter, Health-Sentry und
Mem0-Dedupe laufen als eigener Dienst `mc2-steward.service` (`backend/steward.py`,
Restart=always) — unveränderte Loop-Module, reiner Konfig-Split (MC2-Unit setzt die drei
ENABLED-Schalter auf 0; Zeilen entfernen = Rollback). Neu dadurch: Sentry überwacht erstmals
auch MC2 SELBST + den mc2-gateway (`MC_SENTRY_WATCH_MC2=1`) und meldet ein totes Steuerpult
per Telegram (Briefkasten-Abgabe an MC2 läuft per HTTP, `MC_ANNOUNCE_HTTP`); Warm-Nudge nach
Config-Änderung kommt aus einem mtime-Watch (5 s) statt In-Process. **Bewusst NICHT bewegt:**
reminders_loop (teilt Datei+CRUD mit /api/reminders — Zwei-Schreiber-Risiko) bleibt in MC2.
Danach gibt es genau ZWEI Zeitplan-Systeme mit klaren Rollen: systemd-Timer (Box-Pflege) +
Hermes-Crons (Agenten-Arbeit) — plus den Steward als dritten, eigenen Wächter-Prozess.
- **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,