fix(stack): Mem0 restlos ausgebaut + vier stille Defekte behoben

Systemaudit vom 27.08.2026. Alle Befunde gemessen, nicht vermutet.

VIER STILLE DEFEKTE

1. mc2-steward startete seit Wochen nicht (live: 207.609 Neustarts).
   steward.py importierte services.memory, das beim Mem0-Ausbau geloescht
   wurde -> ImportError bei jedem Start. Re-Warm- und Health-Waechter
   waren damit tot.

2. Jedes Hermes-Update wurde automatisch zurueckgerollt.
   hermes-postcheck.sh prueft vier Dinge, die es seit dem 07.08. nicht mehr
   gibt (Sidecar :8765, /api/memory, memory.provider, mc2-memory-Plugin).
   Die Checks konnten nicht gruen werden -> autoupdate.sh wertete jedes
   Update als rot und rollte es zurueck. Checks ersatzlos entfernt; der
   Tool-Smoke laeuft ohnehin durch den echten Agenten.

3. 7 von 12 Skills waren per Knopfdruck nicht startbar.
   deploy.sh kopiert Skills mit tr '-' '_' nach ~/.hermes/skills/,
   routers/skills.py gab Hermes aber den Ordnernamen MIT Bindestrich.
   Der Knopf meldete Erfolg, ausgefuehrt wurde nichts. Neu: _hermes_name().

4. deploy.sh warf bei jedem Deploy die Live-Modellkonfiguration weg.
   MC2 schreibt /etc/llama-swap/config.yaml selbst; die Repo-Datei ist nur
   ein Abzug (ihm fehlt u.a. kritiker/Devstral). Jetzt: erst sichern, Diff
   zeigen, dann kopieren. MC_DEPLOY_SKIP_SWAP_CONFIG=1 ueberspringt.

MEM0-AUSBAU VOLLENDET (Kriterium 3: 17 -> 0 Dateien)
- mem0_service/, mcp/mcp_memory.py und hermes/plugins/mc2-memory entfernt;
  das Plugin schickte bei JEDEM Turn zwei 404-Requests an tote Routen.
- MEMORY_DB/MEM0_SERVICE_URL, _mem0_reachable(), MC_MEMORY_DB und
  MC_MEM_DEDUPE_ENABLED aus Config/Router/Unit entfernt.
- mem0_ms war strukturell tot (park("retrieve") wird nirgends mehr
  aufgerufen) -> aus Backend, API-Typ und Latenzkarte entfernt.
- Verbinden-Tab: tote Gedaechtnis-MCP-Leitung raus, Status-Kachel bleibt.
- AGENTS.md beschrieb Mem0 noch als aktiv - korrigiert.

GATEWAY-ROBUSTHEIT
- _proxy gab bei ungueltigen Payloads HTTP 500 (gemessen 5/5: Rohtext,
  leerer Body, JSON-Liste, JSON-String, null) -> jetzt 5/5 HTTP 400.
- Bild-Weiche ohne Deckel: 10 Bilder x 2 Versuche x 240 s hielten den
  Client bis zu 80 min. Neu: MC_CODER_IMAGE_MAX (4), Rueckfall auf die
  Vision-Umleitung.
- /v1/models: nicht-JSON von der Engine gab 500 -> jetzt 502.

UNITS UND DEPLOY
- mc2-steward.service, dessen warmset-Drop-in und voice-service.service
  fehlten im Repo, obwohl maintenance.py und stack-postcheck.sh sie
  voraussetzen. 1:1 von der laufenden Box uebernommen.
- deploy.sh startete mc2-steward nie neu; restore.sh liess mc2-gateway und
  mc2-steward mit alter Config weiterlaufen. Beide ergaenzt.

FRONTEND
- useEigenleben rief /api/eigenleben - existiert im Backend nicht und wurde
  nirgends genutzt. Samt Typen entfernt.
- Anleitung beschrieb einen Gedaechtnis-Tab, den es nicht gibt.
- Abgeglichen: alle uebrigen 63 Frontend-Aufrufe treffen echte Routen, alle
  5 SSE-Invalidation-Keys sind gemappt, keine ungefangenen Promises.

Gates: compileall gruen - ruff "All checks passed" - tsc gruen - vite build
gruen (dist aktualisiert) - Importe app/steward/gateway_app gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-27 14:58:42 +02:00
co-authored by Claude Opus 5
parent 34a9862591
commit 84dc34c0a3
65 changed files with 394 additions and 1440 deletions
+3 -3
View File
@@ -216,7 +216,7 @@ def system_status() -> str:
@mcp.tool()
def list_services() -> str:
"""Erreichbarkeit aller Stack-Dienste (Engine, Gateway, Hermes, Mem0, Voice)."""
"""Erreichbarkeit aller Stack-Dienste (Engine, Gateway, Hermes, Voice)."""
data = _get("/api/system/services")
return "\n".join(
f"{'' if s['ok'] else ''} {s['name']} ({s['url']})"
@@ -227,7 +227,7 @@ def list_services() -> str:
@mcp.tool()
def restart_service(service: str) -> str:
"""Startet einen erlaubten Dienst neu. Erlaubt: llama-swap (Engine) | mission-control-2 |
hermes-gateway | hermes-terminal | mem0-service | voice-service. Der Engine-Neustart
hermes-gateway | hermes-terminal | voice-service. Der Engine-Neustart
(llama-swap = System-Dienst) kann das Box-Passwort verlangen, wenn kein NOPASSWD gesetzt ist."""
res = _post("/api/system/restart", {"service": service})
return f"Restart {service}: {'ok' if res.get('ok') else 'Fehler — ' + res.get('err', '')}"
@@ -235,7 +235,7 @@ def restart_service(service: str) -> str:
@mcp.tool()
def backup_now() -> str:
"""Erstellt SOFORT ein Voll-Backup (mem0 + Configs + Secrets). Nutze dies vor riskanten
"""Erstellt SOFORT ein Voll-Backup (Hermes-Configs + Secrets + llama-swap). Nutze dies vor riskanten
Änderungen oder wenn der Nutzer ein Backup will."""
r = _post("/api/system/backup")
return f"Backup erstellt: {json.dumps(r, ensure_ascii=False)}"
-127
View File
@@ -1,127 +0,0 @@
#!/usr/bin/env python3
"""
Mission Control 2.0 — Memory MCP Server (geteiltes Gedächtnis für ALLE Tools).
Stdio-MCP-Server. Läuft als Subprocess von Cline/Claude Code/OpenCode/Hermes.
Ruft die /api/memory-Endpunkte von Mission Control via HTTP auf.
Tool-Beschreibungen bewusst als GUARDS formuliert (konditional, nicht imperativ),
damit kleine Modelle (z.B. Hermes 14B) nicht in Aufruf-Schleifen laufen.
Env: MC_URL (default http://192.168.178.151:9001), MC_TOKEN (optional).
Install (auf dem Rechner des Tools): pip install mcp httpx
"""
import os
import httpx
from mcp.server.fastmcp import FastMCP
MC_URL = os.environ.get("MC_URL", "http://192.168.178.151:9001").rstrip("/")
MC_TOKEN = os.environ.get("MC_TOKEN", "")
mcp = FastMCP("mission-control-memory")
def _h() -> dict:
return {"X-MC-Token": MC_TOKEN} if MC_TOKEN else {}
def _get(path: str, **params):
r = httpx.get(f"{MC_URL}{path}", headers=_h(), params=params, timeout=10)
r.raise_for_status()
return r.json()
def _post(path: str, data: dict):
r = httpx.post(f"{MC_URL}{path}", headers=_h(), json=data, timeout=10)
r.raise_for_status()
return r.json()
def _put(path: str, data: dict):
r = httpx.put(f"{MC_URL}{path}", headers=_h(), json=data, timeout=10)
r.raise_for_status()
return r.json()
def _delete(path: str):
r = httpx.delete(f"{MC_URL}{path}", headers=_h(), timeout=10)
r.raise_for_status()
return r.json()
@mcp.tool()
def get_memories(category: str = "") -> str:
"""Lädt gespeicherte Fakten/Entscheidungen aus dem geteilten Gedächtnis.
Nutze dies EINMAL am Session-Beginn, wenn du Projekt-Kontext brauchst — nicht wiederholt.
category: identity | knowledge | rules | events | (leer = alle)"""
items = _get("/api/memory", **({"category": category} if category else {}))
if not items:
return "Keine Memories gespeichert."
icon = {"identity": "👤", "knowledge": "🔵", "rules": "📋", "events": "🕒"}
return "\n".join(
f"{icon.get(m['category'], '·')} [{m['category']}] {m['content']} (ID: {m['id'][:8]})"
for m in items
)
@mcp.tool()
def search_memories(q: str) -> str:
"""Sucht SEMANTISCH (nach Bedeutung, nicht nur Stichwort) in den Fakten. Nutze dies
VOR add_memory (Dubletten-Check) oder wenn du eine konkrete frühere Entscheidung suchst.
Treffer sind nach Relevanz sortiert."""
items = _get("/api/memory", q=q)
if not items:
return f"Keine Treffer für '{q}'."
def _line(m):
sc = f" {m['score']:.2f}" if isinstance(m.get("score"), (int, float)) else ""
return f"[{m['category']}{sc}] {m['content']} (ID: {m['id'][:8]})"
return "\n".join(_line(m) for m in items)
@mcp.tool()
def add_memory(content: str, category: str = "knowledge", source: str = "agent") -> str:
"""Speichert EINEN dauerhaften Fakt im geteilten Gedächtnis (alle Tools sehen ihn).
Nur aufrufen, WENN gerade etwas Dauerhaftes entstanden ist (Konvention, Architektur-
Entscheidung, Tech-Version, Nutzer-Präferenz) UND es noch nicht existiert (vorher
search_memories!). Knapp & atomar. Existiert ein passender Eintrag → update_memory.
category: identity (Nutzer/Vorlieben) | knowledge (Fakten/Stack) | rules (Regeln/Konventionen) | events (Ereignisse/Entscheidungen)"""
m = _post("/api/memory", {"content": content, "category": category, "source": source})
return f"Gespeichert (ID: {m['id'][:8]}): {content}"
@mcp.tool()
def update_memory(memory_id: str, content: str = "", category: str = "") -> str:
"""Aktualisiert einen bestehenden Eintrag (nur gesetzte Felder)."""
data = {k: v for k, v in (("content", content), ("category", category)) if v}
if not data:
return "Nichts zu aktualisieren."
m = _put(f"/api/memory/{memory_id}", data)
return f"Aktualisiert: {m['content']}"
@mcp.tool()
def delete_memory(memory_id: str) -> str:
"""Löscht einen veralteten/falschen Eintrag anhand seiner ID."""
_delete(f"/api/memory/{memory_id}")
return f"Eintrag {memory_id[:8]} gelöscht."
@mcp.tool()
def learn(text: str) -> str:
"""Lässt das Gedächtnis AUTOMATISCH aus einem Gesprächsausschnitt lernen: reiche
rohe Nutzer-Aussagen/Turns durch — das Gedächtnis EXTRAHIERT die dauerhaften Fakten
selbst und mischt sie ein (ohne Dubletten). Nutze dies, wenn der Nutzer beiläufig etwas
Dauerhaftes über sich/seine Vorlieben/das Projekt erwähnt hat — du musst nicht selbst
den Fakt formulieren. Für einen bereits fertig formulierten Einzel-Fakt → add_memory."""
res = _post("/api/memory/learn", {"text": text})
items = res.get("results", []) if isinstance(res, dict) else []
learned = [r for r in items if r.get("event") in (None, "ADD", "UPDATE")]
if not learned:
return "Nichts Neues gelernt (keine dauerhaften Fakten erkannt)."
return "Gelernt:\n" + "\n".join(f"· {r.get('content')}" for r in learned)
if __name__ == "__main__":
mcp.run()