Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b639870ff | ||
|
|
084dd73f71 | ||
|
|
84b4643f1a | ||
|
|
41f2566b29 | ||
|
|
30ab2c013a | ||
|
|
d508a848f5 |
+168
-49
@@ -1,57 +1,176 @@
|
|||||||
# Savepoint — Mission Control 2.0 (MC2)
|
# Savepoint — Mem0-Ausbau (Referenzaufgabe)
|
||||||
|
|
||||||
_Stand: 2026-08-20, nach der Aufräum-Runde. Alle Zahlen hier sind **auf der Box gemessen**, nicht
|
**Stand:** 2026-08-22, Worktree `referenz/mem0-ausbau` in `F:\Coding Stuff\mc2-referenz`.
|
||||||
aus Doku übernommen._
|
Branch wurde aus `d508a84` (A2's gesicherter Stand) erstellt und enthält dessen 5 bearbeitete Dateien.
|
||||||
|
|
||||||
## Was das ist (in einem Satz)
|
## Auftrag
|
||||||
MC2 ist die **Web-Admin-Konsole** einer 100 % lokalen KI-Appliance („die Box", Bosgame M5 / AMD
|
`docs/aufgaben/referenzaufgabe-mem0-ausbau.md`: Mem0-Reste vollständig entfernen, ohne MC2/Frontend/Deployment zu brechen.
|
||||||
Strix Halo, 128 GB Unified RAM, llama.cpp-Vulkan). Sie verwaltet Engine, Modelle, Gedächtnis,
|
- 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.
|
||||||
Agenten-Status, Updates & Wartung — und dient als Steuerpult für die autonome KI („Lucy").
|
- **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/)
|
## Wichtig: A2-Vorgang (Commit d508a84 — bereits auf diesem Branch)
|
||||||
- **Backend** `backend/` — FastAPI, `:9001` als systemd-**User-Dienst** (reboot-fest, sudo-frei).
|
Diese 5 Dateien wurden vom abgebrochenen Lauf A2 bereits bearbeitet und im Commit `d508a84` gesichert:
|
||||||
- **Frontend** `frontend/` — React/Vite/Tailwind/shadcn. **`frontend/dist` ist ABSICHT im git**
|
1. `backend/config.py` — `MEMORY_DB` und `MC_MEM_DEDUPE_ENABLED` entfernt, `MEM0_SERVICE_URL`-Kommentar bereinigt
|
||||||
(Box hat kein Node; Backend liefert die gebauten Assets direkt aus).
|
2. `backend/routers/system.py` — `_mem0_reachable()` entfernt, `mem0_reachable` aus Systeminfo
|
||||||
- **MC2-Gateway** `:9010` (`/v1`-Datenpfad, model:auto Routing + native Bildweiche).
|
3. `backend/routers/voice.py` — `Mem0-Retrieve` → `Gedächtnis-Retrieve`
|
||||||
- **Modell-Router** `:8080` (llama-swap v250, Vulkan/RADV Engine b10502).
|
4. `backend/routers/zeitmaschine.py` — `Mem0` in Docstrings/Kommentaren → `Gedächtnis`
|
||||||
- **Hermes Agent** `:8642` (`hermes-gateway`, v0.20.4) + `hermes dashboard` auf `:9119` (nur Loopback).
|
5. `backend/services/sentry.py` — `MC_MEM0_DASHBOARD_URL` in Docstring entfernt
|
||||||
- **Sidecars:** `voice_service/` (:8650, faster-whisper STT + Piper TTS), `mcp/` (4 MCP-Server),
|
|
||||||
`client/hermes-pc` (PC-Executor :7777).
|
|
||||||
‼️ `mem0_service/` liegt noch im Baum, ist aber **stillgelegt** — `:8765` antwortet nicht.
|
|
||||||
|
|
||||||
## Modelle live (gemessen 20.08.2026)
|
`git status` war nach d508a84 clean.
|
||||||
| Rolle (Alias) | Modell | Tempo | Zustand |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `hermes` / `fast` | Qwen3.6-35B-A3B (MoE, DFlash) | **95,7 t/s** · Prompt 221 t/s | immer warm |
|
|
||||||
| `coder` | Qwen3.8-27B (dicht, Vision) | **12,6 t/s** · Prompt 144 t/s | ttl 5400, ~29 s Kaltstart |
|
|
||||||
| `debugger` | Muse-Glimmer-30B (DFlash) | — | ttl 600 |
|
|
||||||
| `heavy` | gpt-oss-120b | — | on-demand |
|
|
||||||
| `vision` | Qwen3-VL-30B-A3B-Instruct | — | on-demand |
|
|
||||||
| `embed` / `reranker` | Qwen3-Embedding-0.6B / Qwen3-Reranker-0.6B | — | immer warm |
|
|
||||||
|
|
||||||
Warm-Gruppe `brains` = embed + reranker + Qwen3.6-35B-A3B. Warm-Wächter weckt nur `fast`.
|
## 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
|
||||||
|
|
||||||
**Hintergrund zum Tempo:** Strix Halo liefert ~215 GB/s Speicherbandbreite. Ein *dichtes* Modell
|
## Erledigt in dieser Sitzung
|
||||||
muss pro Token seine vollen Gewichte lesen → 17 GB ÷ 215 GB/s ≈ 12,6 t/s ist das **Hardware-Limit**,
|
|
||||||
kein Konfigurationsfehler. MoE-Modelle lesen pro Token nur einen Bruchteil und sind darum ~8× schneller.
|
|
||||||
|
|
||||||
## Aufgeräumt am 20.08.2026
|
### 1. `backend/steward.py`
|
||||||
- CI-Ampel wieder **grün** (Lauf #77) — zwei ungenutzte `MEM0_SERVICE_URL`-Importe entfernt.
|
- Toter Import `from services import memory as memory_svc` entfernt
|
||||||
- Kaputter systemd-Timer `mc2-morgen-digest` abgeschaltet (der Hermes-Cron macht die Arbeit).
|
- Toter `memory_svc.AUTO_DEDUPE_ENABLED`-Block entfernt
|
||||||
- `mcp-stderr.log` (27 MB, 238k Ping-Zeilen) geleert.
|
- Docstring: `Mem0-Auto-Dedupe` → `Re-Warm-Wächter und Health-Wächter (sentry)`
|
||||||
- Config-Drift aufgelöst: Repo == `/etc/llama-swap/config.yaml`. Warm-Set-Idee geparkt auf
|
- Docstring: `memory.auto_dedupe_loop — Gedächtnis-Dubletten` entfernt
|
||||||
Branch `parked/warm-set-5d93822`.
|
- Docstring: `MC_MEM_DEDUPE_ENABLED` entfernt
|
||||||
- Entfernt: Governor-Reste, OpenCode-/aider-Reste (853 MB), 61 alte Backup-Dateien,
|
- Verifiziert: `memory.py` wurde in Kahlschlag gelöscht, `steward.py` hatte toten Import
|
||||||
verwaiste Modelle Devstral-Small-2 + GLM-4.6V-Flash + mem0 (21,5 GB) → `/srv/models` 209 → 189 GB.
|
|
||||||
- **Fähigkeiten repariert:** `orchestrator`- und `wartung`-Skill sowie `fremdblick.sh` zeigten auf
|
|
||||||
gelöschte Modelle (`Qwen3-Coder-Next`, `GLM-4.6V-Flash`, `GLM-4.7-Flash`) → der Kritiker-Gate war
|
|
||||||
stumm kaputt. Jetzt auf **Rollen-Aliase** umgestellt, damit Modellwechsel sie nicht mehr brechen.
|
|
||||||
- Alles Gelöschte liegt gesichert in `~/archiv-aufraeumen-20260820/` (11 MB).
|
|
||||||
|
|
||||||
## Offen
|
### 2. `backend/services/voice_metrics.py` (alle 9 Treffer bereinigt)
|
||||||
- `mem0_service/` + `_mem0_reachable()` in `backend/routers/system.py` ausbauen (toter Port).
|
- **Docstring (3x):** `Mem0-Retrieve` → `Gedächtnis-Retrieve`
|
||||||
- `~/.hermes/scripts/fremdblick.sh` ist **unversioniert** — gehört ins Repo.
|
- **Line 10:** `Mem0-Retrieve` im Best-effort-Kommentar → `Gedächtnis-Retrieve`
|
||||||
- MCP-Log dauerhaft deckeln (Hermes' `max_size_mb` greift für gekapertes stderr nicht).
|
- **Line 39:** `Mem0-Retrieve` im docstring → `Gedächtnis-Retrieve`
|
||||||
- Verschwundene Wächter: Ampel-Wächter, Prüfstand, Stack-Rundumschlag.
|
- **Line 56:** `Geparkten Mem0-Retrieve` → `Geparkten Gedächtnis-Retrieve`
|
||||||
- Coder-Tempo: Entwurfsmodelle für spekulatives Dekodieren liegen ungenutzt unter
|
- **Line 106:** `Unter-Detail: mem0 (Teil VON hirn...` → `Unter-Detail: memory (Teil VON hirn...`
|
||||||
`/srv/models/Qwen3.8-27B-DFlash2-GGUF/`.
|
- **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.
|
# 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_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
|
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.
|
# 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 —
|
# 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
|
# 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("/")
|
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
|
# 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
|
# /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:
|
def _read_hermes_env(key: str) -> str:
|
||||||
"""Liest einen Schlüssel aus ~/.hermes/.env (Fallback, falls nicht in der Prozess-Env).
|
"""Liest einen Schlüssel aus ~/.hermes/.env (Fallback, falls nicht in der Prozess-Env).
|
||||||
Der MC2-Dienst erbt die Hermes-Secrets sonst nicht."""
|
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")
|
HERMES_API_MODEL = os.environ.get("HERMES_API_MODEL", "hermes")
|
||||||
|
|
||||||
# --- Voice-Sidecar (STT faster-whisper + TTS Piper/Chatterbox) ---------------
|
# --- 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("/")
|
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
|
# 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
|
# (127.0.0.1:7682, base-path /console) und wird von MC2 über den ohnehin offenen Port 9001
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import httpx
|
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 fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from services import backup as backup_svc
|
from services import backup as backup_svc
|
||||||
@@ -42,13 +42,6 @@ def history(minutes: int = 60) -> dict:
|
|||||||
return metrics_history.history(minutes)
|
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:
|
def _voice_reachable() -> bool:
|
||||||
try:
|
try:
|
||||||
return httpx.get(f"{VOICE_SERVICE_URL}/health", timeout=2).status_code == 200
|
return httpx.get(f"{VOICE_SERVICE_URL}/health", timeout=2).status_code == 200
|
||||||
|
|||||||
@@ -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
|
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 +
|
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.
|
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.
|
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")
|
@router.get("/voice/trace")
|
||||||
def voice_trace(limit: int = 20) -> dict:
|
def voice_trace(limit: int = 20) -> dict:
|
||||||
"""Per-Turn-Trace: die letzten `limit` Chat-Turns mit Stufen-Breakdown (STT · Vision · Hirn ·
|
"""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."""
|
damit man den EINEN langsamen Turn sieht, den ein Durchschnitt verschluckt."""
|
||||||
return {"turns": get_trace(limit)}
|
return {"turns": get_trace(limit)}
|
||||||
|
|
||||||
@@ -289,7 +289,7 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
|
|||||||
headers["X-Hermes-Session-Key"] = body.session_key
|
headers["X-Hermes-Session-Key"] = body.session_key
|
||||||
|
|
||||||
async def gen():
|
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.
|
# (Rückruf während) zu EINEM Datensatz -> die Latenz-Ansicht zeigt den einzelnen Hänger.
|
||||||
trace = TurnTrace(session_id=body.session_id, kind="voice")
|
trace = TurnTrace(session_id=body.session_id, kind="voice")
|
||||||
first = True
|
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}
|
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,
|
# 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).
|
# 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"):
|
if os.environ.get("MC_VOICE_NO_THINK", "1") not in ("0", "false", "False"):
|
||||||
payload["chat_template_kwargs"] = {"enable_thinking": False}
|
payload["chat_template_kwargs"] = {"enable_thinking": False}
|
||||||
try:
|
try:
|
||||||
@@ -342,7 +342,7 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
|
|||||||
if first: # Time-To-First-Byte des Hermes-Streams (Verbindungs-Overhead)
|
if first: # Time-To-First-Byte des Hermes-Streams (Verbindungs-Overhead)
|
||||||
trace.note_ttfb()
|
trace.note_ttfb()
|
||||||
first = False
|
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.
|
# chat_ttfb misst nur den SSE-Start (~5 ms) und ist dafür blind.
|
||||||
if first_content and b'"content"' in chunk:
|
if first_content and b'"content"' in chunk:
|
||||||
trace.note_first_content()
|
trace.note_first_content()
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ def list_snapshots() -> dict:
|
|||||||
|
|
||||||
@router.get("/zeitmaschine/inhalt")
|
@router.get("/zeitmaschine/inhalt")
|
||||||
def snapshot_contents(file: str) -> dict:
|
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):
|
if not _FILE_RX.match(file):
|
||||||
raise HTTPException(400, "Ungültiger Snapshot-Name.")
|
raise HTTPException(400, "Ungültiger Snapshot-Name.")
|
||||||
p = backup_svc.BACKUP_DIR / file
|
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);
|
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.
|
Restore läuft bewusst nur per CLI (deploy/restore.sh) — siehe docs/BACKUP.md.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Health-Wächter der Box (Lucy-Proaktivität, Faden A3).
|
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
|
Voice-Sidecar, Platte) und meldet ZUSTANDSWECHSEL in den Melde-Briefkasten
|
||||||
(services/announce.py → Lucy spricht es) und via notify.sh (Telegram).
|
(services/announce.py → Lucy spricht es) und via notify.sh (Telegram).
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
**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
|
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
|
**Gedächtnis-Retrieve** läuft zwar in Hermes, ruft aber MC2s Memory-Endpoint per HTTP zurück → messbar
|
||||||
Unter-Detail INNERHALB der Hirn-Zeit ausgewiesen (kein Doppelzählen). Die **Tool-Runden** dagegen laufen
|
und als Unter-Detail INNERHALB der Hirn-Zeit ausgewiesen (kein Doppelzählen). Die **Tool-Runden**
|
||||||
im Hermes-LLM-Loop ohne Callback an MC2 → für MC2 unsichtbar, sie stecken im „Generierung"-Bucket.
|
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
|
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-
|
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.
|
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.
|
# Bekannte Stufen (für stabile UI-Reihenfolge); unbekannte werden trotzdem erfasst.
|
||||||
STAGES = ("stt", "vision", "memory_retrieve", "chat_ttfb", "chat_first_content", "tts")
|
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.
|
# (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}
|
_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:
|
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."""
|
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:
|
if ms is None or ms < 0 or kind not in _PARKED:
|
||||||
return
|
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:
|
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."""
|
Turns) und frisch ist."""
|
||||||
with _LOCK:
|
with _LOCK:
|
||||||
v = _PARKED.get("retrieve")
|
v = _PARKED.get("retrieve")
|
||||||
@@ -103,7 +104,7 @@ class TurnTrace:
|
|||||||
gehalten; `commit()` schreibt den Datensatz in den Ringpuffer UND speist die rollenden Stats.
|
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.
|
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:
|
def __init__(self, session_id: str = "", kind: str = "voice") -> None:
|
||||||
self.id = uuid.uuid4().hex[:8]
|
self.id = uuid.uuid4().hex[:8]
|
||||||
@@ -113,7 +114,7 @@ class TurnTrace:
|
|||||||
self.session_id = (session_id or "")[:24]
|
self.session_id = (session_id or "")[:24]
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.vision_ms: float | None = None # Bildschirm-Beschreibung (falls Bilder)
|
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.had_images = False
|
||||||
self.error: str | None = None
|
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
|
record_stage("chat_ttfb", (time.perf_counter() - self._brain0) * 1000.0) # SSE-Start (~5 ms), nur rollend
|
||||||
|
|
||||||
def note_first_content(self) -> None:
|
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
|
ms = (time.perf_counter() - self._brain0) * 1000.0
|
||||||
self.hirn_ms = round(ms, 1)
|
self.hirn_ms = round(ms, 1)
|
||||||
record_stage("chat_first_content", ms)
|
record_stage("chat_first_content", ms)
|
||||||
@@ -138,7 +139,7 @@ class TurnTrace:
|
|||||||
def commit(self) -> dict:
|
def commit(self) -> dict:
|
||||||
total = (time.perf_counter() - self.perf0) * 1000.0
|
total = (time.perf_counter() - self.perf0) * 1000.0
|
||||||
stt = _take_stt() # rollend bereits in /voice/stt erfasst
|
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.
|
# 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
|
gen = round(total - self.hirn_ms, 1) if self.hirn_ms is not None else None
|
||||||
rec = {
|
rec = {
|
||||||
@@ -151,7 +152,7 @@ class TurnTrace:
|
|||||||
"vision_ms": self.vision_ms,
|
"vision_ms": self.vision_ms,
|
||||||
"hirn_ms": self.hirn_ms,
|
"hirn_ms": self.hirn_ms,
|
||||||
"gen_ms": gen if (gen is None or gen >= 0) else 0.0,
|
"gen_ms": gen if (gen is None or gen >= 0) else 0.0,
|
||||||
"mem0_ms": mem0,
|
"memory_ms": memory,
|
||||||
"total_ms": round(total, 1),
|
"total_ms": round(total, 1),
|
||||||
"error": self.error,
|
"error": self.error,
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-9
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
MC2-Steward — die Wächter-Loops als EIGENER Prozess (UMBAU v3, P2).
|
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
|
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
|
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
|
sich prinzipbedingt nicht selbst melden. Hier laufen DIESELBEN Loops (unveränderte
|
||||||
@@ -10,15 +10,15 @@ Module) als eigener Mini-Dienst (mc2-steward.service, Restart=always):
|
|||||||
• warmer.rewarm_loop — Warm-Set nachladen (+ Config-Watch ersetzt den
|
• warmer.rewarm_loop — Warm-Set nachladen (+ Config-Watch ersetzt den
|
||||||
In-Process-Nudge aus llamaswap.write_config)
|
In-Process-Nudge aus llamaswap.write_config)
|
||||||
• sentry.sentry_loop — Health-Flanken → Briefkasten (HTTP an MC2) + Telegram;
|
• sentry.sentry_loop — Health-Flanken → Briefkasten (HTTP an MC2) + Telegram;
|
||||||
beobachtet im Steward-Modus AUCH MC2 selbst + mc2-gateway
|
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
|
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
|
/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
|
Steuerpult. Der Briefkasten-Store gehört weiter EXKLUSIV dem MC2-Prozess — dieser
|
||||||
Prozess liefert Meldungen per HTTP ab (announce.py, MC_ANNOUNCE_HTTP).
|
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
|
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.
|
Verhaltens-Code im Steuerpult angefasst. Zeilen dort entfernen = Rollback.
|
||||||
"""
|
"""
|
||||||
@@ -28,7 +28,6 @@ import logging
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from config import CONFIG_PATH
|
from config import CONFIG_PATH
|
||||||
from services import memory as memory_svc
|
|
||||||
from services import sentry, warmer
|
from services import sentry, warmer
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -66,10 +65,6 @@ async def main() -> None:
|
|||||||
warmer.INTERVAL, CONFIG_WATCH_S)
|
warmer.INTERVAL, CONFIG_WATCH_S)
|
||||||
if sentry.ENABLED:
|
if sentry.ENABLED:
|
||||||
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
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:
|
if not tasks:
|
||||||
log.warning("steward: alle Loops per Env deaktiviert — nichts zu tun, Ende.")
|
log.warning("steward: alle Loops per Env deaktiviert — nichts zu tun, Ende.")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ hint=""
|
|||||||
# Stabile Anti-Flail-Orientierung: WER bin ich, WO laufen die Modelle, WAS ist tabu.
|
# Stabile Anti-Flail-Orientierung: WER bin ich, WO laufen die Modelle, WAS ist tabu.
|
||||||
ctx="$(cat <<EOF
|
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.]
|
[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).
|
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.
|
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/.
|
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
|
done
|
||||||
if [ "$state" = "done" ]; then
|
if [ "$state" = "done" ]; then
|
||||||
SUMMARY+=("$name: $behind Commits eingespielt, Gehirn-Check grün.")
|
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
|
return
|
||||||
fi
|
fi
|
||||||
# Rot/Timeout: ERST Selbstreparatur versuchen (Config-Bruch selbst ziehen, neue Version behalten) —
|
# 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.
|
# 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.
|
# Läuft per systemd-Timer (täglich), manuell, oder über den UI-Snapshot-Button.
|
||||||
#
|
#
|
||||||
# Inhalt: mem0 (Chroma + history.db) · ~/.hermes (config.yaml, .env, plugins/) ·
|
# Inhalt: ~/.hermes (config.yaml, .env, plugins/) · /etc/llama-swap/config.yaml ·
|
||||||
# /etc/llama-swap/config.yaml · known-good/ (Versions-Manifest, s. u.)
|
# known-good/ (Versions-Manifest, s. u.)
|
||||||
# NICHT enthalten (bewusst): GGUF-Modelle (riesig, neu ladbar), MC2-Code (Git), venvs.
|
# 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.
|
# 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.
|
# 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="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}"
|
||||||
OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}"
|
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}"
|
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
|
||||||
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
||||||
TS="$(date +%Y%m%d-%H%M%S)"
|
TS="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
|
||||||
STAGE="$(mktemp -d)"
|
STAGE="$(mktemp -d)"
|
||||||
trap 'rm -rf "$STAGE"' EXIT
|
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/config.yaml" ] && cp -a "$HERMES/config.yaml" "$STAGE/hermes/" || true
|
||||||
[ -f "$HERMES/.env" ] && cp -a "$HERMES/.env" "$STAGE/hermes/" || true
|
[ -f "$HERMES/.env" ] && cp -a "$HERMES/.env" "$STAGE/hermes/" || true
|
||||||
[ -d "$HERMES/plugins" ] && cp -a "$HERMES/plugins" "$STAGE/hermes/plugins" || 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}"
|
HERMES_AGENT="${HERMES_AGENT_DIR:-$HOME/.hermes/hermes-agent}"
|
||||||
UV="$(command -v uv || echo "$HOME/.local/bin/uv")"
|
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
|
freeze_venv() { # $1 = python-Pfad, $2 = Zieldatei
|
||||||
if [ ! -x "$1" ]; then echo "venv fehlt: $1" > "$2"; return 0; fi
|
if [ ! -x "$1" ]; then echo "venv fehlt: $1" > "$2"; return 0; fi
|
||||||
"$1" -m pip freeze > "$2" 2>/dev/null \
|
"$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"
|
|| echo "pip freeze fehlgeschlagen: $1" > "$2"
|
||||||
}
|
}
|
||||||
freeze_venv "$SRC/backend/.venv/bin/python" "$KG/pip-backend.txt"
|
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"
|
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.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 "llama-swap : $(/usr/local/bin/llama-swap --version 2>/dev/null | head -1 || echo '?')"
|
||||||
echo "python-venvs :"
|
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')"
|
echo " $P → $("$P" --version 2>&1 || echo 'fehlt')"
|
||||||
done
|
done
|
||||||
} > "$KG/versions.txt" || true
|
} > "$KG/versions.txt" || true
|
||||||
@@ -92,7 +89,7 @@ cat > "$STAGE/MANIFEST.txt" <<EOF
|
|||||||
mc2-state backup
|
mc2-state backup
|
||||||
created : $TS
|
created : $TS
|
||||||
host : $(hostname)
|
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
|
restore : bash ~/mission-control-v2/deploy/restore.sh mc2-state-$TS.tar.gz
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# coder-vollkontext.sh — gibt dem Coder den ganzen Kontext (23.08.2026).
|
|
||||||
#
|
|
||||||
# WARUM: llama-swap startet den Coder mit `-c 131072 --parallel 2`. Das sind
|
|
||||||
# zwei Slots à 65.536 — und nur diese 65.536 stehen einer Sitzung zur Verfuegung.
|
|
||||||
# Der Coder hat aber genau EINEN Verbraucher: OpenChamber. Lucy benutzt `hermes`,
|
|
||||||
# der explore-Subagent auch, `review` benutzt `heavy`. Die zweite Bahn liegt brach.
|
|
||||||
#
|
|
||||||
# Mit `--parallel 1` bekommt eine Sitzung die vollen 131.072 — doppelt so viel
|
|
||||||
# Platz, halb so viel Komprimieren.
|
|
||||||
#
|
|
||||||
# ‼️ NICHT waehrend eines laufenden Auftrags ausfuehren: llama-swap laeuft mit
|
|
||||||
# --watch-config und laedt beim Speichern SOFORT neu. Das schneidet eine
|
|
||||||
# laufende Sitzung mitten im Satz ab.
|
|
||||||
#
|
|
||||||
# Aufruf: bash deploy/coder-vollkontext.sh (Trockenlauf, zeigt den Diff)
|
|
||||||
# bash deploy/coder-vollkontext.sh --ja (wendet an)
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
CONF="${LLAMA_SWAP_CONF:-/etc/llama-swap/config.yaml}"
|
|
||||||
BLOCK="${CODER_BLOCK:- Qwen3.8-27B:}"
|
|
||||||
|
|
||||||
if [ ! -r "$CONF" ]; then echo "ABBRUCH: $CONF nicht lesbar"; exit 1; fi
|
|
||||||
|
|
||||||
TMP="$(mktemp)"
|
|
||||||
cp "$CONF" "$TMP"
|
|
||||||
|
|
||||||
python3 - "$TMP" "$BLOCK" <<'PY'
|
|
||||||
import io, re, sys
|
|
||||||
pfad, block = sys.argv[1], sys.argv[2]
|
|
||||||
z = io.open(pfad, encoding="utf-8").read().split("\n")
|
|
||||||
try:
|
|
||||||
start = next(i for i, l in enumerate(z) if l.startswith(block))
|
|
||||||
except StopIteration:
|
|
||||||
print("ABBRUCH: Block '%s' nicht gefunden" % block); sys.exit(1)
|
|
||||||
ende = next((i for i in range(start + 1, len(z)) if re.match(r"^ [A-Za-z]", z[i])), len(z))
|
|
||||||
|
|
||||||
# Streng auf den Coder-Block begrenzt — `fast`/`hermes` MUESSEN ihre zwei Slots
|
|
||||||
# behalten, dort greifen Lucy und der Explorer wirklich gleichzeitig zu.
|
|
||||||
n_par = n_ctx = 0
|
|
||||||
for i in range(start, ende):
|
|
||||||
if "--parallel 2" in z[i]:
|
|
||||||
z[i] = z[i].replace("--parallel 2", "--parallel 1"); n_par += 1
|
|
||||||
if z[i].strip() == "context: 65536":
|
|
||||||
z[i] = z[i].replace("65536", "131072"); n_ctx += 1
|
|
||||||
|
|
||||||
if n_par != 1 or n_ctx != 1:
|
|
||||||
print(f"ABBRUCH: unerwartet (parallel {n_par}x, context {n_ctx}x) — Konfiguration hat sich geaendert")
|
|
||||||
sys.exit(1)
|
|
||||||
io.open(pfad, "w", encoding="utf-8").write("\n".join(z))
|
|
||||||
PY
|
|
||||||
if [ $? -ne 0 ]; then rm -f "$TMP"; exit 1; fi
|
|
||||||
|
|
||||||
echo "=== Aenderung ==="
|
|
||||||
diff "$CONF" "$TMP"
|
|
||||||
echo
|
|
||||||
|
|
||||||
if [ "${1:-}" != "--ja" ]; then
|
|
||||||
echo "TROCKENLAUF — nichts geaendert. Anwenden mit: bash $0 --ja"
|
|
||||||
rm -f "$TMP"; exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
SICHER="${CONF}.bak-$(date +%Y%m%d-%H%M%S)"
|
|
||||||
sudo -n cp "$CONF" "$SICHER" && echo "Sicherung: $SICHER"
|
|
||||||
sudo -n cp "$TMP" "$CONF" && echo "angewendet — llama-swap laedt durch --watch-config selbst neu"
|
|
||||||
rm -f "$TMP"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "Warte auf den Neustart des Coders..."
|
|
||||||
sleep 8
|
|
||||||
curl -s -m 15 "http://127.0.0.1:8080/v1/models" >/dev/null 2>&1 \
|
|
||||||
&& echo "llama-swap antwortet wieder" \
|
|
||||||
|| echo "ACHTUNG: llama-swap antwortet nicht — Sicherung liegt unter $SICHER"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "Danach in ~/.config/opencode/opencode.json auf dem PC nachziehen:"
|
|
||||||
echo " coder: limit.context 65536 -> 131072"
|
|
||||||
@@ -1,28 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Post-Update-Check nach einem Hermes-Agent-Update: läuft unser geteiltes „Gehirn"
|
# 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.
|
# im UI rot, damit ein kaputtes Gehirn sofort auffällt.
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
MC_URL="${MC_URL:-http://127.0.0.1:9001}"
|
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}"
|
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
||||||
fail=0
|
fail=0
|
||||||
|
|
||||||
echo "=== Hermes Post-Update: Gehirn-Check ==="
|
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 \
|
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
|
&& grep -qE "memory_enabled:[[:space:]]*true" "$HERMES/config.yaml" 2>/dev/null; then
|
||||||
echo "PASS · memory.provider=mc2-memory aktiv"
|
echo "PASS · memory.provider=mc2-memory aktiv"
|
||||||
|
|||||||
@@ -1,199 +0,0 @@
|
|||||||
# Die drei Jobs (KISS-Umbau, 21.08.2026)
|
|
||||||
|
|
||||||
Vorher: **4 Hermes-Crons + 6 systemd-Timer**, verteilt auf zwei Mechanismen.
|
|
||||||
Deshalb fiel am 20.08. tagelang niemandem auf, dass ein Waechter fehlte —
|
|
||||||
niemand schaut an zwei Orten nach.
|
|
||||||
|
|
||||||
Jetzt: **drei Jobs, ein Ort.** `hermes cron list` zeigt die gesamte Automatik.
|
|
||||||
|
|
||||||
| Job | Wann | Art | Skript |
|
|
||||||
|---|---|---|---|
|
|
||||||
| **Daily News Report** | taeglich 07:00 | Agent + Websuche | — |
|
|
||||||
| **KI und Stack Radar** | samstags 08:00 | `--no-agent` | `stack-radar.sh` → `stack-ist.sh` |
|
|
||||||
| **Updates am Sonntag** | sonntags 04:30 | `--no-agent` | `sonntags-update.sh` → `autoupdate.sh` |
|
|
||||||
|
|
||||||
Daneben laufen als stille Rohrleitung weiter: `mc2-backup` (03:33) und
|
|
||||||
`projekte-sync` (stuendlich). Die melden sich nie, die sichern und synchronisieren nur.
|
|
||||||
|
|
||||||
## Der Entwurfsgrundsatz
|
|
||||||
|
|
||||||
> **Fakten sammelt ein Skript, Prosa schreibt das Modell.**
|
|
||||||
|
|
||||||
Der alte Selbsttest war am 20.08. gruen, waehrend vier Dinge kaputt waren: das
|
|
||||||
Kritiker-Gate zeigte seit einem Tag auf ein umbenanntes Modell (404), Lucys
|
|
||||||
`SOUL.md` war blockiert, das Dashboard stand ohne Passwort offen, die CI war rot.
|
|
||||||
Er hatte geprueft, ob Dienste **antworten** — nicht, ob sie **stimmen**.
|
|
||||||
|
|
||||||
`stack-ist.sh` prueft deshalb Ergebnisse: loesen die Rollen-Aliase noch auf? Ist
|
|
||||||
der letzte Timer-Lauf gutgegangen? Ist eine Sicherung juenger als zwei Tage?
|
|
||||||
Liefert die oeffentliche Adresse Daten ohne Anmeldung? Beim ersten Lauf hat es
|
|
||||||
sofort zwei echte Befunde gefunden.
|
|
||||||
|
|
||||||
## Kugelsicher heisst konkret
|
|
||||||
|
|
||||||
- **Das Modell steht nicht im kritischen Pfad.** Antwortet es nicht, gehen die
|
|
||||||
Rohbefunde trotzdem raus. Nur die Prosa faellt weg, nie die Meldung.
|
|
||||||
- **Es meldet immer.** „Alles gruen" ist ein Ergebnis, kein Grund zu schweigen.
|
|
||||||
- **Kein `set -e`.** Ein einzelner fehlschlagender Test darf den Bericht nicht
|
|
||||||
abschneiden.
|
|
||||||
- **Ein Melderweg:** `deploy/notify.sh` erreicht Telegram **und** Lucys
|
|
||||||
Briefkasten (aus dem sie spricht) in einem Aufruf. Die Jobs laufen mit
|
|
||||||
`--deliver local`, damit Hermes nicht ein zweites Mal sendet.
|
|
||||||
- **Kein Fuellmaterial.** Die Prompts verbieten ausgedachte Vorschlaege
|
|
||||||
ausdruecklich — ein Bericht mit Fuellmaterial wird nicht gelesen, und dann
|
|
||||||
auch der echte Befund nicht.
|
|
||||||
|
|
||||||
## Ausbringen
|
|
||||||
|
|
||||||
Die Skripte muessen unter `~/.hermes/scripts/` liegen (Vorgabe von `hermes cron --script`).
|
|
||||||
Quelle ist dieses Verzeichnis:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scp deploy/jobs/*.sh hitonabi@192.168.178.151:/home/hitonabi/.hermes/scripts/
|
|
||||||
ssh hitonabi@192.168.178.151 'cd ~/.hermes/scripts && sed -i "s/\r$//" *.sh && chmod +x *.sh'
|
|
||||||
```
|
|
||||||
|
|
||||||
‼️ Das `sed` ist Pflicht: vom Windows-PC kopierte Dateien haben CRLF, und bash
|
|
||||||
scheitert daran mit unverstaendlichen Meldungen.
|
|
||||||
|
|
||||||
## Abgeschaltet am 21.08.
|
|
||||||
|
|
||||||
| Weg | Warum |
|
|
||||||
|---|---|
|
|
||||||
| Cron `morgen-digest` | geht im Daily News Report auf |
|
|
||||||
| Cron `tech-radar` | geht im Stack Radar auf |
|
|
||||||
| Cron `nacht-wartung` | pruefte Lebenszeichen, nicht Ergebnisse |
|
|
||||||
| Cron `wissens-sync` | **war nie gelaufen** — kein `last_run_at` |
|
|
||||||
| Timer `mc2-bagatell` | **15 Naechte hintereinander „0 eingespielt"** |
|
|
||||||
| Timer `mc2-selfsmoke` | geht in `stack-ist.sh` auf |
|
|
||||||
| Timer `pbs-backup` | doppelt zu `mc2-backup` und seit 21.08. rot (`/srv/models/mem0` gibt es nicht mehr) |
|
|
||||||
| Timer `mc2-autoupdate` | wird jetzt vom Cron „Updates am Sonntag" gestartet |
|
|
||||||
|
|
||||||
Die Unit-Dateien liegen noch da, nur `disable`d — Rueckbau ist ein Befehl.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Nachtrag 21.08.: Persona, Quellen-Links und Stimme
|
|
||||||
|
|
||||||
**Fehler, den ich gemacht hatte:** Mein erster Prompt fuer den Daily News Report
|
|
||||||
schrieb woertlich *„keine Emojis, keine Aufzaehlungspunkte, keine
|
|
||||||
Ueberschriften-Deko"* — und hat damit genau das wegoptimiert, was den Bericht
|
|
||||||
vorher gut machte.
|
|
||||||
|
|
||||||
★★ **Die Formatierung kam nie aus dem Job-Prompt.** Die alten Prompts waren
|
|
||||||
kurz (*„eine praegnante 3-Punkte-Zusammenfassung an den Commander"*). Emojis,
|
|
||||||
Ton und die Anrede stehen in **`~/.hermes/SOUL.md`** — Lucys Persona:
|
|
||||||
|
|
||||||
> Du sprichst den Nutzer IMMER mit **Commander** an.
|
|
||||||
> Locker, herzlich, schlagfertig, charmant, selbstbewusst.
|
|
||||||
|
|
||||||
**Lehre: den Job-Prompt kurz halten und die Persona arbeiten lassen.** Wer im
|
|
||||||
Prompt Stil verbietet, ueberstimmt die Persona — und merkt es erst, wenn die
|
|
||||||
Nachricht seelenlos ankommt.
|
|
||||||
|
|
||||||
### Stimme in Telegram
|
|
||||||
|
|
||||||
`hermes send` kann **`MEDIA:<pfad>`**. Damit geht eine fertige Audiodatei als
|
|
||||||
Anhang nach Telegram. `deploy/jobs/news-melden.sh` macht daraus einen Aufruf:
|
|
||||||
|
|
||||||
```
|
|
||||||
Agent schreibt zwei Dateien
|
|
||||||
/tmp/news-text.md (Emojis, Links, Formatierung) -> notify.sh -> Telegram + Briefkasten
|
|
||||||
/tmp/news-sprich.txt (3-4 Saetze, nichts Vorlesbares fehlt) -> :8650/tts -> WAV -> hermes send MEDIA:
|
|
||||||
```
|
|
||||||
|
|
||||||
‼️ **Nicht Hermes' eingebautes TTS nehmen.** Das steht auf `tts.provider: edge`
|
|
||||||
mit `en-US-AriaNeural` — englisch. Lucys echte Stimme ist MC2s `voice-service`
|
|
||||||
auf `:8650` (Piper `de_DE-thorsten-medium`). Deshalb ruft das Skript den
|
|
||||||
Sidecar direkt per curl.
|
|
||||||
|
|
||||||
‼️ **Kein ffmpeg noetig.** Telegram nimmt die WAV direkt an. Fuer eine echte
|
|
||||||
Sprachnachricht mit Wellenform braeuchte es OGG/Opus und damit ffmpeg — bewusst
|
|
||||||
nicht installiert, eine Abhaengigkeit weniger.
|
|
||||||
|
|
||||||
Der Agent ruft **einen** Befehl auf, alles danach ist deterministisch. Faellt
|
|
||||||
die Stimme aus, geht der Text trotzdem raus.
|
|
||||||
|
|
||||||
### ‼️ Folgefehler der Werkzeug-Abschaltung — gefunden und behoben
|
|
||||||
|
|
||||||
Nach dem Abschalten von `kanban` und `delegation` standen in `SOUL.md` noch
|
|
||||||
zwei Anweisungen, die ins Leere zeigten: *„rufst du sofort dein Werkzeug
|
|
||||||
delegate_task auf"* und *„Ideen traegst du sofort im Kanban-Auftragsbuch ein"*.
|
|
||||||
Genau die Klasse stiller Defekt, die am 19.08. das Kritiker-Gate zerlegt hat.
|
|
||||||
Beide Abschnitte ersetzt (Sicherung: `~/.hermes/SOUL.md.bak-20260821`).
|
|
||||||
|
|
||||||
**Merke: wer einen Werkzeugsatz abschaltet, muss `SOUL.md` mitlesen.**
|
|
||||||
|
|
||||||
### Kugelsicher-Regeln, die dazugekommen sind
|
|
||||||
|
|
||||||
- `hermes cron edit <id> "text"` **speichert nichts** — der Prompt muss ueber
|
|
||||||
**`--prompt`** kommen. Ohne Flag gibt der Befehl den Text nur aus und die
|
|
||||||
alte Fassung bleibt stehen. Nach jeder Aenderung in `jobs.json` nachsehen.
|
|
||||||
- Alte Job-Fassungen liegen im Zustands-Backup:
|
|
||||||
`tar -xzf /srv/models/mc2-backups/mc2-state-*.tar.gz ./hermes/cron/jobs.json`
|
|
||||||
- Tagesaktualitaet muss man erzwingen: Datum per `date` feststellen lassen,
|
|
||||||
mehrere Suchen verlangen, und Meldungen ohne belegbares Datum verwerfen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Nachtrag 3 (21.08.): Lucys ECHTE Stimme, echte Sprachnachricht
|
|
||||||
|
|
||||||
### Der Fehler: :8650 ist nicht Lucy
|
|
||||||
|
|
||||||
Ich hatte `/tts` auf MC2s `voice-service` (`:8650`) ohne Angabe von Engine und
|
|
||||||
Stimme aufgerufen. Dessen `/health` sagt:
|
|
||||||
|
|
||||||
```
|
|
||||||
"engines":["elevenlabs","edge"]
|
|
||||||
```
|
|
||||||
|
|
||||||
Piper und Chatterbox sind dort **gar nicht geladen** — die Vorgabe fiel auf die
|
|
||||||
erste verfuegbare: ElevenLabs *„Artoria DE · Saber · Hermes-Stimme"*. Das ist
|
|
||||||
**Hermes' Stimme**, nicht Lucys. Deutsch und weiblich, deshalb faellt es nicht
|
|
||||||
sofort auf. Beide verfuegbaren Engines sind ausserdem **Cloud** — gegen die
|
|
||||||
100-%-lokal-Praemisse.
|
|
||||||
|
|
||||||
### Lucys Stimme: `lucy-stimme.service` auf `:8021`
|
|
||||||
|
|
||||||
| | |
|
|
||||||
|---|---|
|
|
||||||
| Engine | **Kyutai pocket-tts 2.1.0** (neueste, seit 04.05. unveraendert) |
|
|
||||||
| Modell | **`german_24l`** — die volle Fassung, nicht die destillierte |
|
|
||||||
| Stimme | geklont aus `ref.mp3`, gecacht in `lucy_voice.safetensors` (44 MB) |
|
|
||||||
| Laeuft | `~/.lucy-stimme/`, systemd-**user**-Dienst, `enable`d, CPU |
|
|
||||||
| Start | ~48 s Ladezeit, danach ~5 s fuer 3,7 s Audio |
|
|
||||||
|
|
||||||
Mitgezogen wurde die **ganze Abstimmung**, nicht nur das Modell: `text_norm.py`
|
|
||||||
(Symbole/Pfade/URLs → Zahlen → Akronyme), Emotions-Voreinstellungen mit
|
|
||||||
Anlaufwoertern, Umlaut-Wortliste, Hochpass auf der Referenz, kalibrierter Pegel.
|
|
||||||
Ohne die klingt pocket nicht wie Lucy.
|
|
||||||
|
|
||||||
‼️ Der Sweep im TTS-Plan (*„seriell schlaegt parallel"*) wurde auf einem **9700X**
|
|
||||||
gemessen. Die Box ist ein Ryzen AI MAX+ 395 — das Ergebnis ist **nicht
|
|
||||||
uebertragen**, nur uebernommen. Wer Tempo braucht, misst neu.
|
|
||||||
|
|
||||||
### Recherche 21.08.: pocket bleibt
|
|
||||||
|
|
||||||
Nichts seit Mai schlaegt es auf dieser Achse. Die Alternativen sind 5–17× groesser:
|
|
||||||
Qwen3-TTS 0,6–1,7 Mrd. (Apache 2.0, Deutsch, 3-Sekunden-Klon), NeuTTS Air 0,5 Mrd.
|
|
||||||
(GGUF), CosyVoice2 0,5 Mrd. — gegen pockets **100 Mio.** Fuer einen taeglichen
|
|
||||||
Sprachnachrichten-Job auf CPU ist das der falsche Handel.
|
|
||||||
**Ihr wart bereits auf dem neuesten Stand** — nichts zu aktualisieren.
|
|
||||||
|
|
||||||
### Echte Sprachnachricht braucht OGG/Opus
|
|
||||||
|
|
||||||
Eine WAV kommt in Telegram als **Dateianhang** an. Fuer das runde Sprachmemo mit
|
|
||||||
Wellenform braucht es OGG/Opus — dafuer wurde **ffmpeg installiert** (8.0.1):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ffmpeg -y -i ton.wav -c:a libopus -b:a 32k -ar 48000 -ac 1 ton.ogg
|
|
||||||
hermes send --to telegram "MEDIA:ton.ogg"
|
|
||||||
```
|
|
||||||
|
|
||||||
Ganze Kette (Text + Stimme + Versand) gemessen: **10 Sekunden**.
|
|
||||||
|
|
||||||
### Altlast am Rande
|
|
||||||
|
|
||||||
`F:\Coding Stuff\lucy\lucy-tts\Lucy-Startklar.bat` zeigt auf
|
|
||||||
`mission-control-2\client\lucy-tts` — den Ordner gibt es seit der Repo-Trennung
|
|
||||||
nicht mehr.
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# news-melden.sh — schickt den fertigen Nachrichtenbericht als TEXT und als
|
|
||||||
# SPRACHNACHRICHT an den Commander (21.08.2026, überarbeitet 22.08.).
|
|
||||||
#
|
|
||||||
# Der Agent ruft genau EINEN Befehl auf. Alles danach ist deterministisch —
|
|
||||||
# Zustellung darf nicht davon abhaengen, ob ein Modell die Reihenfolge einhaelt.
|
|
||||||
#
|
|
||||||
# ‼️ WARUM DAS SKRIPT SOFORT ZURUECKKEHRT (Fund vom 22.08., erster echter Lauf):
|
|
||||||
# Hermes' terminal-Werkzeug bricht nach 30 s ab. Die Sprachsynthese dauert
|
|
||||||
# laenger. Der Agent sah den Timeout, hielt den Aufruf fuer gescheitert und
|
|
||||||
# rief das Skript ein zweites Mal auf — der Text ging DOPPELT raus
|
|
||||||
# (07:01:42 und 07:02:14).
|
|
||||||
# Deshalb: Text senden, Stimme abgekoppelt im Hintergrund, sofort raus.
|
|
||||||
# Dazu eine Doppelversand-Sperre, die auch kuenftige Wiederholungen abfaengt.
|
|
||||||
#
|
|
||||||
# Erwartet zwei Dateien, die der Agent vorher geschrieben hat:
|
|
||||||
# $1 Textfassung (mit Emojis, Links, Formatierung — geht nach Telegram)
|
|
||||||
# $2 Sprechfassung (kurz, ohne Links/Emojis — wird vorgelesen)
|
|
||||||
#
|
|
||||||
# ‼️ STIMME: :8021, nicht :8650.
|
|
||||||
# :8021 = lucy-stimme.service — Kyutai pocket-tts, Modell `german_24l`,
|
|
||||||
# Stimme geklont aus ref.mp3. DAS ist Lucy. Lokal, auf der CPU.
|
|
||||||
# :8650 = MC2s voice-service. Dort sind nur `elevenlabs` und `edge` geladen —
|
|
||||||
# beides CLOUD, und die Vorgabe ist die ElevenLabs-Stimme
|
|
||||||
# "Artoria/Saber", also HERMES' Stimme, nicht Lucys.
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
NOTIFY="${NOTIFY:-$HOME/mission-control-v2/deploy/notify.sh}"
|
|
||||||
VOICE="${VOICE_URL:-http://127.0.0.1:8021}"
|
|
||||||
HERMES="${HERMES_BIN:-$HOME/.local/bin/hermes}"
|
|
||||||
TON="/tmp/news-stimme.wav"
|
|
||||||
TON_OGG="/tmp/news-stimme.ogg"
|
|
||||||
|
|
||||||
# ---------------------------------------------- Zweiter Durchgang: Stimme ---
|
|
||||||
# Wird vom ersten Durchgang abgekoppelt neu gestartet. Laeuft beliebig lange,
|
|
||||||
# ohne dass irgendein Werkzeug-Timeout hineinredet.
|
|
||||||
if [ "${1:-}" = "--nur-stimme" ]; then
|
|
||||||
SPRECHDATEI="${2:-/tmp/news-sprich.txt}"
|
|
||||||
[ -s "$SPRECHDATEI" ] || exit 0
|
|
||||||
|
|
||||||
CODE=$(curl -s -m 600 -X POST "$VOICE/tts" \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
--data "$(python3 -c 'import json,sys; print(json.dumps({"text": open(sys.argv[1], encoding="utf-8").read()[:1500]}))' "$SPRECHDATEI")" \
|
|
||||||
-o "$TON" -w '%{http_code}' 2>/dev/null)
|
|
||||||
|
|
||||||
[ "$CODE" = "200" ] && [ -s "$TON" ] || exit 0
|
|
||||||
|
|
||||||
# Telegram zeigt eine ECHTE Sprachnachricht (runde Blase, Wellenform) nur bei
|
|
||||||
# OGG/Opus. Eine WAV kaeme als Dateianhang an. Mono/48k ist das erwartete Format.
|
|
||||||
if ! ffmpeg -y -loglevel error -i "$TON" -c:a libopus -b:a 32k -ar 48000 -ac 1 "$TON_OGG" 2>/dev/null; then
|
|
||||||
TON_OGG="$TON"
|
|
||||||
fi
|
|
||||||
"$HERMES" send --to telegram "MEDIA:$TON_OGG" >/dev/null 2>&1
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ------------------------------------------- Erster Durchgang: Text + Start ---
|
|
||||||
TEXTDATEI="${1:-/tmp/news-text.md}"
|
|
||||||
SPRECHDATEI="${2:-/tmp/news-sprich.txt}"
|
|
||||||
|
|
||||||
if [ ! -s "$TEXTDATEI" ]; then
|
|
||||||
bash "$NOTIFY" -s "[News]" "Der Nachrichtenbericht ist leer geblieben ($TEXTDATEI). Da lief etwas schief."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Doppelversand-Sperre. Greift, egal WARUM ein zweiter Aufruf kommt —
|
|
||||||
# Timeout, Modell-Wiederholung, versehentlicher Handstart.
|
|
||||||
MARKE="/tmp/.news-gesendet-$(sha256sum "$TEXTDATEI" | cut -c1-16)"
|
|
||||||
if [ -f "$MARKE" ]; then
|
|
||||||
ALTER=$(( $(date +%s) - $(stat -c %Y "$MARKE" 2>/dev/null || echo 0) ))
|
|
||||||
if [ "$ALTER" -lt 7200 ]; then
|
|
||||||
echo "gemeldet (dieser Bericht ging vor ${ALTER}s bereits raus — nicht doppelt geschickt)"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
: > "$MARKE"
|
|
||||||
find /tmp -maxdepth 1 -name '.news-gesendet-*' -mtime +2 -delete 2>/dev/null
|
|
||||||
|
|
||||||
bash "$NOTIFY" -s "" "$(cat "$TEXTDATEI")"
|
|
||||||
|
|
||||||
# Stimme abkoppeln: eigene Sitzung, kein Elternprozess, keine offenen Deskriptoren.
|
|
||||||
# Damit ueberlebt sie das Ende dieses Skripts und blockiert es nicht.
|
|
||||||
if [ -s "$SPRECHDATEI" ]; then
|
|
||||||
setsid nohup bash "$0" --nur-stimme "$SPRECHDATEI" >/dev/null 2>&1 < /dev/null &
|
|
||||||
disown 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "gemeldet"
|
|
||||||
exit 0
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# sonntags-update.sh — Job 2: "Updates am Sonntag" (21.08.2026).
|
|
||||||
#
|
|
||||||
# Duenner Aufsatz auf deploy/autoupdate.sh. Bewusst KEIN Neubau: autoupdate.sh
|
|
||||||
# macht seit Juli genau das Richtige — Router -> Engine -> Hermes, jede Ebene
|
|
||||||
# mit Update-Pruefung, Postcheck, und bei Rot automatischem Rueckbau plus
|
|
||||||
# Selbst-Pinnung. Es meldet auch schon selbst ueber notify.sh, also Telegram
|
|
||||||
# UND Lucys Stimme.
|
|
||||||
#
|
|
||||||
# Dieser Aufsatz existiert nur, damit der Job unter `hermes cron list` steht
|
|
||||||
# statt in einem separaten systemd-Timer. Ein Ort fuer alle Automatik — genau
|
|
||||||
# deshalb ist am 20.08. tagelang niemandem aufgefallen, dass ein Waechter fehlte.
|
|
||||||
#
|
|
||||||
# Aufruf ueber Hermes-Cron:
|
|
||||||
# hermes cron create '30 4 * * 0' --name 'Updates am Sonntag' \
|
|
||||||
# --no-agent --script sonntags-update.sh --deliver local
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
AUTOUPDATE="${AUTOUPDATE:-$HOME/mission-control-v2/deploy/autoupdate.sh}"
|
|
||||||
NOTIFY="${NOTIFY:-$HOME/mission-control-v2/deploy/notify.sh}"
|
|
||||||
|
|
||||||
melde() { bash "$NOTIFY" -s "[Box-Update]" "$1" 2>/dev/null || echo "$1" >&2; }
|
|
||||||
|
|
||||||
if [ ! -f "$AUTOUPDATE" ]; then
|
|
||||||
melde "Sonntags-Update ausgefallen: $AUTOUPDATE gibt es nicht. Nichts wurde aktualisiert."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# autoupdate.sh meldet waehrend des Laufs selbst (inkl. der KRITISCH-Faelle, in
|
|
||||||
# denen ein Bericht am Ende zu spaet kaeme). Wir fangen hier nur den Fall ab,
|
|
||||||
# dass es gar nicht erst durchlaeuft — sonst waere das Schweigen zweideutig.
|
|
||||||
AUSGABE="$(bash "$AUTOUPDATE" 2>&1)"
|
|
||||||
CODE=$?
|
|
||||||
|
|
||||||
if [ "$CODE" -ne 0 ]; then
|
|
||||||
melde "Sonntags-Update mit Fehler beendet (exit $CODE). Letzte Zeilen:
|
|
||||||
|
|
||||||
$(printf '%s\n' "$AUSGABE" | tail -15)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Erfolgsfall: autoupdate.sh hat bereits gemeldet. Leere Ausgabe -> Hermes
|
|
||||||
# schweigt, also genau eine Nachricht statt zwei.
|
|
||||||
exit 0
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# stack-ist.sh — der IST-Zustand der Box, in Fakten (21.08.2026).
|
|
||||||
#
|
|
||||||
# Wird von Hermes-Cron "stack-radar" ueber --script eingespeist: die Ausgabe hier
|
|
||||||
# landet im Prompt des Agenten, der daraus den Bericht schreibt.
|
|
||||||
#
|
|
||||||
# GRUNDSATZ: Dieses Skript prueft ERGEBNISSE, nicht Lebenszeichen.
|
|
||||||
# Der alte Selbsttest war am 20.08. gruen, waehrend vier Dinge kaputt waren:
|
|
||||||
# das Kritiker-Gate zeigte auf ein umbenanntes Modell (404), Lucys SOUL.md war
|
|
||||||
# blockiert, das Dashboard stand ohne Passwort offen, die CI war rot.
|
|
||||||
# "Dienst antwortet" haette das nie gefunden. Deshalb wird hier nachgesehen,
|
|
||||||
# ob Dinge STIMMEN — nicht ob sie laufen.
|
|
||||||
#
|
|
||||||
# Nie mit set -e: ein einzelner fehlschlagender Test darf den Bericht nicht
|
|
||||||
# abschneiden. Fehler werden gemeldet, nicht verschwiegen.
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
MC2="${MC2_URL:-http://127.0.0.1:9001}"
|
|
||||||
SWAP="${SWAP_URL:-http://127.0.0.1:8080}"
|
|
||||||
GITEA="${GITEA_URL:-http://192.168.178.153:3000}"
|
|
||||||
OEFFENTLICH="${OEFFENTLICH_URL:-https://mc.tobisniceshomelab.ddnsfree.com}"
|
|
||||||
|
|
||||||
ok() { printf ' OK %s\n' "$*"; }
|
|
||||||
warn() { printf ' ACHTUNG %s\n' "$*"; }
|
|
||||||
bad() { printf ' ROT %s\n' "$*"; }
|
|
||||||
|
|
||||||
echo "IST-ZUSTAND DER BOX — $(date '+%d.%m.%Y %H:%M')"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- Dienste ---
|
|
||||||
echo "DIENSTE"
|
|
||||||
# systemd stellt gescheiterten Units ein "●" voran — das muss weg, sonst meldet
|
|
||||||
# der Bericht lauter Aufzaehlungspunkte als kaputte Dienste.
|
|
||||||
# EGAL: Units, die dauerhaft scheitern und niemanden stoeren. Ein Bericht, der
|
|
||||||
# jeden Tag denselben harmlosen Fehler zeigt, erzieht zum Wegsehen.
|
|
||||||
EGAL="at-spi-dbus-bus.service"
|
|
||||||
fehl=$(systemctl --user list-units --type=service --state=failed --no-legend --no-pager 2>/dev/null \
|
|
||||||
| sed 's/^[^a-zA-Z]*//' | awk '{print $1}')
|
|
||||||
gefunden=0
|
|
||||||
for d in $fehl; do
|
|
||||||
case " $EGAL " in *" $d "*) continue ;; esac
|
|
||||||
bad "Dienst gescheitert: $d"; gefunden=1
|
|
||||||
done
|
|
||||||
[ "$gefunden" = "0" ] && ok "kein Dienst im Fehlerzustand"
|
|
||||||
|
|
||||||
for d in mc2-gateway mission-control-2 hermes-gateway voice-service; do
|
|
||||||
systemctl --user is-active --quiet "$d.service" 2>/dev/null \
|
|
||||||
&& ok "$d laeuft" || bad "$d laeuft NICHT"
|
|
||||||
done
|
|
||||||
systemctl is-active --quiet llama-swap.service 2>/dev/null \
|
|
||||||
&& ok "llama-swap laeuft" || bad "llama-swap laeuft NICHT"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ Timer ---
|
|
||||||
# Nicht "ist der Timer aktiv", sondern "ist der letzte Lauf gutgegangen".
|
|
||||||
echo "TIMER — letzter Lauf"
|
|
||||||
for t in $(systemctl --user list-timers --no-legend --no-pager 2>/dev/null | awk '{print $NF}'); do
|
|
||||||
dienst="${t%.service}.service"
|
|
||||||
case "$dienst" in launchpadlib-*) continue ;; esac # Ubuntu-Eigenes, nicht unseres
|
|
||||||
code=$(systemctl --user show -p ExecMainStatus --value "$dienst" 2>/dev/null)
|
|
||||||
wann=$(systemctl --user show -p ExecMainExitTimestamp --value "$dienst" 2>/dev/null | cut -d' ' -f2,3)
|
|
||||||
if [ -z "$wann" ]; then warn "$dienst ist eingerichtet, aber NIE gelaufen"
|
|
||||||
elif [ "${code:-0}" = "0" ]; then ok "$dienst zuletzt $wann"
|
|
||||||
else bad "$dienst zuletzt GESCHEITERT (exit $code) am $wann"; fi
|
|
||||||
done
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --------------------------------------------------------- Modell-Aliase ---
|
|
||||||
# Der Fund vom 19./20.08.: Skripte und Configs nennen Modelle beim Eigennamen,
|
|
||||||
# das Modell wird umbenannt, und alles laeuft still in 404. Hier wird geprueft,
|
|
||||||
# ob jeder Alias, den irgendwer benutzt, heute noch antwortet.
|
|
||||||
echo "MODELL-ALIASE (Rollen muessen aufloesen)"
|
|
||||||
liste=$(curl -s -m 10 "$MC2/v1/models" 2>/dev/null \
|
|
||||||
| python3 -c 'import sys,json; print(" ".join(m["id"] for m in json.load(sys.stdin)["data"]))' 2>/dev/null)
|
|
||||||
if [ -z "$liste" ]; then bad "Modell-Liste nicht abrufbar ueber $MC2/v1"
|
|
||||||
else
|
|
||||||
ok "verfuegbar: $liste"
|
|
||||||
for rolle in hermes fast heavy; do
|
|
||||||
case " $liste " in
|
|
||||||
*" $rolle "*) ok "Rolle '$rolle' loest auf" ;;
|
|
||||||
*) bad "Rolle '$rolle' FEHLT — Skripte, die sie nennen, laufen in 404" ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
|
|
||||||
# ------------------------------------------------------------- Erreichbar ---
|
|
||||||
echo "ERREICHBARKEIT"
|
|
||||||
for paar in "MC2:$MC2/api/health" "llama-swap:$SWAP/v1/models" "Gitea:$GITEA/api/v1/version"; do
|
|
||||||
name="${paar%%:*}"; url="${paar#*:}"
|
|
||||||
code=$(curl -s -m 10 -o /dev/null -w '%{http_code}' "$url" 2>/dev/null)
|
|
||||||
[ "$code" = "200" ] && ok "$name antwortet (200)" || bad "$name antwortet mit '$code'"
|
|
||||||
done
|
|
||||||
echo
|
|
||||||
|
|
||||||
# ------------------------------------------------ Offen im Internet? --------
|
|
||||||
# Der Fund vom 21.08.: MC2 hing ueber den Nginx Proxy Manager ungeschuetzt im
|
|
||||||
# Netz. ufw sah nichts davon, weil der Proxy im LAN steht. Also von aussen
|
|
||||||
# nachsehen — und zwar auf einem Endpunkt, der ECHTE Daten liefert.
|
|
||||||
echo "OEFFENTLICHE ERREICHBARKEIT"
|
|
||||||
code=$(curl -s -m 15 -o /tmp/.ist-oeff -w '%{http_code}' "$OEFFENTLICH/api/system/status" 2>/dev/null)
|
|
||||||
if [ "$code" = "200" ] && head -c1 /tmp/.ist-oeff 2>/dev/null | grep -q '{'; then
|
|
||||||
bad "MC2 liefert Systemdaten OHNE Anmeldung ins Internet ($OEFFENTLICH)"
|
|
||||||
elif [ "$code" = "401" ] || [ "$code" = "403" ]; then
|
|
||||||
ok "oeffentlicher Zugang verlangt Anmeldung ($code)"
|
|
||||||
elif [ "$code" = "000" ]; then
|
|
||||||
ok "von aussen nicht erreichbar"
|
|
||||||
else
|
|
||||||
warn "oeffentlicher Zugang antwortet mit '$code' — nachsehen"
|
|
||||||
fi
|
|
||||||
rm -f /tmp/.ist-oeff
|
|
||||||
echo
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------- Ports ---
|
|
||||||
echo "OFFENE PORTS (ufw)"
|
|
||||||
sudo -n ufw status 2>/dev/null | grep ALLOW | sed 's/^/ /' || warn "ufw-Status nicht lesbar"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --------------------------------------------------------------- CI-Ampel ---
|
|
||||||
echo "CI-AMPEL (Gitea)"
|
|
||||||
T=$(tr -d '[:space:]' < "$HOME/.config/gitea/create-token" 2>/dev/null)
|
|
||||||
if [ -z "$T" ]; then warn "kein Gitea-Token — CI nicht pruefbar"
|
|
||||||
else
|
|
||||||
curl -s -m 20 -H "Authorization: token $T" "$GITEA/api/v1/user/repos?limit=100" 2>/dev/null \
|
|
||||||
| python3 -c '
|
|
||||||
import sys, json
|
|
||||||
try:
|
|
||||||
repos = json.load(sys.stdin)
|
|
||||||
except Exception as e:
|
|
||||||
print(" ACHTUNG Repo-Liste nicht lesbar:", e); sys.exit(0)
|
|
||||||
print(f" OK {len(repos)} Repos in Gitea")
|
|
||||||
' 2>/dev/null || warn "Repo-Liste nicht lesbar"
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
|
|
||||||
# ------------------------------------------------------------- Sicherung ---
|
|
||||||
echo "SICHERUNGEN"
|
|
||||||
# Pfad aus backup.sh: DEST_DIR="$MODELS_DIR/mc2-backups"
|
|
||||||
SICHER="${MC_BACKUP_DIR:-/srv/models/mc2-backups}"
|
|
||||||
neuestes=$(find "$SICHER" -maxdepth 2 -type f -newermt '-2 days' 2>/dev/null | head -1)
|
|
||||||
anzahl=$(find "$SICHER" -maxdepth 2 -type f 2>/dev/null | wc -l)
|
|
||||||
if [ -n "$neuestes" ]; then
|
|
||||||
ok "Sicherung juenger als 2 Tage vorhanden ($anzahl Staende in $SICHER)"
|
|
||||||
elif [ "$anzahl" -gt 0 ]; then
|
|
||||||
bad "juengste Sicherung ist AELTER als 2 Tage ($anzahl Staende in $SICHER)"
|
|
||||||
else
|
|
||||||
bad "gar keine Sicherung in $SICHER"
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ Platz ---
|
|
||||||
echo "PLATZ UND LAST"
|
|
||||||
# Nur eindeutige Dateisysteme: / und /srv liegen hier auf demselben Geraet,
|
|
||||||
# doppelt gemeldet las sich das wie ein Befund.
|
|
||||||
df -h --output=source,size,used,avail,pcent,target / /srv 2>/dev/null \
|
|
||||||
| awk '!gesehen[$1]++ {printf " %s\n", $0}'
|
|
||||||
echo " RAM: $(free -h | awk '/^Mem:/{print $3" von "$2" belegt"}')"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# ------------------------------------------------------------- Warm/Kalt ---
|
|
||||||
# ---------------------------------------------------------------- Curator ---
|
|
||||||
# Hierher gefaltet am 22.08.2026 aus dem Repo `curator-staleness-check`.
|
|
||||||
# Das war ein eigener Wachhund mit eigenem systemd-Timer und eigener CI-Ampel —
|
|
||||||
# und er ueberwachte einen Cron-Job ueber eine FEST VERDRAHTETE ID
|
|
||||||
# (a47910e2ef05), den es laengst nicht mehr gibt. Er haette bei jedem Lauf
|
|
||||||
# Alarm geschlagen fuer etwas, das gar nicht existiert.
|
|
||||||
# Hier steht dieselbe Frage in vier Zeilen, ohne Repo, Timer und Ampel.
|
|
||||||
echo "CURATOR"
|
|
||||||
AN=$(grep -A3 '^curator:' "$HOME/.hermes/config.yaml" 2>/dev/null | grep -m1 'enabled:' | awk '{print $2}')
|
|
||||||
if [ "${AN:-false}" != "true" ]; then
|
|
||||||
ok "Curator ist abgeschaltet — nichts zu ueberwachen"
|
|
||||||
else
|
|
||||||
# %Ts liefert GANZE Sekunden. %T@ waere ein Float — bash rechnet damit nicht
|
|
||||||
# und bricht mit "invalid arithmetic operator" ab.
|
|
||||||
JUENGSTES=$(find "$HOME/.hermes/logs/curator" -type f -name '*.log' -printf '%Ts\n' 2>/dev/null | sort -rn | head -1)
|
|
||||||
if [ -z "$JUENGSTES" ]; then
|
|
||||||
bad "Curator ist AN, hat aber noch nie etwas protokolliert"
|
|
||||||
else
|
|
||||||
TAGE=$(( ( $(date +%s) - JUENGSTES ) / 86400 ))
|
|
||||||
if [ "$TAGE" -le 7 ]; then ok "Curator war vor $TAGE Tagen zuletzt aktiv"
|
|
||||||
elif [ "$TAGE" -le 21 ]; then warn "Curator seit $TAGE Tagen still — laeuft er noch?"
|
|
||||||
else bad "Curator seit $TAGE Tagen still, obwohl eingeschaltet"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo "MODELLE IM SPEICHER"
|
|
||||||
curl -s -m 10 "$SWAP/running" 2>/dev/null \
|
|
||||||
| python3 -c '
|
|
||||||
import sys, json
|
|
||||||
try:
|
|
||||||
d = json.load(sys.stdin)
|
|
||||||
except Exception:
|
|
||||||
print(" ACHTUNG llama-swap-Status nicht lesbar"); sys.exit(0)
|
|
||||||
lauf = d.get("running", [])
|
|
||||||
if not lauf:
|
|
||||||
print(" OK kein Modell geladen (alles kalt)")
|
|
||||||
for m in lauf:
|
|
||||||
print(" OK " + str(m.get("model")) + " - " + str(m.get("state")))
|
|
||||||
'
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "ENDE IST-ZUSTAND"
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# stack-radar.sh — Job 3: "KI & Stack Radar" (21.08.2026).
|
|
||||||
#
|
|
||||||
# Ablauf: IST-Zustand holen (Fakten) -> Modell bewerten lassen (Prosa)
|
|
||||||
# -> ueber notify.sh melden (Telegram + Lucys Stimme)
|
|
||||||
#
|
|
||||||
# KUGELSICHER heisst hier konkret:
|
|
||||||
# * Die Fakten kommen aus stack-ist.sh, nicht aus dem Modell. Faellt das Modell
|
|
||||||
# aus, werden die ROTEN Zeilen trotzdem gemeldet — roh statt gar nicht.
|
|
||||||
# * Es meldet IMMER. "Alles gruen" ist ein Ergebnis, kein Grund zu schweigen.
|
|
||||||
# * Kein set -e: ein einzelner Fehlschlag darf den Bericht nicht abschneiden.
|
|
||||||
#
|
|
||||||
# Aufruf ueber Hermes-Cron:
|
|
||||||
# hermes cron create '0 8 * * 6' --name 'KI & Stack Radar' \
|
|
||||||
# --no-agent --script stack-radar.sh --deliver local
|
|
||||||
# (--no-agent, weil dieses Skript selbst meldet. Leere Ausgabe = Hermes schweigt.)
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
HIER="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
IST_SKRIPT="${IST_SKRIPT:-$HOME/.hermes/scripts/stack-ist.sh}"
|
|
||||||
NOTIFY="${NOTIFY:-$HOME/mission-control-v2/deploy/notify.sh}"
|
|
||||||
GATEWAY="${GATEWAY_URL:-http://127.0.0.1:9010/v1}"
|
|
||||||
MODELL="${RADAR_MODELL:-fast}" # bewusst NICHT heavy: das laedt 68 GB und der
|
|
||||||
# Radar meldet danach seinen eigenen Speicherverbrauch als Befund
|
|
||||||
|
|
||||||
[ -x "$IST_SKRIPT" ] || IST_SKRIPT="$HIER/stack-ist.sh"
|
|
||||||
|
|
||||||
melde() {
|
|
||||||
if [ -x "$NOTIFY" ] || [ -f "$NOTIFY" ]; then
|
|
||||||
bash "$NOTIFY" -s "[Stack-Radar]" "$1"
|
|
||||||
else
|
|
||||||
# Letzte Rueckfallebene: wenigstens ins Log, damit nichts still verschwindet.
|
|
||||||
echo "[stack-radar] MELDEWEG FEHLT — Bericht folgt roh:" >&2
|
|
||||||
echo "$1" >&2
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ------------------------------------------------------------- 1. Fakten ---
|
|
||||||
IST="$(bash "$IST_SKRIPT" 2>&1)"
|
|
||||||
if [ -z "$IST" ]; then
|
|
||||||
melde "Radar konnte den IST-Zustand nicht erheben — $IST_SKRIPT lieferte nichts. Das ist selbst ein Befund: bitte nachsehen."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
ROT="$(printf '%s\n' "$IST" | grep -E '^\s+(ROT|ACHTUNG)' || true)"
|
|
||||||
ANZ_ROT="$(printf '%s\n' "$ROT" | grep -c 'ROT' || true)"
|
|
||||||
|
|
||||||
# ------------------------------------------------------- 2. Bewertung ------
|
|
||||||
# Das Modell bekommt NUR die Fakten und darf nichts dazuerfinden. Es soll
|
|
||||||
# einordnen, nicht ermitteln.
|
|
||||||
PROMPT="Du bist der Stack-Radar einer selbstgehosteten KI-Box. Unten steht der
|
|
||||||
gemessene IST-Zustand. Schreibe einen kurzen Bericht auf Deutsch, per Du.
|
|
||||||
|
|
||||||
Regeln:
|
|
||||||
- Erfinde NICHTS. Nur was unten steht.
|
|
||||||
- Beginne mit einer Zeile Gesamturteil.
|
|
||||||
- Dann: was ist ROT oder ACHTUNG, und was waere jeweils der naechste Schritt.
|
|
||||||
- Verbesserungsvorschlaege NUR, wenn sich aus den Zahlen unten wirklich etwas
|
|
||||||
Konkretes ergibt. Gibt es nichts, lass den Abschnitt ersatzlos weg. Schreibe
|
|
||||||
lieber gar keinen Vorschlag als einen ausgedachten - ein Bericht mit
|
|
||||||
Fuellmaterial wird nicht gelesen.
|
|
||||||
- Was du an den Messwerten selbst merkwuerdig findest, gehoert unter ACHTUNG,
|
|
||||||
nicht unter Verbesserungen.
|
|
||||||
- Wenn alles gruen ist, sag das in einem Satz und hoere auf.
|
|
||||||
- Keine Ueberschriften-Deko, keine Emojis, hoechstens 200 Woerter.
|
|
||||||
|
|
||||||
IST-ZUSTAND:
|
|
||||||
$IST"
|
|
||||||
|
|
||||||
ANTWORT=""
|
|
||||||
if NUTZLAST="$(python3 -c '
|
|
||||||
import json, sys
|
|
||||||
print(json.dumps({
|
|
||||||
"model": sys.argv[1],
|
|
||||||
"messages": [{"role": "user", "content": sys.argv[2]}],
|
|
||||||
"max_tokens": 900,
|
|
||||||
"temperature": 0.3,
|
|
||||||
}))' "$MODELL" "$PROMPT" 2>/dev/null)"; then
|
|
||||||
ANTWORT="$(curl -s -m 300 -X POST "$GATEWAY/chat/completions" \
|
|
||||||
-H 'Content-Type: application/json' -d "$NUTZLAST" 2>/dev/null \
|
|
||||||
| python3 -c '
|
|
||||||
import json, sys
|
|
||||||
try:
|
|
||||||
print(json.load(sys.stdin)["choices"][0]["message"]["content"].strip())
|
|
||||||
except Exception:
|
|
||||||
pass' 2>/dev/null)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ------------------------------------------------------------- 3. Melden ---
|
|
||||||
if [ -n "$ANTWORT" ]; then
|
|
||||||
melde "$ANTWORT"
|
|
||||||
else
|
|
||||||
# Modell stumm oder kaputt -> die Fakten gehen TROTZDEM raus. Genau dafuer
|
|
||||||
# steht das Modell nicht im kritischen Pfad.
|
|
||||||
if [ -n "$ROT" ]; then
|
|
||||||
melde "Radar: Bewertung durch das Modell hat nicht geklappt — hier die Rohbefunde:
|
|
||||||
|
|
||||||
$ROT"
|
|
||||||
else
|
|
||||||
melde "Radar: alles gruen (Bewertung durch das Modell hat nicht geklappt, die Messung schon)."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Hermes bekommt eine LEERE Ausgabe -> keine zweite Nachricht. Gemeldet wurde
|
|
||||||
# bereits ueber notify.sh, und das erreicht Telegram UND Lucys Stimme.
|
|
||||||
exit 0
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# Arbeitsanweisung (gilt fuer jeden Auftrag)
|
|
||||||
|
|
||||||
Arbeite Datei fuer Datei. Lies HOECHSTENS zwei Dateien, bevor du die erste aenderst.
|
|
||||||
Kein Gesamtplan, keine Vollinventur. Eine halbfertige Aenderung ist wertvoller als
|
|
||||||
eine vollstaendige Analyse ohne Aenderung.
|
|
||||||
|
|
||||||
- Nach jeder geaenderten Datei: kurz sagen, was geaendert wurde, dann zur naechsten.
|
|
||||||
- Nicht erst das ganze Projekt kartieren. Nicht auf Vollstaendigkeit warten.
|
|
||||||
- Wenn du unsicher bist, aendere die wahrscheinlichste Datei und pruefe danach.
|
|
||||||
- Schreiben schlaegt Lesen.
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# OpenCode-Konfiguration des PCs
|
|
||||||
|
|
||||||
**Hier liegt die Quelle. `%USERPROFILE%\.config\opencode\` ist die Kopie.**
|
|
||||||
|
|
||||||
Am 21.08.2026 habe ich die dortige `opencode.json` ueberschrieben, ohne sie
|
|
||||||
vorher zu lesen — sie existierte nur an dieser einen Stelle, ohne Sicherung und
|
|
||||||
ohne Historie. Rekonstruiert aus einer Juli-Sicherung. Damit das nicht nochmal
|
|
||||||
passiert, liegt sie jetzt hier.
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Copy-Item "deploy\opencode-config\*" "$env:USERPROFILE\.config\opencode\" -Force
|
|
||||||
```
|
|
||||||
|
|
||||||
## Modellwahl (Stand 22.08.2026)
|
|
||||||
|
|
||||||
| Rolle | Modell | warum |
|
|
||||||
|---|---|---|
|
|
||||||
| **build** | `aibox/coder` — Qwen3.8-27B (dicht) | macht die eigentliche Arbeit |
|
|
||||||
| **plan** | `aibox/heavy` — gpt-oss-120b | denkt vor, aendert nichts |
|
|
||||||
| **explore** | `aibox/hermes` — Qwen3.6-35B-A3B | liest und sucht, immer warm |
|
|
||||||
| `small_model` | `aibox/hermes` | Titel, Zusammenfassungen |
|
|
||||||
|
|
||||||
★★ **Korrektur einer falschen Empfehlung.** Bis zum 22.08. stand hier `fast`
|
|
||||||
(Qwen3.6-35B-A3B, MoE) als Standard, mit der Begruendung, es schlage den `coder`
|
|
||||||
„in Tempo und Qualitaet". **Das Tempo war gemessen, die Qualitaet nie** — in der
|
|
||||||
Notiz vom 20.08. steht fuer den Coder woertlich „keine agentische Messung".
|
|
||||||
|
|
||||||
Die veroeffentlichten Zahlen sagen etwas anderes:
|
|
||||||
|
|
||||||
| | `fast` (3.6-35B-A3B) | `coder` (3.8-27B) |
|
|
||||||
|---|---|---|
|
|
||||||
| SWE-bench Verified | ~73,4 | 73,4 |
|
|
||||||
| Terminal-Bench 2.1 | — | **73,0** |
|
|
||||||
| DeepSWE 1.1 | — | **42,2** (Vorgaenger 3.6-27B: 13,3) |
|
|
||||||
| OSWorld-Verified | — | **84,3** |
|
|
||||||
|
|
||||||
**Einzelschuss gleichauf, agentisch ueber viele Schritte zieht der Coder davon** —
|
|
||||||
und genau das ist der Betrieb hier. Alle Zahlen stammen von Qwen selbst, teils
|
|
||||||
aus eigenen Benchmark-Fassungen; sie sind ein Indiz, kein Beweis.
|
|
||||||
|
|
||||||
**Der Preis, gemessen:** dieselbe Leseaufgabe brauchte mit `fast` **24,3 s**,
|
|
||||||
mit `coder` **89,3 s** — 3,7× laenger in der Wanduhr. Der Coder ist dicht und
|
|
||||||
bandbreitenlimitiert (17 GB / 215 GB/s ≈ 12,6 t/s, Hardware-Grenze, kein
|
|
||||||
Konfigfehler). Er hat ausserdem `ttl=5400`, faellt also nach 90 Minuten aus dem
|
|
||||||
Speicher und muss neu laden.
|
|
||||||
|
|
||||||
**Was noch aussteht:** der echte Vergleich an einer echten Aufgabe.
|
|
||||||
`F:\Coding Stuff\mc2-referenz` liegt genau dafuer bereit — 17 Dateien, davon 5
|
|
||||||
erledigt, mit objektivem Pruefbefehl `docs/aufgaben/referenz-check.sh`.
|
|
||||||
Erst dieser Lauf entscheidet die Frage; bis dahin ist die Modellwahl begruendet,
|
|
||||||
aber nicht bewiesen.
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://opencode.ai/config.json",
|
|
||||||
"provider": {
|
|
||||||
"aibox": {
|
|
||||||
"npm": "@ai-sdk/openai-compatible",
|
|
||||||
"name": "AI-Box (ueber MC2 :9001)",
|
|
||||||
"options": {
|
|
||||||
"baseURL": "http://192.168.178.151:9001/v1",
|
|
||||||
"apiKey": "local"
|
|
||||||
},
|
|
||||||
"models": {
|
|
||||||
"fast": {
|
|
||||||
"name": "fast - Qwen3.6-35B-A3B (MoE) - schnell, fuer Erkunden und Kleinkram",
|
|
||||||
"tool_call": true,
|
|
||||||
"limit": {
|
|
||||||
"context": 65536,
|
|
||||||
"output": 16384
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"heavy": {
|
|
||||||
"name": "heavy - Planen / Review",
|
|
||||||
"tool_call": true,
|
|
||||||
"limit": {
|
|
||||||
"context": 32768,
|
|
||||||
"output": 8192
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"hermes": {
|
|
||||||
"name": "hermes - Erkunden (immer warm)",
|
|
||||||
"tool_call": true,
|
|
||||||
"limit": {
|
|
||||||
"context": 65536,
|
|
||||||
"output": 8192
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"coder": {
|
|
||||||
"name": "coder - Qwen3.8-27B (dicht) - Standard fuers Bauen, agentisch am staerksten",
|
|
||||||
"tool_call": true,
|
|
||||||
"limit": {
|
|
||||||
"context": 131072,
|
|
||||||
"output": 16384
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"model": "aibox/coder",
|
|
||||||
"small_model": "aibox/hermes",
|
|
||||||
"instructions": [
|
|
||||||
"C:/Users/TobisPC/.config/opencode/ARBEITSANWEISUNG.md"
|
|
||||||
],
|
|
||||||
"autoupdate": false,
|
|
||||||
"agent": {
|
|
||||||
"plan": {
|
|
||||||
"model": "aibox/heavy",
|
|
||||||
"permission": {
|
|
||||||
"edit": "deny",
|
|
||||||
"bash": "deny"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"build": {
|
|
||||||
"model": "aibox/coder",
|
|
||||||
"permission": {
|
|
||||||
"edit": "allow",
|
|
||||||
"webfetch": "allow",
|
|
||||||
"bash": {
|
|
||||||
"*": "allow",
|
|
||||||
"ssh *": "deny",
|
|
||||||
"scp *": "deny",
|
|
||||||
"sftp *": "deny",
|
|
||||||
"ssh arcane@192.168.178.162 *": "allow",
|
|
||||||
"ssh -o StrictHostKeyChecking=no arcane@192.168.178.162 *": "allow",
|
|
||||||
"scp *arcane@192.168.178.162*": "allow"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"explore": {
|
|
||||||
"mode": "subagent",
|
|
||||||
"description": "Codebase schnell durchsuchen, Dateien finden, Fragen zum Code beantworten - nur lesen, laeuft auf dem immer warmen Hirn",
|
|
||||||
"model": "aibox/hermes",
|
|
||||||
"permission": {
|
|
||||||
"edit": "deny",
|
|
||||||
"bash": "deny"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"review": {
|
|
||||||
"mode": "subagent",
|
|
||||||
"description": "Kritischer Code-Review nach jeder Etappe (Pflicht laut AGENTS.md): sucht erfundene APIs/CLI-Flags, stille Abweichungen vom KONZEPT, fehlende Tests, toten Code - meldet Befunde, aendert nichts",
|
|
||||||
"model": "aibox/heavy",
|
|
||||||
"permission": {
|
|
||||||
"edit": "deny",
|
|
||||||
"bash": "deny"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
# OpenCode-Plugin: MC2-Governor
|
|
||||||
|
|
||||||
**Hier liegt die Quelle. Der Einsatzort ist eine Kopie.**
|
|
||||||
|
|
||||||
```
|
|
||||||
deploy/opencode-plugin/mc2-governor.ts <- versioniert, hier aendern
|
|
||||||
| kopieren
|
|
||||||
v
|
|
||||||
%USERPROFILE%\.config\opencode\plugin\ <- wird von OpenCode automatisch geladen
|
|
||||||
```
|
|
||||||
|
|
||||||
Bis zum 21.08.2026 existierte diese Datei **nur** im Konfigordner des PCs — 11 KB
|
|
||||||
verhaltensbestimmender Code ohne Sicherung und ohne Historie. Deshalb liegt sie jetzt hier.
|
|
||||||
|
|
||||||
## Ausbringen
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Copy-Item "deploy\opencode-plugin\mc2-governor.ts" "$env:USERPROFILE\.config\opencode\plugin\" -Force
|
|
||||||
```
|
|
||||||
|
|
||||||
Wirkt sofort in der naechsten OpenCode-Sitzung; OpenChamber muss dafuer nicht neu starten,
|
|
||||||
eine neue Sitzung reicht.
|
|
||||||
|
|
||||||
## Was es tut
|
|
||||||
|
|
||||||
1. **Werkzeug-Zaun** (`tool.execute.before`) — blockt Handgriffe, die ein Agent nie
|
|
||||||
unbeaufsichtigt tun darf. Leitplanke gegen Entgleisen, **keine** Sandbox.
|
|
||||||
2. **Pruef-Tor + Reparaturschleife** (`session.idle`) — sagt der Agent „fertig", laeuft der
|
|
||||||
Verify-Befehl aus der Datei `VERIFY` im Repo-Wurzelverzeichnis. ROT geht als naechster
|
|
||||||
Auftrag zurueck an den Agenten, bis zu `MC2_LOOP_MAX_ROUNDS` mal.
|
|
||||||
‼️ **Schlaeft derzeit:** es gibt in keinem Projekt eine `VERIFY`-Datei. Ohne sie tut
|
|
||||||
diese Funktion nichts — schadet aber auch nicht.
|
|
||||||
3. **Savepoint statt Zusammenfassen** (`session.compacted`) — beim Komprimieren fallen
|
|
||||||
still die Regeln aus dem Kontext; stattdessen wird `SAVEPOINT.md` geschrieben.
|
|
||||||
4. **Stimme** — Ereignisse gehen an MC2 `/api/voice/announce`, Lucy spricht sie.
|
|
||||||
|
|
||||||
Der zugehoerige **Governor-Proxy auf `:8100`** (Token-Zaehler mit Hart-Deckel, Juli 2026)
|
|
||||||
**existiert nicht mehr** — kein Port, kein Ordner, keine Unit. Nachgemessen am 21.08.2026.
|
|
||||||
Dieses Plugin haengt nicht an ihm.
|
|
||||||
|
|
||||||
## Schalter (Umgebungsvariablen)
|
|
||||||
|
|
||||||
| Variable | Default | Wirkung |
|
|
||||||
|---|---|---|
|
|
||||||
| `MC2_BOX_URL` | `http://192.168.178.151:9001` | MC2-Basis fuer Meldungen |
|
|
||||||
| `MC2_FENCE_OFF` | — | `1` schaltet den **ganzen** Zaun ab (alles oder nichts) |
|
|
||||||
| `MC2_LOOP_AUTOFIX` | an | `0` schaltet die Reparaturschleife ab |
|
|
||||||
| `MC2_LOOP_MAX_ROUNDS` | `3` | Runden der Reparaturschleife |
|
|
||||||
| `MC2_LOOP_ANNOUNCE` | an | `0` macht Lucy stumm |
|
|
||||||
| `MC2_LOOP_SILENT` | — | `1` = Meldungen ohne Ton |
|
|
||||||
|
|
||||||
## Aenderung 21.08.2026: `git push` ist frei
|
|
||||||
|
|
||||||
Der Zaun blockte urspruenglich **jedes** `git push` („Veroeffentlichen ist Sache des
|
|
||||||
Menschen"). Das passt nicht mehr: die Bahn ist jetzt PC → Gitea → Box, und jede Aenderung
|
|
||||||
soll auch im git landen. Ein Agent, der committen, aber nicht pushen darf, laesst Arbeit
|
|
||||||
auf einem einzelnen Rechner liegen.
|
|
||||||
|
|
||||||
**Erzwungenes Pushen bleibt verboten** — `--force`, `--mirror`, `--delete`. Das
|
|
||||||
ueberschreibt fremde Arbeit auf dem Server und ist genau die Klasse Unfall, gegen die der
|
|
||||||
Zaun existiert. `--force-with-lease` ist bewusst ausgenommen.
|
|
||||||
|
|
||||||
Beides gemessen: `git push --dry-run origin main` laeuft durch,
|
|
||||||
`git push --force --dry-run origin main` wird mit `[MC2-ZAUN]` abgewiesen.
|
|
||||||
|
|
||||||
## ‼️ Falle: das `bash`-Werkzeug ist auf Windows kein bash
|
|
||||||
|
|
||||||
Es laeuft in **PowerShell**. `tail`, `head`, `grep` und Freunde gibt es dort nicht, und
|
|
||||||
Pipes verhalten sich anders. Wer dem Agenten Befehle vorgibt, muss das beruecksichtigen.
|
|
||||||
|
|
||||||
## ‼️ Falle: Gitea-Anmeldung ist flatterhaft
|
|
||||||
|
|
||||||
Der Git Credential Manager haelt fuer die DDNS-Domain ein **OAuth-Token mit einer Stunde
|
|
||||||
Laufzeit**. Laeuft es ab, scheitert der Push des Agenten mit
|
|
||||||
`remote: Failed to authenticate user` — ein Push aus PowerShell erneuert es, danach geht es
|
|
||||||
wieder. Dauerhafte Loesung waere ein langlebiges Gitea-Token auf die **interne** Adresse
|
|
||||||
`http://192.168.178.153:3000` (die DDNS-Domain ist nachts durch die Zwangstrennung ohnehin
|
|
||||||
zeitweise tot).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ‼️ ABGESCHALTET am 22.08.2026
|
|
||||||
|
|
||||||
**Das Plugin ist nicht mehr aktiv.** Es liegt hier als Quelle und im Konfigordner
|
|
||||||
unter `plugin-archiv/`, aber nicht mehr in `~/.config/opencode/plugin/`.
|
|
||||||
|
|
||||||
**Warum:** Beim ersten echten Arbeitslauf fiel auf, dass bei jeder Komprimierung
|
|
||||||
zwei Dinge uebereinander passierten — OpenChamber dampfte den Verlauf ein, und der
|
|
||||||
Governor schob sofort einen zusaetzlichen Auftrag nach („schreib SAVEPOINT.md").
|
|
||||||
Der Agent verlor dadurch jedes Mal einen Zug an Buchhaltung statt an die Aufgabe.
|
|
||||||
Nachgewiesen: `SAVEPOINT.md` in `mc2-referenz`, geschrieben 22.08. 14:47 mitten
|
|
||||||
im Lauf.
|
|
||||||
|
|
||||||
Der User hatte den Abbau schon am 21.08. angeordnet („alle Guards, alle
|
|
||||||
Governor, Stewards — brauchen wir nicht mehr"). Ich hatte damals das Behalten
|
|
||||||
empfohlen und seine Antwort zum `git push`-Riegel als Zustimmung zum Rest
|
|
||||||
gewertet. **Das war eine Annahme, keine Entscheidung.**
|
|
||||||
|
|
||||||
## Was damit wegfaellt — bewusst
|
|
||||||
|
|
||||||
| war | jetzt |
|
|
||||||
|---|---|
|
|
||||||
| Sperrliste: `rm -rf`, `sudo`, force push, `curl\|sh`, `ssh` ausser Arcane, `npm publish` | **kein Netz mehr** — der Agent darf alles, was die Shell hergibt |
|
|
||||||
| Pruef-Tor + Reparaturschleife (`VERIFY`) | schlief ohnehin, es gab nirgends eine `VERIFY`-Datei im PC-Baum |
|
|
||||||
| SAVEPOINT beim Komprimieren | weg — genau der Grund fuer den Abbau |
|
|
||||||
| Meldungen an Lucys Stimme | weg |
|
|
||||||
|
|
||||||
**Wiederherstellen** (wenn der fehlende Zaun sich raecht):
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Copy-Item "deploy\opencode-plugin\mc2-governor.ts" "$env:USERPROFILE\.config\opencode\plugin\"
|
|
||||||
```
|
|
||||||
|
|
||||||
Plugins werden beim **Start des OpenCode-Servers** geladen — Aendern wirkt erst
|
|
||||||
nach einem Neustart von OpenChamber, nicht sofort. Deshalb liess sich das Plugin
|
|
||||||
auch mitten in einem laufenden Referenzlauf gefahrlos entfernen.
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
/**
|
|
||||||
* MC2-Governor — der "Fahrlehrer" im OpenCode-Agenten.
|
|
||||||
*
|
|
||||||
* Der Governor-Proxy (:8100) ist die Tankuhr: er sieht nur Tokens und zieht die
|
|
||||||
* Notbremse. Dieses Plugin sitzt IM Agenten und sieht alles andere — jeden
|
|
||||||
* Werkzeuggriff, jede Datei, jedes Sitzungsende. Es macht vier Dinge:
|
|
||||||
*
|
|
||||||
* 1. WERKZEUG-ZAUN (tool.execute.before)
|
|
||||||
* Blockt Handgriffe, die ein Agent nie unbeaufsichtigt tun darf: push,
|
|
||||||
* Historie umschreiben, rekursiv loeschen, sudo, Fremd-Hosts. Genau dieser
|
|
||||||
* Zustandsautomat-Zaun hob lokale Modelle in Messungen von 2/10 auf 10/10 —
|
|
||||||
* nicht weil sie schlauer werden, sondern weil sie nicht mehr entgleisen.
|
|
||||||
*
|
|
||||||
* 2. PRUEF-TOR + SCHLEIFE (session.idle)
|
|
||||||
* Sagt der Agent "fertig", laeuft der Verify-Befehl des Projekts (Datei
|
|
||||||
* `VERIFY` im Repo-Wurzelverzeichnis). GRUEN -> Meldung. ROT -> der Fehler
|
|
||||||
* geht als naechster Auftrag automatisch zurueck an den Agenten, bis zu
|
|
||||||
* MC2_LOOP_MAX_ROUNDS mal. Das ist die "Ralph-Schleife", nur mit Bremse.
|
|
||||||
*
|
|
||||||
* 3. SAVEPOINT STATT ZUSAMMENFASSEN (session.compacted)
|
|
||||||
* Beim Komprimieren fallen still die Regeln aus dem Kontext (Paper
|
|
||||||
* "Governance Decay"). Wir schieben stattdessen den Auftrag nach, SAVEPOINT.md
|
|
||||||
* zu schreiben — Wissen lebt in Datei + git, nicht im schrumpfenden Chat.
|
|
||||||
*
|
|
||||||
* 4. STIMME (MC2 /api/voice/announce)
|
|
||||||
* Jedes Ereignis geht mit eigenem Absender `loop` in MC2s Melde-Briefkasten.
|
|
||||||
* Lucy pollt ihn ohnehin und spricht ihn — ohne eine Zeile Lucy-Code.
|
|
||||||
*
|
|
||||||
* Schalter (Umgebungsvariablen):
|
|
||||||
* MC2_BOX_URL MC2-Basis (Default http://192.168.178.151:9001)
|
|
||||||
* MC2_LOOP_AUTOFIX Selbstreparatur (1 = an, Default an)
|
|
||||||
* MC2_LOOP_MAX_ROUNDS max. Reparaturrunden (Default 3)
|
|
||||||
* MC2_LOOP_SILENT 1 = Lucy schweigt (Nachtlauf; Meldungen kommen trotzdem an)
|
|
||||||
* MC2_LOOP_ANNOUNCE 0 = gar keine Meldungen
|
|
||||||
* MC2_FENCE_OFF 1 = Werkzeug-Zaun aus (nur fuer Notfaelle)
|
|
||||||
*
|
|
||||||
* Liegt global unter ~/.config/opencode/plugin/ und wirkt damit in JEDEM Projekt —
|
|
||||||
* am Tag in Zed, nachts im Cron. Ein Regelwerk, zwei Ausloeser.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const BOX_URL = process.env.MC2_BOX_URL || "http://192.168.178.151:9001"
|
|
||||||
const AUTOFIX = process.env.MC2_LOOP_AUTOFIX !== "0"
|
|
||||||
const MAX_ROUNDS = parseInt(process.env.MC2_LOOP_MAX_ROUNDS || "3", 10)
|
|
||||||
const SILENT = process.env.MC2_LOOP_SILENT === "1"
|
|
||||||
const ANNOUNCE_ON = process.env.MC2_LOOP_ANNOUNCE !== "0"
|
|
||||||
const FENCE_OFF = process.env.MC2_FENCE_OFF === "1"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verbotene Shell-Handgriffe. Bewusst als Muster auf der ROHEN Kommandozeile —
|
|
||||||
* ein Agent, der `git push` in ein `bash -c` verpackt, wird trotzdem erwischt.
|
|
||||||
* Kein Anspruch auf Sandbox-Sicherheit: das ist ein Leitplanken-Zaun gegen
|
|
||||||
* Entgleisen, keine Abwehr gegen einen boesartigen Akteur.
|
|
||||||
*/
|
|
||||||
const FENCE: Array<{ rx: RegExp; why: string }> = [
|
|
||||||
// 21.08.2026: normales `git push` ist FREI. Der Weg ist jetzt PC -> Gitea -> Box, und
|
|
||||||
// jede Aenderung soll auch im git landen — ein Agent, der committen, aber nicht pushen
|
|
||||||
// darf, laesst Arbeit auf einem einzelnen Rechner liegen. Was schlecht ist, faengt
|
|
||||||
// hinterher die CI-Ampel; dieser Zaun muss es nicht vorher verhindern.
|
|
||||||
// Erzwungenes Pushen bleibt verboten: das ueberschreibt fremde Arbeit auf dem Server
|
|
||||||
// und ist genau die Klasse Unfall, gegen die der Zaun ueberhaupt existiert.
|
|
||||||
{ rx: /\bgit\s+push\b[^\n]*(--force(?!-with-lease)|\s-f\b|--mirror\b|--delete\b)/,
|
|
||||||
why: "git push --force/--mirror/--delete — ueberschreibt fremde Arbeit auf dem Server. Normales `git push` ist erlaubt; muss wirklich erzwungen werden, sag es dem Menschen." },
|
|
||||||
{ rx: /\bgit\s+reset\s+--hard\b/, why: "git reset --hard — verwirft Arbeit unwiederbringlich." },
|
|
||||||
{ rx: /\bgit\s+clean\s+-[a-z]*f/, why: "git clean -f — loescht ungetrackte Dateien unwiederbringlich." },
|
|
||||||
{ rx: /\bgit\s+(rebase|filter-branch|reflog\s+expire)\b/, why: "Historie umschreiben ist tabu." },
|
|
||||||
{ rx: /\brm\s+-[a-zA-Z]*r[a-zA-Z]*f?\s+\/(?:\s|$)/, why: "rm -rf / — nein." },
|
|
||||||
{ rx: /\brm\s+-[a-zA-Z]*[rf]/, why: "rekursives/erzwungenes Loeschen — bitte gezielt loeschen statt pauschal." },
|
|
||||||
{ rx: /\bsudo\b/, why: "sudo — Rechteausweitung gehoert nicht in einen Agentenlauf." },
|
|
||||||
{ rx: /\b(shutdown|reboot|mkfs|dd\s+if=)/, why: "System-/Datentraeger-Eingriff." },
|
|
||||||
{ rx: /\b(curl|wget)\b[^|]*\|\s*(ba)?sh\b/, why: "Aus dem Netz laden und direkt ausfuehren — klassischer Fussschuss." },
|
|
||||||
{ rx: /\bssh\s+(?!arcane@192\.168\.178\.162|-o\s+StrictHostKeyChecking=no\s+arcane@)/, why: "ssh nur zur freigegebenen Arcane-VM." },
|
|
||||||
{ rx: /\bnpm\s+publish\b|\btwine\s+upload\b/, why: "Veroeffentlichen von Paketen ist Sache des Menschen." },
|
|
||||||
]
|
|
||||||
|
|
||||||
/** Zaehler je Sitzung: wie viele Selbstreparatur-Runden liefen schon? */
|
|
||||||
const rounds = new Map<string, number>()
|
|
||||||
/** Doppel-Feuern verhindern: session.idle kann mehrfach kommen. */
|
|
||||||
const busy = new Set<string>()
|
|
||||||
|
|
||||||
async function announce(subject: string, text: string, priority: "normal" | "silent" = "normal") {
|
|
||||||
if (!ANNOUNCE_ON) return
|
|
||||||
try {
|
|
||||||
await fetch(`${BOX_URL}/api/voice/announce`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
subject,
|
|
||||||
text,
|
|
||||||
source: "loop",
|
|
||||||
priority: SILENT ? "silent" : priority,
|
|
||||||
}),
|
|
||||||
signal: AbortSignal.timeout(4000),
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
/* best effort — eine stumme Lucy darf den Bau nie aufhalten */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MC2Governor = async ({ client, $, directory, worktree }: any) => {
|
|
||||||
const root: string = worktree || directory || process.cwd()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify-Befehl des Projekts lesen. Fehlt die Datei, ist das Pruef-Tor AUS.
|
|
||||||
* Bewusst ueber fs statt `cat`: das Plugin laeuft am Tag auf Windows (Zed) und
|
|
||||||
* nachts auf der Box — `cat` gibt es auf Windows nicht zuverlaessig.
|
|
||||||
*/
|
|
||||||
async function readVerify(): Promise<string | null> {
|
|
||||||
try {
|
|
||||||
const { readFile } = await import("node:fs/promises")
|
|
||||||
const { join } = await import("node:path")
|
|
||||||
const raw = await readFile(join(root, "VERIFY"), "utf8")
|
|
||||||
const cmd = raw
|
|
||||||
.split("\n")
|
|
||||||
.map((l: string) => l.trim())
|
|
||||||
.filter((l: string) => l && !l.startsWith("#"))
|
|
||||||
.join(" && ")
|
|
||||||
return cmd || null
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify ausfuehren. Rueckgabe: {ok, output} — Ausgabe auf das Wesentliche gekuerzt.
|
|
||||||
* `{ raw: cmd }` schiebt den Befehl UNESCAPED in Buns Shell; ein normales
|
|
||||||
* `${cmd}` wuerde die ganze Zeile als EIN Argument uebergeben und nie laufen.
|
|
||||||
* Buns Shell ist plattformunabhaengig — kein `bash -lc`, das auf Windows fehlt.
|
|
||||||
*/
|
|
||||||
async function runVerify(cmd: string): Promise<{ ok: boolean; out: string }> {
|
|
||||||
try {
|
|
||||||
const res = await $`${{ raw: cmd }}`.cwd(root).nothrow().quiet()
|
|
||||||
const out = `${res.stdout?.toString() ?? ""}${res.stderr?.toString() ?? ""}`
|
|
||||||
return { ok: res.exitCode === 0, out: out.slice(-4000) }
|
|
||||||
} catch (e: any) {
|
|
||||||
return { ok: false, out: String(e?.message ?? e).slice(-4000) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Dem laufenden Agenten einen neuen Auftrag schicken (Selbstreparatur-Schleife). */
|
|
||||||
async function sendPrompt(sessionID: string, text: string): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
await client.session.prompt({
|
|
||||||
path: { id: sessionID },
|
|
||||||
body: { parts: [{ type: "text", text }] },
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
// ── 1. Werkzeug-Zaun ───────────────────────────────────────────────────
|
|
||||||
"tool.execute.before": async (input: any, output: any) => {
|
|
||||||
if (FENCE_OFF) return
|
|
||||||
if (input?.tool !== "bash") return
|
|
||||||
const cmd: string = output?.args?.command ?? ""
|
|
||||||
if (!cmd) return
|
|
||||||
for (const rule of FENCE) {
|
|
||||||
if (rule.rx.test(cmd)) {
|
|
||||||
await announce(
|
|
||||||
"[Zaun]",
|
|
||||||
`Ich habe einen Befehl geblockt: ${rule.why}`,
|
|
||||||
"silent",
|
|
||||||
)
|
|
||||||
// Werfen = OpenCode bricht genau diesen Werkzeugaufruf ab und gibt dem
|
|
||||||
// Modell den Grund zurueck. Der Agent arbeitet weiter, nur anders.
|
|
||||||
throw new Error(
|
|
||||||
`[MC2-ZAUN] Blockiert: ${rule.why}\n` +
|
|
||||||
`Befehl war: ${cmd}\n` +
|
|
||||||
`Waehle einen anderen Weg. Wenn das wirklich noetig ist, sag es dem Menschen — ` +
|
|
||||||
`er macht es selbst.`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ── 2.-4. Ereignisse ───────────────────────────────────────────────────
|
|
||||||
event: async ({ event }: any) => {
|
|
||||||
const type: string = event?.type ?? ""
|
|
||||||
const props: any = event?.properties ?? event ?? {}
|
|
||||||
const sessionID: string = props.sessionID || props.sessionId || props.id || ""
|
|
||||||
|
|
||||||
// ── Savepoint statt Zusammenfassen ──────────────────────────────────
|
|
||||||
if (type === "session.compacted" || type === "experimental.session.compacting") {
|
|
||||||
await announce(
|
|
||||||
"[Sitzung]",
|
|
||||||
"Die Sitzung wurde komprimiert — ich lasse den Stand in SAVEPOINT.md sichern.",
|
|
||||||
"silent",
|
|
||||||
)
|
|
||||||
if (sessionID) {
|
|
||||||
await sendPrompt(
|
|
||||||
sessionID,
|
|
||||||
"[MC2-GOVERNOR] Der Kontext wurde gerade komprimiert — dabei gehen still " +
|
|
||||||
"Regeln und Details verloren. Aktualisiere JETZT SAVEPOINT.md: was wirklich " +
|
|
||||||
"erledigt ist (nur was im Code steht), der genaue naechste Schritt, offene " +
|
|
||||||
"Fragen, Stolpersteine. Committe die Datei. Danach arbeite normal weiter.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Pruef-Tor + Selbstreparatur ─────────────────────────────────────
|
|
||||||
if (type !== "session.idle" || !sessionID) return
|
|
||||||
if (busy.has(sessionID)) return
|
|
||||||
|
|
||||||
const cmd = await readVerify()
|
|
||||||
if (!cmd) return // Kein VERIFY im Projekt -> Pruef-Tor bewusst aus.
|
|
||||||
|
|
||||||
busy.add(sessionID)
|
|
||||||
try {
|
|
||||||
const { ok, out } = await runVerify(cmd)
|
|
||||||
const round = rounds.get(sessionID) ?? 0
|
|
||||||
|
|
||||||
if (ok) {
|
|
||||||
rounds.delete(sessionID)
|
|
||||||
await announce("[Pruefung]", "Etappe fertig und die Tests sind gruen.", "normal")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!AUTOFIX || round >= MAX_ROUNDS) {
|
|
||||||
rounds.delete(sessionID)
|
|
||||||
await announce(
|
|
||||||
"[Pruefung]",
|
|
||||||
`Die Tests sind rot und ich habe ${round} Reparaturversuche verbraucht. ` +
|
|
||||||
`Hier komme ich allein nicht weiter, Commander.`,
|
|
||||||
"normal",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rounds.set(sessionID, round + 1)
|
|
||||||
await announce(
|
|
||||||
"[Pruefung]",
|
|
||||||
`Tests rot — ich repariere selbst weiter, Runde ${round + 1} von ${MAX_ROUNDS}.`,
|
|
||||||
"silent",
|
|
||||||
)
|
|
||||||
await sendPrompt(
|
|
||||||
sessionID,
|
|
||||||
`[MC2-PRUEFTOR] Deine Etappe gilt noch NICHT als fertig: der Verify-Befehl des ` +
|
|
||||||
`Projekts ist fehlgeschlagen.\n\n` +
|
|
||||||
`Befehl: ${cmd}\n\n` +
|
|
||||||
`Ausgabe (Ende):\n\`\`\`\n${out}\n\`\`\`\n\n` +
|
|
||||||
`Behebe die Ursache — nicht das Symptom, und schalte keinen Test ab. ` +
|
|
||||||
`Wenn du fertig bist, melde dich normal; ich pruefe dann erneut. ` +
|
|
||||||
`(Reparaturrunde ${round + 1} von ${MAX_ROUNDS}.)`,
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
busy.delete(sessionID)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MC2Governor
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
<#
|
|
||||||
push-und-sync.ps1 — der Handschlag zwischen PC und AI-Box (21.08.2026).
|
|
||||||
|
|
||||||
Bahn: PC (Quelle der Wahrheit) -> Gitea -> Box ~/projekte
|
|
||||||
|
|
||||||
Warum ueberhaupt ein Skript: `projekte-sync` laeuft auf der Box stuendlich. Wer
|
|
||||||
gerade gepusht hat, will aber nicht bis zu 60 Minuten warten. Das hier stoesst
|
|
||||||
denselben Dienst sofort an — kein zweiter Mechanismus, nur ein Ausloeser.
|
|
||||||
|
|
||||||
Warum PowerShell und nicht Git-Bash: Push zum Gitea geht vom PC nur ueber den
|
|
||||||
Git Credential Manager, und den gibt es hier (Lehre vom 24.07.).
|
|
||||||
|
|
||||||
Aufruf (im Projektordner):
|
|
||||||
.\deploy\push-und-sync.ps1
|
|
||||||
.\deploy\push-und-sync.ps1 -NurSync # nicht pushen, nur Box nachziehen
|
|
||||||
.\deploy\push-und-sync.ps1 -Pfad "F:\Coding Stuff\rippy"
|
|
||||||
#>
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[string] $Pfad = (Get-Location).Path,
|
|
||||||
[switch] $NurSync,
|
|
||||||
[string] $BoxHost = "hitonabi@192.168.178.151"
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
function Schritt($text) { Write-Host "`n== $text" -ForegroundColor Cyan }
|
|
||||||
function Gut($text) { Write-Host " $text" -ForegroundColor Green }
|
|
||||||
function Warn($text) { Write-Host " $text" -ForegroundColor Yellow }
|
|
||||||
|
|
||||||
Set-Location $Pfad
|
|
||||||
|
|
||||||
# --- 1. Ist das ueberhaupt ein Repo, und was steht an? ---------------------
|
|
||||||
$null = git rev-parse --is-inside-work-tree 2>$null
|
|
||||||
if ($LASTEXITCODE -ne 0) { throw "Kein Git-Repository: $Pfad" }
|
|
||||||
|
|
||||||
$zweig = (git branch --show-current).Trim()
|
|
||||||
|
|
||||||
# Repo-Name aus der Remote-URL, NICHT aus dem Ordnernamen: der lokale Ordner heisst
|
|
||||||
# z. B. "mission-control-2", das Gitea-Repo aber "mission-control-v2". Der Ordnername
|
|
||||||
# haette die Box-Abfrage still ins Leere laufen lassen.
|
|
||||||
$remote = (git remote get-url origin).Trim()
|
|
||||||
$repo = [System.IO.Path]::GetFileNameWithoutExtension(($remote -split '/')[-1])
|
|
||||||
Schritt "Repo '$repo', Zweig '$zweig'"
|
|
||||||
|
|
||||||
$offen = git status --porcelain
|
|
||||||
if ($offen) {
|
|
||||||
Warn "Es liegen unversionierte/ungespeicherte Aenderungen:"
|
|
||||||
$offen | Select-Object -First 10 | ForEach-Object { Warn " $_" }
|
|
||||||
Warn "Diese werden NICHT mitgeschickt. Erst committen, dann erneut aufrufen."
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 2. Pushen ------------------------------------------------------------
|
|
||||||
if (-not $NurSync) {
|
|
||||||
$ausstehend = git log --oneline "origin/$zweig..HEAD" 2>$null
|
|
||||||
if (-not $ausstehend) {
|
|
||||||
Gut "Nichts zu pushen — Gitea ist bereits auf Stand."
|
|
||||||
} else {
|
|
||||||
Schritt "Push nach Gitea ($(($ausstehend | Measure-Object).Count) Commit(s))"
|
|
||||||
$ausstehend | ForEach-Object { Write-Host " $_" }
|
|
||||||
git push origin $zweig
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw @"
|
|
||||||
Push fehlgeschlagen.
|
|
||||||
Seit 21.08. laeuft der Weg ueber SSH: ssh://gitea@192.168.178.153:2222/...
|
|
||||||
Pruefen mit: ssh -T gitea
|
|
||||||
Kommt dort keine Begruessung, lauscht Giteas SSH-Server nicht (Port 2222,
|
|
||||||
START_SSH_SERVER in /etc/gitea/app.ini) oder id_general ist nicht hinterlegt.
|
|
||||||
Der SSH-Benutzer heisst 'gitea', NICHT 'git' - Gitea laeuft unter diesem Namen.
|
|
||||||
"@
|
|
||||||
}
|
|
||||||
Gut "gepusht"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 3. Box nachziehen lassen --------------------------------------------
|
|
||||||
Schritt "Box zieht nach (projekte-sync)"
|
|
||||||
ssh $BoxHost "systemctl --user start projekte-sync.service"
|
|
||||||
if ($LASTEXITCODE -ne 0) { throw "Sync-Dienst liess sich nicht starten." }
|
|
||||||
|
|
||||||
$stand = ssh $BoxHost "cd ~/projekte/$repo 2>/dev/null && git log --oneline -1 || echo 'NICHT-IN-PROJEKTE'"
|
|
||||||
if ($stand -eq "NICHT-IN-PROJEKTE") {
|
|
||||||
Warn "'$repo' liegt nicht unter ~/projekte."
|
|
||||||
Warn "Bei mission-control-v2 ist das ABSICHT: das Live-Deployment liegt in ~/mission-control-v2"
|
|
||||||
Warn "und wird bewusst nicht automatisch gezogen — es braucht seinen eigenen Deploy-Schritt."
|
|
||||||
} else {
|
|
||||||
Gut "Box steht jetzt auf: $stand"
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host ""
|
|
||||||
+1
-4
@@ -15,11 +15,10 @@ set -euo pipefail
|
|||||||
|
|
||||||
MODELS_DIR="${MC_MODELS_DIR:-/srv/models}"
|
MODELS_DIR="${MC_MODELS_DIR:-/srv/models}"
|
||||||
DEST_DIR="$MODELS_DIR/mc2-backups"
|
DEST_DIR="$MODELS_DIR/mc2-backups"
|
||||||
MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"
|
|
||||||
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
|
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
|
||||||
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
||||||
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
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="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}"
|
||||||
OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}"
|
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"
|
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
|
if [ "$DRY" = 1 ]; then
|
||||||
echo "[dry-run] würde zurücksetzen:"
|
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/config.yaml" ] && echo " • $HERMES/config.yaml"
|
||||||
[ -f "$STAGE/hermes/.env" ] && echo " • $HERMES/.env"
|
[ -f "$STAGE/hermes/.env" ] && echo " • $HERMES/.env"
|
||||||
[ -d "$STAGE/hermes/plugins" ] && echo " • $HERMES/plugins/"
|
[ -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…"
|
echo "→ Dienste stoppen…"
|
||||||
systemctl --user stop $SERVICES 2>/dev/null || true
|
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/config.yaml" ] && { mkdir -p "$HERMES"; cp -a "$STAGE/hermes/config.yaml" "$HERMES/"; }
|
||||||
[ -f "$STAGE/hermes/.env" ] && cp -a "$STAGE/hermes/.env" "$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/"; }
|
[ -d "$STAGE/hermes/plugins" ] && { mkdir -p "$HERMES/plugins"; cp -a "$STAGE/hermes/plugins/." "$HERMES/plugins/"; }
|
||||||
|
|||||||
@@ -3,15 +3,14 @@
|
|||||||
# dass der komplette Inferenz-Stack noch FUNKTIONIERT — nicht nur "Befehl lief durch".
|
# 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).
|
# 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).
|
# inkl. einer ECHTEN 1-Token-Inferenz (beweist, dass ein Modell wirklich lädt & generiert).
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
SWAP_URL="${MC_LLAMA_SWAP_URL:-http://127.0.0.1:8080}"
|
SWAP_URL="${MC_LLAMA_SWAP_URL:-http://127.0.0.1:8080}"
|
||||||
MC_URL="${MC_URL:-http://127.0.0.1:9001}"
|
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
|
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
|
fail=0
|
||||||
|
|
||||||
echo "=== Stack Post-Update: Funktionsprüfung ==="
|
echo "=== Stack Post-Update: Funktionsprüfung ==="
|
||||||
@@ -48,14 +47,14 @@ else
|
|||||||
echo "FAIL · Inferenz auf '$BRAIN' fehlgeschlagen (Modell lädt/generiert nicht)"; fail=1
|
echo "FAIL · Inferenz auf '$BRAIN' fehlgeschlagen (Modell lädt/generiert nicht)"; fail=1
|
||||||
fi
|
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" \
|
eresp="$(curl -s -m 120 -X POST "$SWAP_URL/v1/embeddings" \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d "{\"model\":\"$EMBED\",\"input\":\"ping\"}" 2>/dev/null)"
|
-d "{\"model\":\"$EMBED\",\"input\":\"ping\"}" 2>/dev/null)"
|
||||||
if printf '%s' "$eresp" | grep -q '"embedding"'; then
|
if printf '%s' "$eresp" | grep -q '"embedding"'; then
|
||||||
echo "PASS · Embedding-Modell '$EMBED' liefert Vektoren"
|
echo "PASS · Embedding-Modell '$EMBED' liefert Vektoren"
|
||||||
else
|
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
|
fi
|
||||||
|
|
||||||
# 4. MC2 selbst gesund (Engine + Gateway erreichbar)?
|
# 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
|
||||||
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
|
if [ "$fail" -eq 0 ]; then
|
||||||
echo "=== STACK OK ✓ ==="
|
echo "=== STACK OK ✓ ==="
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/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
|
# eingefroren — kein Auto-Update-Kanal deckt sie ab. Dieser Audit MELDET nur
|
||||||
# (bewusst kein Auto-Update: Python-Deps brechen gern):
|
# (bewusst kein Auto-Update: Python-Deps brechen gern):
|
||||||
# je venv: veraltete Pakete zählen (pip list --outdated) + bekannte
|
# 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).
|
# idempotent je Monat). Läuft als Hermes-Cron --no-agent (stdout = Telegram).
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
VENVS=("$HOME/.mem0/venv:mem0" "$HOME/.voice/venv:voice")
|
VENVS=("$HOME/.voice/venv:voice")
|
||||||
AUDIT_VENV="$HOME/.venv-audit"
|
AUDIT_VENV="$HOME/.venv-audit"
|
||||||
HERMES="$HOME/.local/bin/hermes"
|
HERMES="$HOME/.local/bin/hermes"
|
||||||
MONAT="$(date '+%Y-%m')"
|
MONAT="$(date '+%Y-%m')"
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
# Eingehängt als ExecStartPost=-/usr/local/bin/llama-swap-warmup.sh in llama-swap.
|
# 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
|
# `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
|
# 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
|
# beim Start selbst an. Modelle werden NICHT automatisch zusammen geladen — jedes einzeln
|
||||||
# anstoßen, sonst bleibt es kalt.
|
# anstoßen, sonst bleibt es kalt.
|
||||||
|
|||||||
@@ -1,418 +0,0 @@
|
|||||||
# Umbau: OpenChamber als Coding-Bahn, Lucy bleibt SysAdmin
|
|
||||||
|
|
||||||
_Entschieden 21.08.2026. Rückbau der Hermes-Coding-Bahn ist **erledigt**, der Aufbau von
|
|
||||||
OpenChamber steht aus. Diese Datei ist die Übergabe an die nächste Sitzung._
|
|
||||||
|
|
||||||
## Die Entscheidung
|
|
||||||
|
|
||||||
**Zwei getrennte Bahnen** — der Weg, der bei diesem Stack schon einmal funktioniert hat:
|
|
||||||
|
|
||||||
| Bahn | Werkzeug | Zweck |
|
|
||||||
|---|---|---|
|
|
||||||
| **Betrieb** | Hermes-Runtime (Lucy) | SysAdmin, Telegram, Crons, Gedächtnis, Skills, Stimme, PC-Steuerung |
|
|
||||||
| **Coding** | **OpenChamber** über OpenCode | Nur programmieren. Getrennt von Lucy. |
|
|
||||||
| **Motor** | llama-swap `:8080` / MC2-Gateway `:9010` | beide Bahnen teilen sich die Modelle |
|
|
||||||
| **Steuerpult** | MC2 `:9001` | unverändert |
|
|
||||||
|
|
||||||
**Warum getrennt:** Der Versuch, beides in Hermes Desktop zu vereinen, ist an zwei Dingen
|
|
||||||
gescheitert — die Gateway-Registry akzeptiert kein Benutzer/Passwort (nur Session-Token oder
|
|
||||||
OAuth, beides für eine selbstgehostete Box ungeeignet), und der Sessions-Reiter fiel dadurch
|
|
||||||
immer auf „This device" zurück. Details in der Gedächtnisnotiz vom 20./21.08.
|
|
||||||
|
|
||||||
## Was bereits erledigt ist (21.08.)
|
|
||||||
|
|
||||||
**PC:**
|
|
||||||
- Hermes Desktop + CLI deinstalliert (`hermes uninstall --full`), Reste von Hand entfernt
|
|
||||||
- `AppData\Local\hermes`, `AppData\Roaming\Hermes`, `~\.hermes` — alle weg
|
|
||||||
- Startmenü- und Desktop-Verknüpfungen entfernt
|
|
||||||
- Env-Variablen bereinigt; **`HERMES_PC_TOKEN` bleibt bewusst** (Lucys PC-Executor auf `:7777`)
|
|
||||||
- Node v24.19.0 in `Program Files` ist unabhängig → bleibt, erfüllt OpenChambers Anforderung (≥22)
|
|
||||||
|
|
||||||
**Box:**
|
|
||||||
- Bot-Profile `coder` + `debugger` entfernt (gesichert in `~/archiv-aufraeumen-20260820/`)
|
|
||||||
- `~/.hermes/desktop-ssh/` entfernt, `desktop-auth.env` entfernt
|
|
||||||
- ufw: Port **9119 wieder zu** — nur noch `22 · 9001 · 8080 · 7681`
|
|
||||||
- Dashboard bleibt auf `0.0.0.0:9119` gebunden **mit Anmeldung**, damit MC2s `/hermes-ui`-Proxy
|
|
||||||
nicht mehr passwortlos aus dem LAN erreichbar ist (das war ein offenes Scheunentor).
|
|
||||||
Zugang: `~/.hermes/dashboard-login.txt` (0600), Benutzer `commander`.
|
|
||||||
‼️ Zugangsdaten stehen **nur** in `config.yaml` — die frühere `EnvironmentFile` hat sie still
|
|
||||||
überstimmt und kostete eine Fehlersuche. Eine Quelle, eine Wahrheit.
|
|
||||||
|
|
||||||
**Verifiziert nach dem Rückbau:** 5 Dienste aktiv · 4 MCP-Server · 4 Crons · Telegram `connected`
|
|
||||||
· PC-Executor HTTP 200. Lucy ist vollständig funktionsfähig.
|
|
||||||
|
|
||||||
## Was aufzubauen ist
|
|
||||||
|
|
||||||
### 1. OpenCode auf der Box
|
|
||||||
|
|
||||||
Wurde beim Juli-Rückbau entfernt (`command -v opencode` → leer). Muss neu.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Installation (Box)
|
|
||||||
curl -fsSL https://opencode.ai/install | bash
|
|
||||||
|
|
||||||
# Provider auf den lokalen Motor zeigen lassen: ~/.config/opencode/opencode.json
|
|
||||||
# base_url http://127.0.0.1:9010/v1 (MC2-Gateway, kann model:auto)
|
|
||||||
# oder http://127.0.0.1:8080/v1 (llama-swap direkt)
|
|
||||||
# api_key local
|
|
||||||
```
|
|
||||||
|
|
||||||
Als systemd-**User**-Dienst (wie alle anderen — reboot-fest, sudo-frei):
|
|
||||||
|
|
||||||
```
|
|
||||||
opencode serve --hostname 0.0.0.0 --port 4096
|
|
||||||
```
|
|
||||||
|
|
||||||
Danach `ufw allow from 192.168.178.0/24 to any port 4096 proto tcp`.
|
|
||||||
|
|
||||||
### 2. OpenChamber auf dem PC
|
|
||||||
|
|
||||||
Desktop-Version von den GitHub-Releases (`openchamber/openchamber`, MIT, ~9,1k ★).
|
|
||||||
**Wichtig:** Die Desktop-Variante bringt eine eigene OpenCode-CLI mit — für den Remote-Betrieb
|
|
||||||
muss sie ausgeschaltet werden:
|
|
||||||
|
|
||||||
```
|
|
||||||
OPENCODE_HOST = http://192.168.178.151:4096 ← volle Adresse MIT Port, OHNE Pfad
|
|
||||||
OPENCODE_SKIP_START = true
|
|
||||||
```
|
|
||||||
|
|
||||||
> Fehlt der Port oder hängt ein Pfad dran, **ignoriert OpenChamber die Variable und startet
|
|
||||||
> stillschweigend seinen eigenen Server.** Genau diese Klasse Fehler hat uns bei Hermes einen
|
|
||||||
> halben Tag gekostet.
|
|
||||||
|
|
||||||
Modelle, Provider und Schlüssel liegen in **OpenCode**, nicht in OpenChamber.
|
|
||||||
|
|
||||||
### 3. Modellwahl — nicht wieder den dichten Coder
|
|
||||||
|
|
||||||
★★ **`fast` (Qwen3.6-35B-A3B) nehmen, nicht `coder` (Qwen3.8-27B).** Gemessen:
|
|
||||||
|
|
||||||
| | Ausgabe | Eingabe | SWE-bench Verified |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `fast` Qwen3.6-35B-A3B (MoE) | **95,7 t/s** | 221 t/s | **73,4** |
|
|
||||||
| `coder` Qwen3.8-27B (dicht) | 12,6 t/s | 144 t/s | keine agentische Messung |
|
|
||||||
|
|
||||||
Das dichte Modell ist bandbreitenlimitiert (17 GB ÷ 215 GB/s ≈ 12,6 t/s = Hardware-Limit).
|
|
||||||
Der MoE ist **schneller und besser**.
|
|
||||||
|
|
||||||
### 4. Die Arbeitsanweisung mitnehmen — der wichtigste Fund überhaupt
|
|
||||||
|
|
||||||
Der Agent las in Lauf A eine ganze Nacht lang und schrieb **null Dateien**. Mit dieser Zeile
|
|
||||||
im Auftrag schrieb er in 24 Minuten fünf:
|
|
||||||
|
|
||||||
> **„Arbeite Datei für Datei. Lies HÖCHSTENS zwei Dateien, bevor du die erste änderst.
|
|
||||||
> Kein Gesamtplan, keine Vollinventur. Eine halbfertige Änderung ist wertvoller als eine
|
|
||||||
> vollständige Analyse ohne Änderung."**
|
|
||||||
|
|
||||||
Gehört in OpenChambers System-Prompt bzw. `AGENTS.md` der Projekte.
|
|
||||||
|
|
||||||
## Bekannte Fallen (aus dem Juli-Audit + heute)
|
|
||||||
|
|
||||||
- **Bus-Faktor 1** bei OpenChamber: ~70–80 % der Commits von einer Person. Version **pinnen**,
|
|
||||||
nicht blind auto-updaten.
|
|
||||||
- **Windows-Installer unsigniert** → SmartScreen-Warnung ist erwartbar, kein Alarm.
|
|
||||||
- Nie ohne `--ui-password` über localhost hinaus binden.
|
|
||||||
- `opencode web` (nicht OpenChamber!) hat einen kaputten Projekt-Finder für `$HOME` — falls
|
|
||||||
irgendwo ein leerer Projektbaum auftaucht, ist das der Grund.
|
|
||||||
- Git-Push zum Gitea geht vom PC **nur über PowerShell/GCM**, nicht aus der Git-Bash.
|
|
||||||
|
|
||||||
## Erster Test nach dem Aufbau
|
|
||||||
|
|
||||||
Die Referenzaufgabe liegt bereit und ist halb erledigt — ideal zum Vergleich:
|
|
||||||
|
|
||||||
```
|
|
||||||
Worktree: ~/projekte/mc2-referenz (Zweig referenz/mem0-ausbau)
|
|
||||||
Aufgabe: docs/aufgaben/referenzaufgabe-mem0-ausbau.md
|
|
||||||
Prüfung: bash docs/aufgaben/referenz-check.sh
|
|
||||||
Stand: 5 von 17 Dateien erledigt (Hermes-Lauf A2, abgebrochen durch SSH-Fehler)
|
|
||||||
```
|
|
||||||
|
|
||||||
Damit lässt sich direkt vergleichen: **Wie lange braucht OpenChamber + `fast` für dieselben
|
|
||||||
17 Dateien?** Hermes + dichter Coder brauchte hochgerechnet ~80 Minuten; die Rechnung für
|
|
||||||
`fast` sagt ~30.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Stand nach dem Aufbau (21.08.2026)
|
|
||||||
|
|
||||||
**Beide Bahnen stehen. Der Aufbau ist erledigt, der Referenzlauf steht aus.**
|
|
||||||
|
|
||||||
### Box (`192.168.178.151`)
|
|
||||||
|
|
||||||
| Sache | Wert |
|
|
||||||
|---|---|
|
|
||||||
| OpenCode | **1.18.20**, `~/.opencode/bin/opencode` (eigenständiges Binary, kein Node nötig) |
|
|
||||||
| Dienst | `opencode-server.service` (systemd **user**, `enable`d, Linger=yes → reboot-fest) |
|
|
||||||
| Lauscht | `0.0.0.0:4096`, Arbeitsverzeichnis `~/projekte` |
|
|
||||||
| ufw | Regel 5: `4096/tcp ALLOW IN 192.168.178.0/24` |
|
|
||||||
| Provider | `box` → `http://127.0.0.1:9010/v1` (MC2-Gateway), `apiKey: local` |
|
|
||||||
| Modelle | `box/fast` (Standard) · `box/heavy` · `box/hermes` (small_model) · `box/coder` (nur Vergleich) |
|
|
||||||
| Autoupdate | `false` in `opencode.json` |
|
|
||||||
|
|
||||||
Konfiguration: `~/.config/opencode/opencode.json`.
|
|
||||||
**Nur Rollen-Aliase**, keine Modell-Eigennamen — die Konsolidierung wirft Eigennamen sonst raus (Regel vom 20.08.).
|
|
||||||
|
|
||||||
### Arbeitsanweisung — global verdrahtet
|
|
||||||
|
|
||||||
Liegt als `~/.config/opencode/ARBEITSANWEISUNG.md` und ist über `instructions` in
|
|
||||||
`opencode.json` in **jeder** Sitzung aktiv, nicht nur in Projekten mit `AGENTS.md`.
|
|
||||||
Gegengeprüft: „Wie viele Dateien höchstens?" → `2`. `~` wird von OpenCode expandiert.
|
|
||||||
|
|
||||||
### PC
|
|
||||||
|
|
||||||
| Sache | Wert |
|
|
||||||
|---|---|
|
|
||||||
| OpenChamber | **v1.19.0** (18.08.), `%LOCALAPPDATA%\Programs\@openchamberelectron` |
|
|
||||||
| Signatur | `NotSigned` — erwartet, kein Alarm |
|
|
||||||
| `OPENCODE_HOST` | `http://192.168.178.151:4096` (User-Env, mit Port, ohne Pfad) |
|
|
||||||
| `OPENCODE_SKIP_START` | `true` (User-Env) |
|
|
||||||
|
|
||||||
**Verifiziert:** OpenChamber hält 8 offene TCP-Verbindungen zur Box auf `:4096`,
|
|
||||||
startet **keinen** eigenen OpenCode (kein lokaler Prozess, kein lokaler Port).
|
|
||||||
Damit ist genau der Punkt genommen, an dem Hermes Desktop gescheitert ist.
|
|
||||||
|
|
||||||
**Auto-Update:** kein Riegel nötig. Im Bundle steht `autoUpdater.autoDownload = false` und
|
|
||||||
`autoInstallOnAppQuit = false` — OpenChamber lädt nie von selbst, es meldet nur und wartet
|
|
||||||
auf einen Klick. Die Version ist damit faktisch gepinnt.
|
|
||||||
|
|
||||||
### ‼️ Offene Sicherheitsentscheidung — bewusst so gewählt
|
|
||||||
|
|
||||||
Der Server läuft **ohne Anmeldung** im LAN. Beim Start meldet er selbst:
|
|
||||||
|
|
||||||
```
|
|
||||||
Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.
|
|
||||||
```
|
|
||||||
|
|
||||||
Praktisch heißt das: jedes Gerät im `192.168.178.0/24` kann über die API beliebige
|
|
||||||
Befehle als `hitonabi` auf der Box ausführen — dieselbe Klasse Loch wie das passwortlose
|
|
||||||
Dashboard auf `:9119`, das am 20.08. zugemacht wurde.
|
|
||||||
|
|
||||||
★★ **Ein Riegel wäre da und kostet nichts:** OpenChamber schickt HTTP-Basic-Auth mit, wenn
|
|
||||||
`OPENCODE_SERVER_PASSWORD` gesetzt ist (Benutzer aus `OPENCODE_SERVER_USERNAME`,
|
|
||||||
Standard `opencode`). Server-Variable in der systemd-Unit, gleiche Variable als User-Env
|
|
||||||
auf dem PC — **kein Tunnel, keine Umstellung, der Port bleibt offen im LAN.**
|
|
||||||
|
|
||||||
Der User hat am 21.08. **bewusst dagegen entschieden** („so lassen"). Nicht neu aufrollen,
|
|
||||||
aber hier notiert, damit es kein stiller Defekt bleibt.
|
|
||||||
|
|
||||||
### Nächster Schritt: der Referenzlauf
|
|
||||||
|
|
||||||
Alles steht bereit, nichts wurde am Prüfstand angefasst:
|
|
||||||
|
|
||||||
```
|
|
||||||
Worktree: ~/projekte/mc2-referenz (Zweig referenz/mem0-ausbau)
|
|
||||||
Stand: 5 von 17 Dateien erledigt, referenz-check.sh sagt ROT
|
|
||||||
Prüfung: bash docs/aufgaben/referenz-check.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Die Arbeitsanweisung wirkt dort über die **globale** Instruktion — `AGENTS.md` im Worktree
|
|
||||||
wurde absichtlich **nicht** angefasst, damit der Vergleich zu Lauf A/A2 sauber bleibt.
|
|
||||||
Zu messen: **Wie lange braucht OpenChamber + `fast` für dieselben 17 Dateien?**
|
|
||||||
Hermes + dichter `coder` brauchte hochgerechnet ~80 Minuten; die Rechnung für `fast` sagt ~30.
|
|
||||||
|
|
||||||
### ‼️ Falle: der Ordner-Knopf reicht Windows-Pfade an die Box durch
|
|
||||||
|
|
||||||
**Beim ersten Lauf sofort hineingetappt (21.08., ~1 h verloren).** Die Sitzung stand auf:
|
|
||||||
|
|
||||||
```
|
|
||||||
/home/hitonabi/projekte/F:\Coding Stuff\mission-control-2
|
|
||||||
```
|
|
||||||
|
|
||||||
OpenChamber hatte den **Windows-Pfad des PCs** an den Box-Server durchgereicht, der ihn hinten
|
|
||||||
an sein `WorkingDirectory` klebte. Das Verzeichnis existiert auf der Box nicht → der Agent hat
|
|
||||||
kein Arbeitsverzeichnis, es geht **keine einzige Anfrage** an den Motor raus. In der Oberfläche
|
|
||||||
sieht das aus wie „hängt": Nachricht steht da, eine Zusammenfassung erscheint, danach nichts.
|
|
||||||
`tokens: 0/0`.
|
|
||||||
|
|
||||||
**Ursache, im Bundle nachgesehen:** OpenChamber öffnet Ordner über Electrons natives
|
|
||||||
`showOpenDialog` — den **lokalen** Windows-Dateibrowser. Einen Fern-Auswähler gibt es nicht.
|
|
||||||
Im Fernbetrieb liefert dieser Knopf also *immer* einen unbrauchbaren Pfad.
|
|
||||||
|
|
||||||
**Der richtige Weg:** Seitenleiste → **„Projekt hinzufügen"**. Das ist ein eigener Dialog
|
|
||||||
(`directoryExplorerDialog`) mit Baum **und** Eingabefeld („Enter path or select from tree…"),
|
|
||||||
und der geht über den Server — also über die Box.
|
|
||||||
|
|
||||||
**Diagnose in einem Befehl** — wenn wieder „nichts passiert", zeigt das den Grund sofort:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ssh hitonabi@192.168.178.151 "curl -s http://127.0.0.1:4096/session | python3 -c \"import json,sys; s=json.load(sys.stdin); s.sort(key=lambda x: x['time']['updated'], reverse=True); x=s[0]; print(x['directory'], x.get('model'), x.get('tokens'))\""
|
|
||||||
```
|
|
||||||
|
|
||||||
Steht dort ein Pfad mit `F:\` oder `tokens 0/0`, ist es diese Falle.
|
|
||||||
|
|
||||||
**Merke:** Ein Projekt taucht in der Server-Liste erst auf, wenn dort einmal eine Sitzung lief.
|
|
||||||
`mc2-referenz` wurde am 21.08. mit einem harmlosen Lauf angemeldet (`box/hermes`, „Antworte nur
|
|
||||||
mit OK") — Worktree danach nachgemessen unverändert: 5 geänderte Dateien, HEAD `e04ace2`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ▶▶ Kurswechsel 21.08. (nachmittags): Quelle ist der PC, nicht die Box
|
|
||||||
|
|
||||||
Der Fernbetrieb oben war **falsch herum gedacht**. Der User arbeitet so — und so ist es
|
|
||||||
jetzt gebaut:
|
|
||||||
|
|
||||||
```
|
|
||||||
F:\Coding Stuff\… ← Quelle der Wahrheit, liegt IMMER lokal
|
|
||||||
│ OpenChamber startet seine EIGENE OpenCode-CLI und arbeitet an lokalen Dateien
|
|
||||||
│ git push (PowerShell/GCM)
|
|
||||||
▼
|
|
||||||
Gitea 192.168.178.153:3000
|
|
||||||
│ projekte-sync (stuendlich, --ff-only) — oder sofort per Skript
|
|
||||||
▼
|
|
||||||
AI-Box ~/projekte/… ← Spiegel fuer Deploy / CI / Lucy
|
|
||||||
▲
|
|
||||||
└─ von der Box kommt NUR noch das Modell (Inferenz ueber HTTP)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Damit faellt die Windows-Pfad-Falle ersatzlos weg** — es gibt keine Fernpfade mehr.
|
|
||||||
|
|
||||||
### ★★ Kein Box-Umbau noetig: MC2 `:9001/v1` ist der Modellweg
|
|
||||||
|
|
||||||
Erst war geplant, den Gateway `:9010` ins LAN zu binden. Ueberfluessig — die Gateway-Unit
|
|
||||||
sagt es selbst: *„LAN-Clients kommen weiter ueber MC2 `:9001/v1`, das roh hierher
|
|
||||||
durchreicht."* Der Port ist seit jeher offen.
|
|
||||||
|
|
||||||
Vom PC aus gemessen: `:9001/v1/models` liefert **die Rollen-Aliase** (`fast`, `heavy`,
|
|
||||||
`hermes`, `coder`), `fast` antwortet in **0,7 s**. Kein neuer Port, keine Neubindung,
|
|
||||||
keine ufw-Regel — und **keine Modell-Eigennamen** in der PC-Konfiguration (die Regel vom
|
|
||||||
20.08. bleibt gewahrt). llama-swap `:8080` waere die Alternative gewesen, liefert aber nur
|
|
||||||
Eigennamen (`Qwen3.6-35B-A3B`) → genau der stille 404 vom 19.08.
|
|
||||||
|
|
||||||
### Was auf dem PC steht
|
|
||||||
|
|
||||||
`C:\Users\TobisPC\.config\opencode\opencode.json` — Provider `aibox` →
|
|
||||||
`http://192.168.178.151:9001/v1`, Standard `aibox/fast`, `small_model` `aibox/hermes`.
|
|
||||||
Die Juli-Struktur ist erhalten: Agenten `plan`(heavy) · `build`(fast) · `explore`(hermes) ·
|
|
||||||
`review`(heavy), inklusive **IDE-Zaun** (`ssh`/`scp`/`sftp` deny, gezielte Arcane-Ausnahme;
|
|
||||||
Catch-all `*: allow` steht ZUERST, weil OpenCode `findLast` auswertet).
|
|
||||||
Arbeitsanweisung als `ARBEITSANWEISUNG.md` daneben, ueber `instructions` global geladen.
|
|
||||||
|
|
||||||
Das Plugin `plugin/mc2-governor.ts` wird automatisch mitgeladen: Werkzeug-Zaun,
|
|
||||||
Pruef-Tor-Schleife, Savepoint statt Zusammenfassen, Meldungen an Lucys Stimme.
|
|
||||||
‼️ Es blockt **`git push`** absichtlich — *„Veroeffentlichen ist Sache des Menschen."*
|
|
||||||
Der Agent codet, gepusht wird von Hand (siehe Skript unten).
|
|
||||||
|
|
||||||
**Gebuendelte CLI:** OpenCode **1.18.18** in OpenChambers `resources\opencode-cli`.
|
|
||||||
Im lokalen Betrieb vergibt OpenChamber dem Server automatisch ein Passwort
|
|
||||||
(`OPENCODE_SERVER_PASSWORD`, lifecycle-owned) — `/config` antwortet von aussen mit `401`.
|
|
||||||
Der Schutz, den der Box-Server nicht hatte, ist hier also gratis dabei.
|
|
||||||
|
|
||||||
**Env-Variablen `OPENCODE_HOST` und `OPENCODE_SKIP_START` sind wieder ENTFERNT.**
|
|
||||||
Sie erzwingen den Fernbetrieb; mit ihnen startet OpenChamber keine eigene CLI.
|
|
||||||
|
|
||||||
### Box zurueckgebaut
|
|
||||||
|
|
||||||
`opencode-server.service` ist **gestoppt, deaktiviert und geloescht**, die ufw-Regel fuer
|
|
||||||
`4096` wieder entfernt. Damit erledigt sich die offene Sicherheitsfrage von heute Mittag
|
|
||||||
von selbst: der ungeschuetzte Dienst existiert nicht mehr. ufw steht wieder auf
|
|
||||||
`22 · 9001 · 8080 · 7681`.
|
|
||||||
|
|
||||||
### `deploy/push-und-sync.ps1` — der Handschlag
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\deploy\push-und-sync.ps1 # committen -> pushen -> Box zieht sofort nach
|
|
||||||
.\deploy\push-und-sync.ps1 -NurSync # nur nachziehen
|
|
||||||
.\deploy\push-und-sync.ps1 -Pfad "F:\Coding Stuff\rippy"
|
|
||||||
```
|
|
||||||
|
|
||||||
Stoesst `projekte-sync` sofort an, statt bis zu 60 Minuten auf den Timer zu warten —
|
|
||||||
**kein zweiter Mechanismus, nur ein Ausloeser.** Meldet danach, auf welchem Commit die Box steht.
|
|
||||||
|
|
||||||
‼️ **Falle beim Bauen gefunden:** Der Repo-Name darf **nicht** aus dem Ordnernamen kommen —
|
|
||||||
lokal heisst es `mission-control-2`, in Gitea `mission-control-v2`. Das Skript liest ihn
|
|
||||||
aus der Remote-URL. Getestet gegen beide Faelle: MC2 (Live-Deployment, liegt als
|
|
||||||
Verknuepfung unter `~/projekte`) und `rippy` (normales Projekt, `a14449e`).
|
|
||||||
|
|
||||||
### Offener Punkt: PC-Remote haengt an der DDNS-Domain
|
|
||||||
|
|
||||||
Der PC pusht nach `https://git.tobisniceshomelab.ddnsfree.com/…`, die Box nutzt intern
|
|
||||||
`http://192.168.178.153:3000`. Die Domain ist nachts durch die Zwangstrennung zeitweise
|
|
||||||
tot (Lehre vom 24.07.). Umstellen mit:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git remote set-url origin http://192.168.178.153:3000/Hitonabi/mission-control-v2.git
|
|
||||||
```
|
|
||||||
|
|
||||||
Noch **nicht** gemacht — aendert Git-Konfiguration in den Repos des Users.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Abschluss der Coding Lane (21.08.2026, abends)
|
|
||||||
|
|
||||||
### Der Zugang: ein SSH-Schluessel, kein Token mehr
|
|
||||||
|
|
||||||
**Der Ausloeser:** Mitten in der Sitzung scheiterte ein Push, der eine Stunde
|
|
||||||
vorher noch ging — `remote: Failed to authenticate user`. Der Git Credential
|
|
||||||
Manager haelt fuer Gitea ein **OAuth-Token mit einer Stunde Laufzeit**. Damit ist
|
|
||||||
HTTP fuer einen Agenten, der selbst pushen soll, strukturell ungeeignet.
|
|
||||||
|
|
||||||
**Was dabei herauskam:** Giteas SSH war **nie funktionsfaehig**. In der `app.ini`
|
|
||||||
stand zwar `DISABLE_SSH = false` und `SSH_PORT = 22`, aber im Container gibt es
|
|
||||||
**keinen `git`-Benutzer** und keine `authorized_keys` — Port 22 gehoert dem
|
|
||||||
System-sshd. Es sah nur so aus, als koennte man ueber SSH klonen.
|
|
||||||
|
|
||||||
**Loesung:** Giteas **eingebauten** SSH-Server eingeschaltet (Port 22 ist belegt,
|
|
||||||
deshalb 2222). Der verwaltet die Schluessel selbst, direkt aus der Datenbank —
|
|
||||||
kein `git`-Benutzer, keine `authorized_keys`.
|
|
||||||
|
|
||||||
```ini
|
|
||||||
# /etc/gitea/app.ini, [server] (Sicherung: app.ini.bak-20260821)
|
|
||||||
SSH_PORT = 2222
|
|
||||||
SSH_LISTEN_PORT = 2222
|
|
||||||
START_SSH_SERVER = true
|
|
||||||
SSH_LISTEN_HOST = 0.0.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
‼️‼️ **Der SSH-Benutzer heisst `gitea`, NICHT `git`.** Gitea laeuft unter diesem
|
|
||||||
Namen und weist alles andere ab. Die Fehlermeldung steht nur im Gitea-Log, nicht
|
|
||||||
beim Client — ssh sagt bloss `Permission denied (publickey)`:
|
|
||||||
|
|
||||||
```
|
|
||||||
Invalid SSH username git - must use gitea for all git operations via ssh
|
|
||||||
```
|
|
||||||
|
|
||||||
**Auf dem PC** (`~/.ssh/config`) — noetig, weil hier **acht** Schluessel liegen
|
|
||||||
und ssh sonst der Reihe nach durchprobiert, bis Gitea abbricht:
|
|
||||||
|
|
||||||
```
|
|
||||||
Host 192.168.178.153 gitea gitea.heimnetz
|
|
||||||
HostName 192.168.178.153
|
|
||||||
Port 2222
|
|
||||||
User gitea
|
|
||||||
IdentityFile ~/.ssh/id_general
|
|
||||||
IdentitiesOnly yes
|
|
||||||
```
|
|
||||||
|
|
||||||
Der Schluessel `id_general` ist in Gitea hinterlegt (Nr. 3) — **derselbe, mit dem
|
|
||||||
du auf Box, pve und Arcane kommst.** Von acht Zugangsdaten auf einen.
|
|
||||||
|
|
||||||
Alle sieben PC-Repos stehen jetzt auf
|
|
||||||
`ssh://gitea@192.168.178.153:2222/Hitonabi/<repo>.git`.
|
|
||||||
Das TTT2-Projekt bleibt unberuehrt — es haengt an einer anderen Domain und ist
|
|
||||||
als „Finished" markiert.
|
|
||||||
|
|
||||||
### Gemessen, nicht angenommen
|
|
||||||
|
|
||||||
| Test | Ergebnis |
|
|
||||||
|---|---|
|
|
||||||
| `ssh -T gitea` | „Hi there, Hitonabi! You've successfully authenticated with the key named id_general" |
|
|
||||||
| Push aus PowerShell | 8 Commits, `7c23ebf..25dbfb2` |
|
|
||||||
| **Push aus dem Agenten** | durchgelaufen, **keine Passwortabfrage** |
|
|
||||||
| Erzwungener Push aus dem Agenten | vom Werkzeug-Zaun abgewiesen |
|
|
||||||
| `push-und-sync.ps1` komplett | Commit → Push → Box zieht nach |
|
|
||||||
|
|
||||||
### Der Sonderfall MC2
|
|
||||||
|
|
||||||
`projekte-sync` zieht `mission-control-v2` **absichtlich nicht** — die Box
|
|
||||||
bedient daraus den laufenden Dienst auf `:9001`. Nach einem Push steht dort
|
|
||||||
weiter der alte Commit, und das ist richtig so. Das Live-Deployment braucht
|
|
||||||
seinen eigenen, bewussten Schritt. Alle anderen Projekte werden normal gezogen.
|
|
||||||
|
|
||||||
### Fallen fuer das naechste Mal
|
|
||||||
|
|
||||||
- **`sed` mit `$` und `\n` ueber PowerShell zerlegt sich.** Der erste Anlauf,
|
|
||||||
die `app.ini` zu aendern, lief ins Leere und die Datei blieb unveraendert —
|
|
||||||
ohne Fehlermeldung. In einzelne Ersetzungen zerlegen und **danach nachsehen**.
|
|
||||||
- Ein Dienst-Neustart nach einer Konfig-Aenderung ist noch keine Bestaetigung:
|
|
||||||
Gitea startete brav neu — mit der alten Datei.
|
|
||||||
- Acht SSH-Schluessel auf dem PC sind selbst ein Befund. `IdentitiesOnly yes`
|
|
||||||
ist Pflicht, sonst bricht die Gegenseite vorher ab.
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
# Hermes: Werkzeuge nach Bedarf (21.08.2026)
|
|
||||||
|
|
||||||
## Die Frage war: kann Hermes Werkzeuge je nach Aufgabe an- und abschalten?
|
|
||||||
|
|
||||||
**Ja — auf drei Ebenen, und die wichtigste lief schon.**
|
|
||||||
|
|
||||||
### 1. Automatisch zur Laufzeit — `tool_search` (war bereits an)
|
|
||||||
|
|
||||||
```
|
|
||||||
tools:
|
|
||||||
tool_search:
|
|
||||||
enabled: true
|
|
||||||
threshold_pct: 10
|
|
||||||
listing: auto
|
|
||||||
listing_max_tokens: 4000
|
|
||||||
```
|
|
||||||
|
|
||||||
Im Log nachweisbar:
|
|
||||||
|
|
||||||
```
|
|
||||||
tool_search activated (tier 1): 47 core/visible tools kept,
|
|
||||||
59 deferred (~5636 tokens), listing full (budget ~4000 tokens)
|
|
||||||
```
|
|
||||||
|
|
||||||
Hermes haelt einen Kern sichtbar und schiebt den Rest hinter eine Suche; der Agent
|
|
||||||
zieht nach, was er braucht. **Hier war nichts einzurichten.**
|
|
||||||
|
|
||||||
Aber: das Aufschieben allein sparte nur 1.100–5.600 Tokens, waehrend `kanban` mit
|
|
||||||
22,7 KB im sichtbaren Kern blieb. Der Hebel lag woanders.
|
|
||||||
|
|
||||||
### 2. Global abschalten — `agent.disabled_toolsets`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
hermes config set agent.disabled_toolsets "[computer_use, image_gen, video, video_gen, kanban, delegation]"
|
|
||||||
hermes tools disable session_search
|
|
||||||
```
|
|
||||||
|
|
||||||
Abgeschaltet **anhand von 9 Tagen echter Nutzung**, nicht nach Gefuehl:
|
|
||||||
|
|
||||||
| Werkzeugsatz | Groesse | Nutzung 13.–21.08. |
|
|
||||||
|---|---|---|
|
|
||||||
| `kanban` (14 Werkzeuge) | 22,7 KB | 35× lesen, **1× schreiben** |
|
|
||||||
| `session_search` | 6,8 KB | **1×** |
|
|
||||||
| `delegation` | 5,6 KB | Fan-out, seit OpenChamber tot |
|
|
||||||
|
|
||||||
‼️ `kanban` kennt `hermes tools disable` **nicht** — es haengt an `toolsets` /
|
|
||||||
`platform_toolsets` und muss ueber `agent.disabled_toolsets` weg.
|
|
||||||
|
|
||||||
### 3. Pro Zugangsweg — `platform_toolsets.<platform>`
|
|
||||||
|
|
||||||
★★ **Der groesste Fund.** Es gibt einen eigenen Zugangsweg `cron` (17 Sitzungen im
|
|
||||||
Log), fuer den **kein** Eintrag existierte — Cron-Jobs bekamen also die volle
|
|
||||||
Werkzeugkiste. Von den drei Jobs braucht nur der Nachrichtenbericht ueberhaupt
|
|
||||||
Werkzeuge, und der braucht zwei:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
hermes config set platform_toolsets.cron "[web, terminal]"
|
|
||||||
```
|
|
||||||
|
|
||||||
`web` fuer die Suche, `terminal` um `notify.sh` aufzurufen. Mehr nicht.
|
|
||||||
|
|
||||||
Ausserdem moeglich, hier nicht gebraucht: `hermes -t TOOLSETS` setzt den
|
|
||||||
Werkzeugsatz fuer einen einzelnen Aufruf.
|
|
||||||
|
|
||||||
## Gemessen mit `hermes prompt-size`
|
|
||||||
|
|
||||||
Vorlast **je Anfrage**, Systemprompt plus Werkzeug-Schemata:
|
|
||||||
|
|
||||||
| Zugangsweg | vorher | nachher | |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `cli` | 99,4 KB | **56,5 KB** | −43 % |
|
|
||||||
| `cron` | 63,1 KB | **16,4 KB** | **−74 %** |
|
|
||||||
|
|
||||||
Rund 11.000 Tokens weniger bei jedem interaktiven Zug, rund 12.000 bei jedem
|
|
||||||
Cron-Lauf. Das ist Kontext, der vorher fuer Werkzeugbeschreibungen draufging,
|
|
||||||
die nie aufgerufen wurden.
|
|
||||||
|
|
||||||
## Nachgeprueft, nicht angenommen
|
|
||||||
|
|
||||||
- Lucy interaktiv: Terminal, Dateien, Websuche, Gedaechtnis — alle vier da.
|
|
||||||
- Daily News Report mit nur `[web, terminal]` durchgelaufen und zugestellt.
|
|
||||||
Der Bericht war sogar besser als der Lauf davor.
|
|
||||||
- Sicherung der Konfiguration liegt unter `~/.hermes/config.yaml.bak-kiss-20260821`.
|
|
||||||
|
|
||||||
## Noch drin, wenn es noch schlanker soll
|
|
||||||
|
|
||||||
- `api_server` traegt 44,8 KB (19 Werkzeuge) — mehr als `cli`. Dort haengt noch
|
|
||||||
`hermes-pc-control`, das in 9 Tagen **null Mal** aufgerufen wurde und an dem
|
|
||||||
eine geplante Aufgabe auf dem PC plus ein eigenes Token haengen.
|
|
||||||
- MCP `hermes-web-fetch` (1 Aufruf) doppelt das eingebaute `web_extract`.
|
|
||||||
- MCP `mission-control-voice` (1 Aufruf) doppelt das eingebaute `text_to_speech`.
|
|
||||||
- Die alten Eintraege `kanban` / `delegation` stehen noch in `platform_toolsets`;
|
|
||||||
`agent.disabled_toolsets` sticht sie, aber aufgeraeumt ist es nicht.
|
|
||||||
+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-CCbyojll.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
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* 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-CCbyojll.js";import{S,H as Q}from"./SectionLabel-BfFwUeZU.js";import{C as M}from"./chevron-down-SPeGIim5.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
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* 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-CCbyojll.js";import{R as j}from"./rotate-ccw-DuKtbjZS.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
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* 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-CCbyojll.js";import{C as K}from"./chevron-down-SPeGIim5.js";import{F as O}from"./folder-open-FZLk16T9.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
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+3
-3
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-CCbyojll.js";import{S as Le,H as X}from"./SectionLabel-BfFwUeZU.js";import{C as ee}from"./code-xml-AZtn6xxf.js";import{L as Pe}from"./layers-Dpl2QlP2.js";import{C as U}from"./chevron-down-SPeGIim5.js";import{R as We}from"./rotate-ccw-DuKtbjZS.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
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* 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-CCbyojll.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-CCbyojll.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};
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as E,u as he,a as qe,r as j,j as e,C as Y,L as Fe,b as k,d as Re,q as V,B as re,e as Ie,E as ze,f as le,g as Be,h as Ae,i as Pe,k as Ge,l as ee,X as me,H as be,T as se,s as Oe,m as Te,n as q,o as U,p as He,S as ge,t as Qe,P as Ue,v as Ve,w as We,x as Ze,y as Je}from"./index-CCbyojll.js";import{J as Xe}from"./JobsBar-Ckecoyqy.js";import{C as Ye}from"./code-xml-AZtn6xxf.js";import{R as et}from"./rotate-ccw-DuKtbjZS.js";import{C as ue,Z as ie,L as tt,a as rt}from"./zap-CRNNnxfs.js";import{C as De}from"./chevron-down-SPeGIim5.js";import{L as st}from"./layers-Dpl2QlP2.js";/**
|
import{c as E,u as he,a as qe,r as j,j as e,C as Y,L as Fe,b as k,d as Re,q as V,B as re,e as Ie,E as ze,f as le,g as Be,h as Ae,i as Pe,k as Ge,l as ee,X as me,H as be,T as se,s as Oe,m as Te,n as q,o as U,p as He,S as ge,t as Qe,P as Ue,v as Ve,w as We,x as Ze,y as Je}from"./index-0Xcu6PnS.js";import{J as Xe}from"./JobsBar-JgL4-cSI.js";import{C as Ye}from"./code-xml-BeQlEZLv.js";import{R as et}from"./rotate-ccw-CBM-YWkV.js";import{C as ue,Z as ie,L as tt,a as rt}from"./zap-BHzSmHiL.js";import{C as De}from"./chevron-down-Bak281TY.js";import{L as st}from"./layers-D3tFEnOl.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as s,j as e}from"./index-CCbyojll.js";/**
|
import{c as s,j as e}from"./index-0Xcu6PnS.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* 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-CCbyojll.js";import{J as _}from"./JobsBar-Ckecoyqy.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
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* 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-CCbyojll.js";/**
|
import{c as o}from"./index-0Xcu6PnS.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as e}from"./index-CCbyojll.js";/**
|
import{c as e}from"./index-0Xcu6PnS.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as a}from"./index-CCbyojll.js";/**
|
import{c as a}from"./index-0Xcu6PnS.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+3
-3
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as a}from"./index-CCbyojll.js";/**
|
import{c as a}from"./index-0Xcu6PnS.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as t}from"./index-CCbyojll.js";/**
|
import{c as t}from"./index-0Xcu6PnS.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as a}from"./index-CCbyojll.js";/**
|
import{c as a}from"./index-0Xcu6PnS.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
Vendored
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>Mission Control 2.0</title>
|
<title>Mission Control 2.0</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CCbyojll.js"></script>
|
<script type="module" crossorigin src="/assets/index-0Xcu6PnS.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Dv1-nhwH.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Dv1-nhwH.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Gauge } from "lucide-react"
|
|||||||
import { useVoiceTrace } from "@/lib/queries"
|
import { useVoiceTrace } from "@/lib/queries"
|
||||||
import type { VoiceTurn } from "@/lib/api"
|
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).
|
// INNERHALB von „Hirn" und wird NICHT als eigenes Segment addiert (sonst Doppelzählung).
|
||||||
const SEGMENTS = [
|
const SEGMENTS = [
|
||||||
{ key: "stt_ms", label: "STT", color: "#f59e0b" },
|
{ 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 }) {
|
function TurnRow({ t, scale }: { t: VoiceTurn; scale: number }) {
|
||||||
const title = SEGMENTS.map((s) => `${s.label} ${fmt(t[s.key] as number | null)}`).join(" · ")
|
const title = SEGMENTS.map((s) => `${s.label} ${fmt(t[s.key] as number | null)}`).join(" · ")
|
||||||
+ (t.mem0_ms != null ? ` (davon Gedächtnis ${fmt(t.mem0_ms)})` : "")
|
+ (t.memory_ms != null ? ` (davon Gedächtnis ${fmt(t.memory_ms)})` : "")
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2.5">
|
<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>
|
<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 && (
|
{latest && !latest.error && (
|
||||||
<div className="mt-0.5 font-mono text-[11px] text-muted-foreground/70">
|
<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(" · ")}
|
{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 Gedächtnis {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>
|
||||||
)}
|
)}
|
||||||
</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">
|
<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.
|
Telegram-Turns laufen an MC2 vorbei und erscheinen hier nicht.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ export interface TokenStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Per-Turn-Latenz-Trace (GET /api/voice/trace). Balken-Stufen (zeitlich disjunkt):
|
// 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 {
|
export interface VoiceTurn {
|
||||||
id: string
|
id: string
|
||||||
ts: number // Epoch-Sekunden
|
ts: number // Epoch-Sekunden
|
||||||
@@ -452,9 +452,9 @@ export interface VoiceTurn {
|
|||||||
had_images: boolean
|
had_images: boolean
|
||||||
stt_ms: number | null
|
stt_ms: number | null
|
||||||
vision_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
|
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
|
total_ms: number
|
||||||
error: string | null
|
error: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -345,11 +345,8 @@ description: Treibt Entwicklung mit strikter TDD-Praxis
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Stack>
|
<Stack>
|
||||||
Dein Gedächtnis (Tab <strong>Gedächtnis</strong>) ist <strong>Mem0</strong>-basiert: auto-lernend &
|
Dein Gedächtnis ist auto-lernend & semantisch: Der Agent lernt im Hintergrund nach jedem Turn —
|
||||||
semantisch, <strong>graph-first</strong> dargestellt, mit 4-Typen-Taxonomie
|
egal ob du per Desktop, Konsole, Telegram oder Lucy mit ihm sprichst.
|
||||||
(<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).
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -216,7 +216,7 @@ def system_status() -> str:
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def list_services() -> str:
|
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")
|
data = _get("/api/system/services")
|
||||||
return "\n".join(
|
return "\n".join(
|
||||||
f"{'✅' if s['ok'] else '❌'} {s['name']} ({s['url']})"
|
f"{'✅' if s['ok'] else '❌'} {s['name']} ({s['url']})"
|
||||||
@@ -227,7 +227,7 @@ def list_services() -> str:
|
|||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def restart_service(service: str) -> str:
|
def restart_service(service: str) -> str:
|
||||||
"""Startet einen erlaubten Dienst neu. Erlaubt: llama-swap (Engine) | mission-control-2 |
|
"""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."""
|
(llama-swap = System-Dienst) kann das Box-Passwort verlangen, wenn kein NOPASSWD gesetzt ist."""
|
||||||
res = _post("/api/system/restart", {"service": service})
|
res = _post("/api/system/restart", {"service": service})
|
||||||
return f"Restart {service}: {'ok' if res.get('ok') else 'Fehler — ' + res.get('err', '')}"
|
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()
|
@mcp.tool()
|
||||||
def backup_now() -> str:
|
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."""
|
Änderungen oder wenn der Nutzer ein Backup will."""
|
||||||
r = _post("/api/system/backup")
|
r = _post("/api/system/backup")
|
||||||
return f"Backup erstellt: {json.dumps(r, ensure_ascii=False)}"
|
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