Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b639870ff | ||
|
|
084dd73f71 | ||
|
|
84b4643f1a | ||
|
|
41f2566b29 | ||
|
|
30ab2c013a | ||
|
|
d508a848f5 | ||
|
|
e04ace242c | ||
|
|
d880a76b6a | ||
|
|
ff319ff291 | ||
|
|
6e3440379b | ||
|
|
5f35f0d580 | ||
|
|
43fa747bc3 | ||
|
|
83c6ecc613 | ||
|
|
b316ad40ba | ||
|
|
8c371b946b | ||
|
|
1a2051e988 | ||
|
|
5d93822a7e | ||
|
|
22868795c9 | ||
|
|
9a35be91f9 | ||
|
|
241b1d54c6 | ||
|
|
eb4bcfc4b3 | ||
|
|
d2a175b0ad | ||
|
|
bf145a3f97 |
+171
-21
@@ -1,26 +1,176 @@
|
||||
# Savepoint — Mission Control 2.0 (MC2)
|
||||
# Savepoint — Mem0-Ausbau (Referenzaufgabe)
|
||||
|
||||
_Stand: 2026-08-19. Zustands-Snapshot nach Stack-Vollupdate (Hermes v0.20.4, llama.cpp b10502, llama-swap v250, Qwen 3.8 DFlash-2 Vorbereitung & Doku-Bereinigung)._
|
||||
**Stand:** 2026-08-22, Worktree `referenz/mem0-ausbau` in `F:\Coding Stuff\mc2-referenz`.
|
||||
Branch wurde aus `d508a84` (A2's gesicherter Stand) erstellt und enthält dessen 5 bearbeitete Dateien.
|
||||
|
||||
## Was das ist (in einem Satz)
|
||||
MC2 ist die **Web-Admin-Konsole** einer 100 % lokalen KI-Appliance („die Box", Bosgame M5 / AMD
|
||||
Strix Halo, 128 GB Unified RAM, llama.cpp-Vulkan). Sie verwaltet Engine, Modelle, Gedächtnis,
|
||||
Agenten-Status, Updates & Wartung — und dient als Steuerpult für die autonome KI („Lucy").
|
||||
## Auftrag
|
||||
`docs/aufgaben/referenzaufgabe-mem0-ausbau.md`: Mem0-Reste vollständig entfernen, ohne MC2/Frontend/Deployment zu brechen.
|
||||
- Messlatte: `ruff` clean, Backend `import app` sauber, `grep -ril mem0 backend/ mcp/ deploy/ --include='*.py' --include='*.sh'` = 0 Dateien, `mem0_service/` entfernt, Box-Smoke/Health so weit wie möglich grün.
|
||||
- **Prüfstein:** API-Felder wie `mem0_ms` nicht einfach löschen — Frontend mit anpassen oder Feld sauber aus beiden Seiten entfernen.
|
||||
- Arbeitsweise: Datei für Datei, höchstens zwei vor der ersten Änderung. Nach jeder Änderung ein Satz.
|
||||
- Live-Deployment `~/mission-control-v2` ist TABU.
|
||||
|
||||
## Architektur (Kurzform — Details in AGENTS.md/docs/)
|
||||
- **Backend** `backend/` — FastAPI, `:9001` als systemd-**User-Dienst** (reboot-fest, sudo-frei).
|
||||
- **Frontend** `frontend/` — React/Vite/Tailwind/shadcn. **`frontend/dist` ist ABSICHT im git**
|
||||
(Box hat kein Node; Backend liefert die gebauten Assets direkt aus).
|
||||
- **MC2-Gateway** `:9010` (`/v1`-Datenpfad, model:auto Routing + native Bildweiche).
|
||||
- **Modell-Router** `:8080` (llama-swap, Vulkan/RADV Engine b10502).
|
||||
- **Hermes Agent** `:8642` (`hermes-gateway`, v0.20.4 mit nativem Bot-Mode).
|
||||
- **Sidecars:** `mem0_service/` (:8765, semantischer Chroma-Gedächtnis-Sidecar), `voice_service/` (:8650, faster-whisper STT + Piper TTS), `mcp/` (MCP-Server), `client/hermes-pc` (PC-Executor :7777).
|
||||
## Wichtig: A2-Vorgang (Commit d508a84 — bereits auf diesem Branch)
|
||||
Diese 5 Dateien wurden vom abgebrochenen Lauf A2 bereits bearbeitet und im Commit `d508a84` gesichert:
|
||||
1. `backend/config.py` — `MEMORY_DB` und `MC_MEM_DEDUPE_ENABLED` entfernt, `MEM0_SERVICE_URL`-Kommentar bereinigt
|
||||
2. `backend/routers/system.py` — `_mem0_reachable()` entfernt, `mem0_reachable` aus Systeminfo
|
||||
3. `backend/routers/voice.py` — `Mem0-Retrieve` → `Gedächtnis-Retrieve`
|
||||
4. `backend/routers/zeitmaschine.py` — `Mem0` in Docstrings/Kommentaren → `Gedächtnis`
|
||||
5. `backend/services/sentry.py` — `MC_MEM0_DASHBOARD_URL` in Docstring entfernt
|
||||
|
||||
## Aktueller Zustand (19.08.2026)
|
||||
- **Stack-Vollupdate abgeschlossen:** llama-swap v250, llama.cpp b10502 (Vulkan), Hermes Agent v0.20.4 (Bot-Mode Support).
|
||||
- **Selbsttests:** `self-smoke.sh` zu 100 % grün.
|
||||
- **Codebase-Bereinigung:** Verwaiste Governor-Schnittstellen im Frontend entfernt, Regex-Versionsparsing in `maintenance.py` gefixt, Doku-Wahrheit (Mem0-Sidecar, Modelltabellen, Ports) wiederhergestellt.
|
||||
- **Modelle live:** hermes=Qwen3.6-35B-A3B (immer warm, ~70–90 t/s) · coder=Qwen3-Coder-Next (51,5 t/s) · debugger=Muse-Glimmer-30B · kritiker=Devstral-Small-2-24B (16k Deckel) · dense-planer=Qwen3.8-27B · heavy=gpt-oss-120b · vision=Qwen3-VL-30B · scout=GLM-4.6V · embed/reranker=Qwen3 0.6B.
|
||||
`git status` war nach d508a84 clean.
|
||||
|
||||
## Letzter Sicherungspunkt
|
||||
- Stand: 19.08.2026 nach Stack-Audit & Cleanup.
|
||||
## Wichtig: Kahlschlag-Commit 1e68f62 (bereits auf main)
|
||||
- `backend/services/memory.py` — gelöscht
|
||||
- `backend/routers/memory.py` — gelöscht (`/api/memory` existiert nicht mehr)
|
||||
- `deploy/mc2-steward.service` — gelöscht
|
||||
- `deploy/mem0-service.service` — gelöscht
|
||||
- `deploy/voice-service.service` — gelöscht
|
||||
- `deploy/mem0-konsolidierung.*` — gelöscht
|
||||
|
||||
## Erledigt in dieser Sitzung
|
||||
|
||||
### 1. `backend/steward.py`
|
||||
- Toter Import `from services import memory as memory_svc` entfernt
|
||||
- Toter `memory_svc.AUTO_DEDUPE_ENABLED`-Block entfernt
|
||||
- Docstring: `Mem0-Auto-Dedupe` → `Re-Warm-Wächter und Health-Wächter (sentry)`
|
||||
- Docstring: `memory.auto_dedupe_loop — Gedächtnis-Dubletten` entfernt
|
||||
- Docstring: `MC_MEM_DEDUPE_ENABLED` entfernt
|
||||
- Verifiziert: `memory.py` wurde in Kahlschlag gelöscht, `steward.py` hatte toten Import
|
||||
|
||||
### 2. `backend/services/voice_metrics.py` (alle 9 Treffer bereinigt)
|
||||
- **Docstring (3x):** `Mem0-Retrieve` → `Gedächtnis-Retrieve`
|
||||
- **Line 10:** `Mem0-Retrieve` im Best-effort-Kommentar → `Gedächtnis-Retrieve`
|
||||
- **Line 39:** `Mem0-Retrieve` im docstring → `Gedächtnis-Retrieve`
|
||||
- **Line 56:** `Geparkten Mem0-Retrieve` → `Geparkten Gedächtnis-Retrieve`
|
||||
- **Line 106:** `Unter-Detail: mem0 (Teil VON hirn...` → `Unter-Detail: memory (Teil VON hirn...`
|
||||
- **Line 116:** `Agent + Mem0 + TTFT` → `Agent + Gedächtnis + TTFT`
|
||||
- **Line 133:** `Agent-Overhead + Mem0 + LLM-TTFT` → `Agent-Overhead + Gedächtnis + LLM-TTFT`
|
||||
- **Line 141:** `mem0 = _take_retrieve(...)` → `memory = _take_retrieve(...)` (starker Kommentar)
|
||||
- **Line 154:** `"mem0_ms"` → `"memory_ms"` (API-Feld im JSON-Rückgabewort)
|
||||
|
||||
### 3. `frontend/src/lib/api.ts` (3 Treffer)
|
||||
- **Line 446:** `mem0 = Unter-Detail INNERHALB von hirn` → `memory = Unter-Detail INNERHALB von hirn`
|
||||
- **Line 455:** `Agent + Mem0 + LLM-TTFT` → `Agent + Gedächtnis + LLM-TTFT`
|
||||
- **Line 457:** `mem0_ms: number | null // ... (Mem0-Retrieve)` → `memory_ms: number | null // ... (Gedächtnis-Retrieve)`
|
||||
|
||||
### 4. `frontend/src/components/dashboard/LatencyCard.tsx` (4 Treffer)
|
||||
- **Line 5:** `Mem0 ist ein Unter-Detail` → `Gedächtnis ist ein Unter-Detail`
|
||||
- **Line 42:** `t.mem0_ms` (2x) → `t.memory_ms` (2x)
|
||||
- **Line 106:** `latest.mem0_ms` (2x) → `latest.memory_ms` (2x)
|
||||
- **Line 131:** `Agent + Mem0-Suche` → `Agent + Gedächtnis-Suche`
|
||||
|
||||
### 5. `frontend/src/views/GuideView.tsx` (1 Treffer, kompletter Paragraph)
|
||||
- Stack-Paragraph (Card 8 "Gedächtnis & RAG"): "Dein Gedächtnis ist Mem0-basiert: auto-lernend..." umgeschrieben zu "Dein Gedächtnis ist auto-lernend & semantisch: Der Agent lernt im Hintergrund nach jedem Turn..."
|
||||
- Gotcha-Zeile (Mem0-extraktion) entfernt
|
||||
- Begründung: Gedächtnis-Tab existiert nicht mehr (Kahlschlag), Mem0 wurde durch Hermes-Nativ abgelöst
|
||||
|
||||
### 6. `backend/services/backup.py` (1 Treffer)
|
||||
- Docstring: `Full state backup (mem0 + ...)` → `Full state backup (...)`
|
||||
|
||||
### 7. `deploy/backup.sh` (8 Treffer)
|
||||
- **Line 6-7:** Kommentar `mem0 (Chroma + history.db)` aus Inhalt entfernt
|
||||
- **Line 23:** `MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"` — ganze Zeile gelöscht
|
||||
- **Line 30:** `mkdir -p "$STAGE/mem0"` — aus mkdir entfernt
|
||||
- **Line 32:** `[ -d "$MEM0_DIR" ] && cp -a "$MEM0_DIR/." "$STAGE/mem0/" || true` — ganze Zeile gelöscht
|
||||
- **Line 63:** Kommentar `mem0-venv ist uv-managed` → `voice-venv ist uv-managed`
|
||||
- **Line 71:** `freeze_venv "$HOME/.mem0/venv/bin/python" "$KG/pip-mem0.txt"` — ganze Zeile gelöscht
|
||||
- **Line 82:** `$HOME/.mem0/venv/bin/python` aus for-Schleife entfernt
|
||||
- **Line 95:** MANIFEST: `mem0 (chroma + history.db)` aus Inhalt entfernt
|
||||
|
||||
### 8. `mcp/mcp_mc.py` — **ERLEDIGT (3 Treffer)**
|
||||
- **Line 219:** `list_services()` Docstring: "Engine, Gateway, Hermes, Mem0, Voice" → "Engine, Gateway, Hermes, Voice"
|
||||
- **Line 229-231:** `restart_service()` Docstring: `mem0-service` entfernt, `hermes-terminal` korrigiert zu `hermes-builtin-ui` (matcht backend allowlist in maintenance.py)
|
||||
- **Line 238:** `backup_now()` Docstring: "mem0 + Configs + Secrets" → "Configs + Secrets"
|
||||
- Verifiziert: `rg -i mem0 mcp/mcp_mc.py` = 0 Treffer, `py_compile mcp/mcp_mc.py` OK
|
||||
- **Achtung:** Der mc2-memory-Plugin (`hermes/plugins/mc2-memory/__init__.py`) ruft noch `/api/memory` und `/api/memory/learn` auf. Der Kahlschlag-Commit hat `backend/routers/memory.py` gelöscht → dieser API-Pfad existiert nicht mehr. Der Plugin degraded silently (prefetch="", learn fail). **Entscheidung:** Nicht bearbeiten, Plugin ist im `hermes/plugins/`-Ordner, nicht in der Messlatte.
|
||||
|
||||
### 9. `deploy/agent-hooks/box-steckbrief-inject.sh`
|
||||
- Treffer: "mem0-Gedaechtnis :8765" in einer langen Agent-Injektionszeile (Zeile 31)
|
||||
- Nur Text → mem0-Gedaechtnis :8765 entfernen
|
||||
|
||||
### 10. `deploy/autoupdate.sh`
|
||||
- Treffer: "Gehirn-Check (Mem0, Tools, Voice)" → "Gehirn-Check (Tools, Voice)"
|
||||
|
||||
### 11. `deploy/hermes-postcheck.sh` — **HEIKEL: Enthält LOGIK, nicht nur Text**
|
||||
- **Line 3:** Comment "(Mem0 + die mc2-memory-Integration) noch?" → muss aktualisiert werden
|
||||
- **Line 8:** `MEM0_URL="${MEM0_SERVICE_URL:-http://127.0.0.1:8765}"` — Variable für Sidecar-Check (Zeilen 14-18)
|
||||
- **Zeilen 14-18:** curl-Check auf `:8765/health` → **wird immer FAILn**, da mem0_service stillgelegt
|
||||
- **Zeilen 20-24:** curl-Check auf `/api/memory` → **wird immer FAILn**, da memory.py in Kahlschlag gelöscht
|
||||
- **Zeilen 26-31:** grep-Check für `memory.provider=mc2-memory` in config.yaml → das ist **neues** Hermes-nativ-Memory, kein Mem0-Rest. **NICHT entfernen** (gehört zur aktuellen Architektur).
|
||||
- **Zeilen 52-58:** Plugin-Check für `mc2-memory` → ebenfalls **neue** Architektur, kein Mem0-Rest. **NICHT entfernen**.
|
||||
- **Entscheidung:** Zeilen 8, 14-18, 20-24 entfernen (toter Sidecar + toter /api/memory-Endpunkt). Zeilen 26-31 und 52-58 behalten (gehören zur neuen Hermes-native Memory).
|
||||
|
||||
### 12. `deploy/restore.sh` — **LOGIK: Gegenstück zu backup.sh**
|
||||
- **Line 18:** `MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"` — Variable
|
||||
- **Line 22:** `SERVICES="mem0-service mission-control-2 hermes-gateway"` → "mem0-service" entfernen
|
||||
- **Line 101:** dry-run echo "mem0 → $MEM0_DIR" — mem0-Zeile entfernen
|
||||
- **Line 122:** `if [ -d "$STAGE/mem0" ]; then rm -rf "$MEM0_DIR"; mkdir ...; cp ...; fi` — ganze Zeile entfernen
|
||||
- Da backup.sh kein `$STAGE/mem0` mehr erstellt, ist diese Logik tot (würde nie feuern). Entfernen.
|
||||
|
||||
### 13. `deploy/stack-postcheck.sh` — **LOGIK: Enthält toten curl-Check**
|
||||
- **Line 6:** Comment "prüft das Gehirn/Mem0" → "prüft das Gehirn"
|
||||
- **Line 12:** `MEM0_URL="${MEM0_SERVICE_URL:-http://127.0.0.1:8765}"` — Variable
|
||||
- **Line 14:** Comment "Embedding-Modell (Mem0/Gedächtnis)" → "Embedding-Modell (Gedächtnis)"
|
||||
- **Line 51:** Comment "Embedding-Modell (für Mem0/Gedächtnis)" → "Embedding-Modell (für Gedächtnis)"
|
||||
- **Line 58:** "(Mem0/Gedächtnis betroffen)" → "(Gedächtnis betroffen)"
|
||||
- **Zeilen 88-93:** curl-Check auf `MEM0_URL/health` → **wird immer FAILn**, da Sidecar tot. **Ganze Block entfernen.**
|
||||
- Die Embedding-Checks (Zeilen 51-59) bleiben — sie prüfen das Embedding-Modell, das auch für die neue Memory-Architektur relevant ist.
|
||||
|
||||
### 14. `deploy/warmup.sh`
|
||||
- Treffer: "für Mem0/Gedächtnis" → "für Gedächtnis" (nur Text)
|
||||
|
||||
### 15. `deploy/venv-audit.sh`
|
||||
- **Line 2:** Comment "(Mem0, Voice) altern" → "(Voice) altern"
|
||||
- **Line 12:** `VENVS=("$HOME/.mem0/venv:mem0" "$HOME/.voice/venv:voice")` → mem0-Entry entfernen
|
||||
|
||||
### 16. Ordner `mem0_service/` löschen
|
||||
- Enthält: `app.py`, `migrate.py`, `smoke_test.py`, `requirements.txt`
|
||||
- Toter Sidecar: Port `:8765` antwortet nicht, nicht systemd-registered
|
||||
|
||||
## Noch nicht touchen (out-of-scope / unklar)
|
||||
|
||||
### `voice_service/app.py` + `voice_service/install.sh`
|
||||
- Enthalten "mem0" — gehören zur voice_service, nicht zur Messlatte
|
||||
- voice_service.service wurde in Kahlschlag gelöscht, aber voice_service/ Ordner ist noch vorhanden
|
||||
- **Entscheidung:** Nicht bearbeiten, da nicht in `backend/`, `mcp/`, `deploy/` Messlatte
|
||||
|
||||
### `hermes/plugins/mc2-memory/` (2 Dateien)
|
||||
- `__init__.py`, `plugin.yaml`
|
||||
- Rufen `/api/memory` auf → existiert nicht mehr, degradieren gracefully
|
||||
- Nicht in Messlatte (`deploy/` mit `.py/.sh` — YAML zählt nicht, Python-Plugin ist extern)
|
||||
- **Entscheidung:** Nicht bearbeiten (Hermes-Plugin, nicht Teil von MC2)
|
||||
|
||||
### `deploy/mission-control-2.service` + `deploy/mc2-backup.service`
|
||||
- systemd Service-Dateien (keine `.py` oder `.sh`)
|
||||
- Nicht in Messlatte, aber könnten Mem0-Referenzen haben
|
||||
- **Entscheidung:** Prüfen, ob sie Mem0 nennen; wenn ja, Text anpassen
|
||||
|
||||
### `deploy/llama-swap.config.yaml`
|
||||
- YAML, nicht in Messlatte
|
||||
|
||||
### `frontend/dist/`
|
||||
- Muss nach Änderungen neu gebaut werden (`npm run build`)
|
||||
- **NACHDEM alle .ts/.tsx Dateien bearbeitet sind**
|
||||
|
||||
## Bekannte Fallstricke
|
||||
|
||||
1. **Prüfstein (Frontend-Konsistenz):** `mem0_ms` war ein API-Feld, das das Frontend las. Wurde zu `memory_ms` umbenannt + Frontend angepasst. **Nicht einfach löschen.**
|
||||
2. **Restore vs. Backup:** `restore.sh` ist das Gegenstück zu `backup.sh`. Wenn backup.sh mem0 weglässt, darf restore.sh nicht versuchen, mem0 zu restaurieren. **Sorgfältig prüfen!**
|
||||
3. **`head` funktioniert in PowerShell nicht:** Muss `Select-Object -First N` verwenden.
|
||||
4. **Globs für native Befehle:** `rg` mit Globs muss gequoted werden, sonst spaltet PowerShell falsch.
|
||||
5. **`frontend/dist` ist im git:** Nach Frontend-Änderungen muss `npm run build` laufen und das neue `dist` committet werden.
|
||||
6. **`/api/memory` existiert nicht mehr** (Kahlschlag), aber `frontend/src/lib/queries.ts` ruft es noch via `useMemory()`: `api<Memory[]>(`/api/memory`...`. Das ist ein **vorher bestehendes Problem** (nicht eingeführt in dieser Runde).
|
||||
|
||||
## Nächster Schritt
|
||||
1. **mcp/mcp_mc.py committen** (bereits bearbeitet)
|
||||
2. `deploy/agent-hooks/box-steckbrief-inject.sh` bearbeiten (1 Treffer, Text)
|
||||
3. `deploy/autoupdate.sh`, `deploy/warmup.sh` bearbeiten (je 1 Treffer, Text)
|
||||
4. `deploy/venv-audit.sh` bearbeiten (2 Treffer, Text + Venv-Liste)
|
||||
5. `deploy/stack-postcheck.sh` bearbeiten (6 Treffer, 1 Block-Entfernung, 4 Kommentare)
|
||||
6. `deploy/restore.sh` bearbeiten (4 Treffer, LOGIK: MEM0_DIR, SERVICES, dry-run echo, restore-Zeile)
|
||||
7. `deploy/hermes-postcheck.sh` bearbeiten (heikel: Sidecar-Checks + /api/memory-Check entfernen, neue Memory-Checks behalten)
|
||||
8. `mem0_service/` Ordner löschen
|
||||
9. `ruff` + `import app` prüfen
|
||||
10. `grep -ril mem0 backend/ mcp/ deploy/ --include='*.py' --include='*.sh'` → muss 0 ergeben
|
||||
11. `frontend/dist` neu bauen (Box: `cd frontend && npm run build`)
|
||||
|
||||
+2
-9
@@ -19,13 +19,6 @@ MODELS_DIR = Path(os.environ.get("MC_MODELS_DIR", "/srv/models"))
|
||||
# Persistent neben den Modellen (übersteht Deploys). TTL = Frische-Fenster.
|
||||
DISCOVER_CACHE_PATH = Path(os.environ.get("MC_DISCOVER_CACHE", str(MODELS_DIR / "mc2-discover.json")))
|
||||
DISCOVER_TTL = int(os.environ.get("MC_DISCOVER_TTL", "43200")) # 12 h
|
||||
# Geteiltes Gedächtnis (SQLite, WAL). Persistent neben den Modellen.
|
||||
# Hinweis: nur noch für die einmalige Mem0-Migration relevant — das aktive Gedächtnis
|
||||
# liegt jetzt in Mem0/Chroma hinter dem Sidecar (siehe MEM0_SERVICE_URL).
|
||||
MEMORY_DB = Path(os.environ.get("MC_MEMORY_DB", str(MODELS_DIR / "mc2-memory.db")))
|
||||
# Mem0-Sidecar (auto-lernendes, semantisches Gedächtnis). Läuft im ~/.mem0/venv (Python 3.12),
|
||||
# weil mem0+chromadb unter dem 3.14-Backend nicht laufen. MC2 spricht ihn lokal per HTTP an.
|
||||
MEM0_SERVICE_URL = os.environ.get("MC_MEM0_SERVICE_URL", "http://127.0.0.1:8765").rstrip("/")
|
||||
# Befehl-Vorlage für llama-swap: {model}=GGUF-Pfad, {ctx}=Kontext, ${PORT} bleibt stehen.
|
||||
# Hinweis: --prompt-cache/--prompt-cache-all sind llama-CLI-Flags, NICHT llama-server —
|
||||
# llama-server lehnt sie ab ("invalid argument") und startet dann nicht. Prompt-Caching
|
||||
@@ -72,7 +65,7 @@ V1_UPSTREAM = os.environ.get("MC_V1_UPSTREAM", "").rstrip("/")
|
||||
HERMES_API_URL = os.environ.get("HERMES_API_URL", "http://127.0.0.1:8642").rstrip("/")
|
||||
# API-Key der Hermes-`api_server`-Plattform (~/.hermes/.env: API_SERVER_KEY). Nötig für
|
||||
# /v1/chat/completions (Voice-Pipeline) — Bearer-Auth, sonst 401. Derselbe volle Agent
|
||||
# (Tools + geteiltes Mem0) wie CLI/Telegram, nur über HTTP.
|
||||
# (Tools + geteiltes Gedächtnis) wie CLI/Telegram, nur über HTTP.
|
||||
def _read_hermes_env(key: str) -> str:
|
||||
"""Liest einen Schlüssel aus ~/.hermes/.env (Fallback, falls nicht in der Prozess-Env).
|
||||
Der MC2-Dienst erbt die Hermes-Secrets sonst nicht."""
|
||||
@@ -97,7 +90,7 @@ HERMES_API_KEY = (
|
||||
HERMES_API_MODEL = os.environ.get("HERMES_API_MODEL", "hermes")
|
||||
|
||||
# --- Voice-Sidecar (STT faster-whisper + TTS Piper/Chatterbox) ---------------
|
||||
# Eigenes Python-3.12-venv (~/.voice/venv), analog Mem0-Sidecar. MC2 proxyt nach außen.
|
||||
# Eigenes Python-3.12-venv (~/.voice/venv). MC2 proxyt nach außen.
|
||||
VOICE_SERVICE_URL = os.environ.get("MC_VOICE_SERVICE_URL", "http://127.0.0.1:8650").rstrip("/")
|
||||
# Box-Konsole: ttyd-Web-Terminal (echte Login-Shell). Bindet NUR an Loopback
|
||||
# (127.0.0.1:7682, base-path /console) und wird von MC2 über den ohnehin offenen Port 9001
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"moe": false, "quant": "Q4_K_XL", "ctx": 65536, "tools": true, "vision": true
|
||||
},
|
||||
{
|
||||
"role": "fast", "name": "Qwen3.8-27B", "repo": "unsloth/Qwen3.8-27B-GGUF",
|
||||
"role": "coder", "name": "Qwen3.8-27B", "repo": "unsloth/Qwen3.8-27B-GGUF",
|
||||
"family": "qwen", "generation": 3.8, "total_params_b": 27, "active_params_b": 27,
|
||||
"moe": false, "quant": "Q4_K_M", "ctx": 65536, "tools": true, "vision": true
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ import os
|
||||
import subprocess
|
||||
|
||||
import httpx
|
||||
from config import GATEWAY_URL, HERMES_API_URL, LLAMA_SWAP_URL, MEM0_SERVICE_URL, V1_UPSTREAM, VOICE_SERVICE_URL
|
||||
from config import GATEWAY_URL, HERMES_API_URL, LLAMA_SWAP_URL, V1_UPSTREAM, VOICE_SERVICE_URL
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
from services import backup as backup_svc
|
||||
@@ -42,13 +42,6 @@ def history(minutes: int = 60) -> dict:
|
||||
return metrics_history.history(minutes)
|
||||
|
||||
|
||||
def _mem0_reachable() -> bool:
|
||||
try:
|
||||
return httpx.get(f"{MEM0_SERVICE_URL}/health", timeout=2).status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _voice_reachable() -> bool:
|
||||
try:
|
||||
return httpx.get(f"{VOICE_SERVICE_URL}/health", timeout=2).status_code == 200
|
||||
@@ -95,12 +88,10 @@ def services() -> dict:
|
||||
rows += [
|
||||
{"name": "Wächter (Steward)", "unit": "mc2-steward", "url": "",
|
||||
"ok": _user_unit_active("mc2-steward"), "scope": "user"},
|
||||
{"name": "Hermes-Gateway", "unit": "hermes-gateway", "url": HERMES_API_URL,
|
||||
{"name": "Hermes-Gateway (Agent & Gedächtnis)", "unit": "hermes-gateway", "url": HERMES_API_URL,
|
||||
"ok": a["gateway_reachable"], "scope": "user"},
|
||||
{"name": "Hermes-GUI (Desktop-Gateway)", "unit": "hermes-builtin-ui", "url": a["hermes_ui_url"],
|
||||
"ok": a["hermes_ui_reachable"], "scope": "user"},
|
||||
{"name": "Mem0 (Gedächtnis)", "unit": "mem0-service", "url": MEM0_SERVICE_URL,
|
||||
"ok": _mem0_reachable(), "scope": "user"},
|
||||
{"name": "Voice (STT/TTS)", "unit": "voice-service", "url": VOICE_SERVICE_URL,
|
||||
"ok": _voice_reachable(), "scope": "user"},
|
||||
# ttyd hinter dem /console/-Proxy — war als einziger UI-Dienst NICHT in der Liste,
|
||||
|
||||
@@ -3,7 +3,7 @@ Voice-Endpoints für „Mit Hermes reden" (Browser-Voice + 3D-Avatar).
|
||||
|
||||
Dünner Layer: STT/TTS werden zum Voice-Sidecar (:8650) geproxyt; der Chat geht an den
|
||||
Hermes-`api_server` (:8642, OpenAI-kompatibel) — denselben vollen Agenten mit Tools +
|
||||
geteiltem Mem0 wie CLI/Telegram. Mit stabilem `X-Hermes-Session-Id` hält die Plattform den
|
||||
geteiltem Gedächtnis wie CLI/Telegram. Mit stabilem `X-Hermes-Session-Id` hält die Plattform den
|
||||
Transcript server-seitig, daher schickt der Client je Turn nur die neue User-Nachricht.
|
||||
|
||||
LAN-only (kein Token in der 2.0-Phase), wie die übrigen MC2-Endpoints.
|
||||
@@ -169,7 +169,7 @@ def voice_metrics() -> dict:
|
||||
@router.get("/voice/trace")
|
||||
def voice_trace(limit: int = 20) -> dict:
|
||||
"""Per-Turn-Trace: die letzten `limit` Chat-Turns mit Stufen-Breakdown (STT · Vision · Hirn ·
|
||||
Generierung, Mem0 als Unter-Detail). Neueste zuerst. Für die Latenz-Ansicht im Cockpit —
|
||||
Generierung, Gedächtnis als Unter-Detail). Neueste zuerst. Für die Latenz-Ansicht im Cockpit —
|
||||
damit man den EINEN langsamen Turn sieht, den ein Durchschnitt verschluckt."""
|
||||
return {"turns": get_trace(limit)}
|
||||
|
||||
@@ -289,7 +289,7 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
|
||||
headers["X-Hermes-Session-Key"] = body.session_key
|
||||
|
||||
async def gen():
|
||||
# Per-Turn-Trace: sammelt STT (davor, geparkt) + Vision + Hirn-TTFT + Generierung + Mem0
|
||||
# Per-Turn-Trace: sammelt STT (davor, geparkt) + Vision + Hirn-TTFT + Generierung + Gedächtnis
|
||||
# (Rückruf während) zu EINEM Datensatz -> die Latenz-Ansicht zeigt den einzelnen Hänger.
|
||||
trace = TurnTrace(session_id=body.session_id, kind="voice")
|
||||
first = True
|
||||
@@ -325,7 +325,7 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
|
||||
payload = {"model": body.model or HERMES_API_MODEL, "messages": messages, "stream": True}
|
||||
# Lucys Hirn (Qwen3.6) ist ein Thinking-Modell -> für die gesprochene Assistentin Thinking AUS,
|
||||
# sonst generiert es tausende Reasoning-Token VOR der kurzen Antwort (gemessen: 11k Token, ~30s TTFB).
|
||||
# Gleiches Muster wie die fast-Spur im Gateway (gateway_proxy.py) und die Mem0-Extraktion.
|
||||
# Gleiches Muster wie die fast-Spur im Gateway (gateway_proxy.py) und die Gedächtnis-Extraktion.
|
||||
if os.environ.get("MC_VOICE_NO_THINK", "1") not in ("0", "false", "False"):
|
||||
payload["chat_template_kwargs"] = {"enable_thinking": False}
|
||||
try:
|
||||
@@ -342,7 +342,7 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
|
||||
if first: # Time-To-First-Byte des Hermes-Streams (Verbindungs-Overhead)
|
||||
trace.note_ttfb()
|
||||
first = False
|
||||
# Erster CONTENT-Delta = echte Hirn-Latenz (Agent-Overhead + Mem0 + LLM-TTFT) —
|
||||
# Erster CONTENT-Delta = echte Hirn-Latenz (Agent-Overhead + Gedächtnis + LLM-TTFT) —
|
||||
# chat_ttfb misst nur den SSE-Start (~5 ms) und ist dafür blind.
|
||||
if first_content and b'"content"' in chunk:
|
||||
trace.note_first_content()
|
||||
|
||||
@@ -33,7 +33,7 @@ def list_snapshots() -> dict:
|
||||
|
||||
@router.get("/zeitmaschine/inhalt")
|
||||
def snapshot_contents(file: str) -> dict:
|
||||
"""Top-Level-Komponenten eines Snapshots (mem0, hermes, llama-swap, MANIFEST …)."""
|
||||
"""Top-Level-Komponenten eines Snapshots (hermes, llama-swap, MANIFEST …)."""
|
||||
if not _FILE_RX.match(file):
|
||||
raise HTTPException(400, "Ungültiger Snapshot-Name.")
|
||||
p = backup_svc.BACKUP_DIR / file
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Voll-Zustands-Backup (mem0 + Hermes-Configs/Secrets + llama-swap config).
|
||||
Voll-Zustands-Backup (Hermes-Configs/Secrets + llama-swap config).
|
||||
Delegiert an deploy/backup.sh (eine Quelle der Wahrheit, identisch zum systemd-Timer);
|
||||
Restore läuft bewusst nur per CLI (deploy/restore.sh) — siehe docs/BACKUP.md.
|
||||
"""
|
||||
|
||||
@@ -10,7 +10,7 @@ die häufigste Fehlerquelle. Der Aufrufer übergibt den Host explizit.
|
||||
import json
|
||||
|
||||
import httpx
|
||||
from config import HERMES_BUILTIN_UI_UPSTREAM, LLAMA_SWAP_URL, MEM0_SERVICE_URL, PORT, V1_UPSTREAM
|
||||
from config import HERMES_BUILTIN_UI_UPSTREAM, LLAMA_SWAP_URL, PORT, V1_UPSTREAM
|
||||
|
||||
DEFAULT_HOST = "192.168.178.151"
|
||||
|
||||
@@ -127,7 +127,7 @@ def build_snippets(host: str = DEFAULT_HOST,
|
||||
def check_health() -> dict:
|
||||
"""Live-Erreichbarkeit der drei Leitungen, aus Sicht der Box:
|
||||
Leitung 1 = der ECHTE Modell-Pfad, den Clients nutzen (mc2-gateway bei V1_UPSTREAM,
|
||||
sonst llama-swap direkt), Leitung 2 = Gedächtnis-Sidecar (Mem0),
|
||||
sonst llama-swap direkt), Leitung 2 = Natives Hermes-Gedächtnis (hermes-gateway),
|
||||
Leitung 3 = Desktop-Gateway (Hermes-Builtin-UI)."""
|
||||
gateway = {"ok": False, "detail": "nicht erreichbar"}
|
||||
try:
|
||||
@@ -144,9 +144,12 @@ def check_health() -> dict:
|
||||
memory = {"ok": False, "detail": "nicht erreichbar"}
|
||||
try:
|
||||
with httpx.Client(timeout=3.0) as c:
|
||||
r = c.get(f"{MEM0_SERVICE_URL}/health")
|
||||
memory = ({"ok": True, "detail": "bereit"} if r.status_code == 200
|
||||
else {"ok": False, "detail": f"HTTP {r.status_code}"})
|
||||
# Hermes Gateway stellt das native Gedächtnis & Agent-Loop bereit
|
||||
r = c.get("http://127.0.0.1:8642/health")
|
||||
if r.status_code in (200, 404, 401):
|
||||
memory = {"ok": True, "detail": "Hermes-Nativ bereit"}
|
||||
else:
|
||||
memory = {"ok": False, "detail": f"HTTP {r.status_code}"}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from services import catalog, discover, jobengine, llamaswap, system
|
||||
# System-Dienste (root, via sudo -n NOPASSWD) vs. User-Dienste (systemctl --user).
|
||||
SYSTEM_SERVICES = {"llama-swap"}
|
||||
USER_SERVICES = {"mission-control-2", "mc2-gateway", "mc2-steward", "box-console",
|
||||
"hermes-gateway", "hermes-builtin-ui", "mem0-service", "voice-service"}
|
||||
"hermes-gateway", "hermes-builtin-ui", "voice-service"}
|
||||
|
||||
# Engine-Update: lädt den neuesten Vulkan-Build (deploy/update-engine.sh, läuft als root).
|
||||
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
@@ -3,7 +3,7 @@ Lane-Routing für den eingebauten MC2-Gateway (:9001/v1).
|
||||
|
||||
Zwei virtuelle Lanes, die Clients/IDEs auswählen — der Router pickt das echte Modell:
|
||||
- **chat** (= altes `auto`): Alltag → `fast`, schwer/lang → `heavy`.
|
||||
- **coding**: Code-Arbeit → `coder` (Qwen3-Coder-Next); riesiger/architektonischer Kontext → `heavy`;
|
||||
- **coding**: Code-Arbeit → `coder`; riesiger/architektonischer Kontext → `heavy`;
|
||||
triviale Kurzfrage ohne Code → `fast` (Tempo).
|
||||
|
||||
Regelbasiert, sub-ms, ohne Cloud. Schwellen/Aliases liegen in einer UI-editierbaren Policy
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Health-Wächter der Box (Lucy-Proaktivität, Faden A3).
|
||||
|
||||
Prüft periodisch die Kern-Dienste (Engine, Agent-Hirn, Hermes-Gateway, Mem0,
|
||||
Prüft periodisch die Kern-Dienste (Engine, Agent-Hirn, Hermes-Gateway,
|
||||
Voice-Sidecar, Platte) und meldet ZUSTANDSWECHSEL in den Melde-Briefkasten
|
||||
(services/announce.py → Lucy spricht es) und via notify.sh (Telegram).
|
||||
|
||||
@@ -20,7 +20,7 @@ import time
|
||||
|
||||
import httpx
|
||||
import psutil
|
||||
from config import HERMES_API_URL, MEM0_SERVICE_URL, MODELS_DIR, VOICE_SERVICE_URL
|
||||
from config import HERMES_API_URL, MODELS_DIR, VOICE_SERVICE_URL
|
||||
|
||||
from services import announce, llamaswap
|
||||
|
||||
@@ -70,12 +70,9 @@ CHECKS: dict[str, tuple] = {
|
||||
"brain": (_check_brain,
|
||||
"Mein Gehirn lädt nicht — ich kann gerade nicht richtig denken. Ein Neustart der Engine könnte helfen.",
|
||||
"Mein Gehirn ist wieder geladen. Alles klar bei mir."),
|
||||
"hermes": (lambda: _reach(HERMES_API_URL),
|
||||
"hermes": (lambda: _reach(HERMES_API_URL, "/v1/models"),
|
||||
"Der Agent-Dienst ist ausgefallen — Telegram und meine Tools gehen gerade nicht.",
|
||||
"Der Agent-Dienst läuft wieder."),
|
||||
"mem0": (lambda: _reach(MEM0_SERVICE_URL),
|
||||
"Mein Gedächtnis-Dienst ist ausgefallen — ich merke mir vorübergehend nichts Neues.",
|
||||
"Mein Gedächtnis ist wieder online."),
|
||||
"voice": (lambda: _reach(VOICE_SERVICE_URL),
|
||||
"Der Hör-Dienst auf der Box ist ausgefallen — Spracheingabe könnte haken.",
|
||||
"Der Hör-Dienst läuft wieder."),
|
||||
|
||||
@@ -11,11 +11,12 @@ Zwei Sichten auf dieselben Messungen:
|
||||
|
||||
**Was MC2 messen kann — und was nicht:** MC2 proxyt den Chat nur an Hermes (:8642). Die Stufen STT,
|
||||
Vision, Hirn-TTFT (Zeit bis zum ersten Inhalts-Token) und Generierung sind hier direkt messbar. Der
|
||||
**Mem0-Retrieve** läuft zwar in Hermes, ruft aber MC2s `/api/memory` per HTTP zurück → messbar und als
|
||||
Unter-Detail INNERHALB der Hirn-Zeit ausgewiesen (kein Doppelzählen). Die **Tool-Runden** dagegen laufen
|
||||
im Hermes-LLM-Loop ohne Callback an MC2 → für MC2 unsichtbar, sie stecken im „Generierung"-Bucket.
|
||||
**Gedächtnis-Retrieve** läuft zwar in Hermes, ruft aber MC2s Memory-Endpoint per HTTP zurück → messbar
|
||||
und als Unter-Detail INNERHALB der Hirn-Zeit ausgewiesen (kein Doppelzählen). Die **Tool-Runden**
|
||||
dagegen laufen im Hermes-LLM-Loop ohne Callback an MC2 → für MC2 unsichtbar, sie stecken im
|
||||
„Generierung"-Bucket.
|
||||
|
||||
STT (davor) und Mem0-Retrieve (währenddessen) sind separate HTTP-Requests ohne Turn-ID. Auf einem
|
||||
STT (davor) und Gedächtnis-Retrieve (währenddessen) sind separate HTTP-Requests ohne Turn-ID. Auf einem
|
||||
EIN-Nutzer-Gerät genügt eine schlanke Best-Effort-Korrelation: die zuletzt gemessene STT-Dauer bzw. der
|
||||
letzte Retrieve werden global „geparkt" und vom nächsten Chat-Turn eingesammelt (mit Frist-/Reihenfolge-
|
||||
Check). Kein Turn-ID-Durchreichen durch den Lucy-Client nötig.
|
||||
@@ -36,7 +37,7 @@ _TURNS: deque = deque(maxlen=60) # letzte N vollständige Chat-Turns (Per-Turn-
|
||||
# Bekannte Stufen (für stabile UI-Reihenfolge); unbekannte werden trotzdem erfasst.
|
||||
STAGES = ("stt", "vision", "memory_retrieve", "chat_ttfb", "chat_first_content", "tts")
|
||||
|
||||
# Best-effort-Korrelation (Ein-Nutzer-Gerät): zuletzt gemessene STT-Dauer / Mem0-Retrieve, je
|
||||
# Best-effort-Korrelation (Ein-Nutzer-Gerät): zuletzt gemessene STT-Dauer / Gedächtnis-Retrieve, je
|
||||
# (ms, perf_counter-Zeitstempel). Der nächste passende Chat-Turn sammelt sie ein und leert sie.
|
||||
_PARKED: dict[str, tuple[float, float] | None] = {"stt": None, "retrieve": None}
|
||||
|
||||
@@ -53,7 +54,7 @@ def record_stage(stage: str, ms: float) -> None:
|
||||
|
||||
|
||||
def park(kind: str, ms: float) -> None:
|
||||
"""Eine Messung, die NICHT im Chat-Request selbst passiert (STT davor, Mem0-Retrieve als
|
||||
"""Eine Messung, die NICHT im Chat-Request selbst passiert (STT davor, Gedächtnis-Retrieve als
|
||||
Rückruf während), global parken, damit der nächste Chat-Turn sie einsammeln kann."""
|
||||
if ms is None or ms < 0 or kind not in _PARKED:
|
||||
return
|
||||
@@ -72,7 +73,7 @@ def _take_stt(max_age: float = 20.0) -> float | None:
|
||||
|
||||
|
||||
def _take_retrieve(since_perf: float, max_age: float = 90.0) -> float | None:
|
||||
"""Geparkten Mem0-Retrieve einsammeln, wenn er NACH dem Turn-Start kam (Rückruf während des
|
||||
"""Geparkten Gedächtnis-Retrieve einsammeln, wenn er NACH dem Turn-Start kam (Rückruf während des
|
||||
Turns) und frisch ist."""
|
||||
with _LOCK:
|
||||
v = _PARKED.get("retrieve")
|
||||
@@ -103,7 +104,7 @@ class TurnTrace:
|
||||
gehalten; `commit()` schreibt den Datensatz in den Ringpuffer UND speist die rollenden Stats.
|
||||
|
||||
Balken-Stufen (zeitlich DISJUNKT, ergeben zusammen den Turn): stt · vision · hirn · gen.
|
||||
Unter-Detail: mem0 (Teil VON hirn, wird separat ausgewiesen, aber NICHT zum Balken addiert)."""
|
||||
Unter-Detail: memory (Teil VON hirn, wird separat ausgewiesen, aber NICHT zum Balken addiert)."""
|
||||
|
||||
def __init__(self, session_id: str = "", kind: str = "voice") -> None:
|
||||
self.id = uuid.uuid4().hex[:8]
|
||||
@@ -113,7 +114,7 @@ class TurnTrace:
|
||||
self.session_id = (session_id or "")[:24]
|
||||
self.kind = kind
|
||||
self.vision_ms: float | None = None # Bildschirm-Beschreibung (falls Bilder)
|
||||
self.hirn_ms: float | None = None # Zeit bis zum ersten Inhalts-Token (Agent + Mem0 + TTFT)
|
||||
self.hirn_ms: float | None = None # Zeit bis zum ersten Inhalts-Token (Agent + Gedächtnis + TTFT)
|
||||
self.had_images = False
|
||||
self.error: str | None = None
|
||||
|
||||
@@ -130,7 +131,7 @@ class TurnTrace:
|
||||
record_stage("chat_ttfb", (time.perf_counter() - self._brain0) * 1000.0) # SSE-Start (~5 ms), nur rollend
|
||||
|
||||
def note_first_content(self) -> None:
|
||||
"""Erster Inhalts-Delta = echte Hirn-Latenz (Agent-Overhead + Mem0 + LLM-TTFT)."""
|
||||
"""Erster Inhalts-Delta = echte Hirn-Latenz (Agent-Overhead + Gedächtnis + LLM-TTFT)."""
|
||||
ms = (time.perf_counter() - self._brain0) * 1000.0
|
||||
self.hirn_ms = round(ms, 1)
|
||||
record_stage("chat_first_content", ms)
|
||||
@@ -138,7 +139,7 @@ class TurnTrace:
|
||||
def commit(self) -> dict:
|
||||
total = (time.perf_counter() - self.perf0) * 1000.0
|
||||
stt = _take_stt() # rollend bereits in /voice/stt erfasst
|
||||
mem0 = _take_retrieve(self.perf0) # rollend bereits in /api/memory erfasst
|
||||
memory = _take_retrieve(self.perf0) # Gedächtnis-Retrieve (HTTP-Rückruf), best-effort
|
||||
# Generierung = alles nach dem ersten Inhalts-Token bis Stream-Ende.
|
||||
gen = round(total - self.hirn_ms, 1) if self.hirn_ms is not None else None
|
||||
rec = {
|
||||
@@ -151,7 +152,7 @@ class TurnTrace:
|
||||
"vision_ms": self.vision_ms,
|
||||
"hirn_ms": self.hirn_ms,
|
||||
"gen_ms": gen if (gen is None or gen >= 0) else 0.0,
|
||||
"mem0_ms": mem0,
|
||||
"memory_ms": memory,
|
||||
"total_ms": round(total, 1),
|
||||
"error": self.error,
|
||||
}
|
||||
|
||||
+3
-8
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
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
|
||||
Bisher hingen Re-Warm-Wächter und Health-Wächter (sentry) 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
|
||||
@@ -11,14 +11,14 @@ Module) als eigener Mini-Dienst (mc2-steward.service, Restart=always):
|
||||
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)
|
||||
Die Loop-Schalter (MC_REWARM_ENABLED / MC_SENTRY_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.
|
||||
"""
|
||||
@@ -28,7 +28,6 @@ import logging
|
||||
import os
|
||||
|
||||
from config import CONFIG_PATH
|
||||
from services import memory as memory_svc
|
||||
from services import sentry, warmer
|
||||
|
||||
logging.basicConfig(
|
||||
@@ -66,10 +65,6 @@ async def main() -> None:
|
||||
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
|
||||
|
||||
@@ -28,7 +28,7 @@ hint=""
|
||||
# Stabile Anti-Flail-Orientierung: WER bin ich, WO laufen die Modelle, WAS ist tabu.
|
||||
ctx="$(cat <<EOF
|
||||
[BOX-ORIENTIERUNG — du bist ein Hintergrund-Worker auf der lokalen AI-Box (Linux, Zeit UTC / lokal Europe/Berlin, locale de_DE), NICHT Lucy und kein Dev-Laptop. Du arbeitest EINE Kanban-Aufgabe ab.]
|
||||
Die Modelle / das "Hirn" laufen ausschliesslich ueber den Router llama-swap auf 127.0.0.1:8080 (startet je Modell einen llama-server) bzw. das OpenAI-kompatible Gateway von MC2 auf 127.0.0.1:9001/v1 — es gibt KEINEN anderen Modell-Server, also nicht danach suchen. Weitere Dienste: hermes-gateway :8642, mem0-Gedaechtnis :8765. Der MC2-Live-Checkout ~/mission-control-v2 ist nur zum LESEN da (Schreiben/committen TABU — Code-Arbeit immer im frischen Klon deines Task-Workspaces). Grenzen (was gehoert wohin): MC2 = Steuerpult (verwalten/klicken/zusehen, KEIN Chat-UI), Lucy = Chat & Stimme (eigenes Repo), Hermes-Quelle ist TABU (Agenten-Verhalten nur ueber Config/SOUL/Skill/Hook/MCP aendern). Feste Entscheide/Verbote + volle Grenzen-Landkarte: ~/mission-control-v2/docs/wissen/ (GRENZEN/VERDIKTE/FALLEN/STACK) + ~/wissens-vault/eigenbau-landkarte.md.${hint}
|
||||
Die Modelle / das "Hirn" laufen ausschliesslich ueber den Router llama-swap auf 127.0.0.1:8080 (startet je Modell einen llama-server) bzw. das OpenAI-kompatible Gateway von MC2 auf 127.0.0.1:9001/v1 — es gibt KEINEN anderen Modell-Server, also nicht danach suchen. Weitere Dienste: hermes-gateway :8642. Der MC2-Live-Checkout ~/mission-control-v2 ist nur zum LESEN da (Schreiben/committen TABU — Code-Arbeit immer im frischen Klon deines Task-Workspaces). Grenzen (was gehoert wohin): MC2 = Steuerpult (verwalten/klicken/zusehen, KEIN Chat-UI), Lucy = Chat & Stimme (eigenes Repo), Hermes-Quelle ist TABU (Agenten-Verhalten nur ueber Config/SOUL/Skill/Hook/MCP aendern). Feste Entscheide/Verbote + volle Grenzen-Landkarte: ~/mission-control-v2/docs/wissen/ (GRENZEN/VERDIKTE/FALLEN/STACK) + ~/wissens-vault/eigenbau-landkarte.md.${hint}
|
||||
TRIAGE (19.07. — Drei-Bahnen-Regel): Ordne deine Karte ZUERST einer Bahn zu und bleib darin: Denken (Konzept/Plan), Bauen (Code schreiben/aendern), Betrieb (SSH/Deploy/Dienste). Fuer Betrieb-Arbeit lade IMMER zuerst den Skill betrieb-playbook (Diagnose-Checklisten; max 3 Anlaeufe pro Hindernis, dann kanban_block mit exaktem Fehlertext — die No-Progress-Bremse erzwingt das mechanisch).
|
||||
PROTOKOLL-PFLICHT (15.07.): Beende deine Aufgabe IMMER mit dem Tool-Aufruf kanban_complete (fertig) oder kanban_block (Frage/Sackgasse). Eine Text-Zusammenfassung OHNE diesen Aufruf zaehlt als Absturz (Protokollverstoss) und blockiert die Karte — genau so ging am 14.07. eine fertige Arbeit verloren.
|
||||
REPO-WEGWEISER mission-control-v2 (NICHT raten/suchen — so liegt es): backend/app.py = Einstieg + Router-Mounting · backend/routers/*.py = REST-Endpunkte · backend/services/*.py = Logik · backend/gateway_app.py = /v1-Gateway-Prozess (:9010) · frontend/src/views/*.tsx = Ansichten · frontend/src/lib/api.ts = Typen+Fetch · frontend/src/lib/queries.ts = TanStack-Query-Hooks (Polling) · frontend/src/nav.ts = Sidebar · deploy/ = Skripte/Units/Hooks/Skills · Projekt-Regeln: AGENTS.md (Wurzel) + docs/wissen/.
|
||||
|
||||
@@ -123,7 +123,7 @@ check_hermes(){
|
||||
done
|
||||
if [ "$state" = "done" ]; then
|
||||
SUMMARY+=("$name: $behind Commits eingespielt, Gehirn-Check grün.")
|
||||
notify "Hermes-Agent aktualisiert ($behind Commits). Gehirn-Check (Mem0, Tools, Voice) grün — alles läuft."
|
||||
notify "Hermes-Agent aktualisiert ($behind Commits). Gehirn-Check (Gedächtnis, Tools, Voice) grün — alles läuft."
|
||||
return
|
||||
fi
|
||||
# Rot/Timeout: ERST Selbstreparatur versuchen (Config-Bruch selbst ziehen, neue Version behalten) —
|
||||
|
||||
+6
-9
@@ -3,8 +3,8 @@
|
||||
# zurückkommt. Erzeugt EIN Tarball mc2-state-<ts>.tar.gz, behält die letzten N.
|
||||
# Läuft per systemd-Timer (täglich), manuell, oder über den UI-Snapshot-Button.
|
||||
#
|
||||
# Inhalt: mem0 (Chroma + history.db) · ~/.hermes (config.yaml, .env, plugins/) ·
|
||||
# /etc/llama-swap/config.yaml · known-good/ (Versions-Manifest, s. u.)
|
||||
# Inhalt: ~/.hermes (config.yaml, .env, plugins/) · /etc/llama-swap/config.yaml ·
|
||||
# known-good/ (Versions-Manifest, s. u.)
|
||||
# NICHT enthalten (bewusst): GGUF-Modelle (riesig, neu ladbar), MC2-Code (Git), venvs.
|
||||
#
|
||||
# ACHTUNG: das Tarball enthält ~/.hermes/.env (Secrets) → chmod 600, nicht in Git.
|
||||
@@ -20,16 +20,14 @@ RETAIN="${MC_BACKUP_RETAIN:-14}"
|
||||
# Off-Box-Ziel (leer = deaktiviert). Default: Proxmox-Host, eigener Key mit minimalem Recht.
|
||||
OFFSITE="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}"
|
||||
OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}"
|
||||
MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"
|
||||
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
|
||||
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
||||
TS="$(date +%Y%m%d-%H%M%S)"
|
||||
|
||||
STAGE="$(mktemp -d)"
|
||||
trap 'rm -rf "$STAGE"' EXIT
|
||||
mkdir -p "$STAGE/mem0" "$STAGE/hermes" "$STAGE/llama-swap"
|
||||
mkdir -p "$STAGE/hermes" "$STAGE/llama-swap"
|
||||
|
||||
[ -d "$MEM0_DIR" ] && cp -a "$MEM0_DIR/." "$STAGE/mem0/" || true
|
||||
[ -f "$HERMES/config.yaml" ] && cp -a "$HERMES/config.yaml" "$STAGE/hermes/" || true
|
||||
[ -f "$HERMES/.env" ] && cp -a "$HERMES/.env" "$STAGE/hermes/" || true
|
||||
[ -d "$HERMES/plugins" ] && cp -a "$HERMES/plugins" "$STAGE/hermes/plugins" || true
|
||||
@@ -60,7 +58,7 @@ SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
||||
HERMES_AGENT="${HERMES_AGENT_DIR:-$HOME/.hermes/hermes-agent}"
|
||||
UV="$(command -v uv || echo "$HOME/.local/bin/uv")"
|
||||
|
||||
# pip freeze je venv; mem0-venv ist uv-managed (kein pip-Modul) → uv als Fallback.
|
||||
# pip freeze je venv; voice-venv ist uv-managed (kein pip-Modul) → uv als Fallback.
|
||||
freeze_venv() { # $1 = python-Pfad, $2 = Zieldatei
|
||||
if [ ! -x "$1" ]; then echo "venv fehlt: $1" > "$2"; return 0; fi
|
||||
"$1" -m pip freeze > "$2" 2>/dev/null \
|
||||
@@ -68,7 +66,6 @@ freeze_venv() { # $1 = python-Pfad, $2 = Zieldatei
|
||||
|| echo "pip freeze fehlgeschlagen: $1" > "$2"
|
||||
}
|
||||
freeze_venv "$SRC/backend/.venv/bin/python" "$KG/pip-backend.txt"
|
||||
freeze_venv "$HOME/.mem0/venv/bin/python" "$KG/pip-mem0.txt"
|
||||
freeze_venv "$HOME/.voice/venv/bin/python" "$KG/pip-voice.txt"
|
||||
|
||||
{
|
||||
@@ -79,7 +76,7 @@ freeze_venv "$HOME/.voice/venv/bin/python" "$KG/pip-voice.txt"
|
||||
echo "llama.cpp : $(LD_LIBRARY_PATH=/opt/llamacpp-vulkan /usr/local/bin/llama-server --version 2>&1 | grep -m1 -o 'version: .*' || echo '?')"
|
||||
echo "llama-swap : $(/usr/local/bin/llama-swap --version 2>/dev/null | head -1 || echo '?')"
|
||||
echo "python-venvs :"
|
||||
for P in "$SRC/backend/.venv/bin/python" "$HOME/.mem0/venv/bin/python" "$HOME/.voice/venv/bin/python"; do
|
||||
for P in "$SRC/backend/.venv/bin/python" "$HOME/.voice/venv/bin/python"; do
|
||||
echo " $P → $("$P" --version 2>&1 || echo 'fehlt')"
|
||||
done
|
||||
} > "$KG/versions.txt" || true
|
||||
@@ -92,7 +89,7 @@ cat > "$STAGE/MANIFEST.txt" <<EOF
|
||||
mc2-state backup
|
||||
created : $TS
|
||||
host : $(hostname)
|
||||
inhalt : mem0 (chroma + history.db), hermes (config.yaml, .env, plugins/), llama-swap (config.yaml), known-good/ (Versions-Manifest: pip freeze, Engine, Modelle)
|
||||
inhalt : hermes (config.yaml, .env, plugins/), llama-swap (config.yaml), known-good/ (Versions-Manifest: pip freeze, Engine, Modelle)
|
||||
restore : bash ~/mission-control-v2/deploy/restore.sh mc2-state-$TS.tar.gz
|
||||
EOF
|
||||
|
||||
|
||||
@@ -6,10 +6,6 @@ echo "Starte MC2 Deployment..."
|
||||
# 1. Neuesten Code holen
|
||||
git pull origin main
|
||||
|
||||
# 1.5 Frontend aus dem Release-Branch holen (nur dist Ordner)
|
||||
echo "Hole gebautes Frontend aus release-dist Branch..."
|
||||
git fetch origin release-dist:refs/remotes/origin/release-dist || true
|
||||
git restore --source=origin/release-dist --staged --worktree frontend/dist 2>/dev/null || echo "Kein release-dist Branch gefunden (erster Lauf?)."
|
||||
|
||||
# 2. Abhängigkeiten prüfen (falls sich was geändert hat)
|
||||
echo "Aktualisiere Python Abhängigkeiten..."
|
||||
|
||||
@@ -1,28 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Post-Update-Check nach einem Hermes-Agent-Update: läuft unser geteiltes „Gehirn"
|
||||
# (Mem0 + die mc2-memory-Integration) noch? Exit 0 = alles ok, sonst 1 → der Job wird
|
||||
# (die mc2-memory-Integration) noch? Exit 0 = alles ok, sonst 1 → der Job wird
|
||||
# im UI rot, damit ein kaputtes Gehirn sofort auffällt.
|
||||
set -uo pipefail
|
||||
|
||||
MC_URL="${MC_URL:-http://127.0.0.1:9001}"
|
||||
MEM0_URL="${MEM0_SERVICE_URL:-http://127.0.0.1:8765}"
|
||||
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
||||
fail=0
|
||||
|
||||
echo "=== Hermes Post-Update: Gehirn-Check ==="
|
||||
|
||||
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 curl -sf -m 8 "$MC_URL/api/memory" >/dev/null 2>&1; then
|
||||
echo "PASS · /api/memory antwortet"
|
||||
else
|
||||
echo "FAIL · /api/memory antwortet nicht"; fail=1
|
||||
fi
|
||||
|
||||
if grep -qE "^[[:space:]]*provider:[[:space:]]*'?mc2-memory'?" "$HERMES/config.yaml" 2>/dev/null \
|
||||
&& grep -qE "memory_enabled:[[:space:]]*true" "$HERMES/config.yaml" 2>/dev/null; then
|
||||
echo "PASS · memory.provider=mc2-memory aktiv"
|
||||
|
||||
@@ -24,11 +24,12 @@ models:
|
||||
Qwen3.8-27B:
|
||||
# Qwen 3.8 27B: Dichtes 27B-Modell mit hybrider Linear-Attention (48/64 Schichten linear),
|
||||
# nativem Multimodal-Support (mmproj-BF16) und Tool-Calling via --jinja.
|
||||
# DFlash-2 Drafter (/srv/models/Qwen3.8-27B-DFlash2-GGUF/Qwen3.8-27B-DFlash2-Q8_0.gguf)
|
||||
# liegt bereit und wird aktiviert, sobald upstream llama.cpp 81-Tensor-DFlash2 freigibt.
|
||||
# Unser neuer Haupt-Coder mit 262k Kontext, Thinking Mode und extrem sauberer Code-Qualität.
|
||||
cmd: |
|
||||
llama-server -m /srv/models/Qwen3.8-27B-GGUF/Qwen3.8-27B-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --no-mmap --mmproj /srv/models/Qwen3.8-27B-GGUF/mmproj-BF16.gguf --jinja --parallel 2 -cram 16384 -ctk q8_0 -ctv q8_0
|
||||
ttl: 300
|
||||
ttl: 5400
|
||||
aliases:
|
||||
- coder
|
||||
capabilities:
|
||||
in: [text, image]
|
||||
out: [text]
|
||||
@@ -42,23 +43,11 @@ models:
|
||||
ttl: 600
|
||||
aliases:
|
||||
- debugger
|
||||
- doctor
|
||||
capabilities:
|
||||
in: [text, image]
|
||||
out: [text]
|
||||
tools: true
|
||||
context: 65536
|
||||
Qwen3-Coder-Next:
|
||||
cmd: |
|
||||
llama-server -m /srv/models/Qwen3-Coder-Next-GGUF/Qwen3-Coder-Next-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --no-mmap --jinja --cache-reuse 256 -cram 16384
|
||||
ttl: 5400
|
||||
aliases:
|
||||
- coder
|
||||
capabilities:
|
||||
in: [text]
|
||||
out: [text]
|
||||
tools: true
|
||||
context: 131072
|
||||
Qwen3-Embedding-0.6B:
|
||||
cmd: |
|
||||
llama-server -m /srv/models/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf --host 127.0.0.1 --port ${PORT} --embedding --pooling last -ngl 999 -fa on --no-mmap -c 8192
|
||||
@@ -79,11 +68,6 @@ models:
|
||||
tools: true
|
||||
context: 32768
|
||||
Qwen3-VL-30B-A3B-Instruct:
|
||||
# Lucys AUGEN — seit 07.07. ON-DEMAND (User-Entscheid: „Gehirn + Embedding reichen
|
||||
# komplett; Vision nur wenn Lucy auf dem Desktop an ist oder die IDE-Lane sie braucht").
|
||||
# NICHT mehr in brains; ttl 900 = 15 Min Nachlauf nach dem letzten Blick. Die Lucy-App
|
||||
# wärmt die Augen beim Start selbst an (useVoiceAgent-Warm-Gate). Vorher: warm, ttl 0 —
|
||||
# das kostete ~19 GB Dauerbelegung und verdrängte den 60-GB-Chef-Gutachter.
|
||||
cmd: |
|
||||
llama-server -m /srv/models/Qwen3-VL-30B-A3B-Instruct-GGUF/Qwen3-VL-30B-A3B-Instruct-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 32768 -ngl 999 -fa on --no-mmap --mmproj /srv/models/Qwen3-VL-30B-A3B-Instruct-GGUF/mmproj-F16.gguf --jinja
|
||||
ttl: 900
|
||||
@@ -93,33 +77,6 @@ models:
|
||||
in: [text, image]
|
||||
out: [text]
|
||||
context: 32768
|
||||
GLM-4.6V-Flash:
|
||||
cmd: |
|
||||
llama-server -m /srv/models/GLM-4.6V-Flash-GGUF/GLM-4.6V-Flash-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --no-mmap --mmproj /srv/models/GLM-4.6V-Flash-GGUF/mmproj-BF16.gguf --jinja
|
||||
ttl: 300
|
||||
aliases:
|
||||
- scout
|
||||
capabilities:
|
||||
in: [text, image]
|
||||
out: [text]
|
||||
tools: true
|
||||
context: 131072
|
||||
Devstral-Small-2-24B:
|
||||
# KRITIKER (25.07.2026): Gegenleser aus einer ANDEREN Modellfamilie (Mistral, Apache-2.0,
|
||||
# 24B DICHT, 256k Kontext, 68,0 % SWE-bench Verified vs. 59,2 % bei GLM-4.7-Flash).
|
||||
# Loest GLM-4.7-Flash in der kritiker-Rolle ab. Dicht = auf Strix Halo bandbreitengebunden
|
||||
# und damit langsam beim SCHREIBEN — fuer einen Kritiker egal: er liest viel, schreibt wenig.
|
||||
# --jinja ist Pflicht (Chat-Template, sonst kaputte Tool-Calls).
|
||||
cmd: |
|
||||
llama-server -m /srv/models/Devstral-Small-2-GGUF/mistralai_Devstral-Small-2-24B-Instruct-2512-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 16384 -ngl 999 -fa on --no-mmap --jinja --cache-reuse 256
|
||||
ttl: 1800
|
||||
aliases:
|
||||
- kritiker
|
||||
capabilities:
|
||||
in: [text]
|
||||
out: [text]
|
||||
tools: true
|
||||
context: 16384
|
||||
Qwen3-Reranker-0.6B:
|
||||
# Gedächtnis-Zweitstufe (07.07., Rollen-Audit): ordnet Mem0-Suchtreffer nach echter
|
||||
# Relevanz (/v1/rerank, Mungert-GGUF — Community-Konvertierungen liefern oft Nullscores!).
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
# Projekte-Sync (20.08.2026): haelt ~/projekte mit Gitea deckungsgleich.
|
||||
#
|
||||
# Warum: Der Hermes-Desktop laeuft gegen das Box-Backend, sein Ordner-Waehler zeigt
|
||||
# also das Dateisystem der BOX. Damit dort alle Projekte auftauchen, muessen sie hier
|
||||
# liegen — und aktuell sein.
|
||||
#
|
||||
# Regeln (bewusst konservativ — dieses Skript laeuft unbeaufsichtigt):
|
||||
# • NUR fast-forward (`--ff-only`). Kein merge, kein rebase, kein reset, kein force.
|
||||
# • Repos mit lokalen Aenderungen werden UEBERSPRUNGEN, nicht ueberfahren.
|
||||
# • Live-Deployments werden NIE gezogen (mission-control-v2 wird aus dem Deploy
|
||||
# bedient und pusht selbst) — sie bekommen nur eine Verknuepfung, damit sie im
|
||||
# Waehler auftauchen.
|
||||
# • Bestehende Checkouts in ~ bleiben, wo sie sind; ~/projekte bekommt eine
|
||||
# Verknuepfung darauf statt eines zweiten Klons.
|
||||
# • Interne Gitea-IP, NICHT die DDNS-Domain: die ist nachts durch die
|
||||
# Zwangstrennung zeitweise tot (Lehre vom 24.07.).
|
||||
#
|
||||
# Aufruf: projekte-sync.sh [--dry-run]
|
||||
# Exit: 0 = alles gut · 1 = mindestens ein Repo hatte ein Problem
|
||||
set -uo pipefail
|
||||
|
||||
GITEA="${GITEA_URL:-http://192.168.178.153:3000}"
|
||||
ZIEL="${PROJEKTE_DIR:-$HOME/projekte}"
|
||||
LOG="${PROJEKTE_SYNC_LOG:-$HOME/.hermes/logs/projekte-sync.log}"
|
||||
DRY=0
|
||||
[ "${1:-}" = "--dry-run" ] && DRY=1
|
||||
|
||||
# Repos, die NICHT nach ~/projekte geklont werden (Live-Deployment oder Altlast).
|
||||
# mission-control-v2: laeuft aus ~/mission-control-v2, Dienste arbeiten daraus.
|
||||
# mission-control: archivierte v1.
|
||||
NIE_ZIEHEN="mission-control-v2"
|
||||
UEBERSPRINGEN="mission-control"
|
||||
|
||||
mkdir -p "$ZIEL" "$(dirname "$LOG")"
|
||||
FEHLER=0
|
||||
|
||||
log() { printf '%s %s\n' "$(date '+%F %T')" "$*" | tee -a "$LOG"; }
|
||||
|
||||
TOKEN=""
|
||||
for f in "$HOME/.config/gitea/create-token"; do
|
||||
[ -r "$f" ] && TOKEN="$(tr -d '[:space:]' < "$f")" && break
|
||||
done
|
||||
if [ -z "$TOKEN" ]; then
|
||||
log "ABBRUCH: kein Gitea-Token unter ~/.config/gitea/create-token"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPOS="$(curl -s -m 20 -H "Authorization: token $TOKEN" \
|
||||
"$GITEA/api/v1/user/repos?limit=100" \
|
||||
| python3 -c 'import sys,json; [print(r["name"]) for r in json.load(sys.stdin)]' 2>/dev/null)"
|
||||
if [ -z "$REPOS" ]; then
|
||||
log "ABBRUCH: Gitea lieferte keine Repo-Liste ($GITEA)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "=== Sync-Lauf ($(printf '%s\n' "$REPOS" | wc -l) Repos gemeldet)$([ $DRY -eq 1 ] && echo ' [Trockenlauf]') ==="
|
||||
|
||||
# Vorhandenen Checkout eines Repos finden (oberste Home-Ebene bevorzugt).
|
||||
finde_checkout() {
|
||||
local name="$1" d
|
||||
for d in "$HOME/$name" "$ZIEL/$name"; do
|
||||
[ -d "$d/.git" ] && { printf '%s' "$d"; return 0; }
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
verknuepfe() { # $1=Zielpfad des echten Checkouts, $2=Repo-Name
|
||||
local ziel="$1" name="$2" link="$ZIEL/$name"
|
||||
[ "$ziel" = "$link" ] && return 0
|
||||
[ -L "$link" ] && [ "$(readlink -f "$link")" = "$(readlink -f "$ziel")" ] && return 0
|
||||
[ -e "$link" ] && [ ! -L "$link" ] && { log " ! $name: $link existiert und ist keine Verknuepfung — nichts getan"; return 1; }
|
||||
[ $DRY -eq 1 ] && { log " ~ $name: wuerde verknuepfen -> $ziel"; return 0; }
|
||||
ln -sfn "$ziel" "$link" && log " ~ $name: verknuepft -> ${ziel/$HOME/\~}"
|
||||
}
|
||||
|
||||
for name in $REPOS; do
|
||||
case " $UEBERSPRINGEN " in *" $name "*) log " - $name: uebersprungen (Altlast)"; continue;; esac
|
||||
|
||||
if checkout="$(finde_checkout "$name")"; then
|
||||
case " $NIE_ZIEHEN " in
|
||||
*" $name "*)
|
||||
log " = $name: Live-Deployment, kein Pull"
|
||||
verknuepfe "$checkout" "$name"
|
||||
continue;;
|
||||
esac
|
||||
|
||||
if [ -n "$(git -C "$checkout" status --porcelain 2>/dev/null)" ]; then
|
||||
log " ! $name: lokale Aenderungen — uebersprungen (nichts ueberfahren)"
|
||||
verknuepfe "$checkout" "$name"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ $DRY -eq 1 ]; then
|
||||
log " ~ $name: wuerde pullen (${checkout/$HOME/\~})"
|
||||
else
|
||||
vorher="$(git -C "$checkout" rev-parse --short HEAD 2>/dev/null)"
|
||||
if out="$(git -C "$checkout" pull --ff-only 2>&1)"; then
|
||||
nachher="$(git -C "$checkout" rev-parse --short HEAD 2>/dev/null)"
|
||||
if [ "$vorher" = "$nachher" ]; then
|
||||
log " = $name: aktuell ($vorher)"
|
||||
else
|
||||
log " + $name: $vorher -> $nachher"
|
||||
fi
|
||||
else
|
||||
log " ! $name: Pull fehlgeschlagen — $(printf '%s' "$out" | tail -1)"
|
||||
FEHLER=1
|
||||
fi
|
||||
fi
|
||||
verknuepfe "$checkout" "$name"
|
||||
else
|
||||
if [ $DRY -eq 1 ]; then
|
||||
log " ~ $name: wuerde klonen nach ${ZIEL/$HOME/\~}/$name"
|
||||
elif out="$(git clone -q "$GITEA/Hitonabi/$name.git" "$ZIEL/$name" 2>&1)"; then
|
||||
log " + $name: neu geklont"
|
||||
else
|
||||
log " ! $name: Klonen fehlgeschlagen — $(printf '%s' "$out" | tail -1)"
|
||||
FEHLER=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
log "=== Ende (Fehler: $FEHLER) ==="
|
||||
exit $FEHLER
|
||||
+1
-4
@@ -15,11 +15,10 @@ set -euo pipefail
|
||||
|
||||
MODELS_DIR="${MC_MODELS_DIR:-/srv/models}"
|
||||
DEST_DIR="$MODELS_DIR/mc2-backups"
|
||||
MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"
|
||||
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
|
||||
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
||||
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
||||
SERVICES="mem0-service mission-control-2 hermes-gateway"
|
||||
SERVICES="mission-control-2 hermes-gateway"
|
||||
OFFSITE="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}"
|
||||
OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}"
|
||||
OFFSITE_SSH="ssh -i $OFFSITE_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"
|
||||
@@ -98,7 +97,6 @@ echo "--- Inhalt ---"; cat "$STAGE/MANIFEST.txt" 2>/dev/null || true; echo "----
|
||||
|
||||
if [ "$DRY" = 1 ]; then
|
||||
echo "[dry-run] würde zurücksetzen:"
|
||||
[ -d "$STAGE/mem0" ] && echo " • mem0 → $MEM0_DIR (wird ersetzt)"
|
||||
[ -f "$STAGE/hermes/config.yaml" ] && echo " • $HERMES/config.yaml"
|
||||
[ -f "$STAGE/hermes/.env" ] && echo " • $HERMES/.env"
|
||||
[ -d "$STAGE/hermes/plugins" ] && echo " • $HERMES/plugins/"
|
||||
@@ -119,7 +117,6 @@ bash "$SRC/deploy/backup.sh" || echo " (Pre-Restore-Backup fehlgeschlagen — f
|
||||
echo "→ Dienste stoppen…"
|
||||
systemctl --user stop $SERVICES 2>/dev/null || true
|
||||
|
||||
if [ -d "$STAGE/mem0" ]; then rm -rf "$MEM0_DIR"; mkdir -p "$MEM0_DIR"; cp -a "$STAGE/mem0/." "$MEM0_DIR/"; fi
|
||||
[ -f "$STAGE/hermes/config.yaml" ] && { mkdir -p "$HERMES"; cp -a "$STAGE/hermes/config.yaml" "$HERMES/"; }
|
||||
[ -f "$STAGE/hermes/.env" ] && cp -a "$STAGE/hermes/.env" "$HERMES/"
|
||||
[ -d "$STAGE/hermes/plugins" ] && { mkdir -p "$HERMES/plugins"; cp -a "$STAGE/hermes/plugins/." "$HERMES/plugins/"; }
|
||||
|
||||
@@ -115,7 +115,7 @@ def chunk(t,d):
|
||||
c=t+d; return struct.pack(">I",len(d))+c+struct.pack(">I",zlib.crc32(c))
|
||||
png=b"\x89PNG\r\n\x1a\n"+chunk(b"IHDR",struct.pack(">IIBBBBB",w,h,8,2,0,0,0))+chunk(b"IDAT",zlib.compress(raw))+chunk(b"IEND",b"")
|
||||
url="data:image/png;base64,"+base64.b64encode(png).decode()
|
||||
print(json.dumps({"model":"coder","stream":False,"max_tokens":40,"messages":[{"role":"user","content":[{"type":"image_url","image_url":{"url":url}},{"type":"text","text":"Welche Farbe hat das angehaengte Bild? Antworte nur mit der Farbe."}]}]}))
|
||||
print(json.dumps({"model":"coder","stream":False,"max_tokens":150,"messages":[{"role":"user","content":[{"type":"image_url","image_url":{"url":url}},{"type":"text","text":"Welche Farbe hat das angehaengte Bild? Antworte nur mit der Farbe."}]}]}))
|
||||
PYIN
|
||||
)
|
||||
BW_HDR=$(mktemp); BW_OUT=$(mktemp)
|
||||
|
||||
@@ -44,7 +44,7 @@ heavy-Rollen einer Runde am Stueck (heavy bleibt warm), DANN der Skeptiker (ein
|
||||
aus — fuer diesen Hintergrund-Job gewollt. heavy hat nur **32k Kontext** → halte das Konzept-
|
||||
Dokument kompakt und schleppe KEINE alten Runden-Protokolle mit (siehe Kontext-Uebergabe).
|
||||
- **REASONING-MODELLE brauchen Token-Luft (verifiziert 14.07.).** heavy (gpt-oss) UND der Skeptiker
|
||||
(GLM-4.7-Flash) „denken" erst ausfuehrlich (im `reasoning_content`), bevor die eigentliche Antwort
|
||||
(`fast`) „denken" erst ausfuehrlich (im `reasoning_content`), bevor die eigentliche Antwort
|
||||
in `content` landet — `worker.sh`/`fremdblick.sh` lesen NUR `content`. Zu knappes `max_tokens` =
|
||||
das Modell verbraucht alles beim Denken, `content` bleibt LEER, das Skript meldet „FEHLGESCHLAGEN"
|
||||
(obwohl das Modell lief). GLM verbrennt real ~2000 Tokens Denken fuer 2 Zeilen Antwort. Deshalb
|
||||
|
||||
@@ -60,7 +60,7 @@ Vorschlag ausweisen, was fehlt oder wacklig ist.
|
||||
NIEMALS auf `main` committen, NIEMALS mergen, NIEMALS deployen oder Dienste neu starten.
|
||||
- **Nur im Worktree arbeiten.** Die Live-Instanz `~/mission-control-v2` bleibt unberuehrt.
|
||||
- **Warm-Set-Schutz (Speicher vs Latenz).** Worker werden ueber **`worker.sh`** aufgerufen.
|
||||
Fuer die Umsetzung von Code nutzt du **`Qwen3-Coder-Next`** (laedt klein neben dem Warm-Set).
|
||||
Fuer die Umsetzung von Code nutzt du **`coder`** (laedt klein neben dem Warm-Set).
|
||||
Fuer die initiale **Planung** nutzt du explizit den Chef-Gutachter **`gpt-oss-120b`** (heavy),
|
||||
auch wenn dessen Laden (~15s) Lucys Sprech-Latenz temporaer aussetzt. Das ist fuer diesen
|
||||
Hintergrund-Prozess gewollt.
|
||||
@@ -83,7 +83,7 @@ Bevor du selbst Tasks aufteilst, befragst du zwingend **`gpt-oss-120b`** nach ei
|
||||
printf '%s' "Auftrag: $auftrag" | WORKER_MODEL=gpt-oss-120b WORKER_ROLE=plan ~/.hermes/scripts/worker.sh > /tmp/orch-plan.txt
|
||||
```
|
||||
2. Lies den Plan (`cat /tmp/orch-plan.txt`) und nutze ihn, um den Auftrag in eine **geordnete, nummerierte Liste von Teil-Tasks** zu zerlegen.
|
||||
3. Fuer jeden Teil-Task entscheide: (a) Artefakt, (b) Worker (`Qwen3-Coder-Next` + `build`/`refactor`), (c) Kontext.
|
||||
3. Fuer jeden Teil-Task entscheide: (a) Artefakt, (b) Worker (`coder` + `build`/`refactor`), (c) Kontext.
|
||||
|
||||
### 2. Worktree anlegen (Slug = kurzer Kebab-Case-Name des Auftrags)
|
||||
Worktrees liegen unter `~/.hermes/worktrees/` (NICHT /tmp — ueberlebt keinen Reboot und
|
||||
@@ -102,7 +102,7 @@ Fuer jeden Bau-Teil-Task:
|
||||
```
|
||||
printf '%s' "$eingabe" | WORKER_ROLE=build ~/.hermes/scripts/worker.sh > ~/.hermes/worktrees/orch-<slug>-work/<n>.out
|
||||
```
|
||||
(Default-Modell ist `Qwen3-Coder-Next`. Fuer einen bewusst anderen Worker `WORKER_MODEL=<echte-id>`.)
|
||||
(Default-Modell ist `coder`. Fuer einen bewusst anderen Worker `WORKER_MODEL=<rolle-oder-id>`.)
|
||||
3. **Pruefe die Ausgabe, bevor du sie verwendest:** Beginnt sie mit `FEHLT:`, hast du zu wenig
|
||||
Kontext gegeben → nachliefern und erneut rufen. Hat der Worker versehentlich einen ```-Codezaun
|
||||
oder Prosa drumherum gesetzt, entferne ihn. Dann schreibe das saubere Artefakt mit deinen
|
||||
@@ -164,13 +164,13 @@ GIT-DIFF des Worktrees:
|
||||
$(git -C ~/.hermes/worktrees/orch-<slug> diff --cached origin/main)"
|
||||
```
|
||||
Ist dieser Diff LEER, hast du nicht gestaged (oder nichts gebaut) → NICHT weiter, erst beheben.
|
||||
**Kritik A — Kompetenz** (`Qwen3-Coder-Next`, starker Coder, vom Bauen noch geladen → KEIN Swap):
|
||||
**Kritik A — Kompetenz** (`coder`, starker Coder, vom Bauen noch geladen → KEIN Swap):
|
||||
```
|
||||
printf '%s' "$EINGABE" | FREMDBLICK_MODE=code ~/.hermes/scripts/fremdblick.sh
|
||||
```
|
||||
**Kritik B — Fremd-Blick** (`GLM-4.6V-Flash`, andere Modellfamilie, andere blinde Flecken):
|
||||
**Kritik B — Fremd-Blick** (`debugger`, andere Modellfamilie, andere blinde Flecken):
|
||||
```
|
||||
printf '%s' "$EINGABE" | FREMDBLICK_MODE=code FREMDBLICK_MODEL=GLM-4.6V-Flash FREMDBLICK_MAXTOK=3000 ~/.hermes/scripts/fremdblick.sh
|
||||
printf '%s' "$EINGABE" | FREMDBLICK_MODE=code FREMDBLICK_MODEL=debugger FREMDBLICK_MAXTOK=3000 ~/.hermes/scripts/fremdblick.sh
|
||||
```
|
||||
Reihenfolge bewusst: erst A (Coder-Next ist noch warm), dann B (ein Swap zu GLM). Beide beginnen mit
|
||||
`URTEIL: <ABGELEHNT | FREIGABE-MIT-VORBEHALT | FREIGABE>`. **Warte auf beide.**
|
||||
|
||||
@@ -80,7 +80,7 @@ unangenommene Branches aufbauen — erst wenn die Vor-Etappe in main ist, kommt
|
||||
<git -C ~/.hermes/worktrees/wartung-<slug> diff origin/main>" | FREMDBLICK_MODE=code ~/.hermes/scripts/fremdblick.sh
|
||||
```
|
||||
|
||||
Das läuft auf `Qwen3-Coder-Next` (128k, code-stark, anderes Modell als der Qwen3.6-Worker,
|
||||
Das läuft auf `coder` (128k, code-stark, anderes Modell als der Qwen3.6-Worker,
|
||||
lädt klein neben dem Warm-Set) mit einem Code-Review-Raster. **Warte auf die Ausgabe.** Der
|
||||
Kritiker leitet den Fehlerfall SELBST aus der Bug-Beschreibung ab (glaubt dem Autor nicht),
|
||||
spielt den geänderten Code durch und beginnt mit `URTEIL: <ABGELEHNT | FREIGABE-MIT-VORBEHALT
|
||||
|
||||
@@ -3,15 +3,14 @@
|
||||
# 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
|
||||
# Pendant zu hermes-postcheck.sh (prüft das Gehirn); 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
|
||||
EMBED="${MC_WARMUP_EMBED:-embed}"; EMBED="${EMBED%% *}" # Embedding-Modell (Mem0/Gedächtnis)
|
||||
EMBED="${MC_WARMUP_EMBED:-embed}"; EMBED="${EMBED%% *}" # Embedding-Modell (Gedächtnis)
|
||||
fail=0
|
||||
|
||||
echo "=== Stack Post-Update: Funktionsprüfung ==="
|
||||
@@ -48,14 +47,14 @@ else
|
||||
echo "FAIL · Inferenz auf '$BRAIN' fehlgeschlagen (Modell lädt/generiert nicht)"; fail=1
|
||||
fi
|
||||
|
||||
# 3b. Embedding-Modell (für Mem0/Gedächtnis) lädt und liefert einen Vektor?
|
||||
# 3b. Embedding-Modell (für Gedächtnis) lädt und liefert einen Vektor?
|
||||
eresp="$(curl -s -m 120 -X POST "$SWAP_URL/v1/embeddings" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"model\":\"$EMBED\",\"input\":\"ping\"}" 2>/dev/null)"
|
||||
if printf '%s' "$eresp" | grep -q '"embedding"'; then
|
||||
echo "PASS · Embedding-Modell '$EMBED' liefert Vektoren"
|
||||
else
|
||||
echo "FAIL · Embedding-Modell '$EMBED' lädt/antwortet nicht (Mem0/Gedächtnis betroffen)"; fail=1
|
||||
echo "FAIL · Embedding-Modell '$EMBED' lädt/antwortet nicht (Gedächtnis betroffen)"; fail=1
|
||||
fi
|
||||
|
||||
# 4. MC2 selbst gesund (Engine + Gateway erreichbar)?
|
||||
@@ -85,13 +84,6 @@ if systemctl --user is-enabled --quiet mc2-steward 2>/dev/null; then
|
||||
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"
|
||||
else
|
||||
echo "FAIL · Mem0-Sidecar NICHT erreichbar"; fail=1
|
||||
fi
|
||||
|
||||
if [ "$fail" -eq 0 ]; then
|
||||
echo "=== STACK OK ✓ ==="
|
||||
else
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Venv-Audit (monatlich, 17.07.2026): die Python-Nebendienste (Mem0, Voice) altern
|
||||
# Venv-Audit (monatlich, 17.07.2026): die Python-Nebendienste (Voice) altern
|
||||
# eingefroren — kein Auto-Update-Kanal deckt sie ab. Dieser Audit MELDET nur
|
||||
# (bewusst kein Auto-Update: Python-Deps brechen gern):
|
||||
# je venv: veraltete Pakete zählen (pip list --outdated) + bekannte
|
||||
@@ -9,7 +9,7 @@
|
||||
# idempotent je Monat). Läuft als Hermes-Cron --no-agent (stdout = Telegram).
|
||||
set -uo pipefail
|
||||
|
||||
VENVS=("$HOME/.mem0/venv:mem0" "$HOME/.voice/venv:voice")
|
||||
VENVS=("$HOME/.voice/venv:voice")
|
||||
AUDIT_VENV="$HOME/.venv-audit"
|
||||
HERMES="$HOME/.local/bin/hermes"
|
||||
MONAT="$(date '+%Y-%m')"
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
# Eingehängt als ExecStartPost=-/usr/local/bin/llama-swap-warmup.sh in llama-swap.
|
||||
#
|
||||
# `fast` = das Agent-Hirn (Qwen3.6-35B-A3B), `embed` = das Embedding-Modell
|
||||
# (Qwen3-Embedding-0.6B, für Mem0/Gedächtnis). Die Augen (vision/VL-30B) sind seit 07.07.
|
||||
# (Qwen3-Embedding-0.6B, für Gedächtnis). Die Augen (vision/VL-30B) sind seit 07.07.
|
||||
# ON-DEMAND (User-Entscheid) und gehören NICHT mehr ins Warm-Set — die Lucy-App wärmt sie
|
||||
# beim Start selbst an. Modelle werden NICHT automatisch zusammen geladen — jedes einzeln
|
||||
# anstoßen, sonst bleibt es kalt.
|
||||
|
||||
+4
-4
@@ -6,7 +6,7 @@
|
||||
# neben dem Warm-Set liegt bewusst in DEINER Hand.
|
||||
#
|
||||
# WICHTIG (Speicher & Latenz-Trade-off):
|
||||
# Der Standard-Worker ist Qwen3-Coder-Next (laedt klein und schnell neben dem Warm-Set).
|
||||
# Der Standard-Worker ist die Rolle `coder` (laedt klein und schnell neben dem Warm-Set).
|
||||
# Fuer initiale Planung kann "gpt-oss-120b" gerufen werden. Das Modell passt knapp in den
|
||||
# Speicher (124 GB RAM), aber das on-demand Laden unterbricht Lucys 1-s-Latenz fuer ca.
|
||||
# 15-30 Sekunden. Dieser Latenz-Hit wird fuer die Orchestrator-Planung in Kauf genommen.
|
||||
@@ -16,10 +16,10 @@
|
||||
# in Dateien macht der Manager (Hermes) danach mit seinen eigenen Tools.
|
||||
#
|
||||
# Nutzung: printf '%s' "$teiltask_mit_kontext" | WORKER_ROLE=build worker.sh > /tmp/out.py
|
||||
# printf '%s' "$spec" | WORKER_MODEL=Qwen3-Coder-Next WORKER_ROLE=build worker.sh
|
||||
# printf '%s' "$spec" | WORKER_MODEL=coder WORKER_ROLE=build worker.sh
|
||||
#
|
||||
# Env-Overrides:
|
||||
# WORKER_MODEL echte llama-swap-ID (Default: Qwen3-Coder-Next). NICHT gpt-oss/heavy (siehe oben).
|
||||
# WORKER_MODEL Rollen-Alias oder echte ID (Default: coder). NICHT gpt-oss/heavy (siehe oben).
|
||||
# WORKER_ROLE build (Default) | refactor | plan | prose — waehlt das System-Raster.
|
||||
# WORKER_SYS eigenes System-Raster (uebersteuert WORKER_ROLE).
|
||||
# WORKER_MAXTOK max_tokens (Default 4000 — Code braucht Platz).
|
||||
@@ -29,7 +29,7 @@ set -uo pipefail
|
||||
# Achtung: llama-swap listet unter /v1/models die ECHTEN Modell-IDs, nicht die Gateway-Aliase
|
||||
# (kein "heavy"/"coder"/"scout" hier — die echte ID nehmen).
|
||||
ENDPOINT="${WORKER_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
|
||||
MODEL="${WORKER_MODEL:-Qwen3-Coder-Next}"
|
||||
MODEL="${WORKER_MODEL:-coder}"
|
||||
ROLE="${WORKER_ROLE:-build}"
|
||||
MAXTOK="${WORKER_MAXTOK:-4000}"
|
||||
TEMP="${WORKER_TEMP:-0.1}"
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# Referenz-Check (20.08.2026): prueft die fuenf Fertig-Kriterien der Messlatte.
|
||||
#
|
||||
# Zweck: "fertig" darf keine Auslegungssache sein. Derselbe Befehl fuer den Agenten
|
||||
# (Selbstpruefung) und fuer die Auswertung. Siehe docs/aufgaben/referenzaufgabe-mem0-ausbau.md
|
||||
#
|
||||
# Aufruf: bash docs/aufgaben/referenz-check.sh [worktree-pfad]
|
||||
# Exit: 0 = alle Kriterien gruen · 1 = mindestens eines rot
|
||||
set -uo pipefail
|
||||
|
||||
W="${1:-$HOME/projekte/mc2-referenz}"
|
||||
LINT="${LINT_RUFF:-$HOME/.venv-lint/bin/ruff}"
|
||||
PY="${BACKEND_PY:-$HOME/mission-control-v2/backend/.venv/bin/python}"
|
||||
SMOKE="${SMOKE_SH:-$HOME/mission-control-v2/deploy/self-smoke.sh}"
|
||||
ROT=0
|
||||
|
||||
pruef() { # $1=Nummer $2=Beschreibung $3=Ergebnis(0/1) $4=Detail
|
||||
if [ "$3" -eq 0 ]; then printf ' %s %-34s GRUEN %s\n' "$1" "$2" "${4:-}"
|
||||
else printf ' %s %-34s ROT %s\n' "$1" "$2" "${4:-}"; ROT=1; fi
|
||||
}
|
||||
|
||||
[ -d "$W" ] || { echo "ABBRUCH: Worktree nicht gefunden: $W"; exit 1; }
|
||||
echo "=== Referenz-Check ($W) ==="
|
||||
|
||||
# 1 — Lint
|
||||
if [ -x "$LINT" ]; then
|
||||
out="$("$LINT" check "$W" 2>&1)"; rc=$?
|
||||
pruef 1 "Lint sauber (ruff)" $rc "$(printf '%s' "$out" | tail -1)"
|
||||
else
|
||||
pruef 1 "Lint sauber (ruff)" 1 "ruff fehlt unter $LINT"
|
||||
fi
|
||||
|
||||
# 2 — Backend laedt
|
||||
out="$(cd "$W/backend" && "$PY" -c 'import app' 2>&1)"; rc=$?
|
||||
pruef 2 "Backend importierbar (app)" $rc "$(printf '%s' "$out" | tail -1)"
|
||||
|
||||
# 3 — keine mem0-Reste (docs/ ausgenommen: Historie)
|
||||
n="$(cd "$W" && grep -ril mem0 backend/ mcp/ deploy/ --include='*.py' --include='*.sh' 2>/dev/null | wc -l)"
|
||||
[ "$n" -eq 0 ]; rc=$?
|
||||
pruef 3 "Keine mem0-Reste" $rc "$n Datei(en) mit Treffern"
|
||||
[ "$n" -gt 0 ] && (cd "$W" && grep -ril mem0 backend/ mcp/ deploy/ --include='*.py' --include='*.sh' 2>/dev/null | sed 's/^/ /')
|
||||
|
||||
# 3b — der Sidecar-Ordner selbst
|
||||
if [ -d "$W/mem0_service" ]; then pruef "3b" "Ordner mem0_service entfernt" 1 "liegt noch da"
|
||||
else pruef "3b" "Ordner mem0_service entfernt" 0; fi
|
||||
|
||||
# 4 — Selbsttest (laeuft gegen das LAUFENDE System, nicht gegen den Worktree)
|
||||
if [ -r "$SMOKE" ]; then
|
||||
out="$(timeout 540 bash "$SMOKE" 2>&1)"; rc=$?
|
||||
pruef 4 "Selbsttest self-smoke" $rc "$(printf '%s' "$out" | grep -cE '\bPASS\b') PASS / $(printf '%s' "$out" | grep -cE '\bFAIL\b') FAIL"
|
||||
else
|
||||
pruef 4 "Selbsttest self-smoke" 1 "nicht lesbar: $SMOKE"
|
||||
fi
|
||||
|
||||
# 5 — MC2 lebt
|
||||
curl -s -m 8 localhost:9001/api/health 2>/dev/null | grep -q '"status":"ok"'; rc=$?
|
||||
pruef 5 "MC2 gesund" $rc
|
||||
|
||||
echo
|
||||
if [ $ROT -eq 0 ]; then echo " ERGEBNIS: FERTIG — alle Kriterien gruen."
|
||||
else echo " ERGEBNIS: NICHT fertig — mindestens ein Kriterium ist rot."; fi
|
||||
exit $ROT
|
||||
@@ -0,0 +1,133 @@
|
||||
# Referenzaufgabe: Mem0-Sidecar restlos ausbauen
|
||||
|
||||
> **Zweck dieser Datei:** Messlatte für den Stack-Umbau (Stufe 2, 20.08.2026). Dieselbe Aufgabe wird
|
||||
> mehrfach mit verschiedenen Modellen gefahren. Gemessen wird **nicht** Tokens pro Sekunde, sondern:
|
||||
> *Wurde sie fertig? Wie lange? Wie viele Züge?*
|
||||
> Alle Pfade und Befehle unten sind am 20.08.2026 auf der Box **nachgemessen**, nicht geraten.
|
||||
|
||||
## Hintergrund
|
||||
|
||||
Mem0 wurde im August abgelöst (`bf145a3`, `eb4bcfc`, `2286879`). Der Rückbau blieb unvollständig:
|
||||
Der Sidecar-Ordner liegt noch im Baum, MC2 fragt weiterhin einen toten Port ab.
|
||||
|
||||
Gemessen: `curl http://127.0.0.1:8765/health` → **keine Antwort**. Kein systemd-Dienst dafür,
|
||||
`/srv/models/mem0` bereits entfernt. Es ist also wirklich tot, nicht nur gerade aus.
|
||||
|
||||
## Arbeitsplatz
|
||||
|
||||
**Isolierter Worktree — NICHT das Live-Deployment:**
|
||||
|
||||
```
|
||||
~/projekte/mc2-referenz Zweig: referenz/mem0-ausbau
|
||||
```
|
||||
|
||||
`~/mission-control-v2` bleibt auf `main` und bedient weiter den laufenden Dienst auf `:9001`.
|
||||
**Dort nichts ändern.**
|
||||
|
||||
## ‼️ Arbeitsweise — zuerst lesen
|
||||
|
||||
**Arbeite Datei für Datei. Lies HÖCHSTENS zwei Dateien, bevor du die erste änderst.**
|
||||
|
||||
Nach jeder geänderten Datei gehst du zur nächsten. **Kein Gesamtplan, keine Vollinventur vorab** — du
|
||||
darfst jederzeit nachlesen, wenn du etwas brauchst. Die Liste unten ist eine Landkarte, keine
|
||||
Leseliste.
|
||||
|
||||
*Warum das hier so deutlich steht:* Im ersten Anlauf (20.08., über Nacht) hat der Agent in sechs
|
||||
Sitzungen ~336 Nachrichten und ~360.000 Tokens verbraucht, 19 Dateien gelesen — und **nicht eine
|
||||
einzige Zeile geändert**. Er lief fünfmal in dasselbe Muster: „Ich habe jetzt das vollständige Bild,
|
||||
jetzt lese ich nur noch die restlichen Dateien, *bevor ich anfange*." Dann war das Zug-Limit
|
||||
erreicht. Sein eigenes Fazit: *„Iterationslimit erreicht, bevor ich die erste Zeile geändert habe."*
|
||||
|
||||
Eine halbfertige Änderung ist wertvoller als eine vollständige Analyse ohne Änderung.
|
||||
|
||||
## Auftrag
|
||||
|
||||
Entferne die Mem0-Reste vollständig, ohne etwas anderes kaputtzumachen.
|
||||
|
||||
**17 Dateien haben Treffer** (gemessen 20.08., selbst nachprüfen statt blind vertrauen):
|
||||
|
||||
```
|
||||
backend/steward.py deploy/stack-postcheck.sh
|
||||
backend/config.py deploy/venv-audit.sh
|
||||
backend/routers/voice.py deploy/agent-hooks/box-steckbrief-inject.sh
|
||||
backend/routers/system.py deploy/warmup.sh
|
||||
backend/routers/zeitmaschine.py deploy/autoupdate.sh
|
||||
backend/services/sentry.py deploy/hermes-postcheck.sh
|
||||
backend/services/voice_metrics.py deploy/restore.sh
|
||||
backend/services/backup.py deploy/backup.sh
|
||||
mcp/mcp_mc.py
|
||||
```
|
||||
|
||||
Dazu der Ordner `mem0_service/` (app.py, migrate.py, requirements.txt, smoke_test.py).
|
||||
|
||||
> ‼️ **Der eigentliche Prüfstein:** Wenn eine API-Antwort ein Feld wie `mem0_reachable` liefert, das
|
||||
> das Frontend liest, darf es nicht einfach verschwinden — sonst bricht die Oberfläche. Entweder das
|
||||
> Frontend mit anpassen oder das Feld sauber entfernen. Wer nur `grep mem0` laufen lässt und alles
|
||||
> wegwirft, produziert eine Fassade.
|
||||
|
||||
## Fertig-Kriterien
|
||||
|
||||
Ausgangswert am 20.08.: **1, 2, 4, 5 sind bereits grün** — sie sind Schutzgeländer, nicht die Arbeit.
|
||||
Nur Kriterium 3 ist offen. Wer 3 erfüllt und dabei 1/2/4/5 kaputtmacht, hat versagt.
|
||||
|
||||
| # | Prüfung | Befehl | Ausgangswert |
|
||||
|---|---|---|---|
|
||||
| 1 | Lint sauber | `cd ~/projekte/mc2-referenz && ~/.venv-lint/bin/ruff check .` | ✅ *All checks passed* |
|
||||
| 2 | Backend lädt | `cd ~/projekte/mc2-referenz/backend && ~/mission-control-v2/backend/.venv/bin/python -c "import app"` | ✅ ok |
|
||||
| 3 | **keine mem0-Reste** | `cd ~/projekte/mc2-referenz && grep -ril mem0 backend/ mcp/ deploy/ --include='*.py' --include='*.sh' \| wc -l` | ❌ **17** → muss **0** werden |
|
||||
| 4 | Selbsttest grün | `bash ~/mission-control-v2/deploy/self-smoke.sh` | ✅ grün |
|
||||
| 5 | MC2 gesund | `curl -s localhost:9001/api/health` | ✅ `"status":"ok"` |
|
||||
|
||||
Treffer in `docs/` sind erlaubt — das ist Historie und darf stehenbleiben.
|
||||
|
||||
Selbstprüfung mit einem Befehl:
|
||||
|
||||
```bash
|
||||
bash ~/projekte/mc2-referenz/docs/aufgaben/referenz-check.sh
|
||||
```
|
||||
|
||||
> Der Prüfbefehl liegt bewusst unter `docs/aufgaben/` und **nicht** unter `deploy/`. Im ersten Anlauf
|
||||
> lag er in `deploy/`, enthielt selbst siebenmal das Wort „mem0" (er sucht ja danach) und zählte sich
|
||||
> damit in Kriterium 3 mit — das Kriterium war unerfüllbar. In `docs/` wird er nicht durchsucht.
|
||||
|
||||
## Randbedingungen
|
||||
|
||||
- ‼️ **In einer FRISCHEN Sitzung starten**, nicht in einer fortgesetzten. Beim ersten Anlauf hat der
|
||||
Desktop eine Sitzung vom Vortag weitergeführt und deren Kontext mitgeschleppt — die Messung war
|
||||
dadurch wertlos. Im Desktop also einen **neuen Chat** anlegen, nicht den alten öffnen.
|
||||
- **Nicht pushen.** Der Zweig bleibt lokal, damit der Lauf wiederholbar ist.
|
||||
- **Nichts außerhalb des Worktrees anfassen** — kein `/etc`, keine systemd-Dienste, kein Neustart.
|
||||
- Wenn ein Kriterium nicht erfüllbar ist: **ehrlich sagen und aufhören.** Ein klares
|
||||
„Kriterium 3 scheitert an X" ist wertvoller als ein beschönigter Abschluss.
|
||||
|
||||
## Zurücksetzen zwischen den Läufen
|
||||
|
||||
```bash
|
||||
cd ~/mission-control-v2
|
||||
git worktree remove --force ~/projekte/mc2-referenz
|
||||
git branch -D referenz/mem0-ausbau
|
||||
git worktree add -b referenz/mem0-ausbau ~/projekte/mc2-referenz main
|
||||
```
|
||||
|
||||
## Messprotokoll
|
||||
|
||||
| Lauf | Aufbau | Fertig? | Wanduhr | Züge | Bemerkung |
|
||||
|---|---|---|---|---|---|
|
||||
| — | *Fehlversuch 20.08. 21:10* | ✗ | 76 min, abgebrochen | 8 | Kriterium 3 unerfüllbar + fortgesetzte Alt-Sitzung. **Verwertbar trotzdem:** 19.899 Ausgabe-Tokens, davon nur ~2.600 Zeichen sichtbar → **>95 % unsichtbares Nachdenken**. Kompression feuerte bereits. |
|
||||
| **A** | Qwen3.8-27B, `medium`, `max_turns: 40`, ohne Arbeitsanweisung | ✗ | über Nacht | 6 Sitzungen, ~336 Nachrichten | **0 Dateien geschrieben.** Analyse-Schleife, Zug-Limit erreicht. Werkzeug `file` war die ganze Zeit verfügbar, wurde nie benutzt. ~360k Tokens. Inhaltlich gut: fand `mem0_ms` im Frontend **und** einen toten `memory_svc`-Import in `steward.py`. |
|
||||
| **A2** | dasselbe, **+ Arbeitsanweisung oben**, `max_turns: 80` | | | | ← **als Nächstes.** Prüft, ob die Schleife am Auftrag lag |
|
||||
| D | Qwen3.8-27B, `reasoning_effort: low` | | | | Nächster Hebel, falls A2 auch nur liest |
|
||||
| B | Qwen3.8-27B **+ DFlash-2** | | | | Entwurfsmodell liegt bereit |
|
||||
| C | Qwen3.6-35B-A3B (`fast`, 95,7 t/s) | | | | SWE-bench 73,4 |
|
||||
|
||||
> ★★ **Die Lehre aus Lauf A:** Das ist **kein Tempo-Problem.** Mit 95 t/s statt 12,6 wäre derselbe
|
||||
> Lauf genauso gescheitert — nur schneller. Deshalb kommen A2 und D **vor** den Modellwechseln B und C.
|
||||
|
||||
### Was der Fehlversuch schon gezeigt hat
|
||||
|
||||
Pro Zug erzeugte das Modell ~2.487 Tokens, sichtbar wurden davon 181–451 Zeichen. Bei 12,6 t/s sind
|
||||
das ~26 Minuten reine Erzeugung für ~2.600 Zeichen Text. **Der Engpass ist Denk-Aufwand × Bandbreite,
|
||||
nicht das Harness.** Darum Lauf D vor B und C: er prüft den billigsten Hebel zuerst.
|
||||
|
||||
Inhaltlich war das Modell gut — es fand von selbst den Prüfstein (`mem0_ms` → `memory_ms` im Frontend),
|
||||
statt blind zu löschen.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as A,I as P,f as M,k as B,u as D,r as z,j as e,J as T,b as a,K as g,M as b,N as W,t as f,W as j,O as G,X as I,C as N,n as y,q as i}from"./index-DZ1YxYm3.js";/**
|
||||
import{c as A,I as P,f as M,k as B,u as D,r as z,j as e,J as T,b as a,K as g,M as b,N as W,t as f,W as j,O as G,X as I,C as N,n as y,q as i}from"./index-0Xcu6PnS.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as W,a1 as H,a2 as F,u as G,k as T,r as b,j as e,R as _,b as y,L as m,C as E,_ as O,D as P,a0 as z,a3 as R,a4 as V,X as D,T as C,F as K,n as $,a5 as q,q as U}from"./index-DZ1YxYm3.js";import{S,H as Q}from"./SectionLabel-S9xUYRA9.js";import{C as M}from"./chevron-down-DfQ2ivW0.js";/**
|
||||
import{c as W,a1 as H,a2 as F,u as G,k as T,r as b,j as e,R as _,b as y,L as m,C as E,_ as O,D as P,a0 as z,a3 as R,a4 as V,X as D,T as C,F as K,n as $,a5 as q,q as U}from"./index-0Xcu6PnS.js";import{S,H as Q}from"./SectionLabel-CLJNaK00.js";import{C as M}from"./chevron-down-Bak281TY.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as d,aj as S,r as h,j as e,ak as M,L as p,a0 as z,a5 as C,al as D,am as A,b as B,an as Z,u as E,k as L,n as k,q}from"./index-DZ1YxYm3.js";import{R as j}from"./rotate-ccw-ZeFYj9aL.js";/**
|
||||
import{c as d,aj as S,r as h,j as e,ak as M,L as p,a0 as z,a5 as C,al as D,am as A,b as B,an as Z,u as E,k as L,n as k,q}from"./index-0Xcu6PnS.js";import{R as j}from"./rotate-ccw-CBM-YWkV.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as x,r as c,z as D,A as I,j as e,D as f,t as z,B,F as H,b as M,L as E,G as T,C as F}from"./index-DZ1YxYm3.js";import{C as K}from"./chevron-down-DfQ2ivW0.js";import{F as O}from"./folder-open-2CxxqRqr.js";/**
|
||||
import{c as x,r as c,z as D,A as I,j as e,D as f,t as z,B,F as H,b as M,L as E,G as T,C as F}from"./index-0Xcu6PnS.js";import{C as K}from"./chevron-down-Bak281TY.js";import{F as O}from"./folder-open-DbWikJy5.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
-36
File diff suppressed because one or more lines are too long
+36
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as $,j as e,b as S,a2 as ve,u as Se,k as De,r as d,n as z,a6 as Ke,_ as O,L as D,X as Q,a7 as xe,F as T,a3 as he,C as Ce,T as ze,a4 as Ee,a8 as pe,q as Ae,R as fe}from"./index-DZ1YxYm3.js";import{S as Le,H as X}from"./SectionLabel-S9xUYRA9.js";import{C as ee}from"./code-xml-C3wJsmdC.js";import{L as Pe}from"./layers-svfcdzxU.js";import{C as U}from"./chevron-down-DfQ2ivW0.js";import{R as We}from"./rotate-ccw-ZeFYj9aL.js";/**
|
||||
import{c as $,j as e,b as S,a2 as ve,u as Se,k as De,r as d,n as z,a6 as Ke,_ as O,L as D,X as Q,a7 as xe,F as T,a3 as he,C as Ce,T as ze,a4 as Ee,a8 as pe,q as Ae,R as fe}from"./index-0Xcu6PnS.js";import{S as Le,H as X}from"./SectionLabel-CLJNaK00.js";import{C as ee}from"./code-xml-BeQlEZLv.js";import{L as Pe}from"./layers-D3tFEnOl.js";import{C as U}from"./chevron-down-Bak281TY.js";import{R as We}from"./rotate-ccw-CBM-YWkV.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1 +1 @@
|
||||
import{u,ah as m,k as x,j as s,y as n,ai as b,b as p,n as f,q as h}from"./index-DZ1YxYm3.js";function y(){const d=u(),{data:t=[]}=m(),{showAlert:l,dialogElement:o}=x();async function i(e){try{await f(`/api/jobs/${e}/cancel`,{method:"POST"}),d.invalidateQueries({queryKey:h.jobs})}catch(c){l("Fehler",c.message)}}const a=t.filter(e=>e.state==="running"||e.state==="queued"),r=t.filter(e=>e.state!=="running"&&e.state!=="queued").slice(-3);return a.length===0&&r.length===0?null:s.jsxs("div",{className:"space-y-3 mc-card p-4",children:[s.jsx("div",{className:"text-[11px] font-semibold text-muted-foreground uppercase tracking-wider",children:"Aktive Downloads"}),a.map(e=>s.jsxs("div",{className:"space-y-1.5 p-3 rounded-xl bg-background/20 border border-border/40",children:[s.jsxs("div",{className:"flex justify-between items-center text-xs",children:[s.jsx("span",{className:"font-semibold truncate max-w-[250px]",children:e.label}),s.jsxs("div",{className:"flex items-center gap-3",children:[s.jsxs("span",{className:"text-muted-foreground font-mono",children:[e.progress??0,"% • ",n(e.done_bytes),"/",n(e.total_bytes),e.eta_s?` • ETA ${b(e.eta_s)}`:""]}),s.jsx("button",{onClick:()=>i(e.id),className:"text-[10px] text-red-400 hover:text-red-300 font-semibold border border-red-500/25 bg-red-500/5 px-2 py-0.5 rounded transition-all cursor-pointer",children:"Abbrechen"})]})]}),s.jsx("div",{className:"h-1.5 overflow-hidden rounded-full bg-muted",children:s.jsx("div",{className:"h-full rounded-full bg-primary transition-all duration-500",style:{width:`${e.progress??0}%`}})})]},e.id)),r.map(e=>s.jsxs("div",{className:"flex justify-between items-center text-xs text-muted-foreground px-1",children:[s.jsx("span",{className:"truncate",children:e.label}),s.jsx("span",{className:p("font-semibold text-[10px] px-1.5 py-0.5 rounded uppercase font-mono",e.state==="done"?"bg-emerald-500/10 text-emerald-400":"bg-amber-500/10 text-amber-400"),children:e.state})]},e.id)),o]})}export{y as J};
|
||||
import{u,ah as m,k as x,j as s,y as n,ai as b,b as p,n as f,q as h}from"./index-0Xcu6PnS.js";function y(){const d=u(),{data:t=[]}=m(),{showAlert:l,dialogElement:o}=x();async function i(e){try{await f(`/api/jobs/${e}/cancel`,{method:"POST"}),d.invalidateQueries({queryKey:h.jobs})}catch(c){l("Fehler",c.message)}}const a=t.filter(e=>e.state==="running"||e.state==="queued"),r=t.filter(e=>e.state!=="running"&&e.state!=="queued").slice(-3);return a.length===0&&r.length===0?null:s.jsxs("div",{className:"space-y-3 mc-card p-4",children:[s.jsx("div",{className:"text-[11px] font-semibold text-muted-foreground uppercase tracking-wider",children:"Aktive Downloads"}),a.map(e=>s.jsxs("div",{className:"space-y-1.5 p-3 rounded-xl bg-background/20 border border-border/40",children:[s.jsxs("div",{className:"flex justify-between items-center text-xs",children:[s.jsx("span",{className:"font-semibold truncate max-w-[250px]",children:e.label}),s.jsxs("div",{className:"flex items-center gap-3",children:[s.jsxs("span",{className:"text-muted-foreground font-mono",children:[e.progress??0,"% • ",n(e.done_bytes),"/",n(e.total_bytes),e.eta_s?` • ETA ${b(e.eta_s)}`:""]}),s.jsx("button",{onClick:()=>i(e.id),className:"text-[10px] text-red-400 hover:text-red-300 font-semibold border border-red-500/25 bg-red-500/5 px-2 py-0.5 rounded transition-all cursor-pointer",children:"Abbrechen"})]})]}),s.jsx("div",{className:"h-1.5 overflow-hidden rounded-full bg-muted",children:s.jsx("div",{className:"h-full rounded-full bg-primary transition-all duration-500",style:{width:`${e.progress??0}%`}})})]},e.id)),r.map(e=>s.jsxs("div",{className:"flex justify-between items-center text-xs text-muted-foreground px-1",children:[s.jsx("span",{className:"truncate",children:e.label}),s.jsx("span",{className:p("font-semibold text-[10px] px-1.5 py-0.5 rounded uppercase font-mono",e.state==="done"?"bg-emerald-500/10 text-emerald-400":"bg-amber-500/10 text-amber-400"),children:e.state})]},e.id)),o]})}export{y as J};
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
import{I as b,u as f,K as h,r as d,j as e,b as c,J as g,T as p,R as j,Q as N,n as v,U as w,q as x}from"./index-DZ1YxYm3.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[a===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(p,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:u,disabled:l,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{y as KonsoleView};
|
||||
import{I as b,u as f,K as h,r as d,j as e,b as c,J as g,T as p,R as j,Q as N,n as v,U as w,q as x}from"./index-0Xcu6PnS.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[a===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(p,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:u,disabled:l,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{y as KonsoleView};
|
||||
+7
-7
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as s,j as e}from"./index-DZ1YxYm3.js";/**
|
||||
import{c as s,j as e}from"./index-0Xcu6PnS.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
var M=r=>{throw TypeError(r)};var O=(r,e,t)=>e.has(r)||M("Cannot "+t);var a=(r,e,t)=>(O(r,e,"read from private field"),t?t.call(r):e.get(r)),y=(r,e,t)=>e.has(r)?M("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),v=(r,e,t,n)=>(O(r,e,"write to private field"),n?n.call(r,t):e.set(r,t),t),j=(r,e,t)=>(O(r,e,"access private method"),t);import{a9 as B,aa as F,ab as E,ac as I,ad as P,u as A,r as f,ae as J,af as Q,c as T,ag as U,j as s,G as D,T as H,b as K,L,M as G,q as V,n as R}from"./index-DZ1YxYm3.js";import{J as _}from"./JobsBar-DQS6cTPH.js";var c,g,o,h,m,w,C,q,z=(q=class extends B{constructor(e,t){super();y(this,m);y(this,c);y(this,g);y(this,o);y(this,h);v(this,c,e),this.setOptions(t),this.bindMethods(),j(this,m,w).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var n;const t=this.options;this.options=a(this,c).defaultMutationOptions(e),F(this.options,t)||a(this,c).getMutationCache().notify({type:"observerOptionsUpdated",mutation:a(this,o),observer:this}),t!=null&&t.mutationKey&&this.options.mutationKey&&E(t.mutationKey)!==E(this.options.mutationKey)?this.reset():((n=a(this,o))==null?void 0:n.state.status)==="pending"&&a(this,o).setOptions(this.options)}onUnsubscribe(){var e;this.hasListeners()||(e=a(this,o))==null||e.removeObserver(this)}onMutationUpdate(e){j(this,m,w).call(this),j(this,m,C).call(this,e)}getCurrentResult(){return a(this,g)}reset(){var e;(e=a(this,o))==null||e.removeObserver(this),v(this,o,void 0),j(this,m,w).call(this),j(this,m,C).call(this)}mutate(e,t){var n;return v(this,h,t),(n=a(this,o))==null||n.removeObserver(this),v(this,o,a(this,c).getMutationCache().build(a(this,c),this.options)),a(this,o).addObserver(this),a(this,o).execute(e)}},c=new WeakMap,g=new WeakMap,o=new WeakMap,h=new WeakMap,m=new WeakSet,w=function(){var t;const e=((t=a(this,o))==null?void 0:t.state)??I();v(this,g,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},C=function(e){P.batch(()=>{var t,n,l,x,d,p,k,i;if(a(this,h)&&this.hasListeners()){const u=a(this,g).variables,N=a(this,g).context,S={client:a(this,c),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(n=(t=a(this,h)).onSuccess)==null||n.call(t,e.data,u,N,S)}catch(b){Promise.reject(b)}try{(x=(l=a(this,h)).onSettled)==null||x.call(l,e.data,null,u,N,S)}catch(b){Promise.reject(b)}}else if((e==null?void 0:e.type)==="error"){try{(p=(d=a(this,h)).onError)==null||p.call(d,e.error,u,N,S)}catch(b){Promise.reject(b)}try{(i=(k=a(this,h)).onSettled)==null||i.call(k,void 0,e.error,u,N,S)}catch(b){Promise.reject(b)}}}this.listeners.forEach(u=>{u(a(this,g))})})},q);function W(r,e){const t=A(),[n]=f.useState(()=>new z(t,r));f.useEffect(()=>{n.setOptions(r)},[n,r]);const l=f.useSyncExternalStore(f.useCallback(d=>n.subscribe(P.batchCalls(d)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),x=f.useCallback((d,p)=>{n.mutate(d,p).catch(J)},[n]);if(l.error&&Q(n.options.throwOnError,[l.error]))throw l.error;return{...l,mutate:x,mutateAsync:l.mutate}}/**
|
||||
var M=r=>{throw TypeError(r)};var O=(r,e,t)=>e.has(r)||M("Cannot "+t);var a=(r,e,t)=>(O(r,e,"read from private field"),t?t.call(r):e.get(r)),y=(r,e,t)=>e.has(r)?M("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),v=(r,e,t,n)=>(O(r,e,"write to private field"),n?n.call(r,t):e.set(r,t),t),j=(r,e,t)=>(O(r,e,"access private method"),t);import{a9 as B,aa as F,ab as E,ac as I,ad as P,u as A,r as f,ae as J,af as Q,c as T,ag as U,j as s,G as D,T as H,b as K,L,M as G,q as V,n as R}from"./index-0Xcu6PnS.js";import{J as _}from"./JobsBar-JgL4-cSI.js";var c,g,o,h,m,w,C,q,z=(q=class extends B{constructor(e,t){super();y(this,m);y(this,c);y(this,g);y(this,o);y(this,h);v(this,c,e),this.setOptions(t),this.bindMethods(),j(this,m,w).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var n;const t=this.options;this.options=a(this,c).defaultMutationOptions(e),F(this.options,t)||a(this,c).getMutationCache().notify({type:"observerOptionsUpdated",mutation:a(this,o),observer:this}),t!=null&&t.mutationKey&&this.options.mutationKey&&E(t.mutationKey)!==E(this.options.mutationKey)?this.reset():((n=a(this,o))==null?void 0:n.state.status)==="pending"&&a(this,o).setOptions(this.options)}onUnsubscribe(){var e;this.hasListeners()||(e=a(this,o))==null||e.removeObserver(this)}onMutationUpdate(e){j(this,m,w).call(this),j(this,m,C).call(this,e)}getCurrentResult(){return a(this,g)}reset(){var e;(e=a(this,o))==null||e.removeObserver(this),v(this,o,void 0),j(this,m,w).call(this),j(this,m,C).call(this)}mutate(e,t){var n;return v(this,h,t),(n=a(this,o))==null||n.removeObserver(this),v(this,o,a(this,c).getMutationCache().build(a(this,c),this.options)),a(this,o).addObserver(this),a(this,o).execute(e)}},c=new WeakMap,g=new WeakMap,o=new WeakMap,h=new WeakMap,m=new WeakSet,w=function(){var t;const e=((t=a(this,o))==null?void 0:t.state)??I();v(this,g,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},C=function(e){P.batch(()=>{var t,n,l,x,d,p,k,i;if(a(this,h)&&this.hasListeners()){const u=a(this,g).variables,N=a(this,g).context,S={client:a(this,c),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(n=(t=a(this,h)).onSuccess)==null||n.call(t,e.data,u,N,S)}catch(b){Promise.reject(b)}try{(x=(l=a(this,h)).onSettled)==null||x.call(l,e.data,null,u,N,S)}catch(b){Promise.reject(b)}}else if((e==null?void 0:e.type)==="error"){try{(p=(d=a(this,h)).onError)==null||p.call(d,e.error,u,N,S)}catch(b){Promise.reject(b)}try{(i=(k=a(this,h)).onSettled)==null||i.call(k,void 0,e.error,u,N,S)}catch(b){Promise.reject(b)}}}this.listeners.forEach(u=>{u(a(this,g))})})},q);function W(r,e){const t=A(),[n]=f.useState(()=>new z(t,r));f.useEffect(()=>{n.setOptions(r)},[n,r]);const l=f.useSyncExternalStore(f.useCallback(d=>n.subscribe(P.batchCalls(d)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),x=f.useCallback((d,p)=>{n.mutate(d,p).catch(J)},[n]);if(l.error&&Q(n.options.throwOnError,[l.error]))throw l.error;return{...l,mutate:x,mutateAsync:l.mutate}}/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as o}from"./index-DZ1YxYm3.js";/**
|
||||
import{c as o}from"./index-0Xcu6PnS.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as e}from"./index-DZ1YxYm3.js";/**
|
||||
import{c as e}from"./index-0Xcu6PnS.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-DZ1YxYm3.js";/**
|
||||
import{c as a}from"./index-0Xcu6PnS.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+4
-4
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-DZ1YxYm3.js";/**
|
||||
import{c as a}from"./index-0Xcu6PnS.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as t}from"./index-DZ1YxYm3.js";/**
|
||||
import{c as t}from"./index-0Xcu6PnS.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-DZ1YxYm3.js";/**
|
||||
import{c as a}from"./index-0Xcu6PnS.js";/**
|
||||
* @license lucide-react v0.460.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>Mission Control 2.0</title>
|
||||
<script type="module" crossorigin src="/assets/index-DZ1YxYm3.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CkiwsfL2.css">
|
||||
<script type="module" crossorigin src="/assets/index-0Xcu6PnS.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Dv1-nhwH.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Gauge } from "lucide-react"
|
||||
import { useVoiceTrace } from "@/lib/queries"
|
||||
import type { VoiceTurn } from "@/lib/api"
|
||||
|
||||
// Balken-Stufen in kanonischer Reihenfolge (zeitlich disjunkt). Mem0 ist ein Unter-Detail
|
||||
// Balken-Stufen in kanonischer Reihenfolge (zeitlich disjunkt). Gedächtnis ist ein Unter-Detail
|
||||
// INNERHALB von „Hirn" und wird NICHT als eigenes Segment addiert (sonst Doppelzählung).
|
||||
const SEGMENTS = [
|
||||
{ key: "stt_ms", label: "STT", color: "#f59e0b" },
|
||||
@@ -39,7 +39,7 @@ function culprit(t: VoiceTurn): { label: string; color: string; ms: number } | n
|
||||
|
||||
function TurnRow({ t, scale }: { t: VoiceTurn; scale: number }) {
|
||||
const title = SEGMENTS.map((s) => `${s.label} ${fmt(t[s.key] as number | null)}`).join(" · ")
|
||||
+ (t.mem0_ms != null ? ` (davon Mem0 ${fmt(t.mem0_ms)})` : "")
|
||||
+ (t.memory_ms != null ? ` (davon Gedächtnis ${fmt(t.memory_ms)})` : "")
|
||||
return (
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="w-14 shrink-0 text-right font-mono text-[10px] text-muted-foreground/60">{ago(t.ts)}</span>
|
||||
@@ -103,7 +103,7 @@ export function LatencyCard() {
|
||||
{latest && !latest.error && (
|
||||
<div className="mt-0.5 font-mono text-[11px] text-muted-foreground/70">
|
||||
{SEGMENTS.filter((s) => (latest[s.key] as number | null) != null).map((s) => `${s.label} ${fmt(latest[s.key] as number)}`).join(" · ")}
|
||||
{latest.mem0_ms != null && <span className="text-muted-foreground/50"> · davon Mem0 {fmt(latest.mem0_ms)}</span>}
|
||||
{latest.memory_ms != null && <span className="text-muted-foreground/50"> · davon Gedächtnis {fmt(latest.memory_ms)}</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -128,7 +128,7 @@ export function LatencyCard() {
|
||||
)}
|
||||
|
||||
<div className="mt-3 border-t border-border/30 pt-2 text-[10px] leading-relaxed text-muted-foreground/70">
|
||||
„Hirn" = Zeit bis zum ersten Wort (Agent + Mem0-Suche + Modell); Tool-Runden stecken in „Antwort". Reine
|
||||
„Hirn" = Zeit bis zum ersten Wort (Agent + Gedächtnis-Suche + Modell); Tool-Runden stecken in „Antwort". Reine
|
||||
Telegram-Turns laufen an MC2 vorbei und erscheinen hier nicht.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -443,7 +443,7 @@ export interface TokenStats {
|
||||
}
|
||||
|
||||
// Per-Turn-Latenz-Trace (GET /api/voice/trace). Balken-Stufen (zeitlich disjunkt):
|
||||
// stt · vision · hirn · gen. mem0 = Unter-Detail INNERHALB von hirn (nicht zum Balken addieren).
|
||||
// stt · vision · hirn · gen. memory = Unter-Detail INNERHALB von hirn (nicht zum Balken addieren).
|
||||
export interface VoiceTurn {
|
||||
id: string
|
||||
ts: number // Epoch-Sekunden
|
||||
@@ -452,9 +452,9 @@ export interface VoiceTurn {
|
||||
had_images: boolean
|
||||
stt_ms: number | null
|
||||
vision_ms: number | null
|
||||
hirn_ms: number | null // Zeit bis erstes Inhalts-Token (Agent + Mem0 + LLM-TTFT)
|
||||
hirn_ms: number | null // Zeit bis erstes Inhalts-Token (Agent + Gedächtnis + LLM-TTFT)
|
||||
gen_ms: number | null // Generierung nach dem ersten Token bis Stream-Ende
|
||||
mem0_ms: number | null // Teil VON hirn (Mem0-Retrieve), best-effort
|
||||
memory_ms: number | null // Teil VON hirn (Gedächtnis-Retrieve), best-effort
|
||||
total_ms: number
|
||||
error: string | null
|
||||
}
|
||||
|
||||
@@ -121,18 +121,18 @@ export function useLucyHealth(): LucyHealth {
|
||||
}
|
||||
})())
|
||||
|
||||
// 3) Lucys Gedächtnis — Mem0-Dienst.
|
||||
// 3) Lucys Gedächtnis — Hermes Native Gedächtnis (integriert in hermes-gateway).
|
||||
checks.push(((): LucyCheck => {
|
||||
const base = { id: "memory", label: "Lucys Gedächtnis", icon: BookMarked, critical: true }
|
||||
const ok = svcOk("mem0-service")
|
||||
const ok = svcOk("hermes-gateway")
|
||||
if (!loaded || ok === undefined) return { ...base, status: "loading", detail: "Wird geprüft …" }
|
||||
return ok
|
||||
? { ...base, status: "ok", detail: "Merkt sich Neues und findet Altes wieder." }
|
||||
? { ...base, status: "ok", detail: "Hermes-Natives Gedächtnis aktiv (SQLite & Embeddings)." }
|
||||
: {
|
||||
...base,
|
||||
status: "down",
|
||||
detail: "Lucy kann sich gerade nichts merken — das Gedächtnis antwortet nicht.",
|
||||
repair: { kind: "restart", service: "mem0-service", label: "Gedächtnis neu starten" },
|
||||
detail: "Lucy kann sich gerade nichts merken — das Gateway antwortet nicht.",
|
||||
repair: { kind: "restart", service: "hermes-gateway", label: "Gateway neu starten" },
|
||||
}
|
||||
})())
|
||||
|
||||
|
||||
@@ -345,11 +345,8 @@ description: Treibt Entwicklung mit strikter TDD-Praxis
|
||||
</div>
|
||||
</div>
|
||||
<Stack>
|
||||
Dein Gedächtnis (Tab <strong>Gedächtnis</strong>) ist <strong>Mem0</strong>-basiert: auto-lernend &
|
||||
semantisch, <strong>graph-first</strong> dargestellt, mit 4-Typen-Taxonomie
|
||||
(<Pill>Identität</Pill> · <Pill>Wissen</Pill> · <Pill>Regeln</Pill> · <Pill>Ereignisse</Pill>). Der Agent
|
||||
lernt im Hintergrund nach jedem Turn — egal ob du per Desktop, Konsole, Telegram oder Lucy mit ihm sprichst.
|
||||
<br /><strong>Gotcha:</strong> ein gelöschter Fakt kann wieder auftauchen, solange die Original-Nachricht noch im Verlauf steht (additive Extraktion).
|
||||
Dein Gedächtnis ist auto-lernend & semantisch: Der Agent lernt im Hintergrund nach jedem Turn —
|
||||
egal ob du per Desktop, Konsole, Telegram oder Lucy mit ihm sprichst.
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
@@ -498,7 +495,7 @@ description: Treibt Entwicklung mit strikter TDD-Praxis
|
||||
<LinkItem href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview" name="Prompt-Engineering (Anthropic)" note="die beste praktische Anleitung." />
|
||||
<LinkItem href="https://github.com/anthropics/anthropic-cookbook" name="Anthropic Cookbook" note="lauffähige Rezepte für Tools, RAG, Agenten." />
|
||||
<LinkItem href="https://www.promptingguide.ai" name="Prompt Engineering Guide" note="herstellerneutrales Nachschlagewerk." />
|
||||
<LinkItem href="https://github.com/mem0ai/mem0" name="Mem0" note="die auto-lernende Memory-Schicht hinter deinem Gedächtnis." />
|
||||
<LinkItem href="https://hermes-agent.nousresearch.com" name="Hermes Agent" note="die autonome Open-Source Agenten-Plattform unserer Box." />
|
||||
<LinkItem href="https://www.reddit.com/r/LocalLLaMA/" name="r/LocalLLaMA" note="der Puls der lokalen-LLM-Szene." />
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -100,7 +100,7 @@ function Landkarte() {
|
||||
<NodeBox x={444} y={198} w={396} h={78} color={T.sky} title="Modell-Stall (llama-swap)"
|
||||
sub="Hirn · Groß · Coder · Augen · Späher · Kritiker"
|
||||
sub2="+ Embedding & Sortierer fürs Gedächtnis" />
|
||||
<NodeBox x={444} y={286} w={190} h={56} color={T.indigo} title="Mem0-Gedächtnis" sub="lernt automatisch mit" />
|
||||
<NodeBox x={444} y={286} w={190} h={56} color={T.indigo} title="Natives Gedächtnis" sub="Hermes v0.20 SQLite & Embed" />
|
||||
<NodeBox x={650} y={286} w={190} h={56} color={T.amber} title="Wissens-Vault" sub="Träume & Notizen (git)" />
|
||||
<NodeBox x={444} y={352} w={396} h={52} color={T.emerald} title="Selbstheilung & Backups"
|
||||
sub="Health-Checks · Auto-Revert bei Rot · PBS-Sicherung" />
|
||||
|
||||
@@ -48,6 +48,11 @@ const ROLE_METADATA: Record<string, { title: string; desc: string; icon: any }>
|
||||
title: "Gedächtnis-Sortierer",
|
||||
desc: "Sortiert und gewichtet gespeicherte Fakten nach Relevanz — damit die richtige Info sofort da ist.",
|
||||
icon: Search
|
||||
},
|
||||
debugger: {
|
||||
title: "Debugger & Selbstheiler",
|
||||
desc: "Agentisches Debugging, multimodale Fehleranalyse und Selbstreparatur (z.B. Muse-Glimmer).",
|
||||
icon: Brain
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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-builtin-ui | 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 (Configs + Secrets). 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)}"
|
||||
|
||||
@@ -1,483 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Mem0-Sidecar für Mission Control 2.0 — das auto-lernende, semantische Gedächtnis.
|
||||
|
||||
WARUM ein eigener Dienst? Mem0 + chromadb laufen nur unter Python 3.12 (`~/.mem0/venv`),
|
||||
das MC2-Backend aber unter Python 3.14 (kann mem0 nicht importieren). Darum kapselt dieser
|
||||
schlanke FastAPI-Dienst die Mem0-Memory-Instanz und exponiert sie auf localhost. MC2
|
||||
(`backend/services/memory.py`) spricht ihn per HTTP an — die `/api/memory`-API-Form bleibt
|
||||
nach außen unverändert (UI + MCP-Server kompatibel).
|
||||
|
||||
Aufbau:
|
||||
- Vektor-Store : Chroma embedded (kein Docker auf der Box) — Pfad MEM0_CHROMA_PATH.
|
||||
- Embeddings : llama.cpp `embed`-Rolle über llama-swap (/v1/embeddings, 1024 Dim).
|
||||
- LLM-Extraktion: lokales `fast`-Hirn (Qwen3.6) über llama-swap (/v1/chat/completions).
|
||||
Thinking wird abgeschaltet (NoThinkLLM), sonst bricht die JSON-Extraktion ab.
|
||||
|
||||
Läuft als systemd-User-Dienst (deploy/mem0-service.service) im `~/.mem0/venv`.
|
||||
Bind: 127.0.0.1 (nur lokal; MC2 proxyt nach außen).
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from mem0 import Memory
|
||||
from mem0.configs.llms.openai import OpenAIConfig
|
||||
from mem0.llms.openai import OpenAILLM
|
||||
from pydantic import BaseModel
|
||||
|
||||
log = logging.getLogger("mem0_service")
|
||||
|
||||
# --- Konfiguration (alles über Env überschreibbar; Defaults = Box-Stand) ----------
|
||||
USER_ID = os.environ.get("MEM0_USER_ID", "mission-control") # ein geteiltes Gehirn
|
||||
EMBED_URL = os.environ.get("MEM0_EMBED_URL", "http://127.0.0.1:8080/v1")
|
||||
LLM_URL = os.environ.get("MEM0_LLM_URL", "http://127.0.0.1:8080/v1")
|
||||
EMBED_MODEL = os.environ.get("MEM0_EMBED_MODEL", "embed")
|
||||
LLM_MODEL = os.environ.get("MEM0_LLM_MODEL", "fast")
|
||||
EMBED_DIMS = int(os.environ.get("MEM0_EMBED_DIMS", "1024"))
|
||||
CHROMA_PATH = os.environ.get("MEM0_CHROMA_PATH", "/srv/models/mem0/chroma")
|
||||
HISTORY_DB = os.environ.get("MEM0_HISTORY_DB", "/srv/models/mem0/history.db")
|
||||
COLLECTION = os.environ.get("MEM0_COLLECTION", "mc2")
|
||||
API_KEY = os.environ.get("MEM0_API_KEY", "sk-local") # llama.cpp ignoriert den Key
|
||||
LLM_MAX_TOKENS = int(os.environ.get("MEM0_LLM_MAX_TOKENS", "2048"))
|
||||
SEARCH_TOP_K = int(os.environ.get("MEM0_SEARCH_TOP_K", "50"))
|
||||
|
||||
# --- Rerank-Zweitstufe (07.07.2026): Die Vektor-Suche findet ÄHNLICHES, der Reranker ordnet
|
||||
# nach ECHTER Relevanz (Qwen3-Reranker via llama-swap /v1/rerank). Es ändert sich NUR die
|
||||
# Reihenfolge: `score` bleibt der Vektor-Score (RECALL_MIN_SCORE im Hermes-Plugin bleibt
|
||||
# kalibriert), Konsumenten, die vorne abschneiden (RECALL_LIMIT), bekommen automatisch die
|
||||
# relevantesten Fakten. Jeder Fehler → lautlos Vektor-Reihenfolge (Gedächtnis bricht NIE).
|
||||
RERANK_ENABLED = os.environ.get("MC_RERANK_ENABLED", "1") != "0"
|
||||
RERANK_URL = os.environ.get("MC_RERANK_URL", "http://127.0.0.1:8080/v1/rerank")
|
||||
RERANK_MODEL = os.environ.get("MC_RERANK_MODEL", "Qwen3-Reranker-0.6B")
|
||||
RERANK_TIMEOUT = float(os.environ.get("MC_RERANK_TIMEOUT", "8"))
|
||||
RERANK_CANDIDATES = int(os.environ.get("MC_RERANK_CANDIDATES", "20")) # nur die Top-N neu ordnen (Latenz)
|
||||
|
||||
# Kategorien = fundierte Memory-Taxonomie (semantisch/prozedural/episodisch), bewusst knapp (4).
|
||||
# Mem0-OSS kann nicht nativ klassifizieren (Cloud-Feature) → wir lassen das Hirn beim Lernen
|
||||
# einordnen (classify_facts). Die Beschreibungen steuern die Treffsicherheit der Auto-Zuordnung.
|
||||
CATEGORIES = ("identity", "knowledge", "rules", "events")
|
||||
DEFAULT_CATEGORY = "knowledge"
|
||||
CATEGORY_DESCRIPTIONS = {
|
||||
"identity": "Identität & Vorlieben des Nutzers: wer er ist (Name, Rolle), wie er angesprochen werden "
|
||||
"will, persönliche Vorlieben und seine Arbeitsweise.",
|
||||
"knowledge": "Wissen & Fakten: stabile Fakten über Stack, Projekte, Tools, Infrastruktur und Umgebung.",
|
||||
"rules": "Regeln & Konventionen: verbindliche Anweisungen und Workflows — wie etwas gemacht werden "
|
||||
"soll, Do's und Don'ts.",
|
||||
"events": "Ereignisse & Entscheidungen: was passiert ist, getroffene Entscheidungen, zeitgebundene Vorgänge.",
|
||||
}
|
||||
|
||||
|
||||
class NoThinkLLM(OpenAILLM):
|
||||
"""`fast` (Qwen3.6) ist ein Thinking-Modell. Unter response_format=json_object
|
||||
verbrennt das Reasoning Tokens und schneidet die JSON-Fakten ab (leeres Ergebnis).
|
||||
`enable_thinking=false` (chat_template_kwargs) → deterministische, schnelle Extraktion."""
|
||||
|
||||
def generate_response(self, messages, response_format=None, tools=None,
|
||||
tool_choice="auto", **kwargs):
|
||||
eb = dict(kwargs.pop("extra_body", {}) or {})
|
||||
eb.setdefault("chat_template_kwargs", {"enable_thinking": False})
|
||||
return super().generate_response(
|
||||
messages, response_format=response_format, tools=tools,
|
||||
tool_choice=tool_choice, extra_body=eb, **kwargs,
|
||||
)
|
||||
|
||||
|
||||
def build_memory() -> Memory:
|
||||
os.environ.setdefault("OPENAI_API_KEY", API_KEY)
|
||||
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
|
||||
os.makedirs(CHROMA_PATH, exist_ok=True)
|
||||
os.makedirs(os.path.dirname(HISTORY_DB), exist_ok=True)
|
||||
cfg = {
|
||||
"vector_store": {"provider": "chroma", "config": {
|
||||
"collection_name": COLLECTION, "path": CHROMA_PATH}},
|
||||
"embedder": {"provider": "openai", "config": {
|
||||
"model": EMBED_MODEL, "embedding_dims": EMBED_DIMS,
|
||||
"openai_base_url": EMBED_URL, "api_key": API_KEY}},
|
||||
"llm": {"provider": "openai", "config": {
|
||||
"model": LLM_MODEL, "openai_base_url": LLM_URL, "api_key": API_KEY,
|
||||
"temperature": 0.1, "max_tokens": LLM_MAX_TOKENS}},
|
||||
"history_db_path": HISTORY_DB,
|
||||
# Sprache zwingend Deutsch erzwingen — mem0s englischer Extraktions-Prompt zieht sonst
|
||||
# Richtung Englisch. Eigennamen/Fachbegriffe bleiben unangetastet.
|
||||
"custom_instructions": (
|
||||
"RELEVANZ (ZWINGEND, WICHTIGSTE REGEL): Extrahiere NUR dauerhaft nützliche Fakten ÜBER DEN "
|
||||
"NUTZER (Identität, Vorlieben, Arbeitsweise), seine Projekte/Entscheidungen/Stände, stabile "
|
||||
"Fakten über Stack/Tools/Infrastruktur sowie verbindliche Regeln und Workflows. "
|
||||
"NIEMALS FAKTEN ÜBER DEN ASSISTENTEN SELBST (ZWINGEND): Extrahiere KEINE Aussagen über den "
|
||||
"Assistenten/die KI (Hermes, Lucy), seine Fähigkeiten, seinen Zustand, seine Tools/MCP-Server, "
|
||||
"seine Cronjobs, Commit-/Versionsstände oder welche Modelle er nutzt. Das Subjekt eines Fakts "
|
||||
"ist IMMER der Nutzer oder sein Projekt — niemals 'der Assistent', 'Hermes' oder 'Lucy'. "
|
||||
"KEINE ZEITGEBUNDENEN ZUSTÄNDE: Ignoriere transiente Momentaufnahmen (Fehler-/Job-Läufe, "
|
||||
"Commit-Rückstand, Versions-Updates, 'gerade dabei'-/'hochgefahren'-/'aufgewärmt'-Status, "
|
||||
"Test- und Onboarding-Fragen). IGNORIERE VOLLSTÄNDIG und gib KEINE Fakten zurück bei: "
|
||||
"Tagesnachrichten und Welt-/Politik-/Börsen-/Promi-Ereignissen, Wetter, allgemeinem Weltwissen "
|
||||
"ohne Bezug zum Nutzer, vom Assistenten erzeugten Zusammenfassungen oder Recherche-Ergebnissen, "
|
||||
"Smalltalk, sowie Meta-Aussagen über den Gesprächsverlauf (z.B. 'der Nutzer fragt nach X', "
|
||||
"'der Nutzer möchte Y prüfen', 'der Assistent tat Z'). Speichere nur, was in Wochen noch "
|
||||
"nützlich ist; im Zweifel NICHT speichern. "
|
||||
"SPRACHE (ZWINGEND): Formuliere ALLE Fakten auf DEUTSCH — unabhängig von der "
|
||||
"Eingabesprache, niemals auf Englisch übersetzen. Eigennamen, Befehle, Pfade, Modell- und "
|
||||
"Produktnamen sowie etablierte Fachbegriffe (z.B. PowerShell-Push, Deploy, Qwen3.6, Neovim, "
|
||||
"Vulkan) bleiben unverändert. Schreibe jeden Fakt knapp und atomar — genau eine Aussage."
|
||||
),
|
||||
}
|
||||
m = Memory.from_config(cfg)
|
||||
# LLM gegen die Thinking-freie Variante tauschen (gleiche Verbindung).
|
||||
m.llm = NoThinkLLM(OpenAIConfig(
|
||||
model=LLM_MODEL, openai_base_url=LLM_URL, api_key=API_KEY,
|
||||
temperature=0.1, max_tokens=LLM_MAX_TOKENS,
|
||||
))
|
||||
return m
|
||||
|
||||
|
||||
_mem: Memory | None = None
|
||||
|
||||
|
||||
def mem() -> Memory:
|
||||
global _mem
|
||||
if _mem is None:
|
||||
_mem = build_memory()
|
||||
return _mem
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_app: FastAPI):
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
try:
|
||||
mem() # Memory beim Start aufbauen (Chroma öffnen, Modelle anpingen)
|
||||
log.info("Mem0 bereit (chroma=%s, embed=%s, llm=%s)", CHROMA_PATH, EMBED_MODEL, LLM_MODEL)
|
||||
except Exception:
|
||||
log.exception("Mem0-Init fehlgeschlagen (Dienst läuft, /health meldet down)")
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(title="MC2 Mem0 Sidecar", lifespan=lifespan)
|
||||
|
||||
|
||||
# --- Mapping Mem0 <-> MC2-API-Form -----------------------------------------------
|
||||
def _to_item(r: dict, score=None) -> dict:
|
||||
meta = r.get("metadata") or {}
|
||||
out = {
|
||||
"id": r.get("id"),
|
||||
"content": r.get("memory", ""),
|
||||
"category": meta.get("category") or "stable",
|
||||
"bereich": meta.get("bereich") or "",
|
||||
"source": meta.get("source") or "auto",
|
||||
"created_at": r.get("created_at") or "",
|
||||
"updated_at": r.get("updated_at") or r.get("created_at") or "",
|
||||
}
|
||||
if score is not None:
|
||||
try:
|
||||
out["score"] = round(float(score), 4)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
class MemIn(BaseModel):
|
||||
content: str
|
||||
category: str = DEFAULT_CATEGORY
|
||||
source: str = "manual"
|
||||
|
||||
|
||||
class MemUp(BaseModel):
|
||||
content: str | None = None
|
||||
category: str | None = None
|
||||
|
||||
|
||||
class LearnIn(BaseModel):
|
||||
text: str | None = None
|
||||
messages: list[dict] | None = None
|
||||
source: str = "auto"
|
||||
category: str = DEFAULT_CATEGORY
|
||||
classify: bool = True # Hirn ordnet die gelernten Fakten automatisch ein
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health() -> dict:
|
||||
try:
|
||||
mem().get_all(filters={"user_id": USER_ID}, top_k=1)
|
||||
return {"ok": True}
|
||||
except Exception as exc:
|
||||
raise HTTPException(503, f"mem0 nicht bereit: {exc}")
|
||||
|
||||
|
||||
def _rerank(query: str, items: list[dict]) -> list[dict]:
|
||||
"""Suchtreffer nach echter Relevanz umordnen; bei jedem Fehler unverändert zurück."""
|
||||
if not (RERANK_ENABLED and len(items) > 1):
|
||||
return items
|
||||
try:
|
||||
import httpx
|
||||
r = httpx.post(RERANK_URL, json={
|
||||
"model": RERANK_MODEL,
|
||||
"query": query,
|
||||
"documents": [i.get("content", "") for i in items],
|
||||
"top_n": len(items),
|
||||
}, timeout=RERANK_TIMEOUT)
|
||||
r.raise_for_status()
|
||||
results = r.json().get("results") or []
|
||||
ranked = []
|
||||
for s in sorted(results, key=lambda x: x.get("relevance_score", 0.0), reverse=True):
|
||||
idx = s.get("index")
|
||||
if isinstance(idx, int) and 0 <= idx < len(items):
|
||||
item = dict(items[idx])
|
||||
item["rerank_score"] = round(float(s.get("relevance_score", 0.0)), 4)
|
||||
ranked.append(item)
|
||||
# Nur übernehmen, wenn der Reranker ALLE Kandidaten bewertet hat (sonst Verlustgefahr).
|
||||
return ranked if len(ranked) == len(items) else items
|
||||
except Exception as exc:
|
||||
log.debug("rerank übersprungen: %s", exc)
|
||||
return items
|
||||
|
||||
|
||||
@app.get("/memory")
|
||||
def list_memory(q: str = "", category: str = "", bereich: str = "") -> list[dict]:
|
||||
"""q gesetzt → semantische Suche (mit Relevanz-Score). Sonst → alle Fakten.
|
||||
bereich='alltag'|'projekt' filtert (Fakten OHNE Bereichs-Feld laufen immer mit,
|
||||
damit Alt-Bestand nie verschwindet)."""
|
||||
if q:
|
||||
res = mem().search(q, filters={"user_id": USER_ID}, top_k=SEARCH_TOP_K)
|
||||
items = [_to_item(r, r.get("score")) for r in res.get("results", [])]
|
||||
# Zweitstufe: die vorderen Kandidaten nach echter Relevanz ordnen (Rest bleibt hinten dran).
|
||||
items = _rerank(q, items[:RERANK_CANDIDATES]) + items[RERANK_CANDIDATES:]
|
||||
else:
|
||||
res = mem().get_all(filters={"user_id": USER_ID}, top_k=1000)
|
||||
items = [_to_item(r) for r in res.get("results", [])]
|
||||
items.sort(key=lambda i: i.get("created_at", ""), reverse=True)
|
||||
if category:
|
||||
items = [i for i in items if i["category"] == category]
|
||||
if bereich in ("alltag", "projekt"):
|
||||
items = [i for i in items if i.get("bereich") in (bereich, "")]
|
||||
return items
|
||||
|
||||
|
||||
@app.post("/memory", status_code=201)
|
||||
def add_memory(body: MemIn) -> dict:
|
||||
"""Manueller/agentischer Einzel-Fakt → VERBATIM speichern (infer=False, keine
|
||||
LLM-Umformung). Auto-Lernen aus Gesprächen läuft über /learn (infer=True)."""
|
||||
content = body.content.strip()
|
||||
if not content:
|
||||
raise HTTPException(400, "content leer")
|
||||
res = mem().add(
|
||||
[{"role": "user", "content": content}],
|
||||
user_id=USER_ID, infer=False,
|
||||
metadata={"category": body.category, "source": body.source},
|
||||
)
|
||||
results = res.get("results", [])
|
||||
if not results:
|
||||
raise HTTPException(500, "Mem0 hat nichts gespeichert")
|
||||
new_id = results[0]["id"]
|
||||
full = mem().get(new_id) or {}
|
||||
return _to_item(full) if full else _to_item({"id": new_id, "memory": content,
|
||||
"metadata": {"category": body.category, "source": body.source}})
|
||||
|
||||
|
||||
@app.put("/memory/{mid}")
|
||||
def update_memory(mid: str, body: MemUp) -> dict:
|
||||
existing = mem().get(mid)
|
||||
if not existing:
|
||||
raise HTTPException(404, "Eintrag nicht gefunden")
|
||||
cur = _to_item(existing)
|
||||
new_content = body.content.strip() if body.content is not None else cur["content"]
|
||||
new_cat = body.category if body.category is not None else cur["category"]
|
||||
mem().update(mid, data=new_content,
|
||||
metadata={"category": new_cat, "source": cur["source"]})
|
||||
full = mem().get(mid) or {}
|
||||
return _to_item(full)
|
||||
|
||||
|
||||
@app.delete("/memory/{mid}")
|
||||
def delete_memory(mid: str) -> dict:
|
||||
if not mem().get(mid):
|
||||
raise HTTPException(404, "Eintrag nicht gefunden")
|
||||
mem().delete(mid)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.get("/graph")
|
||||
def graph(min_score: float = 0.45, top_k: int = 3) -> dict:
|
||||
"""Fakten als Graph: Knoten = Fakten, Kanten = semantische Ähnlichkeit (Kosinus
|
||||
der gespeicherten Embeddings, je Knoten die top_k Nachbarn ≥ min_score). Für die
|
||||
Obsidian-artige Gedächtnis-Visualisierung im UI."""
|
||||
items = mem().get_all(filters={"user_id": USER_ID}, top_k=2000).get("results", [])
|
||||
nodes = [_to_item(r) for r in items]
|
||||
node_ids = [n["id"] for n in nodes]
|
||||
idx = {nid: i for i, nid in enumerate(node_ids)}
|
||||
|
||||
# Embeddings direkt aus der Chroma-Collection ziehen (kein Re-Embedding).
|
||||
col = mem().vector_store.collection
|
||||
raw = col.get(include=["embeddings"])
|
||||
raw_ids = raw.get("ids") or []
|
||||
raw_embs = raw.get("embeddings")
|
||||
edges: list[dict] = []
|
||||
vecs = [None] * len(node_ids)
|
||||
have = 0
|
||||
if raw_embs is not None:
|
||||
for rid, emb in zip(raw_ids, raw_embs):
|
||||
if rid in idx and emb is not None:
|
||||
vecs[idx[rid]] = emb
|
||||
have += 1
|
||||
|
||||
if have >= 2:
|
||||
import numpy as np
|
||||
present = [i for i, v in enumerate(vecs) if v is not None]
|
||||
M = np.array([vecs[i] for i in present], dtype=float)
|
||||
norms = np.linalg.norm(M, axis=1, keepdims=True)
|
||||
norms[norms == 0] = 1.0
|
||||
Mn = M / norms
|
||||
sim = Mn @ Mn.T
|
||||
seen: set[tuple[int, int]] = set()
|
||||
for a in range(len(present)):
|
||||
order = np.argsort(-sim[a])
|
||||
cnt = 0
|
||||
for b in order:
|
||||
if b == a:
|
||||
continue
|
||||
s = float(sim[a][b])
|
||||
if s < min_score:
|
||||
break
|
||||
i, j = present[a], present[b]
|
||||
key = (min(i, j), max(i, j))
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
edges.append({"source": node_ids[i], "target": node_ids[j],
|
||||
"weight": round(s, 3)})
|
||||
cnt += 1
|
||||
if cnt >= top_k:
|
||||
break
|
||||
return {"nodes": nodes, "edges": edges}
|
||||
|
||||
|
||||
def classify_facts(facts: list[tuple[str, str]]) -> dict:
|
||||
"""Ordnet jeden Fakt per LLM einer Kategorie UND einem Bereich zu →
|
||||
{id: {"category": <slug>, "bereich": "alltag"|"projekt"}}.
|
||||
|
||||
Bereich (19.07.2026, Trennung der Welten): "alltag" = der Nutzer als Person
|
||||
(Vorlieben, Leben, Arbeitsweise, Entscheidungen), "projekt" = Technik-/Code-/
|
||||
Infrastruktur-Stände. Grundlage für einen späteren Recall-Filter, damit
|
||||
Projekt-Schutt nicht in Alltags-Gespräche streut — OHNE zweites Gedächtnis.
|
||||
Nutzt dasselbe (Thinking-freie) Hirn wie die Extraktion. Fehler = leeres Mapping."""
|
||||
if not facts:
|
||||
return {}
|
||||
import json as _json
|
||||
import re as _re
|
||||
cat_lines = "\n".join(f"- {k}: {v}" for k, v in CATEGORY_DESCRIPTIONS.items())
|
||||
system = (
|
||||
"Du bist ein präziser Klassifikator für ein Langzeitgedächtnis. Ordne jeden Fakt GENAU EINER "
|
||||
"Kategorie zu. Verfügbare Kategorien (nur diese Slugs verwenden):\n" + cat_lines +
|
||||
"\nOrdne zusätzlich jeden Fakt GENAU EINEM Bereich zu: 'alltag' (der Nutzer als Person: "
|
||||
"Vorlieben, Leben, Anrede, Arbeitsweise, grundsätzliche Entscheidungen) oder 'projekt' "
|
||||
"(Technik: Code, Modelle, Server, Infrastruktur, Projekt- und Fehlerstände). "
|
||||
"Antworte NUR als JSON-Objekt der Form "
|
||||
"{\"<id>\": {\"kategorie\": \"<slug>\", \"bereich\": \"alltag|projekt\"}, …}."
|
||||
)
|
||||
body = "\n".join(f"{fid}: {txt}" for fid, txt in facts)
|
||||
try:
|
||||
resp = mem().llm.generate_response(
|
||||
messages=[{"role": "system", "content": system},
|
||||
{"role": "user", "content": "Fakten:\n" + body}],
|
||||
response_format={"type": "json_object"},
|
||||
)
|
||||
m = _re.search(r"\{.*\}", resp or "", _re.DOTALL)
|
||||
data = _json.loads(m.group(0)) if m else {}
|
||||
out: dict = {}
|
||||
for k, v in data.items():
|
||||
if isinstance(v, str): # Alt-Form {"id": "slug"} weiter akzeptieren
|
||||
if v in CATEGORIES:
|
||||
out[str(k)] = {"category": v, "bereich": ""}
|
||||
elif isinstance(v, dict):
|
||||
cat = v.get("kategorie") or v.get("category") or ""
|
||||
ber = v.get("bereich") or ""
|
||||
if cat in CATEGORIES or ber in ("alltag", "projekt"):
|
||||
out[str(k)] = {"category": cat if cat in CATEGORIES else DEFAULT_CATEGORY,
|
||||
"bereich": ber if ber in ("alltag", "projekt") else ""}
|
||||
return out
|
||||
except Exception:
|
||||
log.exception("Auto-Klassifikation fehlgeschlagen")
|
||||
return {}
|
||||
|
||||
|
||||
# Deterministischer Guard: mem0-OSS extrahiert trotz custom_instructions hartnäckig Fakten ÜBER
|
||||
# den Assistenten selbst (Hermes/Lucy) und transiente Zustände (Commit-Rückstand, Cronjob-Läufe,
|
||||
# Onboarding-/Test-Fragen). Diese Muster werden nach der Extraktion verworfen (der Fakt wird wieder
|
||||
# gelöscht) — reine Sicherungsschicht, unabhängig vom Extraktions-LLM.
|
||||
_JUNK_FACT_PATTERNS = [
|
||||
r"^\s*(?:der|die|das)\s+assistent", # "Der Assistent ..." (Selbstbeschreibung)
|
||||
r"\bassistent(?:in)?\s+(?:hermes|lucy)\b", # "... Assistent Hermes/Lucy ..."
|
||||
r"\bhermes[- ]agent\b",
|
||||
r"commits?\s+hinter", # Versions-Rückstand (transient)
|
||||
r"\bcronjob\b",
|
||||
r"beim\s+letzten\s+(?:lauf|mal)",
|
||||
r"onboarding[-\s]?frage|\btest-?frage\b",
|
||||
r"hat\s+den\s+assistenten\s+gebeten",
|
||||
r"auf\s+betriebstemperatur|hochgefahren",
|
||||
r"aktualisiert\s+sein\s+ged(?:ä|ae)chtnis", # Selbst-Meta über das Gedächtnis
|
||||
]
|
||||
_JUNK_FACT_RE = re.compile("|".join(_JUNK_FACT_PATTERNS), re.IGNORECASE)
|
||||
|
||||
|
||||
def _is_junk_fact(text: str) -> bool:
|
||||
"""True, wenn der extrahierte Fakt Assistenten-Meta oder ein transienter Zustand ist."""
|
||||
return bool(_JUNK_FACT_RE.search(text or ""))
|
||||
|
||||
|
||||
@app.post("/learn")
|
||||
def learn(body: LearnIn) -> dict:
|
||||
"""Auto-Lernen: Gesprächs-Turns/Text durchreichen → Mem0 EXTRAHIERT Fakten selbst
|
||||
(infer=True). Anschließend ordnet das Hirn jeden neuen Fakt automatisch einer Kategorie zu."""
|
||||
msgs = body.messages
|
||||
if not msgs and body.text:
|
||||
msgs = [{"role": "user", "content": body.text}]
|
||||
if not msgs:
|
||||
raise HTTPException(400, "text oder messages erforderlich")
|
||||
res = mem().add(
|
||||
msgs, user_id=USER_ID, infer=True,
|
||||
metadata={"category": body.category, "source": body.source},
|
||||
)
|
||||
results = res.get("results", [])
|
||||
|
||||
# Guard: frisch extrahierte Assistenten-Meta/transiente Fakten sofort wieder verwerfen.
|
||||
kept = []
|
||||
for r in results:
|
||||
if r.get("event") in ("ADD", "UPDATE") and _is_junk_fact(r.get("memory", "")):
|
||||
try:
|
||||
mem().delete(r["id"])
|
||||
except Exception:
|
||||
log.debug("Junk-Fakt-Löschung fehlgeschlagen für %s", r.get("id"))
|
||||
log.info("mem0 guard: Junk-Fakt verworfen: %s", (r.get("memory") or "")[:80])
|
||||
continue
|
||||
kept.append(r)
|
||||
results = kept
|
||||
|
||||
cats: dict = {}
|
||||
if body.classify:
|
||||
to_classify = [(r["id"], r.get("memory", "")) for r in results
|
||||
if r.get("id") and r.get("memory") and r.get("event") in ("ADD", "UPDATE")]
|
||||
cats = classify_facts(to_classify)
|
||||
for r in results:
|
||||
info = cats.get(r.get("id"))
|
||||
if info:
|
||||
meta = {"category": info["category"], "source": body.source}
|
||||
if info.get("bereich"):
|
||||
meta["bereich"] = info["bereich"]
|
||||
try:
|
||||
mem().update(r["id"], data=r.get("memory", ""), metadata=meta)
|
||||
except Exception:
|
||||
log.debug("Kategorie-Update fehlgeschlagen für %s", r.get("id"))
|
||||
|
||||
return {"results": [
|
||||
{"id": r.get("id"), "content": r.get("memory"), "event": r.get("event"),
|
||||
"category": (cats.get(r.get("id")) or {}).get("category", body.category),
|
||||
"bereich": (cats.get(r.get("id")) or {}).get("bereich", "")}
|
||||
for r in results
|
||||
]}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="127.0.0.1", port=int(os.environ.get("MEM0_PORT", "8765")))
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Einmalige Migration: alte SQLite-Fakten (`memories`-Tabelle aus dem v1/v2-Memory)
|
||||
→ Mem0. Läuft im ~/.mem0/venv. Idempotent über eine Marker-Datei.
|
||||
|
||||
Jeder Alt-Eintrag wird VERBATIM übernommen (infer=False), Kategorie/Source als
|
||||
Metadaten. Beim aktuellen Box-Stand ist die DB leer → No-Op, aber für
|
||||
Reproduzierbarkeit/künftige Migrationen vorhanden.
|
||||
|
||||
Aufruf: ~/.mem0/venv/bin/python migrate.py [pfad/zur/mc2-memory.db]
|
||||
"""
|
||||
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
from app import USER_ID, mem # nutzt dieselbe Mem0-Instanz/Config
|
||||
|
||||
DEFAULT_DB = os.environ.get("MC_MEMORY_DB", "/srv/models/mc2-memory.db")
|
||||
MARKER = os.environ.get("MEM0_MIGRATION_MARKER", "/srv/models/mem0/.migrated_sqlite")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
db_path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_DB
|
||||
if os.path.exists(MARKER):
|
||||
print(f"Migration bereits erledigt (Marker {MARKER}).")
|
||||
return 0
|
||||
if not os.path.exists(db_path):
|
||||
print(f"Keine Alt-DB unter {db_path} — nichts zu migrieren.")
|
||||
os.makedirs(os.path.dirname(MARKER), exist_ok=True)
|
||||
open(MARKER, "w").close()
|
||||
return 0
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT content, category, source FROM memories ORDER BY created_at"
|
||||
).fetchall()
|
||||
except sqlite3.OperationalError:
|
||||
rows = []
|
||||
conn.close()
|
||||
|
||||
m = mem()
|
||||
n = 0
|
||||
for r in rows:
|
||||
content = (r["content"] or "").strip()
|
||||
if not content:
|
||||
continue
|
||||
m.add([{"role": "user", "content": content}], user_id=USER_ID, infer=False,
|
||||
metadata={"category": r["category"] or "stable",
|
||||
"source": r["source"] or "migrated"})
|
||||
n += 1
|
||||
|
||||
os.makedirs(os.path.dirname(MARKER), exist_ok=True)
|
||||
open(MARKER, "w").close()
|
||||
print(f"Migriert: {n} Einträge aus {db_path} → Mem0.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,9 +0,0 @@
|
||||
# Mem0-Sidecar-Deps im ~/.mem0/venv (Python 3.12). BEWUSST GEPINNT — Upgrades nur
|
||||
# absichtlich, weil der Sidecar mem0-INTERNA nutzt (NoThink-LLM-Swap, Roh-Zugriff auf die
|
||||
# Chroma-Collection für den Graphen, Annahmen übers Result-Format). Ein unkontrolliertes
|
||||
# mem0/chromadb-Upgrade kann das still brechen.
|
||||
# Upgrade-Prozedur (Backup → Pin anheben → smoke_test.py): siehe docs/UPGRADE.md.
|
||||
mem0ai==2.0.8
|
||||
chromadb==1.5.9
|
||||
fastapi==0.138.1
|
||||
uvicorn[standard]==0.49.0
|
||||
@@ -1,71 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Smoke-Test für den Mem0-Sidecar — VOR und NACH jedem mem0/chromadb-Upgrade laufen lassen.
|
||||
Prüft genau die Pfade, die mem0-INTERNA nutzen (LLM-Extraktion/NoThink, Auto-Einordnung,
|
||||
Embeddings/Suche, Roh-Chroma-Graph). Nutzt eine WEGWERF-Collection unter /tmp — das echte
|
||||
Gedächtnis (/srv/models/mem0) wird NICHT angefasst.
|
||||
|
||||
Aufruf: ~/.mem0/venv/bin/python ~/mission-control-v2/mem0_service/smoke_test.py
|
||||
Exit 0 = alles grün, sonst 1.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
# WICHTIG: Env vor dem Import von app setzen (app liest die Pfade beim Modul-Load).
|
||||
_TMP = tempfile.mkdtemp(prefix="mem0_smoke_")
|
||||
os.environ["MEM0_CHROMA_PATH"] = os.path.join(_TMP, "chroma")
|
||||
os.environ["MEM0_HISTORY_DB"] = os.path.join(_TMP, "history.db")
|
||||
os.environ["MEM0_COLLECTION"] = f"smoke_{int(time.time())}"
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import app
|
||||
|
||||
_fails: list[str] = []
|
||||
|
||||
|
||||
def check(name: str, cond: bool, detail: str = "") -> None:
|
||||
print(("PASS" if cond else "FAIL"), "—", name, ("" if cond else f":: {detail}"))
|
||||
if not cond:
|
||||
_fails.append(name)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
app.mem() # Memory bauen (öffnet Chroma, pingt Modelle)
|
||||
|
||||
# 1) Verbatim add + Rücklesen
|
||||
r = app.add_memory(app.MemIn(content="Smoke: der Himmel ist blau.", category="knowledge", source="smoke"))
|
||||
check("verbatim add + get", bool(r.get("id")) and "blau" in r.get("content", ""), str(r)[:160])
|
||||
|
||||
# 2) Embeddings / semantische Suche (Bedeutung, nicht Wort)
|
||||
res = app.list_memory(q="Welche Farbe hat der Himmel?")
|
||||
check("semantische Suche", any("blau" in i.get("content", "") for i in res), str(res)[:160])
|
||||
|
||||
# 3) Auto-Lernen: LLM-Extraktion (NoThink) + Deutsch erzwungen
|
||||
lr = app.learn(app.LearnIn(text="Ich heiße Max und programmiere am liebsten in Go.", source="smoke"))
|
||||
items = lr.get("results", [])
|
||||
check("Auto-Lernen extrahiert Fakten", len(items) > 0, str(lr)[:160])
|
||||
joined = " ".join((i.get("content") or "") for i in items).lower()
|
||||
check("Extraktion auf Deutsch", any(w in joined for w in ("heißt", "programmiert", "mag", "bevorzugt", "name", "nutzt")), joined[:160])
|
||||
|
||||
# 4) Auto-Einordnung in die 4 gültigen Kategorien
|
||||
cats = {i.get("category") for i in items if i.get("category")}
|
||||
check("Auto-Einordnung gültige Kategorie", bool(cats) and cats <= set(app.CATEGORIES), str(cats))
|
||||
|
||||
# 5) Graph aus den Roh-Embeddings (Chroma-Interna)
|
||||
g = app.graph()
|
||||
check("Graph liefert Knoten", len(g.get("nodes", [])) >= 1, str({k: len(v) for k, v in g.items()}))
|
||||
|
||||
print("\n" + ("ALLE TESTS GRÜN ✓" if not _fails else f"FEHLGESCHLAGEN: {_fails}"))
|
||||
return 0 if not _fails else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
code = main()
|
||||
finally:
|
||||
shutil.rmtree(_TMP, ignore_errors=True)
|
||||
sys.exit(code)
|
||||
Reference in New Issue
Block a user